Skip to content

Compressed translations: escape rare characters instead of 16-bit tables - #11373

Open
dhalbert wants to merge 4 commits into
adafruit:translation-shrinkfrom
dhalbert:ja-ko-escapes
Open

Compressed translations: escape rare characters instead of 16-bit tables#11373
dhalbert wants to merge 4 commits into
adafruit:translation-shrinkfrom
dhalbert:ja-ko-escapes

Conversation

@dhalbert

Copy link
Copy Markdown
Collaborator

Claude wrote the code and did the measurements in this PR, with @dhalbert directing and reviewing each step.

Stacked on #11369; targets translation-shrink. Until #11369 merges, the diff shows both commits.

Motivation

ja and ko have more than 127 distinct non-ASCII characters, so #11369 leaves them on the old 16-bit table mode: every entry of values[] and words[] costs two bytes, for the sake of a few hundred kanji and hangul that are mostly used once or twice. Their text is 77% and 89% ASCII, which the 8-bit scheme compresses well.

What changes

  • py/maketranslationdata.py: only the most frequent non-ASCII characters get dense 8-bit codes. For languages that overflow, the generator tries 32, 48, 64 and 80 dense characters and keeps the smallest result; ja picks 48 and ko 32. Languages whose whole alphabet fits are unchanged.
  • The remaining characters are escaped. Symbol 2 is followed by a fixed-width index into a small uint16_t rare_chars[] for characters used more than once. Symbol 3 is followed by a raw 16-bit code point for characters used once; those are cheaper without a table entry. Dictionary words cannot contain escaped characters.
  • The 16-bit table mode and translation_requires_uint16 are removed, and mchar_t is replaced by uint8_t.
  • supervisor/shared/translate/translate.c: two branches next to the existing qstr escape. Each is guarded by a compile-time count of escaped characters, so for a language with none the compiler removes the branch and the decoder is the same size as before.
  • supervisor/shared/translate/compressed_string.h: format comment updated.

Results

Bytes free in flash on metro_m4_express, same commit, #11369 versus this PR:

Language Before After Gain
ja 7,368 8,076 +708
ko 7,600 8,420 +820
fr 2,816 2,816 0

fr and en_US generate byte-identical output to #11369. The generator takes about 4 seconds longer for ja and ko because of the four dense-alphabet tries; other languages are unaffected.

Testing

  • Unix coverage port: 979 tests pass.
  • Metro M4 Express in ja: locale.getlocale(), ValueError, %q-formatted AttributeError, the errno text path, SyntaxError, and messages containing kanji, kana and fullwidth punctuation all decode correctly at the REPL.

🤖 Generated with Claude Code

…timal parsing

Rework the translation compressor in `py/maketranslationdata.py` and the
decoder in `translate.c`:

- Renumber a language's non-ASCII characters into 0x80.. by frequency and
  map them back through `alphabet[]`, so every symbol fits in 8 bits and
  the dictionary gets the remaining code points. Languages with more than
  127 distinct non-ASCII characters (ja, ko) keep the 16-bit fallback.
- Choose Huffman codes per class of the previously decoded byte (start or
  space, letter, other, ...) through `class_map[]`; the generator tries
  several class presets and keeps the smallest result.
- Tokenize each string by dynamic programming for the fewest bits instead
  of greedy longest match, deciding per occurrence whether a qstr
  reference pays off. Dictionary words left unused are dropped.

The format is documented in `compressed_string.h`. Same-commit gains on
the tightest languages: metro_m4_express fr +1,208 bytes free, pl +1,160;
metro_m0_express pl +672. The decoder grows 76-108 bytes. Verified on the
unix coverage port (979 tests) and on a Metro M4 Express in fr and en_US.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@dhalbert
dhalbert requested a review from tannewt September 14, 2026 00:30
Add `translation_class_t` and `translation_symbol_t` enums to
`compressed_string.h` and use the names in `base_class()`, the decode
loop, and the matching Python constants in `maketranslationdata.py`,
instead of bare 0..6 and 1. No change to generated data or firmware.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
dhalbert and others added 2 commits September 13, 2026 23:24
Write each preset as the list of Huffman tables to build, each naming the
base classes that share it, and derive `class_map[]` with `class_map_for()`
instead of spelling out index tuples. Generated output is unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Translations with more than 127 distinct non-ASCII characters (ja, ko) used
`uint16_t` for every `values[]` and `words[]` entry, doubling their tables
for the sake of a few hundred kanji and hangul, most used once or twice.

Keep only the most frequent non-ASCII characters in the dense 8-bit
alphabet (the generator tries 32, 48, 64 and 80 and keeps the smallest
result) and escape the rest: symbol 2 followed by an index into a small
`uint16_t rare_chars[]` for characters used more than once, symbol 3
followed by a raw 16-bit code point for characters used once. Dictionary
words cannot contain escaped characters. The 16-bit table mode and
`translation_requires_uint16` are gone; `mchar_t` is replaced by
`uint8_t`.

Languages whose alphabet already fit are byte-identical to before, and the
two decoder branches fold away for them. On metro_m4_express: ja +708
bytes free, ko +820, fr unchanged. Verified on the unix coverage port
(979 tests) and on a Metro M4 Express in ja.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant