Save 20% with WELCOMEView sales
How To Translate Scripts Using AI (FiveM Guide)

How to Translate FiveM Scripts Using AI

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)

  1. Extract & freeze source
  • Keep English (or your source) as locales/en.json.
  • Enforce key naming: domain.action.subject (e.g., inventory.drop.confirm).
  1. Create/extend a glossary
  • CSV or JSON map of canonical terms → target terms. Example:
source,target
EMS,Rettungsdienst
PD,Polizei
Mechanic,Mechaniker

  1. Protect placeholders & markup
  • Placeholders: %{name}, %s, %d, {0}
  • FiveM color codes: ~r~, ~g~, ~s~; chat codes: ^1, ^2
  • NUI/HTML tags: <b>, <span>
  1. Translate via API (batch)
  • Send values only, keep keys unchanged.
  • Supply glossary and style (tone) to the model/engine.
  1. 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).
  1. Human spot‑check (5–10 minutes)
  • Review commands, error messages, and long UI strings.
  1. 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.lua with a _U helper.
  • 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.json as source of truth; create a CI job that diffs en.json and updates only changed keys in targets.
  • Keep a CHANGELOG.i18n.md for 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


Internal resources (related reading)


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