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 and this lists every character that falls outside the set the engine actually guarantees. 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.