The translation is not the problem. tr("MENU_START") returns 开始, the table is loaded, the locale is right. What is missing is a glyph — and Godot quietly went and found one on your computer, in a font file that is not inside your export. Paste your translation CSV below — or, if you have no translation at all, a scene or a script straight out of your project — and this lists every character that falls outside the set the engine actually guarantees. A project written entirely in English is already in this the day someone types Continue → into a Button. It runs the LocGuard scanner in your browser; nothing is uploaded.
Godot's built-in UI font is Open Sans SemiBold, and it is a subset. Asking the engine itself — ThemeDB.fallback_font.has_char(cp) for every codepoint in the BMP, the emoji planes and CJK Extension B — gives an exact number: 1010 codepoints, in 92 ranges (G1, Y1). That enumeration is the table this page scans against; it is generated by the verification script, not typed by hand.
| script | in the built-in font? | claim |
|---|---|---|
| printable ASCII 0x20–0x7E | yes, all of it | G2 |
| Latin-1 accents — é ã ç (fr/pt/es) | yes | G3 |
| Latin Extended-A — ł š ğ (pl/cs/tr) | yes | G4 |
| Vietnamese — ạ | yes | G5 |
| Cyrillic — Ж | yes | G6 |
| Greek — α | yes | G7 |
| Hebrew — א | yes | G8 |
| Arabic — ب | no | G9 |
| Chinese — 你 我 | no | G10 |
| Japanese kana — あ ア | no | G11 |
| Korean — 가 | no | G12 |
| Thai — ก | no | G13 |
| Devanagari — क | no | G14 |
| emoji — 😀 | no | G15 |
| arrows — ← ↑ → ↓ | no | G16 |
| — … • “ ” ’ € | yes | G17 |
| ✓ ★ ♥ ♪ | no | G18 |
Two of those rows are the reason this page exists. This is not a "non-Latin languages" problem. A project with no translation at all, whose entire UI is English, ships a hex box the moment a designer writes Continue → Next or OK ✓ (G16, G18). And the em dash, the ellipsis, the curly quotes and the euro sign are in the font (G17) — so "avoid non-ASCII" is not the rule, and guessing does not work. That is why the set is measured.
Because Godot borrows. Shaping 你好 with the built-in font still produces two real glyphs (S1) — and they carry a font_rid that is not the built-in font's (S2). OS.get_system_font_path_for_text() names where they came from (S3):
OS.get_system_font_path_for_text("Sans", "你好")
["/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc",
"/usr/share/fonts/opentype/unifont/unifont.otf"]
An absolute OS path, not a res:// path. Same for an emoji in a UI string (Y2). None of it travels inside your .pck. Your development machine — a full desktop with a font package installed — is the single worst place to test whether your player can read your game.
FontFile.allow_system_fallback = false reproduces it, and it is the closest thing to a player's machine you can run in CI:
S4), while an ASCII string is untouched (S5);S6);开始 that came back from tr() is 2 undrawable glyphs (T3). The data is right and the pixels are not.If you go looking for this in code, do not test for a zero glyph index — you will find nothing. Godot marks an undrawable codepoint by putting the codepoint itself in index and leaving font_rid invalid (S4b):
你好 | index=20320 (U+4F60) font_rid=RID(0) valid=false
That pair is the hex box on screen, and it is the only reliable way to detect this from inside the running game.
get_string_size() returns a non-zero width for text the font cannot draw (S8) — layout does not collapse, so nothing upstream trips.has_char() answers false for U+4F60 while shaping is drawing it from a borrowed font (S7). The project-side check and the screen disagree, and both are telling the truth about different things.tr() returns the translated string, not the key (T2) — so every "Godot prints the raw key" checklist, including ours, sends you looking in the wrong place.var body := FontFile.new()
body.load_dynamic_font("res://fonts/NotoSansSC-Regular.ttf")
var ui := ThemeDB.fallback_font.duplicate()
ui.allow_system_fallback = false # fail loudly here, not on a player's PC
ui.fallbacks = [body]
Measured: with system fallback off, a Font in fallbacks takes the arrow's undrawable glyph to zero (S10). Measured just as carefully: the same fallback does not fix Chinese, because DejaVu Sans has no CJK either (S11). A fallback only covers what it actually owns — so "add a fallback font" is not advice until you know which codepoints you are shipping. Which is what the tool at the top answers.
Every rule in LocGuard answers "is this translation table complete and consistent", and is fixed by editing the table. This one answers "can the engine draw it", and the fix is a font file. Different question, different artifact — and a check that would be wrong to fail a translator's build over. LocGuard Pro is the Godot 4 editor dock and CLI that catch the table-side failures — missing keys, empty translations, placeholder drift, unbalanced BBCode — before your players do: blobsmith.itch.io/locguard. The free CLI is locguard-lite.
git clone https://github.com/leobaray/locguard docs/verify_font_glyphs.sh /path/to/Godot_v4.7-stable_linux.x86_64 dump.json
Every claim id on this page is printed by that script. The optional second argument re-dumps the coverage ranges as JSON — that file is the source of the table this page scans against, so a Godot upgrade is a re-run and a diff, not a rewrite. Last run: 35 passed / 0 failed against 4.7.stable.official.5b4e0cb0f. The full write-up is docs/font-glyphs-missing-in-translations.md, and the scanner this page runs is docs/glyph-scan-core.js — byte for byte the file the command line uses.
node docs/scan_project_glyphs.js /path/to/your/godot/project node docs/scan_project_glyphs.js scenes/hud.tscn --json # exit 1 on findings node docs/scan_project_glyphs.js /path/to/project --all # sweep .csv tables too
Same table, same matching, every file at once — zero dependencies, reads only, writes nothing. It is deliberately narrow: a res:// path, a node path, a comment and a print() are skipped even when they hold the same character, because a report with noise in it is a report nobody acts on.
The claim above is easier to believe against code nobody here wrote, so that is where it was measured: godotengine/godot-demo-projects pinned at 34fc995 — 1028 scene and script files, 1806 strings a player can see. Five files come back, and honesty about what they are is the point of showing them — none of the five is the accidental Continue → this page opens with. Two are worth reading closely:
loading/runtime_save_load/runtime_save_load.tscn (scene, 10 strings)
153: U+2194 "↔" Arrows ×1 in text
"abcdefghijklmnopqrstuvwxyz\nABCDEFGHIJKLM\nNOPQRSTUVWXYZ\n12..."
→ NOT in the built-in font, even though the text around it is ASCII
2d/physics_platformer/tileset_edit.tscn (scene, 1 string)
152: U+0010 "\u0010" Control character ×14 in text
"This scene serves as a tool for editing the tileset.\n\u0010Nod..."
→ not a glyph at all - a stray control byte in the string; delete it
Neither is what you would guess. The first is a hidden font-sampler Label — alphabet, digits, ()[]{}<>, éàç, ×÷±≠ø and then ↔ — so the character is deliberate; what is accidental is that it sits on the last line of a multi-line property, five lines below the property name, where the first version of this scanner never looked. That one file is why it reads whole values instead of lines. The second is not a font problem at all: fourteen U+0010 control bytes inside a Label, which is a typo you delete, not a font you bundle — so the scanner says that instead of filing it under “bundle a font”.
The other three are the expected kind, and still worth naming: gui/bidi_and_font_features/bidi.tscn and gui/ui_mirroring/ui_mirroring.tscn carry Thai and Arabic, and 3d/labels_and_texts/3d_labels_and_texts.tscn uses U+E800–E80B — Private Use Area, i.e. an icon font, which is fine if that font is in the export and set on that control, and nothing at all otherwise. Total: 81 character occurrences across 5 files. So: in 1806 strings written by other people, this scanner found one real typo, one icon font, two correctly-translated scenes and one font sampler — and zero false alarms. The Continue → case is not rare because it is unlikely; it is absent here because these are engine demos, not shipped UI.
The number that matters more is the other one, 1023 files reported as silent. Every uncovered codepoint in all of them was checked again at the byte level, ignoring what the extractor chose to read, and exactly one survives: misc/os_test/os_test.gd passing 你好 to OS.get_system_font_path_for_text() — an argument to the font-probing API, not a string on screen. Correct silence. Both halves of that run are asserted by test/project-glyph.test.js, and re-running it is three commands, printed at the top of that file.
This scanner runs in your terminal too — it and eight others are one MIT zip with no dependencies: nine scanners for a Godot 4 project.