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:

lua
ESX.ShowNotification('You have no money')

the code writes:

lua
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:

text
my_script/
  fxmanifest.lua
  config.lua
  locales/
    en.lua
    es.lua
  shared/locale.lua

locales/en.lua:

lua
Locales = Locales or {}

Locales['en'] = {
    no_money = 'You have no money',
    bought   = 'You bought %s for $%s',
}

locales/es.lua:

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:

lua
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
end

Manifest, with the locales first and the config before the function:

lua
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:

lua
Config = {}
Config.Locale = 'en'

Usage, with %s placeholders filled by string.format:

lua
print(L('bought', 'bread', 5))   -- You bought bread for $5

The 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 en keys 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:

lua
-- fxmanifest.lua
shared_script '@ox_lib/init.lua'
files { 'locales/*.json' }
json
{
  "no_money": "You have no money",
  "bought": "You bought %s for $%s"
}
lua
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:

lua
-- fxmanifest.lua
shared_scripts {
    '@es_extended/imports.lua',
    '@es_extended/locale.lua',
    'locales/*.lua',
    'config.lua',
}
lua
-- locales/en.lua
Locales['en'] = {
    ['no_money'] = 'You have no money',
}
lua
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:

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 β†’

Keep reading