Zum Artikel springen

Typografie Veröffentlicht Lesezeit 9 Min.

Warum deine arabische Schrift deinen englischen Text setzt

Die Datei enthält fast sicher Latein — Zain trägt 166 Codepoints zwischen U+0041 und U+024F, Cairo 291 — und Font-Matching läuft pro Codepoint, also gewinnt die erste Familie im Stack, die die Glyphe hat. Diese Website hat diesen Fehler und sein genaues Gegenteil gleichzeitig ausgeliefert, in drei Sprachen.

Kurz gesagt

Weil unicode-range kein Subsetter ist und Font-Matching pro Codepoint läuft. Wenn die arabische Datei lateinische Glyphen enthält — Zain liefert 166 zwischen U+0041 und U+024F, Cairo 291, und alle drei arabischen Familien, die diese Website je genutzt hat, tragen den vollen druckbaren ASCII-Satz aus 95 Codepoints —, dann gewinnt jede Regel, die diese Familie an den Anfang des Stacks stellt, auch jedes lateinische Zeichen. Eine unicode-range auf den arabischen Faces behebt das: Die Datei ändert sich nicht, nur die Menge der Codepoints, für die sie antworten darf. Der umgekehrte Fehler hat dieselbe Ursache. Beziehe jede arabische Regel auf das Dokumentelement, und das eine arabische Wort in einer englischen Seite bekommt gar kein arabisches Face mehr, sondern fällt auf das System-Naskh durch oder auf Tofu.

Was du mitnehmen solltest

  1. unicode-range beantwortet eine einzige Frage — darf dieses Face diesen Codepoint bedienen — und zwar pro Zeichen, nachdem das Face bereits über Gewicht und Stil gewählt wurde.
  2. Zähle die lateinische Abdeckung deiner arabischen Schrift, bevor du behauptest, sie habe keine: Zain hat 166 Codepoints zwischen U+0041 und U+024F, Cairo 291, und beide tragen den vollen druckbaren ASCII-Satz.
  3. Eine Familie einzuschränken schafft eine neue Klasse von Codepoints, die sie nicht mehr setzen darf, angefangen beim Leerzeichen zwischen zwei Wörtern — jeder Stack, der sie nennt, braucht also einen Fallback dahinter.
  4. Einem arabischen Face deinen lateinischen Familiennamen zu geben funktioniert nur, wenn alle anderen Deskriptoren übereinstimmen, denn genau das ist die Bedingung der Spezifikation dafür, beide als ein Composite Face zu behandeln.
  5. Eine Regel, die auf dem Dokumentelement sitzt, ist eine Aussage über die Seite — und ein Sprachumschalter ist per Definition ein Run, dessen Sprache von der der Seite abweicht.
  6. Prüfe mit CSS.getPlatformFontsForNode oder dem Block Rendered Fonts, niemals mit dem Computed Style: der meldet den Stack, den du geschrieben hast, nicht das Face, das gesetzt hat.

arabic.css:95 · Zain 166 · Cairo 291

Warum setzt meine arabische Schrift meinen englischen Text?

Weil die Schriftdatei nicht das ist, was ihr Name nahelegt. Öffne irgendeine arabische Webschrift mit fontTools und zähle die Codepoints, die sie zwischen U+0041 und U+024F deklariert: Zain, das Face, das diese Website heute ausliefert, hat 166; Cairo, das Face davor, hat 291; Noto Kufi Arabic hat 248. Alle drei tragen obendrein den vollständigen druckbaren ASCII-Satz aus 95 Codepoints. Eine arabische Schrift ganz ohne Latein ist die Ausnahme, nicht die Regel.

Den Rest erledigt das Font-Matching, und zwar pro Zeichen statt pro Run. Für jeden Codepoint geht der Browser die font-family-Liste durch und hält bei der ersten Familie an, die eine Glyphe dafür hat — nirgends im Algorithmus wird gefragt, zu welchem Schriftsystem das Zeichen gehört. Stell eine arabische Familie in einer pauschalen Regel nach vorn, und sie gewinnt auch das Lateinische, lautlos, weil das Ergebnis lesbar und nur ein bisschen falsch ist.

Dieses Repository hat jahrelang das Gegenteil behauptet. Der Kopf von src/styles/locale/arabic.css nannte den Grund dafür, Cairo in jedem Stack nach vorn zu stellen: Die Dateien „tragen nur arabische Glyphen — Latein fällt auf Quicksand durch, und die eigene Identität der Site hält für Namen und Code.“ Gemessen war das falsch. Jeder Projektname, jeder HTML- und CSS-Chip und die Zeichenkette Google AI Essentials auf den arabischen Seiten wurden in Cairo gesetzt, 381 lateinische Runs, über die gesamte Lebensdauer des Builds.

Abdeckung mit fontTools 4.60.2 über die ausgelieferten woff2-Dateien gezählt, 26. August 2026. Cairo und Noto Kufi wurden aus der Git-Historie gelesen; der Wechsel zu Zain hat sie aus dem Arbeitsverzeichnis entfernt.
Schrift Druckbares ASCII, U+0020 bis U+007E Arabisch, U+0600 bis U+06FF Latein, U+0041 bis U+024F
Zain Regular, Bold, ExtraBold 95 109 166
Cairo Regular, SemiBold 95 102 291
Noto Kufi Arabic Bold 95 256 248
Quicksand Regular 95 0 357

Die letzte Zeile der Tabelle ist gleich doppelt wichtig. Quicksand trägt null arabische Codepoints, und deshalb hat ein arabischer Run, dessen Stack keine arabische Familie nennt, nirgendwohin auszuweichen — das ist die zweite Hälfte dieses Artikels, und dieselbe Website hat sie zur selben Zeit ausgeliefert, in der Gegenrichtung.

CSS Fonts 4 §4.5 · initial U+0-10FFFF

unicode-range ist nicht die Datei, nicht die Familie und nicht das Dokument

Der Deskriptor beantwortet genau eine Frage — darf dieses Face für diesen Codepoint verwendet werden — und CSS Fonts 4 gibt ihm in einem einzigen Absatz zwei Aufgaben. Die Vereinigung der deklarierten Bereiche ist „ein Hinweis für User Agents bei der Entscheidung, ob eine Schriftressource für einen gegebenen Textrun geladen wird“, und dann, deutlich härter: „User Agents dürfen die Schrift für Codepoints außerhalb dieser Menge weder laden noch verwenden.“ Der Initialwert ist U+0-10FFFF, ein Face ohne deklarierte Range darf also für ganz Unicode antworten — genau das taten hier bis August alle Faces.

Der Inhalt der Datei ist es nicht, und die Spezifikation sagt das ausdrücklich: „Die effektive Character Map ist die Schnittmenge der von unicode-range definierten Codepoints mit der Character Map der Schrift.“ Die vier Zain-Faces hier deklarieren 1.264 Codepoints und decken 252 davon ab, 19,9 Prozent. Drei der deklarierten Blöcke sind in der Datei vollständig leer, und U+200F, die Rechts-nach-links-Marke, ebenfalls. Nichts geht kaputt, denn die Schnittmenge ist die Regel, und die Spezifikation segnet es ab, breite Bereiche zu deklarieren, ohne die Schrift zu prüfen.

Subsetting ist es auch nicht, und diese Verwechslung kostet mehr als einen Vokabelpunkt. Subsetting ist ein Build-Schritt, der die Schrift umschreibt, und in diesem Projekt wurde gemessen, dass er das Schriftsystem zerstört: Arabische Punkte sind eigene Glyphen, die erst beim Rendern zusammengesetzt werden, und ein Subsetter, der wegwirft, was er für unerreichbar hält, wirft sie weg. Die Entscheidung fiel für Cairo und gilt unverändert für Zain. Diese Dateien sind mit Absicht vollständige Schriften, mit einer eingeschränkten Range davor.

Was die vier Zain-Faces deklarieren, gegen das, was src/assets/fonts/zain/regular.woff2 tatsächlich enthält.
Deklarierte Range Codepoints deklariert Codepoints abgedeckt
U+0600-06FF, Arabic 256 109
U+0750-077F, Arabic Supplement 48 0
U+0870-088E, Arabic Extended-B 31 0
U+08A0-08FF, Arabic Extended-A 96 0
U+200F, Rechts-nach-links-Marke 1 0
U+FB50-FDFF, Presentation Forms-A 688 54
U+FE70-FEFF, Presentation Forms-B 144 89
Alle sieben, wie auf jedem Face deklariert 1.264 252

Zwei Codepoints laufen andersherum — in der Datei vorhanden, in der Range abwesend. U+0020 und U+0030 stehen beide in Zains Character Map und fallen in keinen der deklarierten Bereiche, dem Browser ist Zain für ein Leerzeichen oder eine Null also verboten. Das ist Absicht, es ist der Grund, warum die Lücke zwischen zwei arabischen Wörtern bei Quicksand landet, und es hat eine Folge im letzten Abschnitt, die hier noch niemand gemessen hat.

nav-lang.css · 5 faces · 70 routes

Warum dieselbe Technik auf der Website davor scheiterte

Der Vorgänger dieser Website hat unicode-range zuerst probiert und daraus geschlossen, dass es nicht funktioniert. Er hängte Faces mit arabischer Range unter die eigenen Familiennamen der Site — Cairo unter Quicksand, Cairo noch einmal unter Quicksand Label, Noto Kufi Arabic unter Quicksand Display — genau um einen Kaskadenkampf mit einem 183 KB großen gemeinsamen Stylesheet zu vermeiden. Fünf Faces, auf allen 70 Routen. Sein eigener Kommentar hält das Ergebnis fest: „Chromium löste arabische Runs weiter über die Quicksand-Faces mit voller Range auf und fiel auf das System-Naskh.“

Die Messung stimmte, der Schluss daraus nicht. unicode-range wird konsultiert, nachdem ein Face bereits über den Stil gewählt wurde, nicht davor, und Faces fallen für diesen Schritt nur dann zu einem Composite Face zusammen, wenn ihre übrigen Deskriptoren identisch sind: „Eine Gruppe von Faces, die über @font-face-Regeln mit identischen Deskriptorwerten, aber unterschiedlichen unicode-range-Werten definiert sind, gilt für diesen Schritt als ein einziges Composite Font Face.“ Diese hier waren nicht identisch. Die lateinischen Faces der Site deklarierten Gewichtsbereiche von 200 300, 400 und 500 900; die arabischen 100 500 und 501 900.

Die Verengung über das Gewicht ließ also mehr als ein Face stehen, und die Spezifikation gibt die Entscheidung dann aus der Hand. Der User Agent „muss eine einzelne Schrift aus der passenden Menge wählen“, und „welche Schrift gewählt wird, kann sich zwischen mehreren User Agents und mehreren Betriebssystemplattformen unterscheiden“. Wählte er ein lateinisches Face, war die arabische Glyphe nicht da, und der nächste Satz macht die Tür zu: „Glyphen aus anderen Faces der Familie werden nicht berücksichtigt.“ Die Familie wird als Ganzes verworfen, der nächste Familienname wird probiert, und am Ende der Liste läuft der Fallback auf installierte Schriften, dessen Ergebnis „zwischen User Agents variieren kann“. Das ist das System-Naskh.

Die Anordnung, die den Fehler erzeugte, rekonstruiert aus der nav-lang.css des Golden Master. Zwei Regeln teilen sich einen Familiennamen und widersprechen sich in jedem anderen Deskriptor.
/* die arabische Schrift, gehängt unter den eigenen Familiennamen */
@font-face {
  font-family: 'Quicksand';
  src: url('Cairo-SemiBold.ar.v2.woff2') format('woff2');
  font-weight: 501 900;
  unicode-range: U+0600-06FF, U+0750-077F, U+08A0-08FF,
    U+FB50-FDFF, U+FE70-FEFF, U+200C-200F;
}
 
/* und, anderswo im selben 183-KB-Sheet, das echte Quicksand */
@font-face {
  font-family: 'Quicksand';
  src: url('Quicksand-SemiBold.woff2') format('woff2');
  font-weight: 500 900;
  /* keine unicode-range: der Initialwert ist ganz Unicode */
}
 
/* die Deskriptoren unterscheiden sich, also sind das zwei Faces, kein */
/* Composite Face. Die Stilverengung läuft zuerst und behält evtl. beide. */

Dasselbe Repository liefert das Gegenbeispiel, und es lohnt sich, es neben den Fehler zu stellen. Zehn Demo-Schriftverzeichnisse tragen Google Fonts’ eigenen latin- und latin-ext-Split, erzeugt von tools/localize-demo-fonts.mjs: 109 Faces behalten, 100 Faces anderer Schriftsysteme weggelassen. Diese Faces haben bis auf die Range tatsächlich identische Deskriptoren, bilden also ein Composite Face, und das Ergebnis ist in jeder Engine deterministisch. Google teilt ein Design über Bereiche auf. Der Golden Master hängte ein anderes Design unter einen bestehenden Namen bei einem anderen Gewicht — die eine Anordnung, die die Composite-Regel nicht abdeckt.

legacy 11-route probe · comment said 2.5 KB

Ein arabisches Wort kostete 50.096 Bytes auf jeder englischen Seite

Die zweite Rechnung aus dieser Anordnung wurde mit einer Fetch-Probe über elf Routen der alten Site gemessen. Cairo-SemiBold.ar.v2.woff2, 50.096 Bytes, lag nirgends im Preload und wurde auf allen elf Routen geladen — auch auf jeder englischen und deutschen. Sie kam mit initiatorType css zwischen 610 und 640 ms auf fünf gewöhnlichen Routen an, mit 1.118,4 ms auf der 404-Seite und mit 1.464,2 ms auf der Startseite.

Der Auslöser war eine einzige Zeichenkette: das arabische Label des Sprachumschalters, vier Zeichen in einem Navigationsmenü, das niemand geöffnet hatte. Der Kommentar neben dem Code behauptete, englische und deutsche Seiten lüden dafür einmalig rund 2,5 KB. Das Architektur-Protokoll benennt die Diskrepanz nüchtern — um etwa das Zwanzigfache daneben — und ergänzt den Teil, der daraus einen Defekt statt einer Kuriosität macht: Wer gegen den Kommentar statt gegen die Messung optimiert, findet die 50 KB nie.

Font-Fetches über eine Elf-Routen-Probe der Website, die diese hier ersetzt hat. Jede Zahl darin ist eine Messung des alten Builds, nicht dieses.
Datei Preload auf Geladen auf Bytes
Quicksand-Regular.woff2 en, de, ar 11 von 11 30.844
Quicksand-Medium.woff2 en, de 10 von 11 30.784
Quicksand-SemiBold.woff2 en, de 11 von 11 31.084
Quicksand-Light.woff2 nie 0 von 11 29.000
Cairo-Regular.ar.v2.woff2 ar 1 von 11 49.084
Cairo-SemiBold.ar.v2.woff2 nie 11 von 11 50.096
NotoKufiArabic-Bold.ar.v2.woff2 ar 1 von 11 60.720
Alle sieben Dateien 281.612

Ob eine versteckte Zeichenkette etwas kostet, ist eine Frage nach Boxen, nicht nach Sichtbarkeit. Die Basisregel des alten Menüpanels trug im ausgelieferten Stylesheet den offenen clip-path; der geschlossene Zustand, display none und eine flachgedrückte Ellipse, wurde beim Boot von der Laufzeit als Inline-Style geschrieben. Das Panel wurde also beim ersten Paint gelayoutet und erst danach versteckt — und ein gelayouteter Textrun ist ein Textrun, für den die Download-Regel gilt.

Dieser Build deklariert den geschlossenen Zustand statisch — das Panel trägt display none im ausgelieferten Sheet, im eigenen Kommentar als der endgültige und der ausgegebene Zustand beschrieben. Das sollte heißen: keine Boxen und kein Fetch, bis das Menü öffnet. Auf diesem Build ist es nicht gemessen, und der vorige Build hat die entsprechende Behauptung als falsch gemessen — behandle es also als Mechanismus mit angehängter Prüfung, nicht als Ergebnis. Öffne das Netzwerk-Panel, filtere auf Font, lade eine englische Seite, öffne dann das Menü und schau zu.

arabic.css:104-146 · 4 faces, one range

Die Range, wie ausgeliefert, und das Loch, das sie am selben Tag aufriss

Die Regel kam mit dem Schriftwechsel, und der Grund war Drift. Zains Latein ist ein anderes Design als das von Cairo: Versalhöhe 69,5 Prozent des Gevierts gegen Cairos 69,1, aber x-Höhe 46,0 gegen 50,0 — 8 Prozent kleiner bei praktisch gleicher Versalhöhe. Eine unentdeckte Capture wäre unter dem neuen Face sichtbar schlimmer geworden, also brachte die Änderung, die Zain einführte, auch die Deklaration mit, die Zain das Antworten für Latein ganz verbietet.

Vier Faces, zwei synthetische Familien, eine identische Range auf allen, geteilt bei Gewicht 650, damit die bestehenden 600er- und 700er-Anhebungen ohne eine einzige geänderte Regel den richtigen Schnitt wählen. Die Nutzlast ist bis auf ein Kilobyte genau neutral: 161.096 Bytes Zain gegen die 159.900, die die beiden ersetzten Familien kosteten, ein Unterschied von 1.196 Bytes.

src/styles/locale/arabic.css, gekürzt. Die Mono-Regel unten ist die bewusste Umkehrung: Zain steht hinter den Monospace-Faces, nicht davor, damit Latein und Ziffern ihre Mono behalten und nur arabische Glyphen sich Zain leihen.
@font-face {
  font-family: 'Zain AB';
  src: url('../assets/fonts/zain/regular.woff2') format('woff2');
  font-weight: 100 500;
  font-display: swap;
  size-adjust: 112%;
  unicode-range:
    U+0600-06FF, U+0750-077F, U+0870-088E, U+08A0-08FF,
    U+200F, U+FB50-FDFF, U+FE70-FEFF;
}
 
html[lang='ar'] body,
html[lang='ar'] body * {
  font-family: 'Zain AB', Quicksand, Arial, sans-serif;
}
 
/* Zain hier ZULETZT, mit Absicht: Ziffern und Bezeichner behalten die Mono */
html[lang='ar'] :is(code, pre, .cdx-code) {
  font-family: ui-monospace, menlo, 'Zain AB', 'Courier New', monospace;
}

Eine Range hinzuzufügen schafft sofort eine Klasse von Codepoints, die die Familie nicht mehr setzen darf, und der erste davon ist das Leerzeichen zwischen zwei arabischen Wörtern. Jeder Stack, der Zain nennt, braucht jetzt eine Familie dahinter, die alles abdeckt, was die Range ausschließt — und einer hatte keine. Dem Display-Stack fehlte Quicksand, also landeten das Leerzeichen in einer zweiwörtigen arabischen Überschrift, das Und-Zeichen in einer anderen und jede Ziffer bei Arial: zwei Arial-Glyphen in der größten Schrift der Seite, gefunden mit CSS.getPlatformFontsForNode.

Die Messung, die belegt, dass die Range wirkt, ist dieselbe Messung, die das Loch fand, das sie aufriss. Nach dem Fix löst jede lateinische Glyphe auf den arabischen Seiten zu Quicksand oder Menlo auf und keine einzige zu Zain.

Die beiden size-adjust-Werte in dieser Auflistung, 112 Prozent für den Fließtext und 103 Prozent für Display, stammen aus einer eigenen Messung — dem Verhältnis von Zahn und Alif.

P11-03 · found and fixed 2026-08-26

Der umgekehrte Fehler: ein arabisches Wort ohne arabisches Face

Der Sprachumschalter druckt alle drei Sprachnamen in ihrer eigenen Sprache auf allen drei Dokumenten, und genau darum geht es: Wer die aktuelle Seite nicht lesen kann, erkennt trotzdem die eigene. Das Markup sagte bereits die Wahrheit. Der arabische Anker trägt hreflang, lang und dir, und die Zeichenkette ist einmal deklariert und in allen drei Content-Bundles als geteilt gelistet, sodass kein Locale sie übersetzt.

Was ihr fehlte, war ein Face. Jede arabische Regel im Stylesheet war auf das Dokumentelement bezogen, und das ist eine Tatsache über die Seite, nicht über den Textrun. Auf dem englischen und dem deutschen Dokument erbte das Label stattdessen das lateinische Label-Token — Quicksand Label, dann Arial, dann sans-serif — und Quicksand enthält exakt null Codepoints zwischen U+0600 und U+06FF. Der Browser hat den Stack erschöpft und den Fallback auf installierte Schriften laufen lassen.

Gemessen unter macOS in WebKit und in Chromium: Das Label wurde im Naskh der Plattform gesetzt, neben vier Quicksand-Labels, in anderem Gewicht, anderer Farbdichte und anderer optischer Größe. Auf einem Gerät ohne arabische Systemschrift erscheint es als Tofu. An dem Run war nichts falsch außer dem CSS, und das CSS hatte eine Frage über das Dokument gestellt, wo die Frage vier Zeichen betraf.

العربية
Die Zeichenkette selbst, einmal deklariert und im englischen, deutschen und arabischen Bundle als geteilt gelistet, damit alle drei Dokumente sie auf Arabisch drucken, statt sie zu übersetzen. Von Anfang an richtig.
das Markup
src/components/site-nav.html setzt hreflang, lang und dir auf den Anker und fragt den Splitter nach Zeilen statt nach Wörtern und Zeichen. Von Anfang an richtig.
das Stylesheet
Jede Regel, die ein arabisches Face hätte liefern können, war auf das Dokumentelement bezogen. Auf den beiden lateinischen Dokumenten traf keine einzige zu, und der Run fiel ganz aus dem Stack.
Drei Schichten desselben Wortes, und die eine Schicht, die falsch war.

Er überlebte ein eigens gebautes RTL-Gate, ein dreisprachiges Content-Register und einen Build, der Parität über drei Locales erzwingt — denn keines davon stellt der englischen Seite eine Frage über Arabisch. Gefunden wurde er, indem jemand den Kopf der Datei las und ihm nicht glaubte.

Der arabische Anker des Umschalters verlässt sich auf den lateinischen Seiten für seine Bidi-Behandlung auf das User-Agent-Stylesheet, und das ist die Art von Lücke, die logische Eigenschaften nicht schließen.

utilities beats components, no !important

Auf die Sprache zielen, nicht auf das Dokument

Der Fix sind zwei Regeln, und die zweite Hälfte der ersten trägt Last, statt Gürtel und Hosenträger zu sein. Die Familie kommt auf das Sprachattribut und auf seine Nachfahren, weil components/button.css font-family aus einem Token auf dem span im Anker setzt — und Vererbung schlägt niemals eine Deklaration am Element selbst. Eine Regel nur auf dem Anker würde ausgerechnet an dem Element verlieren, das die Tinte trägt.

Sie kommt ohne !important aus, wegen der Layer-Reihenfolge. src/styles/order.json deklariert reset, tokens, base, layout, components, sections, utilities, overrides; locale/arabic.css sitzt in utilities und button.css in components, zwei Layer früher, und ein späterer Layer schlägt jede Spezifität, die ein früherer schreiben kann. Die arabische Pauschalregel daneben trägt ein einziges !important, für letter-spacing, und sie ist die einzige gerechtfertigte Pauschalregel ihrer Art im Projekt. Diese Regel braucht keines.

  1. Die Sprache benennen, nicht die Seite Ersetze den dokumentbezogenen Selektor durch einen auf dem Sprachattribut, überall dort, wo etwas für einen arabischen Run gelten muss, wo immer er auftaucht. Der Dokumentbezug bleibt für alles, was wirklich das Dokument betrifft, etwa die Laufrichtung.
  2. Das Element erreichen, das die Tinte trägt Ergänze die Nachfahren-Hälfte des Selektors. Ein Kind, das seine eigene font-family deklariert, erbt deine nicht — die Regel muss dieses Kind also mittreffen.
  3. Den Layer entscheiden lassen Leg die Regel in einen Layer nach dem, der das Token deklariert hat. Kein Spezifitätskampf, kein !important, und derselbe Mechanismus hindert ein Section-Sheet daran, eine Locale-Korrektur zu überschreiben.
  4. Die Laufweite auf null setzen Der Body setzt letter-spacing auf minus 0,005em, und der Label-Span deklariert keine eigene, also erbte das arabische Wort sie auf den lateinischen Seiten. Arabisch wird nie gesperrt: Laufweite bricht die verbundene Schrift, die Buchstaben berühren sich nicht mehr, und das Wort hört auf, ein Wort zu sein.
  5. Zeilenbox und Clip-Fenster gemeinsam öffnen Das Label steht durch die Button-Komponente auf line-height 1 und wird bei 94 Prozent seiner Box beschnitten. Zains Content-Box liegt bei size-adjust 112 Prozent bei 1,859em, bei line-height 1 ist das halbe Leading also negativ und die Tinte läuft über den Schnitt hinaus. Die Regel setzt 1,55 und schiebt das Fenster auf 114 Prozent.
  6. Für die Metriken auf die Sprach-Pseudoklasse wechseln Die Metrik-Regel nutzt :lang(ar) statt des Attributselektors. Die Inhaltssprache wird an Nachfahren vererbt und über erweiterte Filterung gematcht, sie deckt also auch ar-EG und ar-Arab-EG ab, was ein exakter Attributtreffer nicht tut.
Die fünf Teile des Fixes, in der Reihenfolge, in der sie nötig waren. Das Face allein änderte die Glyphen und ließ drei geerbte lateinische Eigenschaften falsch stehen.

Die Clip-Arithmetik reproduziert sich exakt, und so wurden die Zahlen in dieser Regel gewählt statt geraten. Zains hhea gibt Ascent 869 und Descent minus 459 auf 800 Einheiten pro Geviert; bei size-adjust 112 Prozent sind das 1,2166em und 0,6426em, eine Content-Box von 1,8592em. Bei line-height 1 beträgt das halbe Leading minus 0,4296em, und die Grundlinie liegt 0,7870em unter der Oberkante der Zeilenbox, was die tiefste Tinte des Labels auf 1,2505em bringt, gegen einen Schnitt bei 0,9400em. Bei 1,55 mit dem Fenster auf 114 Prozent landet die Tinte bei 1,5716em, das Fenster bei 1,7670em und der Hover-Zwilling bei 1,8454em, in dieser Reihenfolge. Glyphentiefen sind an isolierten Formen gemessen.

Die Datei formuliert die Invariante als Zwilling unter Fenster, Fenster unter Tinte, und sie hält fest, was passiert, wenn sie bricht. Im Footer-Link der alten Site lief die Tinte fünf Pixel über den Schnitt hinaus, ein finales Yeh verlor seine Punkte, und das Wort wurde als ein anderes Wort gesetzt — eine Falschschreibung der eigenen Handschrift des Inhabers. Auf den arabischen Seiten war es behoben und auf den englischen und deutschen seither live, an dem einen arabischen Wort, das diese Dokumente enthalten.

Die 1,55 in dieser Regel ist eine Clip-Fenster-Zahl und keine Kollisionszahl, und der Unterschied zwischen beiden ist der gemessene Boden für arabische Zeilenhöhe.

split-registry.js:121-190 · read once

Derselbe Fehler, eine Ebene tiefer, im JavaScript

Die Split-Registry liest die Sprache des Dokuments genau einmal, beim Boot, und ihr Kommentar nennt einen ordentlichen Grund: lang und dir werden vom Build auf das Dokumentelement geschrieben und ändern sich danach nie, pro Element zu fragen wären also 55 Lesungen einer einzigen Tatsache und 55 Gelegenheiten, sie unterschiedlich zu lesen. Aus dieser einen Lesung entscheidet sie dann, ob ein Run in Zeichen zerlegt werden darf.

Auf den arabischen Seiten stimmt das. Auf den englischen und deutschen ist es der Fehler des Stylesheets in einer anderen Sprache: Das Dokument ist en oder de, das arabische Label würde also pro Zeichen zerlegt. Zeichenweises Zerlegen ist in einer verbundenen Schrift nicht kosmetisch. Die Reveal-Komponente macht aus jedem Zeichen ein inline-block, und das muss sie, weil transform auf eine nicht ersetzte Inline-Box nicht wirkt — und eine atomare Box pro Buchstabe ist eine Box, über die der Shaper nicht verbinden kann. Das Wort kommt in isolierten Formen heraus, also falsch geschrieben.

Das Markup umgeht das von Hand. Dieser eine Span fragt den Splitter nach Zeilen, während die englischen und deutschen Labels nach Wörtern und Zeichen fragen, und der eigene Kommentar des Moduls räumt es ein: Die Komponentendatei war für das eine Label, das jemandem aufgefallen war, längst von Hand zu diesem Schluss gekommen.

Vier Fehler auf dieser Website und ihrem Vorgänger. Drei davon sind derselbe Fehler: eine Frage über das Dokument, wo die Frage den Run betraf.
Fehler Wo Was der Browser tat Was es kostete
Zu viel Capture die arabischen Seiten, Cairo zuerst und ohne Range beantwortete jeden lateinischen Codepoint aus der arabischen Datei 381 lateinische Runs in der falschen Schrift
Zu viel Capture, beim Download die alten englischen und deutschen Seiten ließ zwei Faces stehen und wählte willkürlich eines 50.096 B auf 11 von 11 Routen
Zu wenig Capture die englischen und deutschen Seiten, jede Regel am Dokument erschöpfte den Stack und lief in den Fallback auf installierte Schriften 1 Wort im System-Naskh
Zu wenig Capture, im Skript die englischen und deutschen Seiten, die Split-Registry hätte eine verbundene Schrift in atomare Inline-Boxen zerlegt 1 Wort falsch geschrieben auf dem Schirm

Das automatische Gate kann drei der vier Zeilen unten nicht sehen. tools/golden/rtl.mjs läuft über jeden Textknoten und prüft, dass jeder arabische Run Zain anfordert und jeder lateinische Run einen lateinischen Fallback behält — aber nur auf arabischen Routen. Es lädt die englische Startseite als Kontrolle für den Splitter, den Header, das Diptychon und die Leiste und stellt ihr die Schriftfrage nie. Ein Grep über Tests und Contract-Checks liefert für die neue sprachbezogene Regel weiterhin nichts. Ein eigens gebautes RTL-Gate lief grün, während der Defekt auf den beiden meistbesuchten Dokumenten des Produkts lebte.

Warum ein inline-block pro Buchstabe ein arabisches Wort zerbricht und was ein Splitter stattdessen tun muss, ist das Problem des Splittens verbundener Schriften.

glyphCount, not fontFamily

Wie beweist du, welches Face tatsächlich gewonnen hat?

Der Computed Style sagt es dir nicht. Er meldet den Stack, den du geschrieben hast, und das eigene Gate dieses Repositorys sagt das in einem Kommentar an der Stelle, an der es aufgibt: Ein rein lateinischer Run in einem arabischen Dokument darf legitim zuerst die arabische Familie anfordern, unicode-range ist das, was ihn weiter zu Quicksand leitet, und ein Computed Style kann das nicht sehen. Was das Gate prüfen kann, ist die schwächere, immer noch nützliche Eigenschaft: dass kein Run eine Familie anfordert, hinter der kein lateinischer Fallback steht.

CSS.getPlatformFontsForNode über das Chrome DevTools Protocol entscheidet die Sache. Es liefert pro Knoten die Plattform-Familien, die den Text tatsächlich gesetzt haben, ob jede geladen oder lokal aufgelöst wurde und wie viele Glyphen jede beigesteuert hat. Das hat die beiden Arial-Glyphen in einer Display-Überschrift gefunden und danach bestätigt, dass keine lateinische Glyphe auf den arabischen Seiten Zain ist. Von Hand ist es der Block Rendered Fonts unten im Computed-Bereich.

Zain Regular, bei 112 Prozent 0,7028 em
Quicksand Regular, bei 94,2 Prozent 0,5605 em
Cairo Regular, bei 100 Prozent 0,5600 em
Quicksand Medium, bei 93,0 Prozent 0,5589 em
Die Vorbreite von U+0030 in jedem Face, in Geviert, nach size-adjust. Auf diese Zahl löst 1ch auf.Mit fontTools über die ausgelieferten Dateien gemessen. Die Balken sind maßstäblich gegen Zain bei 0,7028em.
CSS.getPlatformFontsForNode
Liefert familyName, isCustomFont und glyphCount für jede Plattformschrift, die den Text eines Knotens gesetzt hat. Das einzige der vier, das beantwortet, welches Face gewonnen hat.
Rendered Fonts, Computed-Bereich
Dieselben Daten von Hand, unten im Bereich. Mehrere Familien an einem Element bedeuten, dass darin ein Fallback stattgefunden hat.
das Netzwerk-Panel, gefiltert auf Font
Eine andere Frage: ob die Datei überhaupt geladen wurde. Eine geladene Datei kann trotzdem jeden Codepoint an eine Familie vor ihr verloren haben.
document.fonts.check()
Eine Falle. MDN sagt, es sei nicht dafür gedacht zu prüfen, ob ein bestimmter Schriftstil gerendert werden kann, und sein Text-Argument ist per Vorgabe ein einzelnes Leerzeichen — gegen ein Face, dessen Range U+0020 ausschließt, ist die Antwort gehaltlos. Übergib die echte Zeichenkette.
Vier Werkzeuge, und die Frage, die jedes beantworten kann und die es nicht kann.

Eine Folge dieser Range ist hier nicht gemessen, und sie ist auf jeder Site einen Blick wert, die ch für das Zeilenmaß nutzt. Die Einheit ch ist die Vorbreite von U+0030 in der Schrift, die es setzt, und U+0030 liegt außerhalb der Zain-Deklaration, der Browser darf Zain dafür also nicht verwenden und Quicksand liefert es stattdessen. Der Weg von Cairo zu Quicksand hat 1ch um 0,09 Prozent verschoben. Wäre Zain in voller Range geblieben, wäre 1ch um 25,4 Prozent gewachsen, über mehr als zwanzig ch-basierte Maße in diesem Stylesheet.

Dieselbe Klausel hat eine leisere Wirkung. Die erste verfügbare Schrift — die, gegen die ex, cap und lh auflösen — ist definiert als die erste Schrift, für die U+0020 nicht durch eine unicode-range ausgeschlossen ist, und U+0020 liegt ebenfalls außerhalb der Zain-Range, Zain kann hier also auf keinem Dokument je die erste verfügbare Schrift sein. Die Vorbreiten oben sind gemessen; die gerenderten Spaltenbreiten auf den arabischen Seiten davor und danach nicht. Vergleiche sie selbst, bevor du dich in die eine oder andere Richtung festlegst.

Der dreisprachige Build, aus dem jede einzelne dieser Zahlen gelesen wurde, ist einer der zehn Konzept-Websites.

Fragen

Macht unicode-range meine Schriftdatei kleiner?

Nein. An der Datei ändert sich gar nichts. CSS Fonts 4 nennt die Vereinigung der deklarierten Bereiche einen Hinweis für User Agents, die entscheiden, ob eine Schriftressource für einen gegebenen Textrun geladen wird, und MDN sagt das Ergebnis klar: Nutzt die Seite mindestens ein Zeichen aus dem Bereich, wird die ganze Schrift geladen. Was eine Datei kleiner macht, ist Subsetting, ein Build-Schritt, der die Schrift umschreibt — und auf dieser Website wurde gemessen, dass Subsetting die arabische Zeichenkomposition zerstört, weil die Punkte eigene Glyphen sind, die erst beim Rendern zusammengesetzt werden, und der Subsetter wegwarf, was er für unerreichbar hielt.

Muss meine unicode-range zu dem passen, was wirklich in der Schrift steckt?

Nein, und die Spezifikation sagt es unumwunden: Die effektive Character Map ist die Schnittmenge der von unicode-range definierten Codepoints mit der Character Map der Schrift. Diese Website deklariert vier arabische Blöcke, von denen drei in der Datei vollständig leer sind, dazu eine Rechts-nach-links-Marke, die die Schrift ebenfalls nicht trägt — 1.264 deklarierte Codepoints gegen 252 abgedeckte. Nichts geht kaputt. Was du nicht tun darfst, ist das Umgekehrte: eine Range enger zu deklarieren als die Codepoints, die gesetzt werden sollen, denn dann ist dem Browser das Face dafür verboten.

Warum wählt der Browser das falsche Face, wenn ich eine arabische Schrift unter meinen lateinischen Familiennamen hänge?

Weil unicode-range erst konsultiert wird, nachdem über den Stil ein einzelnes Face gewählt wurde. Faces fallen für diesen Schritt nur dann zu einem Composite Face zusammen, wenn ihre übrigen Deskriptoren identisch sind. Deklariere dein arabisches Face auf font-weight 100 500 und deine lateinischen Faces auf 200 300, 400 und 500 900, dann sind sie nicht identisch, das Gewichts-Matching kann also mehrere stehen lassen — und die Spezifikation erlaubt dem User Agent dann, eines willkürlich zu wählen und von anderen User Agents abzuweichen. Wählt er das lateinische Face, fehlt die arabische Glyphe, Glyphen aus anderen Faces der Familie werden nicht berücksichtigt, und die Familie wird als Ganzes verworfen. Google Fonts’ latin- und latin-ext-Split funktioniert, weil seine Faces tatsächlich identische Deskriptoren haben.

Meine englische Seite zeigt ein arabisches Wort in der falschen Schrift. Warum?

Fast sicher, weil deine arabischen Styles auf das Dokument bezogen sind statt auf den Run. Jede Regel im arabischen Sheet dieser Website war auf das Dokumentelement bezogen, und das ist eine Tatsache über die Seite. Das arabische Label des Sprachumschalters — auf allen drei Dokumenten gedruckt, damit auch die eigene Sprache findet, wer die aktuelle Seite nicht lesen kann — erbte deshalb das lateinische Label-Token, das zu Quicksand und dann zu Arial auflöst. Quicksand enthält null arabische Codepoints, der Browser hat also den Stack erschöpft und den Fallback auf installierte Schriften laufen lassen, dessen Ergebnis laut Spezifikation zwischen User Agents variieren kann. Auf einem Gerät ohne arabische Systemschrift ist es Tofu.

Soll ich den lang-Attributselektor nehmen oder die Sprach-Pseudoklasse?

Die Pseudoklasse ist die korrektere von beiden und meistens das, was du willst. Sie trifft die Inhaltssprache des Elements, also auch Nachfahren, die sie erben, und sie nutzt erweiterte Filterung, sodass :lang(ar) auch ar-EG und ar-Arab-EG trifft. Ein Attributselektor trifft nur das Element, das das Attribut buchstäblich mit genau diesem Wert trägt. Diese Website nutzt für die Familie den Attributselektor plus eine ausdrückliche Nachfahren-Hälfte, weil das Kindelement seine eigene font-family deklariert und Vererbung nie eine Deklaration schlägt — und für die Metriken die Pseudoklasse.

Quellen

In diesem Repository gemessen

  • src/styles/locale/arabic.css Die vier Zain-Faces und ihre einzige unicode-range, die Stacks, die sie nutzen, die Clip-Fenster-Arithmetik und die sprachbezogene Regel, die den arabischen Run in einem lateinischen Dokument repariert.
  • src/assets/fonts/zain/ Die drei Schnitte, aus denen jede Abdeckungszahl und jede Vorbreite oben gezählt wurde, ausgeliefert mit ihrer SIL Open Font License.
  • docs/evidence/legacy-forensics/a10-css.md Die Fünf-Faces-Anordnung des Vorgängers, seine eigene Erklärung, warum sie scheiterte, und die Elf-Routen-Fetch-Probe hinter der Zeile mit 50.096 Bytes.
  • tools/golden/rtl.mjs Das RTL-Gate, das die Schriftauflösung nur auf arabischen Routen prüft, und der Kommentar, der genau festhält, was ein Computed Style nicht sehen kann.
  • src/runtime/animation/split-registry.js Die eine Lesung der Dokumentsprache beim Boot und die Entscheidung über zeichenweises Zerlegen, die daraus folgt.

Abgeglichen mit

Alaa Abbod

Geschrieben von

Alaa Abbod

Creative Developer — Herne, Deutschland

Designer und Entwickler, der barrierefreie Websites, Mobile Apps, Onlineshops und visuelle Identitäten als eine einzige Arbeit baut, von Hand. Diese Website erscheint aus einer Quelle in Englisch, Deutsch und Arabisch, und daher kommen die meisten dieser Fragen.

Bitte dreh dein Gerät,
Diese Seite ist hochkant gebaut.