Localización e i18n en FiveM: scripts multilingües
Crea scripts de FiveM en varios idiomas. Archivos de idioma, claves alternativas y herramientas usadas por ox_lib y otros recursos de código abierto.
Agency Scripts
Fundador y desarrollador principal en Agency Scripts
Por qué es importante la localización en los servidores de FiveM
Las comunidades de roleplay de FiveM están repartidas por todo el mundo, con enormes bases de jugadores en Alemania, Francia, Brasil, Turquía y decenas de otros países. Si tus scripts solo admiten inglés, estás dejando fuera a una gran parte de los posibles clientes y limitando las comunidades que pueden usar tus recursos. Un script bien localizado adapta todos los textos visibles para el usuario, notificaciones, menús y mensajes de error al idioma que prefiera el jugador. No es solo una mejora de calidad de vida, sino una ventaja competitiva que distingue los scripts aficionados de los profesionales. La buena noticia es que implementar la internacionalización (i18n) en FiveM es sencillo una vez que entiendes el patrón.
Configuración del sistema de idiomas
La base de cualquier sistema de localización es una forma estructurada de almacenar y recuperar cadenas traducidas. El enfoque más común en FiveM es usar archivos de idioma, uno por idioma, almacenados en un directorio locales dentro de tu recurso. Cada archivo exporta una tabla de pares clave-valor, donde la clave es un identificador único y el valor es la cadena traducida. Aquí tienes cómo estructurar tu módulo de idioma:
-- 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.',
}
Creación de la función de traducción
El núcleo del sistema es una función de traducción que busca claves y admite el formato de cadenas con argumentos variables. Esta función debería recurrir al idioma predeterminado si falta una clave en el idioma activo y advertir a los desarrolladores sobre las traducciones que faltan durante el desarrollo, en lugar de mostrar claves sin procesar a los jugadores.
-- 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
Uso de traducciones en tus scripts
Una vez cargado el módulo de idioma, usar traducciones en todo tu script es tan sencillo como llamar a la L() función con la clave y los argumentos de formato necesarios. Esto mantiene limpio el código de tu script y separa por completo el contenido de la lógica.
-- 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)
Detección del idioma por jugador
Un sistema de localización realmente profesional detecta automáticamente el idioma de cada jugador. Puedes hacerlo leyendo la configuración del idioma del juego del cliente o permitiendo que los jugadores elijan su idioma mediante una configuración o un comando. El método de detección del lado del cliente usa el GetCurrentLanguage nativo, que devuelve el idioma del juego como un código de dos letras. Luego puedes enviarlo al servidor para que todas las notificaciones de ese jugador usen su idioma preferido.
-- 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)
Almacenamiento del idioma de cada jugador en el servidor
En el servidor, guarda la preferencia de idioma de cada jugador para poder dar formato a los mensajes en el idioma correcto antes de enviarlos. Esto es fundamental para las notificaciones activadas por el servidor y los mensajes de chat que se originan en la lógica del servidor, donde no tienes acceso directo a la configuración de idioma del cliente.
-- 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
Localización de NUI e interfaces JavaScript
Muchos scripts de FiveM usan NUI (HTML/JS) para sus interfaces de usuario, y estas también necesitan localización. El mejor método es enviar la tabla de idioma completa al marco NUI cuando se inicializa y luego usar una función de traducción en JavaScript que replique la de Lua. Así evitas tener que hacer llamadas NUI constantes de ida y vuelta para cada texto.
// 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);
});
}
Configuración del manifiesto del recurso
Tu fxmanifest.lua debe incluir todos los archivos de idioma para que se carguen cuando se inicie el recurso. Usa un patrón glob para detectar automáticamente los nuevos archivos de idioma que añadas, sin tener que actualizar el manifiesto cada vez. Asegúrate de que el módulo de idioma compartido se cargue antes que los archivos de datos de idioma.
-- 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/**/*',
}
Buenas prácticas para la localización en FiveM
- Usa claves descriptivas en lugar de ID numéricos. Claves como
inventory_fullse explican por sí mismas y facilitan el mantenimiento más quemsg_042. - Usa siempre marcadores de formato (
%s,%d) para valores dinámicos en lugar de concatenar cadenas. El orden de las palabras cambia según el idioma, por lo que los valores deben poder insertarse en distintas posiciones. - Incluye comentarios de contexto en tus archivos de idioma para que los traductores entiendan dónde se muestra cada texto y qué representan los argumentos de formato.
- Prueba con textos largos. Los textos en alemán suelen ser un 30% más largos que los textos en inglés. Asegúrate de que los elementos de tu interfaz puedan mostrar traducciones más largas sin que se descuadre el diseño.
- Nunca incluyas textos visibles para el usuario directamente en el código. Todas las notificaciones, etiquetas de menú, textos de ayuda y mensajes de error deben pasar por la
L()función, aunque al principio solo admitas un idioma. - Ofrece un comando para cambiar el idioma como
/lang depara que los jugadores puedan cambiar el idioma detectado automáticamente en cualquier momento.