You pressed Generate POT.
It skipped some of your game.

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.

    The short version

    What survives the trip from your project into the .pot, measured on Godot 4.7.stable.official.5b4e0cb0f:

    You wroteIn the .pot?
    tr("KEY"), atr(), tr_n(), with or without contextyes
    tr(SOME_CONST) where the const is a string literalyes
    tr("A" + "B")yes
    tr(some_variable), tr(dict["key"])no
    TranslationServer.translate("KEY")no
    label.text = "Hello" in GDScriptyes
    window.title = "Hello" in GDScriptno
    text / tooltip_text / placeholder_text / title / dialog_text / ok_button_text in a .tscnyes
    OptionButton / MenuButton / PopupMenu / ItemList item text in a .tscnyes
    TabBar tab titles (tab_0/title)no
    a TabContainer child's node nameyes — the node name becomes a msgid
    @export var s := "Hello", and its per-node override in a sceneno
    anything in a scene not itself listed in translations_pot_filesno
    anything in a .tres, .json, or any non-.gd/.tscn fileno
    your project nameyes — 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.

    The four ways a whole file disappears

    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'.

    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 subtree

    Setting 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.

    GDScript: the expressions the parser cannot follow

    The generator reads source text, so it only sees keys it can resolve statically.

    ConstructResult
    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.

    The assignments it does follow, including one it shouldn't

    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:

    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.

    Everything here is measured, not remembered

    This matters more than usual on this topic. Generate POT has no command-line entry pointgodot-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.

    What a linter can and cannot add

    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 generatorLocGuard
    scene not listed in translations_pot_filesnoyes — it scans the tree
    TabBar tab titlesnoyes
    .tres under a property nobody listsnono
    TranslationServer.translate("KEY")nono
    @export string set on a node in a scenenono
    tr(SOME_CONST)yesno — no constant resolution
    dialog_text / ok_button_textyesno
    label.text = "..." in GDScriptyesno
    TabContainer child node nameyesno
    tr("A" + "B")yes — folded keyreports A, which is the wrong key
    a string excluded via auto_translate_modecorrectly absentreported 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.

    Run it over the whole project, in CI

    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

    The full write-up, with the verification script

    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

    LocGuard CLI — free, MIT, no dependencies

    The linter over your translation table: missing keys, placeholder drift, unbalanced BBCode, empty translations, orphan keys, overflow budgets. github.com/leobaray/locguard

    LocGuard Pro — the in-editor dock

    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

    Different symptom?

    This page is about strings that never reached the translator. The two neighbouring failures have different causes and their own measured checklists:

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