Skip to content

Commit 0d68f57

Browse files
jakejackson1claude
andcommitted
feat: serve the language settings, the last route the Font Manager mocked
`GET`/`POST /fonts/settings` is the final entry in the client's `PENDING_ROUTES`, so the Language tab stops reading fixtures and the shim that fed it — `mockOnly()`, `enablePendingRoutes()`, the array itself — goes with it. `api/mock/` is now the Jest seam and nothing else. `Registry::language_map()` is the settings screen's view of the two maps §4.4 already computed: one row per code in `$default ∪ $effective`, grouped the way `get_grouped_fonts()` groups the sidebar so a label and an order can only be wrong in one place. It files a row under its *default* font rather than its current one, so a code moved elsewhere stays with the pack that claimed it and an admin can see what they changed it from. "Changed" stays derived — `font !== default_font` — rather than becoming a second fact that can disagree with the first. The sentinel is `*` on the wire in both directions, not the `''` §4.4 specified. The panel's font select carries a `*` option and no empty one, so `''` would have drawn as "no selection" and saved back as whatever option came first. `Registry::LANGUAGE_NONE` is now the one spelling, shared with `effective_language_map()` and with the client through `constants.js`. Two defects fell out of pinning the §4.4 behaviour table, both older than this change: `get_document_script()` looked up `Ucdn::LATIN`, a constant mPDF does not define — the select has always offered the bare script name — so every value of that setting was silently ignored and the script fell back to the one the document language implies. `Registry::script_constant()` now owns the vocabulary: it folds mPDF's `SCRIPT_` prefix either way, and the route refuses a name no constant answers to rather than storing a `BANANA` that looks like a setting and does nothing. `Registry::scripts()` ships the offerable list so the client's third copy could be deleted. `Model_Form_Settings::settings_sanitize()` wrote `[]` for any unanswered single-value select, which is a `TypeError` in `strtoupper( $settings['pdf_size'] )` under PHP 8 and a `font-family: Array` in the stylesheet. It writes `''` now; `multicheck` and a `multiple` select still get an array. `Registry::setting()` stays anyway, because sites carry years of `[]` already on disk and the reader is what makes those readable — it replaces the one-off `is_string()` guard `Coverage_Resolver` had grown for the same reason. `prune_language_overrides()` lives on `Registry` beside `effective_language_map()`, its read-side twin, rather than in the controller that posts to it: the upgrade routine, a repair pass and WP-CLI all have to be able to write a valid override map without restating the rule. `default_language_map()` is memoised on the repository stamp, since three callers of one request were each re-sorting every installed row to build it. The four keys are deliberately *not* registered as settings fields. §4.8 asked for that so they would be "sanitised/exported with the rest", and they would be neither — `settings_sanitize()` is tab-scoped and there is no settings export tool — while a section the sanitiser *can* reach has its unanswered fields blanked, which for `font_language_overrides` means an unrelated save wipes the map. Per-PDF `pdf_language` *is* registered, in `form_settings_appearance`, where the framework genuinely carries it: rendering, sanitising and a REST enum. `Language_To_Font::labels()` is a table of ~190 language codes and ~59 `und-<script>` ones — the union of every code a published pack claims, every code the frozen 6.x map carries, and the Latin-script languages the default select needs but no pack has to. It is memoised per locale, because one of its callers is `get_registered_fields()` and that runs on every REST request the site serves, `GET /download/{entry}/{pdf}` included. An unnamed code degrades to the raw code; a test locks the one code set that is knowable in advance. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent bce1703 commit 0d68f57

23 files changed

Lines changed: 1773 additions & 149 deletions

src/Fonts/Coverage_Resolver.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,8 @@ public function for_locales( array $wp_locales ): array {
9595
*
9696
* The language goes through `Registry`, so a PDF naming none still asks for the site's — `default_pdf_language`
9797
* has no trigger of its own. The font is read raw, since `get_default_font()` would filter it against the fonts
98-
* already registered, which is the key being asked for — but only when it is a string: the settings array
99-
* reaches this straight off the save, before anything has validated its shape.
98+
* already registered, which is the key being asked for; `Registry::setting()` is what makes reading it raw safe
99+
* on a settings array that has not been validated yet.
100100
*
101101
* @return array[] One `Install_Queue::enqueue_once()` request per entry
102102
*
@@ -105,7 +105,7 @@ public function for_locales( array $wp_locales ): array {
105105
public function for_settings( array $pdf ): array {
106106
return $this->requests(
107107
array_merge(
108-
$this->rows_for_font( is_string( $pdf['font'] ?? null ) ? $pdf['font'] : '' ),
108+
$this->rows_for_font( Registry::setting( $pdf, 'font' ) ),
109109
$this->rows_for_languages( [ $this->registry->get_document_language( $pdf ) ] )
110110
)
111111
);

src/Fonts/Language_To_Font.php

Lines changed: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,291 @@
3030
*/
3131
class Language_To_Font implements LanguageToFontInterface {
3232

33+
/**
34+
* `und-<script>` code => script name
35+
*
36+
* ISO 15924 by way of mPDF, which asks with the tag `Mpdf::markScriptToLang()` built from the run's Unicode
37+
* block. Every script a published pack claims is here, every script a 6.x site's adopted fonts still answer
38+
* for, and every tag the catalogue's coverage chips name (`ScriptChips.js`), so a pack cannot draw a name on
39+
* its card and a bare code in the language table.
40+
*
41+
* Its own constant rather than part of `LANGUAGE_LABELS` for that reason: this half has a counterpart to stay
42+
* level with. The names deliberately differ from the chips' — "Arabic script" here because the row sits next
43+
* to `ar`, "Arabic" there because nothing sits next to it.
44+
*
45+
* @since 7.0
46+
*/
47+
public const SCRIPT_LABELS = [
48+
'und-arab' => 'Arabic script',
49+
'und-armn' => 'Armenian script',
50+
'und-beng' => 'Bengali script',
51+
'und-bopo' => 'Bopomofo',
52+
'und-brai' => 'Braille',
53+
'und-cans' => 'Canadian Aboriginal syllabics',
54+
'und-copt' => 'Coptic script',
55+
'und-cprt' => 'Cypriot syllabary',
56+
'und-cyrl' => 'Cyrillic script',
57+
'und-deva' => 'Devanagari script',
58+
'und-dsrt' => 'Deseret',
59+
'und-egyp' => 'Egyptian hieroglyphs',
60+
'und-ethi' => 'Ethiopic script',
61+
'und-geor' => 'Georgian script',
62+
'und-glag' => 'Glagolitic',
63+
'und-goth' => 'Gothic script',
64+
'und-grek' => 'Greek script',
65+
'und-gujr' => 'Gujarati script',
66+
'und-guru' => 'Gurmukhi script',
67+
'und-hang' => 'Hangul',
68+
'und-hani' => 'Han',
69+
'und-hans' => 'Han (Simplified)',
70+
'und-hant' => 'Han (Traditional)',
71+
'und-hebr' => 'Hebrew script',
72+
'und-hira' => 'Hiragana',
73+
'und-ital' => 'Old Italic',
74+
'und-jpan' => 'Japanese',
75+
'und-kali' => 'Kayah Li',
76+
'und-kana' => 'Katakana',
77+
'und-khar' => 'Kharoshthi',
78+
'und-khmr' => 'Khmer script',
79+
'und-knda' => 'Kannada script',
80+
'und-kore' => 'Korean',
81+
'und-lana' => 'Tai Tham',
82+
'und-laoo' => 'Lao script',
83+
'und-latn' => 'Latin script',
84+
'und-linb' => 'Linear B',
85+
'und-mlym' => 'Malayalam script',
86+
'und-mong' => 'Mongolian script',
87+
'und-mtei' => 'Meetei Mayek',
88+
'und-mymr' => 'Myanmar script',
89+
'und-ogam' => 'Ogham',
90+
'und-orya' => 'Odia script',
91+
'und-osma' => 'Osmanya',
92+
'und-runr' => 'Runic',
93+
'und-shaw' => 'Shavian',
94+
'und-sinh' => 'Sinhala script',
95+
'und-sund' => 'Sundanese script',
96+
'und-syrc' => 'Syriac script',
97+
'und-talu' => 'New Tai Lue',
98+
'und-taml' => 'Tamil script',
99+
'und-tavt' => 'Tai Viet',
100+
'und-telu' => 'Telugu script',
101+
'und-tfng' => 'Tifinagh',
102+
'und-thai' => 'Thai script',
103+
'und-tibt' => 'Tibetan script',
104+
'und-xsux' => 'Cuneiform',
105+
'und-yiii' => 'Yi script',
106+
'und-zsye' => 'Emoji',
107+
];
108+
109+
/**
110+
* Language code => language name
111+
*
112+
* Both the two- and three-letter forms, because both are map keys: mPDF's own language list carries the pairs
113+
* and the catalogue's `languages` column mirrors it, so a pack claiming `bn` and `ben` produces two rows an
114+
* admin can set independently. The Latin-script languages at the end are not claimed by any pack — they are
115+
* what the "Default document language" select needs, since a site's answer there is usually the one language
116+
* no pack has to carry.
117+
*
118+
* @since 7.0
119+
*/
120+
public const LANGUAGE_LABELS = [
121+
'ab' => 'Abkhazian',
122+
'abk' => 'Abkhazian',
123+
'am' => 'Amharic',
124+
'amh' => 'Amharic',
125+
'ar' => 'Arabic',
126+
'ara' => 'Arabic',
127+
'as' => 'Assamese',
128+
'asm' => 'Assamese',
129+
'av' => 'Avaric',
130+
'ava' => 'Avaric',
131+
'ba' => 'Bashkir',
132+
'bak' => 'Bashkir',
133+
'ban' => 'Balinese',
134+
'be' => 'Belarusian',
135+
'bel' => 'Belarusian',
136+
'ben' => 'Bengali',
137+
'bg' => 'Bulgarian',
138+
'bh' => 'Bihari',
139+
'bih' => 'Bihari',
140+
'bku' => 'Buhid',
141+
'blt' => 'Tai Dam',
142+
'bn' => 'Bengali',
143+
'bo' => 'Tibetan',
144+
'bod' => 'Tibetan',
145+
'bug' => 'Buginese',
146+
'bul' => 'Bulgarian',
147+
'ce' => 'Chechen',
148+
'che' => 'Chechen',
149+
'chr' => 'Cherokee',
150+
'chu' => 'Church Slavonic',
151+
'chv' => 'Chuvash',
152+
'cop' => 'Coptic',
153+
'cr' => 'Cree',
154+
'cre' => 'Cree',
155+
'cu' => 'Church Slavonic',
156+
'cv' => 'Chuvash',
157+
'div' => 'Dhivehi',
158+
'dv' => 'Dhivehi',
159+
'dz' => 'Dzongkha',
160+
'dzo' => 'Dzongkha',
161+
'el' => 'Greek',
162+
'ell' => 'Greek',
163+
'fa' => 'Persian',
164+
'fas' => 'Persian',
165+
'got' => 'Gothic',
166+
'gu' => 'Gujarati',
167+
'guj' => 'Gujarati',
168+
'he' => 'Hebrew',
169+
'heb' => 'Hebrew',
170+
'hi' => 'Hindi',
171+
'hin' => 'Hindi',
172+
'hnn' => 'Hanunoo',
173+
'hy' => 'Armenian',
174+
'hye' => 'Armenian',
175+
'ii' => 'Sichuan Yi',
176+
'iii' => 'Sichuan Yi',
177+
'iku' => 'Inuktitut',
178+
'iu' => 'Inuktitut',
179+
'ja' => 'Japanese',
180+
'jpn' => 'Japanese',
181+
'jv' => 'Javanese',
182+
'ka' => 'Georgian',
183+
'kan' => 'Kannada',
184+
'kas' => 'Kashmiri',
185+
'kat' => 'Georgian',
186+
'kaz' => 'Kazakh',
187+
'khm' => 'Khmer',
188+
'kir' => 'Kyrgyz',
189+
'kk' => 'Kazakh',
190+
'km' => 'Khmer',
191+
'kn' => 'Kannada',
192+
'ko' => 'Korean',
193+
'kom' => 'Komi',
194+
'kor' => 'Korean',
195+
'ks' => 'Kashmiri',
196+
'ku' => 'Kurdish',
197+
'kur' => 'Kurdish',
198+
'kv' => 'Komi',
199+
'ky' => 'Kyrgyz',
200+
'lao' => 'Lao',
201+
'lif' => 'Limbu',
202+
'lis' => 'Lisu',
203+
'lo' => 'Lao',
204+
'mal' => 'Malayalam',
205+
'mar' => 'Marathi',
206+
'mk' => 'Macedonian',
207+
'mkd' => 'Macedonian',
208+
'ml' => 'Malayalam',
209+
'mn' => 'Mongolian',
210+
'mr' => 'Marathi',
211+
'my' => 'Burmese',
212+
'mya' => 'Burmese',
213+
'ne' => 'Nepali',
214+
'nep' => 'Nepali',
215+
'nqo' => "N'Ko",
216+
'oj' => 'Ojibwe',
217+
'oji' => 'Ojibwe',
218+
'or' => 'Odia',
219+
'ori' => 'Odia',
220+
'os' => 'Ossetian',
221+
'oss' => 'Ossetian',
222+
'pa' => 'Punjabi',
223+
'pan' => 'Punjabi',
224+
'phn' => 'Phoenician',
225+
'ps' => 'Pashto',
226+
'pus' => 'Pashto',
227+
'ru' => 'Russian',
228+
'rus' => 'Russian',
229+
'sa' => 'Sanskrit',
230+
'san' => 'Sanskrit',
231+
'sat' => 'Santali',
232+
'sd' => 'Sindhi',
233+
'si' => 'Sinhala',
234+
'sin' => 'Sinhala',
235+
'snd' => 'Sindhi',
236+
'sr' => 'Serbian',
237+
'srp' => 'Serbian',
238+
'su' => 'Sundanese',
239+
'syl' => 'Sylheti',
240+
'syr' => 'Syriac',
241+
'ta' => 'Tamil',
242+
'tam' => 'Tamil',
243+
'tat' => 'Tatar',
244+
'tbw' => 'Tagbanwa',
245+
'tdd' => 'Tai Nuea',
246+
'te' => 'Telugu',
247+
'tel' => 'Telugu',
248+
'tg' => 'Tajik',
249+
'tgk' => 'Tajik',
250+
'th' => 'Thai',
251+
'tha' => 'Thai',
252+
'ti' => 'Tigrinya',
253+
'tir' => 'Tigrinya',
254+
'tk' => 'Turkmen',
255+
'tl' => 'Tagalog',
256+
'tt' => 'Tatar',
257+
'tuk' => 'Turkmen',
258+
'ug' => 'Uyghur',
259+
'uga' => 'Ugaritic',
260+
'uig' => 'Uyghur',
261+
'uk' => 'Ukrainian',
262+
'ukr' => 'Ukrainian',
263+
'ur' => 'Urdu',
264+
'urd' => 'Urdu',
265+
'vai' => 'Vai',
266+
'vi' => 'Vietnamese',
267+
'vie' => 'Vietnamese',
268+
'xcr' => 'Carian',
269+
'xlc' => 'Lycian',
270+
'xld' => 'Lydian',
271+
'yi' => 'Yiddish',
272+
'yid' => 'Yiddish',
273+
'zh' => 'Chinese',
274+
'zh-hk' => 'Chinese (Hong Kong)',
275+
'zh-tw' => 'Chinese (Taiwan)',
276+
'zho' => 'Chinese',
277+
278+
/* Latin-script languages no pack claims, for the "Default document language" select */
279+
'af' => 'Afrikaans',
280+
'az' => 'Azerbaijani',
281+
'ca' => 'Catalan',
282+
'cs' => 'Czech',
283+
'cy' => 'Welsh',
284+
'da' => 'Danish',
285+
'de' => 'German',
286+
'en' => 'English',
287+
'es' => 'Spanish',
288+
'et' => 'Estonian',
289+
'eu' => 'Basque',
290+
'fi' => 'Finnish',
291+
'fil' => 'Filipino',
292+
'fr' => 'French',
293+
'ga' => 'Irish',
294+
'gl' => 'Galician',
295+
'hr' => 'Croatian',
296+
'hu' => 'Hungarian',
297+
'id' => 'Indonesian',
298+
'is' => 'Icelandic',
299+
'it' => 'Italian',
300+
'lt' => 'Lithuanian',
301+
'lv' => 'Latvian',
302+
'ms' => 'Malay',
303+
'nb' => 'Norwegian (Bokmål)',
304+
'nl' => 'Dutch',
305+
'nn' => 'Norwegian (Nynorsk)',
306+
'pl' => 'Polish',
307+
'pt' => 'Portuguese',
308+
'ro' => 'Romanian',
309+
'sk' => 'Slovak',
310+
'sl' => 'Slovenian',
311+
'sq' => 'Albanian',
312+
'sv' => 'Swedish',
313+
'sw' => 'Swahili',
314+
'tr' => 'Turkish',
315+
'uz' => 'Uzbek',
316+
];
317+
33318
/**
34319
* @var array<string, string> language or script code => font key
35320
* @since 7.0
@@ -106,4 +391,45 @@ public static function candidates( string $mode ): array {
106391
public static function locale_to_language( string $wp_locale ): string {
107392
return strtolower( str_replace( '_', '-', $wp_locale ) );
108393
}
394+
395+
/**
396+
* Every code the settings screen can show, as `code => display name`
397+
*
398+
* Returned once beside the map by `GET /fonts/settings` and used three ways — the default-language select, the
399+
* rows of the language table and the "add a language" list — so a name and a code can only disagree in one
400+
* place. Translated here rather than in the constants because a `const` cannot call `__()`, and never on the
401+
* render path: nothing under `getLanguageOptions()` reads this.
402+
*
403+
* @return array<string, string>
404+
*
405+
* @since 7.0
406+
*/
407+
public static function labels(): array {
408+
/*
409+
* ~250 `__()` lookups and a sort, memoised because `Helper_Options_Fields::get_registered_fields()` is one
410+
* caller and that runs on every REST request the site serves — `GET /download/{entry}/{pdf}` included.
411+
* Keyed on the locale rather than a bare flag: `switch_to_locale()` is how a notification email renders a
412+
* PDF in the recipient's language, and a table built before the switch would answer in the wrong one.
413+
*/
414+
static $cache = [];
415+
416+
$locale = determine_locale();
417+
418+
if ( isset( $cache[ $locale ] ) ) {
419+
return $cache[ $locale ];
420+
}
421+
422+
$labels = [];
423+
424+
foreach ( static::LANGUAGE_LABELS + static::SCRIPT_LABELS as $code => $label ) {
425+
/* phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText -- a constant table of literals, one per language */
426+
$labels[ $code ] = __( $label, 'gravity-pdf' );
427+
}
428+
429+
asort( $labels );
430+
431+
$cache[ $locale ] = $labels;
432+
433+
return $labels;
434+
}
109435
}

0 commit comments

Comments
 (0)