Volver al blog
Tutorial7 min de lectura

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_full se explican por sí mismas y facilitan el mantenimiento más que msg_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 de para que los jugadores puedan cambiar el idioma detectado automáticamente en cualquier momento.

¿Listo para empezar?

Consigue los scripts en nuestra tienda, o únete al Discord para soporte, novedades y un adelanto de lo que viene.