
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.
This guide is part of our complete FiveM content creation guide, covering everything from MLO design to scripting, vehicle modding, and building your creator brand.
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://vertexmods.com/en/blog/translate-fivem-scripts-with-ai
- How To Create a FiveM Server — spin up a test bed for QA: https://vertexmods.com/en/blog/how-to-set-up-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
Stay in the Loop
Get the latest FiveM tutorials, mod releases, and exclusive updates delivered to your inbox.
No spam. Unsubscribe anytime.