Save 20% today Use code WELCOME at checkout. WELCOME

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)

  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
Luke
Luke

I'm Luke, I am a gamer and love to write about FiveM, GTA, and roleplay. I run a roleplay community and have about 10 years of experience in administering servers.

Articles: 570