Wróć do bloga
Tutorial6 min czytania

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_full dokumentują się same i ułatwiają utrzymanie w porównaniu do msg_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 de dzięki czemu gracze mogą w każdej chwili zmienić automatycznie wykryty język.

Gotowy, aby zacząć?

Kup skrypty w naszym sklepie lub dołącz do Discorda, aby uzyskać wsparcie, aktualizacje i sprawdzić nadchodzące nowości.