It translates in the editor.
The export ships in English.

Nothing failed. The export dialog said success, the log said success, and the build your tester opened is in one language — or in raw keys. The editor loads your translation tables from disk; the export loads whatever the preset decided to pack, and a preset has three silent ways to leave them out. Paste your export_presets.cfg and project.godot below and this names which one is yours, per translation path. Runs in your browser, nothing is uploaded.

Both files are plain text in your project root. export_presets.cfg is often gitignored — it is still on the machine that builds.

60-second triage, if you would rather ask the build

The editor cannot answer this question — only the exported game can. Put this in your main scene, export, and run the build from a terminal:

func _ready() -> void:
	print("locales: ", TranslationServer.get_loaded_locales())
	print("active:  ", TranslationServer.get_locale())
	print("sample:  ", tr("MENU_START"))
What the exported build printsCause
locales: []X1 or X2 — the tables are not in the pack
locales listed, yours missing, text in EnglishX3 — a stale path in locale/translations
locales listed, raw keys on screennot an export problem — the key-instead-of-translation checklist
translates in the terminal, not on screennot an export problem — what actually froze

You do not need export templates to reproduce any of it. --export-pack writes a .pck without them, and the engine runs that pack the way a real build does:

godot --headless --path . --export-pack "Linux" /tmp/game.pck
godot --headless --main-pack /tmp/game.pck

X1 — the preset exports "selected resources", and nothing selects a translation

Symptom: get_loaded_locales() is empty in the build, raw keys everywhere, and the same project translates fine in the editor.

Cause: in the export dialog's Resources tab the mode is Export selected resources (and dependencies). Godot walks the dependency graph out of the scenes you ticked. Your .translation files are referenced by project.godot — by a setting, not by a scene — so nothing in that graph points at them and they are dropped. Measured with a preset whose only selected file is the main scene: the export reports success (E3a), no table is in the pack (E3b), the build loads zero locales (E3c) and tr("MENU_START") returns MENU_START (E3d).

Fix: go back to Export all resources in the project, or tick the tables themselves:

export_filter="all_resources"
# or, keeping the selective mode:
export_filter="resources"
export_files=PackedStringArray("res://main.tscn", "res://t.en.translation", "res://t.pt.translation")

This is the one that hits teams who switched to selective export to shrink a build, months before anyone noticed the languages were gone.

X2 — an exclude filter ate the translation files

Symptom: identical to X1 — zero locales, raw keys, success reported.

Cause: Filters to exclude files/folders from project matches your tables. *.translation is the obvious pattern; the realistic one is a folder pattern like localization/* added to keep source material out of the build, which also removes the compiled tables that live in that folder. Measured: the export succeeds (E4a), the tables are gone (E4b), the build prints the raw key (E4c).

Fix: exclude the sources by extension — *.csv, *.po, *.xlsx — and never a folder that also holds .translation files.

X3 — a path in locale/translations that no longer exists

Symptom: the sneakiest of the three, because nobody sees keys. The build shows correct English to a player who picked Portuguese. Testers report "the language button does nothing"; QA closes it as cosmetic.

Cause: project.godot lists a .translation path that is not there — the CSV was renamed, a locale column dropped, the file moved. The setting keeps the stale path. Measured: the export does not fail (E6a), the missing table is skipped while the others load (E6b), and tr() under that locale returns English, not the key (E6c).

The English comes from internationalization/locale/fallback, which defaults to en. The fallback is doing its job — which is exactly why the failure is invisible: a missing table looks like a translator who did not finish. To catch it in CI, assert the count you expect:

assert(TranslationServer.get_loaded_locales().size() == 4)

Three things the internet tells you that this harness measured as false

"CI has to run an import pass before exporting"

The claim is that --export-pack on a fresh clone — no .godot/, no .import sidecars, no .translation files, all correctly gitignored — produces a build with no translations, so you must run --editor --quit first. On 4.7 it does not: the export imports the project itself. The export succeeds (E5a), the tables are in the pack (E5c), the build translates (E5d).

Why the myth survives is itself measured (E5b): while importing, the log fills with

ERROR: Cannot open file 'res://t.en.translation'.
ERROR: Failed loading resource: res://t.en.translation.

— red lines naming the exact files you are worried about, printed before the importer builds them. If your CI log shows those and the build works, the lines are noise. Check the build, not the log.

"The CSV has to be in the export"

It is not in a working build and never was. The pack of a correct export holds t.en.translation, t.pt.translation, t.csv.import — and no t.csv (E2b, E2c). The CSV is an import source; the editor compiles it into one binary .translation per locale column, and those are what ship. If some code of yours reads res://translations.csv at runtime, it works in the editor and breaks only in the export (E2a vs E2b) — a real bug, a different one.

"Add *.csv to the include filter"

With exclude_filter="*.csv" the build still translates (E7a, E7b). With include_filter="*.csv" the CSV still does not reach the pack (E7c) — the include filter ships non-resource files, and a CSV that Godot imports as a translation is a resource, so the filter does not apply to it. The advice is not just unnecessary; it cannot do what it promises.

Where this leaves a linter

None of the three causes is in your translation table: X1 and X2 live in export_presets.cfg, X3 in project.godot. A table linter is structurally blind to all three, and the checker above exists precisely because that blind spot is real — it reads the two config files, not your strings.

What the linter is for is the job that runs right before the export: the table itself.

The full write-up, with the verification script

All 26 claims, the harness that exports a .pck and runs it, and the exact file list of a correct pack. docs/translations-missing-in-exported-build.md

LocGuard CLI — free, MIT, no dependencies

Missing keys, placeholder drift, unbalanced BBCode, empty translations, orphan keys. Non-zero exit, made for the CI step before the build: node cli.js /path/to/project --csv translations.csv. github.com/leobaray/locguard

LocGuard Pro — the in-editor dock

The same rules without leaving Godot: scan from a dock, double-click a finding to open the file at the line, ready-made CI presets. LocGuard Pro on itch.io

Different symptom?

Also from the studio

Blobsmith — Godot 4 autotile generator

Six tiles in, a wired 47-bitmask TileSet out. The other half-hour of Godot busywork nobody should be doing by hand. Blobsmith on itch.io · free Lite version · what it does

Measured on Godot 4.7.stable.official.5b4e0cb0f, 26 claims, 0 failures. Re-run them with docs/verify_export_translations.sh <godot-binary>; if a future engine build changes an answer, the script names the claim that moved.