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чтобы игроки могли в любой момент переопределить автоматически определённый язык.