The button reported no failure. The .pot was written. Your translator returned it complete, and part of the game still ships in English — because those strings were never in the template to begin with. Paste the .pot Godot generated and one scene or script below: this runs LocGuard's extractor in your browser and names the strings your project uses that the template does not contain. Below the tool is the full catalogue of what the generator takes and what it drops, each line measured against Godot 4.7 by pressing the button.
Nothing is uploaded — both boxes are read in this page. Start with the scene you suspect: the one whose text came back untranslated.
What survives the trip from your project into the .pot, measured on Godot 4.7.stable.official.5b4e0cb0f:
| You wrote | In the .pot? | |
|---|---|---|
tr("KEY"), atr(), tr_n(), with or without context | yes | |
tr(SOME_CONST) where the const is a string literal | yes | |
tr("A" + "B") | yes | |
tr(some_variable), tr(dict["key"]) | no | |
TranslationServer.translate("KEY") | no | |
label.text = "Hello" in GDScript | yes | |
window.title = "Hello" in GDScript | no | |
text / tooltip_text / placeholder_text / title / dialog_text / ok_button_text in a .tscn | yes | |
OptionButton / MenuButton / PopupMenu / ItemList item text in a .tscn | yes | |
TabBar tab titles (tab_0/title) | no | |
a TabContainer child's node name | yes — the node name becomes a msgid | |
@export var s := "Hello", and its per-node override in a scene | no | |
anything in a scene not itself listed in translations_pot_files | no | |
anything in a .tres, .json, or any non-.gd/.tscn file | no | |
| your project name | yes — always, with no source reference |
Several omissions people still cite have been fixed and are not worth working around any more: OptionButton and MenuButton popup items (godot#95160, godot#88017) do land in the template in 4.7. Others, like godot#73565 (.tres), open since February 2023, are exactly as broken as advertised.
These cost the most and are the hardest to notice, because the .pot still gets written and the button still reports success. The only trace is in the editor log, which nobody reads after a click that appeared to work:
ERROR: Cannot parse file 'res://data.tres': unrecognized file extension. Skipping. ERROR: Cannot parse file 'res://strings.json': unrecognized file extension. Skipping. ERROR: Cannot parse file 'res://sub': unrecognized file extension. Skipping. ERROR: Cannot open file 'res://ghost.tscn'.
.tres or any other resource file. Custom Resource scripts are where dialogue, item names and quest text usually live, and the generator refuses them by extension, before looking at the contents. Nothing you put in a .tres will be picked up..json, .csv, .txt or any other data file. Same rejection. The supported route is an EditorTranslationParserPlugin that teaches the editor your format. Until you write one, listing the file changes nothing except adding a line to the log.res://sub is refused with the same message. translations_pot_files is a list of individual files: adding a directory does not recurse into it, and adding a new scene to a folder you already listed does not add its strings.The consequence is the expensive part. The list is manual and silent in both directions: every new scene must be added by hand, and a scene instanced by a listed scene is not covered by it — only the property overrides written into the parent are. A sub-scene's own text values are absent unless that sub-scene is listed itself. To ask whether a scene contributed anything at all:
grep -c 'my_scene.tscn' generated.pot
auto_translate_mode silently removes a subtreeSetting auto_translate_mode = Disabled on a node — the documented way to stop one label from being translated — also removes it from the template, which is reasonable. What is easy to miss is that the mode is inherited: the default on every node is Inherit, so disabling it on a container, a panel, or the scene's root removes every descendant string in that scene from the .pot, with no warning. Measured: a root with auto_translate_mode = 2 produced a template containing none of its scene's strings; a child that opts back in with auto_translate_mode = 1 reappears.
The generator reads source text, so it only sees keys it can resolve statically.
| Construct | Result |
|---|---|
tr(some_var) | no — even when the variable is assigned a literal one line above |
tr(dict["key"]) | no — subscripts, godot#85848 |
tr(SOME_CONST) | yes — constants are resolved, to the value, not the identifier |
tr("A" + "B") | yes — folded to AB, which is the key the engine looks up too |
{"k": tr("KEY")} / [tr("KEY")] | yes — tr() inside dictionary and array literals is found |
tr("He said \"hi\"") | yes — escaped correctly (godot#80004 is fixed) |
And the one that costs real money: TranslationServer.translate("KEY") is not extracted. Nor is translate_plural(). This is the documented API for translating with an explicit locale or outside a Node, it takes a plain string literal the parser could trivially read, and every key that goes through it is missing from your template. If you have a localization singleton wrapping TranslationServer — a very common shape — none of your keys are in your .pot.
label.text = "Hello", .tooltip_text, .placeholder_text — assignments to known translatable property names are extracted, which is more than most people expect. Two inconsistencies fall out of that:
window.title = "Hello" is not extracted, although title = written in a .tscn is. Same property, same class, different answer depending on where you set it.some_dictionary["text"] = "Hello" is extracted. The match is on the property name, not on the type of what is assigned to, so an ordinary dictionary with a "text" key injects junk msgids for a translator to translate. Harmless, and the reason your .pot contains strings you cannot find anywhere on screen.Two smaller surprises, for completeness: a TabContainer child's node name becomes a msgid (that widget uses child names as tab labels, so renaming a node silently changes a translation key) — while a TabBar, whose titles live in tab_0/title, gets nothing: same widget to the player, opposite outcome. And your project name is always in the .pot, emitted with no #: source comment, because it is what the OS shows as the window title. Not a bug — just the one msgid every Godot template contains, and the one that confuses people diffing templates.
This matters more than usual on this topic. Generate POT has no command-line entry point — godot-proposals#10986 is still open — so almost every page written about it is quoting the issue tracker rather than a run, which is how fixed bugs stay in circulation for years.
So the harness presses the button: it boots the editor headless with a throwaway EditorPlugin that walks the editor's own control tree, finds Generate inside Project Settings → Localization → POT Generation, emits its pressed signal, and answers the EditorFileDialog that opens with a file_selected signal. The .pot that lands on disk is the same bytes a human gets from the same click, and every claim above is an assertion against those bytes and against the editor log:
git clone https://github.com/leobaray/locguard cd locguard node docs/verify_pot_generation.js /path/to/Godot_v4.7-stable_linux.x86_64 # 61/61 claims node docs/verify_pot_generation.js /path/to/Godot_v4.7-stable_linux.x86_64 --selftest # flips two expectations on purpose and requires the run to go red
The selftest is there because a harness that cannot fail is not evidence. Point the script at a newer engine build and it names the claim that moved: if 4.8 fixes .tres or breaks constant resolution, you will see which line changed rather than wondering whether this page aged.
The checker at the top of this page runs src/core.js — the same file, byte for byte, that the LocGuard CLI runs. It reads your project directly instead of a file list, so it covers some of the cases above and not others. This table is asserted by the same script, on the same probe project, so it cannot drift from the code:
| POT generator | LocGuard | |
|---|---|---|
scene not listed in translations_pot_files | no | yes — it scans the tree |
TabBar tab titles | no | yes |
.tres under a property nobody lists | no | no |
TranslationServer.translate("KEY") | no | no |
@export string set on a node in a scene | no | no |
tr(SOME_CONST) | yes | no — no constant resolution |
dialog_text / ok_button_text | yes | no |
label.text = "..." in GDScript | yes | no |
TabContainer child node name | yes | no |
tr("A" + "B") | yes — folded key | reports A, which is the wrong key |
a string excluded via auto_translate_mode | correctly absent | reported anyway |
Neither tool is a superset of the other, and the two rows at the bottom are LocGuard's own defects, listed here because the alternative is a table that flatters us. The honest summary: run Generate POT for the template, and use a linter for the classes of string the template structurally cannot hold — above all the .tres and unlisted-scene cases, which are the ones that ship.
The checker above reads one file because you pasted one file. The CLI walks every .gd, .cs and .tscn in a project, compares them against your translation table, and exits non-zero — so a scene added without its strings fails the build instead of failing a player:
node cli.js /path/to/your/godot/project --csv translations.csv
All 61 claims, the headless harness that presses the button, and the coverage table asserted against the linter's source. docs/pot-generation-what-it-misses.md
The linter over your translation table: missing keys, placeholder drift, unbalanced BBCode, empty translations, orphan keys, overflow budgets. github.com/leobaray/locguard
Everything above 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
This page is about strings that never reached the translator. The two neighbouring failures have different causes and their own measured checklists:
MENU_START instead of "Start"), in every language, from the first frame — the string is in your table and still comes out raw: Godot 4 shows the raw key instead of the translation — the complete checklist.label.text = tr("KEY") stores the translated value where the engine expects the key: what actually froze.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. Re-run the claims yourself with node docs/verify_pot_generation.js <godot-binary>; if a future engine build changes an answer, the script names the claim that moved.