Files
2026-10-14 04:20:24 +02:00

7.4 KiB
Raw Permalink Blame History

International text

Text is drawn by Embedded React's software renderer. Glyphs come from two places:

  • Baked fonts. An imported TTF/OTF is rasterized at build time, at the sizes the app uses, into bitmap glyphs compiled into the executable. This is the fast path, and the only one for text the baked font fully covers.
  • Runtime fonts. Characters the baked font lacks, and scripts that need shaping (Devanagari, Bengali, Arabic), are drawn from subset Noto fonts the app packages with textFonts: HarfBuzz shapes them, SheenBidi orders right-to-left runs, and glyphs are rasterized on demand (native/shared/text_shaper.cpp). A string goes to this path only when it contains such a character or is laid out right to left; its baked characters keep their baked glyphs, so Latin text and digits inside a Japanese or Arabic sentence still use the app's font.

apps/text-lab shows a sample in each of the thirteen most spoken languages (Latin, Cyrillic, CJK, Devanagari, Bengali, Arabic and Urdu), CJK wrapping and truncation, and a right-to-left box (npm run dev -- --app text-lab).

Baked glyph sets

A baked font covers printable ASCII plus the codepoints its assets.config.js entry names. Text known at build time is checked by the font baker, which warns about missing glyphs. Text that arrives at runtime (catalog titles, user names) cannot be checked, so pick a named set that covers the languages you show:

glyphs Adds
'common' Typographic quotes, dashes, bullets, currency and math signs
'latin-ext' common, Latin-1 Supplement, Latin Extended-A, Ș Ț ẞ, „ ‚ ‹ ›, € ₹ ₺ ₽ №
'cyrillic' common, U+0400–U+045F, Ґ ґ, №
'european' latin-ext, cyrillic and modern Greek (tonos, final sigma)

'latin-ext' covers Spanish, French, German, Portuguese, Indonesian and the other Latin-alphabet European languages; Vietnamese is not included. 'european' adds about 470 glyphs to each baked size (about 65 KB at 24 px with the default 4 bits per pixel).

export default {
  fonts: {'Inter-Regular': {glyphs: 'european', extraGlyphs: '…'}},
};

A codepoint the baked font lacks draws as ?, unless a runtime font has it.

Runtime fonts

List the scripts an app shows in app.json:

"textFonts": ["chinese", "japanese", "devanagari", "bengali", "arabic"]
Script Languages Font (subset) Characters Package
chinese Chinese (Simplified) Noto Sans SC GB 2312: 6,763 hanzi, CJK punctuation, full-width forms 1.6 MB
japanese Japanese Noto Sans JP JIS X 0208: kana, 6,355 kanji (levels 1 and 2), punctuation 1.7 MB
devanagari Hindi, Marathi, Nepali Noto Sans Devanagari Devanagari, Devanagari Extended, Vedic Extensions 0.16 MB
bengali Bengali, Assamese Noto Sans Bengali Bengali 0.09 MB
arabic Arabic, Urdu, Persian Noto Sans Arabic Arabic, Supplement, Extended-A, presentation forms 0.11 MB

All five add 3.7 MB to the package. Urdu is drawn in Noto Sans Arabic's Naskh style, not Nastaliq.

The build pins each font in dependencies.lock.json, subsets it with HarfBuzz (tools/font_subset.cpp, cached in .deps/fonts/subset/) and packages it in fonts/ with its OFL notice in notices/Noto-fonts-LICENSE.txt. The desktop preview reads the same files from .build/<app>/generated/fonts/. Han characters outside one font fall back to the other when both are listed, so a Japanese UI that lists both also draws kanji beyond JIS X 0208.

Kana always uses the Japanese font. Han characters shared by both use the Chinese forms unless the language is Japanese: the shaper starts with the system language (DeviceInfo.get().language) and Fonts.setLanguage(tag) overrides it (see Native API).

Memory

Nothing is loaded until a string needs a runtime font. Then the font file is read whole and kept (the package sizes above). Rasterized glyphs live in one least-recently-used cache capped at 4 MiB, about 7,000 CJK glyphs at 24 px; the 128 most recent shaped strings are kept as well (about 1 KB each for a label). An app showing every script therefore holds at most about 8 MB for text (3.7 MB of fonts, 4 MiB of glyphs, up to 0.2 MB of layouts), from the title heap, never the JavaScript heap. HarfBuzz and SheenBidi add about 0.8 MB of code to the executable.

Line breaking

Runtime text breaks where the bitmap path does (newlines, spaces, inside a word only when nothing else fits) and also between any two CJK characters, following the basic kinsoku rules: closing punctuation (。、,」』)), small kana, ー and iteration marks never start a line, and opening brackets (「『() never end one. Devanagari, Bengali, Arabic and Urdu break only at spaces (and after a zero-width space, U+200B). A word wider than its line breaks between grapheme clusters: a letter keeps its marks and a conjunct (consonant, virama, consonant) stays whole. numberOfLines truncates with … (from the baked font, or ... when it lacks one), after whole clusters. tools/test_text.py checks these layouts with text-lab's fonts.

Shaping

Devanagari and Bengali conjuncts, reordered vowel signs and reph, and Arabic joining forms and ligatures (lam-alef) come from the fonts' OpenType tables through HarfBuzz's shapers. Their marks reach further below the baseline than Latin, so a string containing them gets a taller line box: about 14% for Devanagari and Bengali and 28% for Arabic over Inter's (CJK keeps Inter's). Joined scripts take no letterSpacing, and faux bold does not widen them, so their joins stay intact. Each node's text is limited to 255 bytes (ER_TEXT_MAX): about 85 CJK or Devanagari characters. Each styled segment of a nested Text is limited to 63 bytes (ER_SPAN_TEXT_MAX, about 21 such characters), at most four segments. Longer text is cut at the last whole character, never inside a UTF-8 sequence or between a letter and its marks or conjunct.

Right-to-left text

Text containing Arabic or Hebrew letters is ordered by the Unicode bidirectional algorithm (SheenBidi), one paragraph per line feed: numbers and Latin names inside Arabic stay left to right (PS5 تحميل 24.2 GB), brackets are mirrored, and lines break in logical order before each line is reordered.

Style On Values Effect
direction any node, inherited 'inherit' (default), 'ltr', 'rtl' A right-to-left row lays its children out from the right edge and a column starts its cross axis at the right, as in Yoga. Text below it is right to left.
writingDirection Text 'auto' (default), 'ltr', 'rtl' The paragraph's base direction; 'auto' takes the inherited direction, else the first strong character.
textAlign Text 'auto'/'start' (default), 'end', 'left', 'right', 'center' start and end follow the paragraph direction; left and right do not.

An Arabic or Urdu interface sets direction: 'rtl' once on its root view. A truncated right-to-left line keeps its start at the right and puts … at its left end. The text-start and text-end utilities follow the direction.

Limits: a horizontal ScrollView keeps left-to-right order (reverse its data for right-to-left); margins, padding and left/right insets stay physical (there are no marginStart/paddingEnd styles); focus moves by laid-out position, so the D-pad follows the mirrored layout.