Lokalizacja i i18n w FiveM: Wielojęzyczne skrypty
Wydawaj skrypty FiveM w wielu językach. Pliki locale, klucze zapasowe, narzędzia i wzorce używane przez ox_lib oraz inne popularne zasoby open-source.
Agency Scripts
Założyciel i główny deweloper w Agency Scripts
Dlaczego lokalizacja ma znaczenie dla serwerów FiveM
Społeczności roleplay FiveM działają na całym świecie, gromadząc ogromne liczby graczy w Niemczech, Francji, Brazylii, Turcji i dziesiątkach innych krajów. Jeśli Twoje skrypty obsługują tylko język angielski, odcinasz się od ogromnej części potencjalnych klientów i ograniczasz społeczności, które mogą korzystać z Twoich zasobów. Prawidłowo zlokalizowany skrypt dostosowuje wszystkie teksty dla użytkownika, powiadomienia, menu i komunikaty błędów do preferowanego języka gracza. To nie tylko funkcja poprawiająca wygodę (quality-of-life), ale przewaga konkurencyjna, która odróżnia skrypty amatorskie od profesjonalnych. Dobra wiadomość jest taka, że wdrożenie internacjonalizacji (i18n) w FiveM jest proste, gdy zrozumiesz schemat.
Konfiguracja systemu lokalizacji
Podstawą każdego systemu lokalizacji jest uporządkowany sposób przechowywania i pobierania przetłumaczonych ciągów znaków. Najczęstszym podejściem w FiveM jest używanie plików locale, po jednym na język, przechowywanych w locales katalogu wewnątrz Twojego zasobu. Każdy plik eksportuje tabelę par klucz-wartość, gdzie kluczem jest unikalny identyfikator, a wartością przetłumaczony ciąg. Oto jak ustrukturyzować moduł locale:
-- locales/en.lua
Locales = Locales or {}
Locales['en'] = {
['job_started'] = 'You have started your shift as %s.',
['job_ended'] = 'You have ended your shift. Earnings: $%d',
['not_enough_money'] = 'You do not have enough money. You need $%d.',
['inventory_full'] = 'Your inventory is full. Free up some space first.',
['vehicle_spawned'] = 'Your vehicle has been spawned nearby.',
['access_denied'] = 'You do not have permission to do that.',
['cooldown_active'] = 'Please wait %d seconds before doing that again.',
['item_received'] = 'You received %dx %s.',
}
-- locales/de.lua
Locales = Locales or {}
Locales['de'] = {
['job_started'] = 'Du hast deine Schicht als %s begonnen.',
['job_ended'] = 'Du hast deine Schicht beendet. Verdienst: $%d',
['not_enough_money'] = 'Du hast nicht genug Geld. Du brauchst $%d.',
['inventory_full'] = 'Dein Inventar ist voll. Schaffe zuerst Platz.',
['vehicle_spawned'] = 'Dein Fahrzeug wurde in der Naehe gespawnt.',
['access_denied'] = 'Du hast keine Berechtigung dafuer.',
['cooldown_active'] = 'Bitte warte %d Sekunden, bevor du das erneut tust.',
['item_received'] = 'Du hast %dx %s erhalten.',
}
Tworzenie funkcji tłumaczenia
Rdzeniem systemu jest funkcja tłumaczenia, która wyszukuje klucze i obsługuje formatowanie ciągów znaków ze zmienną liczbą argumentów. Funkcja ta powinna płynnie przełączać się na język domyślny, gdy w aktywnym locale brakuje klucza, a podczas prac deweloperskich ostrzegać programistów o brakujących tłumaczeniach zamiast wyświetlać graczom surowe klucze.
-- shared/locale.lua
local currentLocale = 'en'
local fallbackLocale = 'en'
function SetLocale(locale)
if Locales[locale] then
currentLocale = locale
else
print(('[^1LOCALE^0] Language "%s" not found, falling back to "%s"'):format(locale, fallbackLocale))
currentLocale = fallbackLocale
end
end
function L(key...)
local str = nil
if Locales[currentLocale] and Locales[currentLocale][key] then
str = Locales[currentLocale][key]
elseif Locales[fallbackLocale] and Locales[fallbackLocale][key] then
print(('[^3LOCALE^0] Missing key "%s" for locale "%s", using fallback'):format(key, currentLocale))
str = Locales[fallbackLocale][key]
end
if not str then
print(('[^1LOCALE^0] Missing translation key: "%s"'):format(key))
return key
end
if ... then
return str:format(...)
end
return str
end
Używanie tłumaczeń w twoich skryptach
Gdy moduł locale zostanie załadowany, korzystanie z tłumaczeń w całym skrypcie jest tak proste, jak wywołanie L() funkcję z kluczem i wszelkimi argumentami formatowania. Utrzymuje to kod skryptu w czystości i całkowicie oddziela treść od logiki.
-- server/main.lua
RegisterNetEvent('myresource:startJob', function(jobName)
local src = source
local xPlayer = ESX.GetPlayerFromId(src) -- or your framework equivalent
if not xPlayer then return end
if not HasPermission(src, jobName) then
TriggerClientEvent('ox_lib:notify', src, {
title = L('access_denied'),
type = 'error'
})
return
end
ActiveJobs[src] = { name = jobName, started = os.time() }
TriggerClientEvent('ox_lib:notify', src, {
title = L('job_started', jobName),
type = 'success'
})
end)
Wykrywanie języka dla każdego gracza
Prawdziwie profesjonalny system lokalizacji wykrywa język każdego gracza automatycznie. Możesz to osiągnąć, odczytując ustawienia języka gry klienta lub pozwalając graczom wybrać język w konfiguracji lub za pomocą komendy. Podejście oparte na wykrywaniu po stronie klienta wykorzystuje GetCurrentLanguage native, który zwraca język gry jako dwuliterowy kod. Możesz go następnie przesłać na serwer, aby wszystkie powiadomienia dla tego gracza używały jego preferowanego języka.
-- client/locale_detect.lua
CreateThread(function()
local gameLang = GetCurrentLanguage()
-- Map GTA language codes to your locale codes
local langMap = {
['en-us'] = 'en',
['de-de'] = 'de',
['fr-fr'] = 'fr',
['es-es'] = 'es',
['pt-br'] = 'pt',
['it-it'] = 'it',
['pl-pl'] = 'pl',
['tr-tr'] = 'tr',
['ru-ru'] = 'ru',
['zh-cn'] = 'zh',
['ja-jp'] = 'ja',
['ko-kr'] = 'ko',
}
local detected = langMap[gameLang] or 'en'
SetLocale(detected)
TriggerServerEvent('myresource:setPlayerLocale', detected)
end)
Przechowywanie języka gracza po stronie serwera
Po stronie serwera przechowuj preferencje językowe każdego gracza, aby móc formatować wiadomości w odpowiednim języku przed ich wysłaniem. Jest to kluczowe w przypadku powiadomień wywoływanych przez serwer oraz wiadomości na czacie pochodzących z logiki po stronie serwera, gdzie nie masz bezpośredniego dostępu do ustawień języka klienta.
-- server/locale_manager.lua
local PlayerLocales = {}
RegisterNetEvent('myresource:setPlayerLocale', function(locale)
local src = source
if Locales[locale] then
PlayerLocales[src] = locale
else
PlayerLocales[src] = 'en'
end
end)
AddEventHandler('playerDropped', function()
PlayerLocales[source] = nil
end)
function GetPlayerLocale(src)
return PlayerLocales[src] or 'en'
end
function LForPlayer(src, key...)
local locale = GetPlayerLocale(src)
local str = nil
if Locales[locale] and Locales[locale][key] then
str = Locales[locale][key]
elseif Locales['en'] and Locales['en'][key] then
str = Locales['en'][key]
end
if not str then return key end
if ... then return str:format(...) end
return str
end
Lokalizacja interfejsów NUI i JavaScript
Wiele skryptów FiveM używa NUI (HTML/JS) dla swoich interfejsów użytkownika, a one również wymagają lokalizacji. Najlepszym podejściem jest przesłanie całej tabeli tłumaczeń do ramki NUI podczas jej inicjalizacji, a następnie użycie funkcji tłumaczącej w JavaScript, która odzwierciedla tę z Lua. Pozwala to uniknąć ciągłych callbacków NUI tam i z powrotem dla każdego tekstu.
// nui/js/locale.js
let currentLocale = {};
let fallbackLocale = {};
window.addEventListener('message', (event) => {
if (event.data.action === 'setLocale') {
currentLocale = event.data.locale || {};
fallbackLocale = event.data.fallback || {};
updateAllTranslations();
}
});
function L(key...args) {
let str = currentLocale[key] || fallbackLocale[key] || key;
if (args.length > 0) {
let i = 0;
str = str.replace(/%[sd]/g, () => args[i++] ?? '');
}
return str;
}
function updateAllTranslations() {
document.querySelectorAll('[data-locale]').forEach((el) => {
const key = el.getAttribute('data-locale');
el.textContent = L(key);
});
}
Konfiguracja manifestu zasobu
Twój fxmanifest.lua musi zawierać wszystkie pliki locale, aby zostały załadowane podczas startu zasobu. Użyj wzorca glob, aby automatycznie dołączać nowe pliki locale bez konieczności każdorazowej aktualizacji manifestu. Upewnij się, że współdzielony moduł locale ładuje się przed plikami danych locale.
-- fxmanifest.lua
fx_version 'cerulean'
game 'gta5'
shared_scripts {
'shared/locale.lua',
'locales/*.lua',
}
client_scripts {
'client/locale_detect.lua',
'client/main.lua',
}
server_scripts {
'server/locale_manager.lua',
'server/main.lua',
}
ui_page 'nui/index.html'
files {
'nui/**/*',
}
Dobre praktyki dotyczące lokalizacji FiveM
- Używaj opisowych kluczy zamiast numerycznych ID. Klucze takie jak
inventory_fulldokumentują się same i ułatwiają utrzymanie w porównaniu domsg_042. - Zawsze używaj placeholderów formatowania (
%s,%d) dla wartości dynamicznych zamiast łączenia ciągów znaków. Różne języki mają różny szyk wyrazów, więc wartości muszą dać się wstawiać w różnych miejscach. - Dołącz komentarze kontekstowe w Twoich plikach lokalizacyjnych, aby tłumacze wiedzieli, gdzie wyświetlany jest dany ciąg znaków i co oznaczają argumenty formatowania.
- Testuj z długimi ciągami znaków. Niemiecki tekst jest zazwyczaj o 30% dłuższy niż angielski. Upewnij się, że Twoje elementy UI radzą sobie z dłuższymi tłumaczeniami bez psucia layoutu.
- Nigdy nie wpisuj na sztywno tekstów widocznych dla użytkownika. Każde powiadomienie, etykieta menu, tekst pomocy i komunikat o błędzie powinny przechodzić przez
L()funkcję, nawet jeśli początkowo wspierasz tylko jeden język. - Udostępnij komendę wyboru języka polub
/lang dedzięki czemu gracze mogą w każdej chwili zmienić automatycznie wykryty język.