FiveM script locales: translatable scripts with en.lua, ox_lib and ESX
Make your FiveM script translatable: a locales folder with en.lua and es.lua, a simple locale function, ox_lib's locale system, ESX TranslateCap and a language config.
Your script has 'You have no money' hard-coded in forty places. A server owner in Madrid or Berlin wants it in their language, and the only way is to edit the code. The fix is a locale layer: strings live in per-language files, the code asks for keys. This guide shows a simple plain-Lua version, then ox_lib's system and ESX's.
The idea
Instead of:
ESX.ShowNotification('You have no money')the code writes:
ESX.ShowNotification(L('no_money'))and a language file supplies the text for no_money. Translators then change files, never code, and open locale files can ship unencrypted (see escrow_ignore).
A plain Lua locale function
Folder layout:
my_script/
fxmanifest.lua
config.lua
locales/
en.lua
es.lua
shared/locale.lualocales/en.lua:
Locales = Locales or {}
Locales['en'] = {
no_money = 'You have no money',
bought = 'You bought %s for $%s',
}locales/es.lua:
Locales = Locales or {}
Locales['es'] = {
no_money = 'No tienes dinero',
bought = 'Compraste %s por $%s',
}shared/locale.lua, the function that looks a key up:
function L(key, ...)
local lang = Locales[Config.Locale] or Locales['en']
local text = lang[key] or Locales['en'][key] or key
if select('#', ...) > 0 then
return text:format(...)
end
return text
endManifest, with the locales first and the config before the function:
fx_version 'cerulean'
game 'gta5'
shared_scripts {
'config.lua',
'locales/*.lua',
'shared/locale.lua',
}
client_script 'client/main.lua'
server_script 'server/main.lua'And in config.lua:
Config = {}
Config.Locale = 'en'Usage, with %s placeholders filled by string.format:
print(L('bought', 'bread', 5)) -- You bought bread for $5The fallbacks matter: a missing key in es falls back to en, and a missing key everywhere returns the key itself, so you see no_money on screen instead of an error. A hole in a translation then shows up as a visible key, not a crash.
Tip: keep the same keys in every language file. A small script that compares the
enkeys with each other file catches missing translations before release.
ox_lib's locale system
If your script already depends on ox_lib, you can use its built-in locales instead of writing L(). The shape is: load ox_lib in the manifest, put your translations in a locales/ folder (ox_lib uses .json files named by language, like locales/en.json), include them with files, and call lib.locale() once:
-- fxmanifest.lua
shared_script '@ox_lib/init.lua'
files { 'locales/*.json' }{
"no_money": "You have no money",
"bought": "You bought %s for $%s"
}lib.locale() -- load the language
lib.notify({ description = locale('no_money'), type = 'error' })
print(locale('bought', 'bread', 5))Check the ox_lib docs for the current format and for how the language is chosen (a server convar), because file layout and option names have changed between versions. If you see ox_lib/init.lua not found, fix that first: ox_lib init.lua not found.
ESX and TranslateCap
ESX scripts have their own locale pattern. Locale files fill a Locales table and the framework provides a translation function:
-- fxmanifest.lua
shared_scripts {
'@es_extended/imports.lua',
'@es_extended/locale.lua',
'locales/*.lua',
'config.lua',
}-- locales/en.lua
Locales['en'] = {
['no_money'] = 'You have no money',
}TriggerEvent('esx:showNotification', TranslateCap('no_money'))Older scripts use _U('no_money') for the same thing. TranslateCap is the current name in recent ESX Legacy, and the language is selected by Config.Locale or the framework's own setting. As with ox_lib, check the docs of the ESX version you run for the exact file names and which of the two functions it expects.
QBCore has a similar Lang module (locales/en.lua using Lang:t('key')) that works the same way: a table per language and a function that resolves keys.
Choosing the language
Offer one config line, near the top of config.lua:
Config.Locale = 'en' -- 'en', 'es', 'fr'...If your script is a mix of client, server and NUI, remember that NUI text needs its own translations: send the strings to the page once when it opens, instead of writing English in the HTML. See FiveM NUI with React and Vite for passing data to the page.
Checklist
| Symptom | Fix |
|---|---|
| English text hard-coded in code | Move it to locales/en.lua and call L('key') |
attempt to index a nil value on Locales[...] |
Load locale files before the code that reads them |
| Raw key shows on screen | The key is missing in the chosen language and in en |
| Wrong language used | Check Config.Locale and the locale convar |
ox_lib locale() returns the key |
Call lib.locale() first and include locales/*.json in files |
| NUI still English | Pass the translated strings to the page |
Quick answers
Where should I put translations in a FiveM script?
In a locales/ folder with one file per language, such as en.lua and es.lua, loaded before the code that uses them. The script then asks for a key, never for a literal string.
Does ox_lib have a locale system?
Yes. It reads locale files and gives you locale('key') after you call lib.locale(). Check the ox_lib docs for the current file format and the setting that picks the language.
How do I choose the language?
With a Config.Locale value in your config, or with a convar your framework or ox_lib reads. Fall back to English when a key or a language is missing.
Scripts that skip this problem
Mic PhoneA foldable phone that unfolds into a tablet and carries onto a player's real phone.View script β
Shop CreatorBuild a shop in under a minute β owners, employees, vaults and robberies included.View script β
Item Creator V2Create usable items with animations, props, effects and more β without writing code.View script β