fix(i18n): render Amharic in an Ethiopic font

Both themes set `fontFamily: Inter`, and Inter carries no Ge'ez glyphs. So
every Amharic string in a bilingual system — 45-73 KB of locale data per app —
has been rendering in whatever font the OS happened to substitute: different on
Windows, macOS and Android, and matching neither the design nor each other.

Noto Sans Ethiopic goes directly after Inter in one stack rather than being
swapped in under `[lang='am']`. Browsers fall back per glyph, not per element,
so a single stack renders Latin in Inter and Ge'ez in Noto automatically —
including within one string. That is the case that matters here: a registry is
full of mixed-script lines like an Amharic name beside a Latin IMO number, and
a language-scoped swap renders half of those in the wrong face.

Heading line-heights go up a step at the same time. Ge'ez has taller ascenders
and deeper descenders than Latin, so headings set to Inter's natural leading
clip once Ethiopic is actually being rendered. This was deliberately held back
from the theme refactor so that change could prove it altered nothing.

The gallery gains Ge'ez headings and a mixed-script line, so the baselines
cover the case rather than only Latin.

Follow-up: both fonts still load from Google Fonts via a render-blocking CSS
@import. Self-hosting with a subset and preload links is worth doing —
government networks frequently make fonts.googleapis.com slow or unreachable,
and silent Amharic fallback is precisely the failure this commit fixes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
fitse-yotor
2026-08-21 12:20:49 +03:00
parent b63e07305a
commit e74ef1c672
12 changed files with 30 additions and 16 deletions

View File

@@ -15,13 +15,18 @@ import { createTheme, rem } from '@mantine/core';
/**
* The type stack.
*
* Ge'ez support is added in the font stage, not here: Inter carries no Ethiopic
* glyphs today, so every Amharic string falls back to an arbitrary OS font.
* Inserting "Noto Sans Ethiopic" changes metrics, which would make this
* structural refactor look like a visual change.
* Noto Sans Ethiopic sits directly after Inter rather than being swapped in by
* a `[lang='am']` rule. Browsers fall back per *glyph*, not per element, so one
* stack renders Latin in Inter and Ge'ez in Noto automatically — including
* inside a single string. That matters here: a registry is full of mixed-script
* lines like an Amharic name beside a Latin IMO number, and a language-scoped
* swap would render one half of those in the wrong face.
*
* Inter carries no Ge'ez glyphs at all, so before this the Amharic half of a
* bilingual system rendered in whatever the OS happened to substitute.
*/
export const EMA_FONT_STACK =
'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
'Inter, "Noto Sans Ethiopic", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
export const baseTheme = createTheme({
fontFamily: EMA_FONT_STACK,
@@ -29,16 +34,16 @@ export const baseTheme = createTheme({
headings: {
fontFamily: EMA_FONT_STACK,
fontWeight: '700',
// Exactly the portal's original values. Ethiopic's taller ascenders may
// want a little more leading, but that is a typography change and belongs
// with the font work — not smuggled into a structural refactor whose whole
// claim is that it changes nothing for the portal.
// A little looser than the portal's original values. Ge'ez has taller
// ascenders and deeper descenders than Latin, so a heading set to Inter's
// natural leading clips its Amharic rendering — which only became visible
// once Ethiopic was actually being rendered rather than substituted.
sizes: {
h1: { fontSize: rem(32), lineHeight: '1.25' },
h2: { fontSize: rem(25), lineHeight: '1.3' },
h3: { fontSize: rem(21), lineHeight: '1.35' },
h4: { fontSize: rem(17), lineHeight: '1.4' },
h5: { fontSize: rem(15), lineHeight: '1.45' },
h1: { fontSize: rem(32), lineHeight: '1.3' },
h2: { fontSize: rem(25), lineHeight: '1.35' },
h3: { fontSize: rem(21), lineHeight: '1.4' },
h4: { fontSize: rem(17), lineHeight: '1.45' },
h5: { fontSize: rem(15), lineHeight: '1.5' },
},
},