Skip to content

Missing Glyph Checker — Localization Text vs Bitmap Font

Before a Korean or Japanese build ships boxes instead of letters: compare the text in your localization files with the glyphs a BMFont actually contains.

Boxes (□) or blank gaps where Korean or Japanese letters should be mean that the font has no glyph for those code points. A bitmap font contains only the characters baked into it, one char id= line per glyph in its .fnt. The checker pulls the translated text out of your .po (the msgstr values), .json, .csv/.tsv or .txt, compares every distinct character with the .fnt (or the font built in the UI Lab's Font stage), and lists each missing character with its Unicode code point, how often it is used and the first lines it appears on. It cannot read a TTF's character map yet.

Drop a UI panel, button, glyph sheet or UI sheet (PNG)
Open the UI Lab
Runs in your browser. Files are not uploaded.
UI Lab: a CC0 UI panel with nine-slice guides and previews at several sizes
The real UI Lab with a CC0 asset (Kenney).
Outputs
  • Nine-sliceMeasured· Pillow
  • Bitmap fontMeasured· independent .fnt parser
  • Button statesMeasured· Pillow
  • Engine importUNVERIFIED

Why letters turn into boxes

Every character is a Unicode code point, and a BMFont stores it in decimal: 시 is U+C2DC, which the .fnt writes as char id=49884. The check is plain set membership: the distinct characters of the text minus the char id values of the font. Scripts are large, which is why game fonts for Korean or Japanese are built from the strings the game actually uses: the precomposed Hangul syllables U+AC00–U+D7A3 are 11,172 code points, and the CJK Unified Ideographs block U+4E00–U+9FFF spans 20,992.

The usual causes are a translation updated after the font was baked; a font built from the source-language text; look-alike characters with different code points (full-width ! U+FF01 against ! U+0021); and decomposed Hangul, where 가 arrives as the jamo U+1100 U+1161 instead of the single syllable U+AC00. The checker compares code points exactly, so it reports all of these.

What is read from each file matters. From a .po only msgstr (and msgstr[n]) values are taken, never the msgid source keys, but the header entry's msgstr (Language:, Content-Type:) counts as text too. JSON contributes every string value, CSV and TSV every cell including the key and source columns. Whitespace, including the no-break space U+00A0 and the ideographic space U+3000, is skipped because it is not drawn as a glyph.

Example: a Korean .po against a 4-glyph .fnt

The font was baked from 시, 작, 끝 and 내. The report below is what the checker returns for this file:

ko.po (9 lines)                         font.fnt (4 glyphs)
msgid ""                                char id=49884   시 U+C2DC
msgstr ""                               char id=51089   작 U+C791
"Language: ko\n"                        char id=45149   끝 U+B05D
msgid "Start"  →  msgstr "시작"         char id=45236   내 U+B0B4
msgid "Quit"   →  msgstr "끝내기"

text checked   "Language: ko" / "시작" / "끝내기"   (msgid values are not read)
missing        기 U+AE30 ×1   (text line 5)
               a ×2, g ×2, :, L, e, k, n, o, u   (header entry, text line 2)
to add         기 = 0xAE30 = 44592 → the rebuilt font needs "char id=44592"

The Latin letters come from the header entry, not from a translation. Either add ASCII to the font (most games need digits and Latin letters anyway) or ignore those rows; the real gap here is 기.

Where the checked text comes from

FileWhat is readWhat is ignored
.pomsgstr and msgstr[n] values with their continuation lines (the header entry included)msgid, msgctxt, comments
.jsonEvery string value at any depthObject keys, numbers, booleans
.csv / .tsvEvery cell of every row, header row and key column includedSeparators and quotes
.txt or pasted textEverythingLine breaks and other whitespace
.fnt (glyph source)The id of every char lineKerning, pages, metrics

What it does

  • Reads .po (msgstr values), JSON (all string values), CSV/TSV (all cells) and plain text.
  • Compares with a .fnt's glyph list or the font made in the Lab's Font stage.
  • Every missing character with its count and the first lines it appears on.
  • Downloads missing-glyphs.json and a plain list to paste into a font tool.

How to do it

  1. Choose the glyph source (a .fnt or the Lab's font).
  2. Drop the localization file or paste text.
  3. Read the missing list.
  4. Export the report.

Check the result

  • After adding the characters and rebuilding the font, run the check again: it should say that nothing is missing and name how many distinct characters were checked.
  • Open missing-glyphs.json: every entry has char, codepoint as U+XXXX, count and lines; missing-glyphs.txt is the same characters on one line, ready to paste into a font tool's character field.
  • In the game, show one test string that contains every character of the language file: no box and no blank should remain.

Troubleshooting

SymptomLikely causeHow to checkFix
Latin letters (L, a, n, g…) are reported for a Korean or Japanese .poThe header entry's msgstr (Language:, Content-Type:) is read as textThe listed lines point at the first lines of the extracted textAdd ASCII to the font, or ignore those rows
The English source column of a CSV is reportedEvery CSV/TSV cell is read, including key and source columnsMost missing characters are Latin and appear on every lineExport only the target-language column, or paste its text instead
A character that looks present is reportedA look-alike with another code point (full-width !, ~, a different dash)Compare the U+ code in the report with the .fnt idsAdd that exact code point to the font, or normalise the text before export
Hangul syllables are reported although the font has themThe text is decomposed: jamo U+1100–U+11FF instead of syllablesThe report lists U+11xx codesNormalise the strings to NFC in your localisation pipeline
Nothing is missing but the game still shows boxesThe game draws with another font or weight than the checked .fnt, or the UI Lab font was built in font-file mode, where a character missing from the TTF is rendered by the browser's fallback font and baked inCheck the .fnt the build actually ships; look at the glyph sheet for letters in a different styleCheck against the shipped .fnt; in Godot a BMFont can list fallback fonts in its importer
The line numbers do not match the fileLines are counted in the extracted text, not in the original fileLine 2 of a .po report is the header textSearch the file for the character itself
A TTF or OTF cannot be chosen as the glyph sourceThe font's cmap table is not parsed in this release—Build a .fnt from the font first, then check against it

Outputs and how each was checked

"Measured" means the downloaded file was opened again outside the page (Pillow, numpy or an independent parser) and the property was measured. Loading these files in a game engine was not tested.

TargetFilesCheck
Nine-slicenine-slice.json · previews · SETUP.mdMeasured PillowEvery resized preview keeps all four corner blocks unscaled: the same bytes in Chromium; in Firefox semi-transparent pixels can shift by one alpha-rounding step. 0 gaps, 0 overlaps.
Bitmap fontfont.png · font.fnt (BMFont text) · font.jsonMeasured independent .fnt parserThe .fnt was read back by a parser written separately in the test: header and every glyph rect match.
Button statesstates/*.png · strip · states.jsonMeasured PillowNormal equals the source (byte for byte in Chromium; in Firefox semi-transparent colour within one alpha-rounding step); every rect in states.json cuts exactly its state out of the strip.
Engine importGodot NinePatchRect · Unity Sprite border · BMFont importUNVERIFIEDNot run in Godot or Unity.

Evidence

Nine-slice corners unscaled at 100×40, 300×80, 800×200 and 420×120; border suggestion 6·6·6·6 from a run of 12 identical columns; button states: normal equal to the source, disabled grey at alpha 128, focus ring exact; .fnt glyph records equal the JSON; missing glyphs from a .po listed exactly; black on white measured 21:1. Images pass through a browser canvas: Chromium returns the same bytes; Firefox rounds the colour of semi-transparent pixels to one premultiplied step (measured on a Kenney button: up to 4 levels at alpha 63), while alpha and opaque pixels stay exact.

Limits

  • No kerning pairs, one font page, no MSDF; the SDF output is Beta.
  • A TTF's cmap is not parsed, so the missing-glyph check needs a .fnt or the Lab's own font.
  • One atlas page; touching elements count as one.
  • Rendering inside Godot or Unity was not run: the engine numbers (patch margins, L-B-R-T border) are documented, not engine-tested.

Versions and sources

Measured in Chromium for docs/UI-LAB.md: a .po checked against a 4-glyph .fnt reported exactly a 시 작 끝 내 기 with the right counts and line numbers, and the present 가 was not reported. The worked example above was recomputed with the same functions; its header-entry rows are how the current parser behaves. The .fnt field meanings follow the AngelCode BMFont documentation, the .po structure the GNU gettext manual.

Official documentation used

Questions

Can I check against a TTF font?

Not yet: a TTF's cmap table is not parsed in this release, so use a .fnt or the Lab's own font.

Was it checked on real files?

A .po file against a 4-glyph .fnt reported exactly the 6 missing characters with the right counts and line numbers.

Is it free, and do I need an account?

Everything on these pages is free and works without an account. There is no watermark and no export that is held back.

Are my sprites uploaded?

No. The Studio decodes, packs and exports in this browser tab. Projects are autosaved in the browser (IndexedDB) and can be saved as a .nerulio file.

Does it need Aseprite or an engine plugin installed?

No. .aseprite files are read and written in the browser, and each engine bundle carries what that engine needs (for Godot, the import settings and scene; for Unity and the Godot TileSet, a small editor script).