How To Translate Scripts Using AI (FiveM Guide)
Audience: FiveM server owners, scripters, and maintainers who want high‑quality translations without breaking placeholders or UI.
TL;DR
- Centralize all text in locale files (JSON or Lua tables). Never hard‑code strings in game logic.
- Protect placeholders (e.g.,
%s,%d,%{name},{0},~r~,^1) during translation. - Use AI for first‑pass translation + glossary + automated checks → quick human review → ship.
- Keep a single source of truth (usually English), diff for changes, and regenerate only changed keys.
Why translate your FiveM scripts
- Accessibility & growth: Localized servers attract more players and keep them engaged.
- Professionalism: Consistent terminology across commands, UIs, and error messages.
- Contributor‑friendly: Clear locale structure invites community PRs.
If you need a foundational refresher on structure and best practices, see: How To Translate FiveM Scripts (The Right Way).
Architecture: the right way to localize
Goal: Zero user‑visible strings inside gameplay code. Route everything through a locale layer.
Recommended resource layout
my_resource/ ├─ fxmanifest.lua ├─ locales/ │ ├─ en.json # source language (single source of truth) │ ├─ de.json # translated (generated/edited) │ ├─ es.json # translated (generated/edited) │ └─ qa.rules.json # optional: placeholder whitelist & checks ├─ client/ │ └─ main.lua ├─ server/ │ └─ main.lua └─ shared/ └─ i18n.lua # translation helper
fxmanifest.lua (minimal example)
fx_version 'cerulean'
game 'gta5'
lua54 'yes'
shared_scripts {
'shared/i18n.lua',
}
files {
'locales/*.json'
}
shared/i18n.lua (lightweight loader + placeholder substitution)
local LOCALE = GetConvar('my_locale', 'en')
local CACHE = {}
local function loadJSON(path)
local file = io.open(path, 'r')
if not file then return {} end
local content = file:read('*a')
file:close()
local ok, data = pcall(function() return json.decode(content) end)
return ok and data or {}
end
local function readLocale(lang)
if CACHE[lang] then return CACHE[lang] end
local file = ('locales/%s.json'):format(lang)
local dict = loadJSON(file)
CACHE[lang] = dict
return dict
end
local function interpolate(str, vars)
if not vars then return str end
for k, v in pairs(vars) do
str = str:gsub('%%{'..k..'}', tostring(v)) -- %{name}
end
return str
end
function _U(key, vars)
local dict = readLocale(LOCALE)
local src = dict[key]
if not src then
-- fallback to English if missing
src = readLocale('en')[key] or key
end
return interpolate(src, vars)
end
exports('Translate', _U)
Usage in client/server code
-- Client
lib.notify({
title = _U('notify_title'),
description = _U('welcome_player', { name = GetPlayerName(PlayerId()) }),
})
-- Server
print(('[MyRes] %s'):format(_U('server_started')))
locales/en.json (source)
{
"notify_title": "Server Message",
"welcome_player": "Welcome, %{name}!",
"server_started": "Server module is ready.",
"no_permission": "You do not have permission.",
"items_remaining": "%{count} items remaining"
}
AI translation workflow (fast and safe)
- Extract & freeze source
- Keep English (or your source) as
locales/en.json. - Enforce key naming:
domain.action.subject(e.g.,inventory.drop.confirm).
- Create/extend a glossary
- CSV or JSON map of canonical terms → target terms. Example:
source,target EMS,Rettungsdienst PD,Polizei Mechanic,Mechaniker
- Protect placeholders & markup
- Placeholders:
%{name},%s,%d,{0} - FiveM color codes:
~r~,~g~,~s~; chat codes:^1,^2 - NUI/HTML tags:
<b>,<span>…
- Translate via API (batch)
- Send values only, keep keys unchanged.
- Supply glossary and style (tone) to the model/engine.
- Automated QA
- Validate JSON.
- Verify placeholder parity (every placeholder in source exists in target).
- Flag forbidden changes (e.g., altered color codes or added punctuation when disallowed).
- Human spot‑check (5–10 minutes)
- Review commands, error messages, and long UI strings.
- Ship & iterate
- Keep a translation memory (previous outputs) to avoid re‑translating unchanged keys.
Guardrails: prompt & rules that actually work
LLM prompt for JSON batch translation
Task: Translate JSON values from English to <TARGET_LANGUAGE> for a FiveM/GTA RP context.
Rules:
- KEEP KEYS UNCHANGED.
- PRESERVE all placeholders exactly: %{var}, %s, %d, {0}, ~r~, ~g~, ^1, ^2, etc.
- Keep capitalization and code-style tokens (commands, /slash commands) unchanged.
- Do not add quotes, extra punctuation, or change meaning.
- Return ONLY valid JSON with the same structure.
JSON to translate:
<PASTE en.json CONTENT HERE>
Regex you can use in a QA script
- Placeholders:
%%\{[A-Za-z0-9_]+\} - C printf:
%(?:\d+\$)?[sdif] - Chat codes:
\^\d - Tilde color codes:
~[rgbso]~
Example: translate with DeepL (Node.js)
Works great for one‑off jobs or CI.
package.json (scripts)
{
"type": "module",
"scripts": {
"i18n:translate:de": "node tools/translate-deepl.js en de",
"i18n:check": "node tools/i18n-check.js"
}
}
tools/translate-deepl.js
import fs from 'fs';
import path from 'path';
import assert from 'assert';
import fetch from 'node-fetch';
const [,, srcLang, dstLang] = process.argv;
const apiKey = process.env.DEEPL_API_KEY; // set in CI/ENV
assert(apiKey, 'DEEPL_API_KEY is required');
const src = JSON.parse(fs.readFileSync('locales/en.json', 'utf8'));
const out = {};
const GLOSSARY = {
'EMS': 'Rettungsdienst',
'PD': 'Polizei',
};
function protect(str){
// Replace placeholders with tokens DeepL won't alter
return str
.replace(/%\{([^}]+)\}/g, '⟦$1⟧')
.replace(/%s/g, '⟪S⟫')
.replace(/%d/g, '⟪D⟫');
}
function restore(str){
return str
.replace(/⟦([^⟧]+)⟧/g, '%{$1}')
.replace(/⟪S⟫/g, '%s')
.replace(/⟪D⟫/g, '%d');
}
async function translate(text){
const res = await fetch('https://api.deepl.com/v2/translate', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
auth_key: apiKey,
text: text,
source_lang: srcLang.toUpperCase(),
target_lang: dstLang.toUpperCase(),
formality: 'prefer_more'
})
});
const json = await res.json();
if (!json.translations) throw new Error(JSON.stringify(json));
return json.translations[0].text;
}
for (const [k, v] of Object.entries(src)) {
const protectedText = protect(v);
// Glossary pre-pass (simple):
let glossed = protectedText;
for (const [from, to] of Object.entries(GLOSSARY)) {
glossed = glossed.replace(new RegExp(`\\b${from}\\b`, 'g'), to);
}
// Translate
// eslint-disable-next-line no-await-in-loop
const tr = await translate(glossed);
out[k] = restore(tr);
}
fs.writeFileSync(`locales/${dstLang}.json`, JSON.stringify(out, null, 2));
console.log(`Wrote locales/${dstLang}.json`);
tools/i18n-check.js (placeholder parity)
import fs from 'fs';
const src = JSON.parse(fs.readFileSync('locales/en.json', 'utf8'));
const dst = JSON.parse(fs.readFileSync('locales/de.json', 'utf8'));
const reVar = /%\{[^}]+\}/g;
const reS = /%s/g;
const reD = /%d/g;
let ok = true;
for (const k of Object.keys(src)) {
const a = (src[k].match(reVar)||[]).length === (dst[k]?.match(reVar)||[]).length;
const b = (src[k].match(reS)||[]).length === (dst[k]?.match(reS)||[]).length;
const c = (src[k].match(reD)||[]).length === (dst[k]?.match(reD)||[]).length;
if (!(a && b && c)) {
console.error('Placeholder mismatch for key:', k);
ok = false;
}
}
process.exit(ok ? 0 : 1);
Using LLMs (OpenAI/others) effectively
- Chunk by topic/domain for better context (e.g., inventory, police, jobs).
- Provide short descriptions per group (two lines) to define tone and audience.
- Few‑shot examples: 2–3 correctly translated pairs with placeholders improve consistency.
- Retry policy: re-run only failed keys flagged by
i18n-check.
Few‑shot template (system + user)
System: You translate FiveM game UI strings for <TARGET_LANGUAGE>.
- Keep keys unchanged, preserve placeholders, keep tone concise.
User examples:
EN: "You have %{count} fines."
DE: "Du hast %{count} Strafzettel."
EN: "~r~Error:~s~ You lack permission."
DE: "~r~Fehler:~s~ Dir fehlt die Berechtigung."
Now translate the following JSON values from English to <TARGET_LANGUAGE>. Return valid JSON only:
<PASTE JSON HERE>
NUI (HTML/JS) translations
For browser UIs, a client‑side library is practical.
Recommended approach
- Use a JSON bundle per language in
web/locales/<lang>.json. - Load with your UI framework and expose a
t(key, vars)helper. - Keep the same keys as server locales to reduce cognitive load.
Minimal JS helper
const dict = await (await fetch(`/locales/${lang}.json`)).json();
export function t(key, vars){
let s = dict[key] || key;
for (const [k,v] of Object.entries(vars||{})) s = s.replace(`%{${k}}`, v);
return s;
}
ESX/QBCore specifics
- Many ESX scripts ship
locales/en.lua,locales/de.luawith a_Uhelper. - If you use Lua tables for locales, keep one style across your repo. Mixing JSON and Lua for the same resource increases maintenance cost.
- QBCore often uses config‑driven messages. Migrate repeated strings to locale files to avoid divergence.
Lua table locale (if you prefer Lua over JSON)
Locales = Locales or {}
Locales['en'] = {
no_permission = 'You do not have permission.',
welcome_player = 'Welcome, %{name}!'
}
Locales['de'] = {
no_permission = 'Du hast keine Berechtigung.',
welcome_player = 'Willkommen, %{name}!'
}
Quality gates before you ship
- JSON/Lua parse check in CI.
- Placeholder parity (regex checks as shown).
- Forbidden changes: disallow edits to
/commands, keybind letters, color/chat codes. - Length deltas: flag +40% growth for UI buttons; may break layout.
- Smoke test: spin up your server and spot‑check critical flows.
New to setting up a server for testing? Follow this starter: How To Create a FiveM Server.
Maintenance strategy
- Treat
en.jsonas source of truth; create a CI job that diffsen.jsonand updates only changed keys in targets. - Keep a
CHANGELOG.i18n.mdfor translators. - Encourage community to contribute via PRs; document your style guide and glossary in
/docs/i18n.md.
Common pitfalls (and fixes)
- Broken placeholders → Use automated checks and protection tokens.
- Inconsistent terminology → Maintain a glossary and enforce it in prompts and pre‑processing.
- Mixed locales in code → Fail CI if strings are detected outside
locales/. - RTL languages → Ensure your NUI CSS sets
direction: rtl;and uses fonts with RTL support. - Casing & punctuation drift → Instruct AI explicitly, and run a linter to normalize punctuation.
External resources
- DeepL API — developer docs: https://www.deepl.com/docs-api
- Google Cloud Translation — docs & best practices: https://cloud.google.com/translate/docs
- FiveM Resource Manifest (fxmanifest.lua) — reference: https://docs.fivem.net/docs/scripting-reference/resource-manifest/resource-manifest/
Internal resources (related reading)
- How To Translate FiveM Scripts (The Right Way) — workflow & patterns: https://fivemx.com/fivem-scripts-translation/
- How To Create a FiveM Server — spin up a test bed for QA: https://fivemx.com/how-to-create-a-fivem-server/
Copy‑paste checklists
Pre‑translation
- All strings centralized in
locales/en.json(or Lua table) - Keys follow a naming convention
- Glossary prepared
- Placeholders audited
Run
- Batch translate with glossary
- Save output to
locales/<lang>.json
QA
- JSON/Lua valid
- Placeholder parity OK
- Forbidden tokens unchanged
- Length deltas acceptable
- Human spot‑check done
Ship
- CI green
- Changelog updated
- Invite community feedback






