Назад к блогу
Tutorial6 мин чтения

FiveM Localization and i18n: Многоязычные скрипты

Отгружайте скрипты FiveM на нескольких языках. Файлы локализации, запасные ключи, инструменты и шаблоны, используемые ox_lib и другими крупными ресурсами с открытым исходным кодом.

Agency Scripts

Основатель и ведущий разработчик Agency Scripts

Почему локализация важна для FiveM серверов

Ролевые сообщества FiveM охватывают весь мир, с огромным количеством игроков в Германии, Франции, Бразилии, Турции и десятках других стран. Если ваши скрипты поддерживают только английский, вы теряете огромную часть потенциальных клиентов и ограничиваете сообщества, которые могут использовать ваши ресурсы. Правильно локализованный скрипт адаптирует весь пользовательский текст, уведомления, меню и сообщения об ошибках под предпочитаемый игроком язык. Это не просто удобство, а конкурентное преимущество, отделяющее любительские скрипты от профессиональных. Хорошая новость в том, что реализация интернационализации (i18n) в FiveM проста, если понять схему.

Настройка системы локализации

Основа любой системы локализации , структурированный способ хранения и получения переведённых строк. Самый распространённый подход в FiveM , использование файлов локалей, по одному на язык, хранящихся в locales каталог внутри вашего ресурса. Каждый файл экспортирует таблицу пар ключ-значение, где ключ , уникальный идентификатор, а значение , переведенная строка. Вот как структурировать ваш модуль локали:

-- 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.',
}

Создание функции перевода

Ядро системы , это функция перевода, которая ищет ключи и поддерживает форматирование строк с переменными аргументами. Эта функция должна корректно переключаться на язык по умолчанию, если ключ отсутствует в активной локали, и предупреждать разработчиков о пропущенных переводах во время разработки, а не показывать игрокам сырые ключи.

-- 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

Использование переводов в ваших скриптах

После загрузки модуля локали использование переводов в вашем скрипте так же просто, как вызов L() функция с ключом и любыми аргументами формата. Это сохраняет код скрипта чистым и полностью отделяет контент от логики.

-- 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)

Определение языка для каждого игрока

По-настоящему профессиональная система локализации автоматически определяет язык каждого игрока. Это можно сделать, считав настройку языка игры клиента или позволив игрокам выбрать язык через конфиг или команду. Клиентский подход к определению использует GetCurrentLanguage native, который возвращает язык игры в виде двухбуквенного кода. Затем вы можете отправить это на сервер, чтобы все уведомления для этого игрока использовали предпочитаемый язык.

-- 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)

Хранение локали для каждого игрока на стороне сервера

На стороне сервера сохраняйте предпочтение локали каждого игрока, чтобы форматировать сообщения на правильном языке перед отправкой. Это критично для уведомлений, инициируемых сервером, и сообщений чата, которые исходят из серверной логики, где у вас нет прямого доступа к настройкам локали клиента.

-- 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

Локализация NUI и JavaScript интерфейсов

Многие скрипты FiveM используют NUI (HTML/JS) для своих пользовательских интерфейсов, и им тоже нужна локализация. Лучший подход , отправлять всю таблицу локализации в NUI-фрейм при инициализации, а затем использовать JavaScript-функцию перевода, которая зеркально повторяет Lua-функцию. Это избегает постоянных обратных вызовов NUI для каждой строки.

// 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);
    });
}

Конфигурация манифеста ресурса

Ваш fxmanifest.lua требуется включить все файлы локализации, чтобы они загружались при старте ресурса. Используйте шаблон glob, чтобы автоматически подхватывать любые новые файлы локализации без необходимости обновлять манифест каждый раз. Убедитесь, что общий модуль локализации загружается до файлов с данными локализации.

-- 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/**/*',
}

Лучшие практики локализации FiveM

  • Используйте описательные ключи вместо числовых ID. Ключи, такие как inventory_full самодокументируемы и облегчают обслуживание по сравнению с msg_042.
  • Всегда используйте плейсхолдеры формата (%s, %d) для динамических значений вместо конкатенации строк. В разных языках разный порядок слов, поэтому значения должны вставляться в разные позиции.
  • Включить комментарии контекста в ваших локализационных файлах, чтобы переводчики понимали, где отображается каждая строка и что означают аргументы формата.
  • Тест с длинными строками. Немецкий текст обычно на 30% длиннее английского. Убедитесь, что ваши элементы интерфейса выдерживают более длинные переводы без нарушения макета.
  • Никогда не хардкодьте строки, видимые пользователю. Каждое уведомление, метка меню, текст помощи и сообщение об ошибке должны проходить через L() функция, даже если вы изначально поддерживаете только один язык.
  • Предоставьте команду языка like /lang de чтобы игроки могли в любой момент переопределить автоматически определённый язык.

Готовы начать?

Возьмите скрипты в нашем магазине или заходите в Discord за поддержкой, обновлениями и анонсами.