Por que a localização é importante para servidores FiveM
As comunidades de RPG FiveM abrangem todo o mundo, com enormes bases de jogadores na Alemanha, França, Brasil, Turquia e dezenas de outros países. Se seus scripts suportam apenas o inglês, você está eliminando uma grande parte de clientes em potencial e limitando as comunidades que podem usar seus recursos. Um script devidamente localizado adapta todos os textos, notificações, menus e mensagens de erro voltados para o usuário ao idioma preferido do jogador. Este não é apenas um recurso de qualidade de vida, mas uma vantagem competitiva que separa os roteiros amadores dos profissionais. A boa notícia é que implementar a internacionalização (i18n) no FiveM é simples quando você entende o padrão.
Configurando o sistema local
A base de qualquer sistema de localização é uma forma estruturada de armazenar e recuperar strings traduzidas. A abordagem mais comum no FiveM é usar arquivos de localidade, um por idioma, armazenados em um diretório locales dentro do seu recurso. Cada arquivo exporta uma tabela de pares chave-valor onde a chave é um identificador exclusivo e o valor é a string traduzida. Veja como estruturar seu módulo de localidade:
-- 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.',
}
Construindo a Função de Tradução
O núcleo do sistema é uma função de tradução que procura chaves e suporta formatação de strings com argumentos variáveis. Esta função deve voltar normalmente para um idioma padrão quando uma chave estiver faltando na localidade ativa e deve alertar os desenvolvedores sobre traduções faltantes durante o desenvolvimento, em vez de mostrar chaves brutas aos jogadores.
-- 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
Usando traduções em seus scripts
Depois que o módulo locale for carregado, usar traduções em todo o seu script é tão simples quanto chamar a função L() com a chave e quaisquer argumentos de formato. Isso mantém o código do script limpo e separa totalmente o conteúdo da 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)
Detecção de idioma por jogador
Um sistema de localização verdadeiramente profissional detecta automaticamente o idioma de cada jogador. Você pode fazer isso lendo a configuração de idioma do jogo do cliente ou permitindo que os jogadores escolham seu idioma por meio de uma configuração ou comando. A abordagem de detecção do lado do cliente usa o nativo GetCurrentLanguage, que retorna a linguagem do jogo como um código de duas letras. Você pode então enviar isso ao servidor para que todas as notificações desse jogador usem o idioma de sua preferência.
-- 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)
Armazenamento local por jogador no servidor
No lado do servidor, armazene a preferência de localidade de cada jogador para que você possa formatar as mensagens no idioma correto antes de enviá-las. Isso é fundamental para notificações acionadas pelo servidor e mensagens de bate-papo originadas da lógica do lado do servidor, onde você não tem acesso direto à configuração de localidade do 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
Localizando interfaces NUI e JavaScript
Muitos scripts FiveM usam NUI (HTML/JS) para suas interfaces de usuário e também precisam de localização. A melhor abordagem é enviar toda a tabela de localidade para o quadro NUI quando ele for inicializado e, em seguida, usar uma função de tradução JavaScript que espelhe a de Lua. Isso evita retornos de chamada NUI constantes para cada string.
// 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);
});
}
Configuração do manifesto de recursos
Seu fxmanifest.lua precisa incluir todos os arquivos de localidade para que sejam carregados quando o recurso for iniciado. Use um padrão glob para selecionar automaticamente quaisquer novos arquivos de localidade adicionados sem precisar atualizar o manifesto todas as vezes. Certifique-se de que o módulo de localidade compartilhado seja carregado antes dos arquivos de dados de localidade.
-- 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/**/*',
}
Práticas recomendadas para localização FiveM
- Use chaves descritivas em vez de IDs numéricos. Chaves como
inventory_fullsão autodocumentadas e facilitam a manutenção do quemsg_042. - Sempre use marcadores de formato (
%s,%d) para valores dinâmicos em vez de concatenação de strings. Idiomas diferentes têm ordens de palavras diferentes, portanto, os valores precisam poder ser inseridos em posições diferentes. - Inclua comentários de contexto em seus arquivos de localidade para que os tradutores entendam onde cada string é exibida e o que os argumentos de formato representam.
- Teste com strings longas. O texto em alemão normalmente é 30% mais longo que o em inglês. Certifique-se de que os elementos da IU possam lidar com traduções mais longas sem interromper o layout.
- Nunca codifique strings voltadas para o usuário. Cada notificação, rótulo de menu, texto de ajuda e mensagem de erro devem passar pela função
L(), mesmo se você oferecer suporte inicialmente a apenas um idioma. - Forneça um comando de idioma como
/lang depara que os jogadores possam substituir o idioma detectado automaticamente a qualquer momento.
