1use std::collections::BTreeMap;
87
88use crate::diagnostic::{Diagnostic, codes};
89use crate::l10n::{L10nDoc, tag};
90
91#[derive(Clone, Copy, Debug, PartialEq, Eq)]
93pub struct ChromeString {
94 pub key: &'static str,
96 pub en: &'static str,
98 pub args: usize,
100 pub slot: usize,
102}
103
104impl ChromeString {
105 fn tagged_as(&self, text: &str) -> String {
110 tag(self.key, text)
111 }
112
113 pub fn tagged(&self) -> String {
115 self.tagged_as(self.en)
116 }
117}
118
119macro_rules! chrome {
122 ($( $(#[$m:meta])* $name:ident = $slot:literal, $key:literal, $args:literal, $en:expr; )*) => {
123 $(
124 $(#[$m])*
125 pub const $name: ChromeString = ChromeString {
126 key: $key, en: $en, args: $args, slot: $slot,
127 };
128 )*
129 pub const ALL: &[ChromeString] = &[$($name),*];
133 };
134}
135
136chrome! {
137 OBJECTIVE_NEW = 0, "delvewright.ui.objective.new", 1, "New objective: %s";
139 OBJECTIVE_COMPLETE = 1, "delvewright.ui.objective.complete", 1, "Objective complete: %s";
141 CAMPAIGN_COMPLETE = 2, "delvewright.ui.campaign.complete", 1, "%s — complete.";
143 CAMPAIGN_SIGNATURE = 3, "delvewright.ui.campaign.signature", 0, "A Delvewright delve.";
145 CAMPAIGN_BANNER = 4, "delvewright.ui.campaign.banner", 0, "Delve Complete";
147 LOBBY_WAITING = 5, "delvewright.ui.lobby.waiting", 2, "Waiting for the party — %s / %s";
150 CLASS_TITLE = 6, "delvewright.ui.class.title", 0, "Choose your class";
152 CLASS_BODY = 7, "delvewright.ui.class.body", 0, "Pick the kit you will carry.";
154 BOUNDARY_MESSAGE = 8, "delvewright.ui.boundary.message", 0,
157 "The tide turns you back — the delve lies behind you.";
158 GATE_SEALED = 9, "delvewright.ui.gate.sealed", 0, "The way is sealed.";
161 BONFIRE_TITLE = 10, "delvewright.ui.bonfire.title", 0, crate::stages::BONFIRE_PROMPT_EN;
163 BONFIRE_REST = 11, "delvewright.ui.bonfire.rest", 0, crate::stages::BONFIRE_REST_LABEL_EN;
165 BONFIRE_SAVE = 12, "delvewright.ui.bonfire.save", 0, crate::stages::BONFIRE_SAVE_LABEL_EN;
167}
168
169type Table = [&'static str; 13];
171
172const ZH_HANS: Table = [
178 "新目标:%s",
179 "目标完成:%s",
180 "%s——完成。",
181 "一场 Delvewright 秘境。",
182 "秘境通关",
183 "等待队伍集结 —— %s / %s",
184 "选择你的职业",
185 "挑选你要携带的装备。",
186 "潮水将你送回——秘境在你身后。",
187 "此路已封。",
188 "篝火",
189 "休息并存档",
190 "仅存档",
191];
192
193const ZH_HANT: Table = [
194 "新目標:%s",
195 "目標完成:%s",
196 "%s——完成。",
197 "一場 Delvewright 秘境。",
198 "秘境通關",
199 "等待隊伍集結 —— %s / %s",
200 "選擇你的職業",
201 "挑選你要攜帶的裝備。",
202 "潮水將你送回——秘境在你身後。",
203 "此路已封。",
204 "篝火",
205 "休息並存檔",
206 "僅存檔",
207];
208
209const JA: Table = [
210 "新しい目標: %s",
211 "目標達成: %s",
212 "%s — クリア。",
213 "Delvewright の探索です。",
214 "探索完了",
215 "パーティを待っています — %s / %s",
216 "クラスを選択",
217 "携行する装備を選ぼう。",
218 "潮に押し戻された — 探索地は後ろだ。",
219 "道は閉ざされている。",
220 "焚き火",
221 "休息してセーブ",
222 "セーブのみ",
223];
224
225const KO: Table = [
226 "새 목표: %s",
227 "목표 완료: %s",
228 "%s — 완료.",
229 "Delvewright 탐험.",
230 "탐험 완료",
231 "일행을 기다리는 중 — %s / %s",
232 "직업을 선택하세요",
233 "가져갈 장비를 고르세요.",
234 "조류가 당신을 되돌려 보낸다 — 탐험지는 뒤에 있다.",
235 "길이 막혀 있다.",
236 "모닥불",
237 "휴식하고 저장",
238 "저장만",
239];
240
241const DE: Table = [
242 "Neues Ziel: %s",
243 "Ziel erfüllt: %s",
244 "%s – abgeschlossen.",
245 "Ein Delvewright-Abenteuer.",
246 "Abenteuer abgeschlossen",
247 "Warten auf die Gruppe – %s / %s",
248 "Wähle deine Klasse",
249 "Wähle die Ausrüstung, die du mitnimmst.",
250 "Die Flut trägt dich zurück – das Abenteuer liegt hinter dir.",
251 "Der Weg ist versperrt.",
252 "Lagerfeuer",
253 "Rasten und speichern",
254 "Nur speichern",
255];
256
257const FR: Table = [
258 "Nouvel objectif : %s",
259 "Objectif accompli : %s",
260 "%s — terminé.",
261 "Une aventure Delvewright.",
262 "Aventure terminée",
263 "En attente du groupe — %s / %s",
264 "Choisissez votre classe",
265 "Choisissez l'équipement que vous emporterez.",
266 "La marée vous repousse — l'aventure est derrière vous.",
267 "Le passage est scellé.",
268 "Feu de camp",
269 "Se reposer et sauvegarder",
270 "Sauvegarder seulement",
271];
272
273const ES: Table = [
274 "Nuevo objetivo: %s",
275 "Objetivo completado: %s",
276 "%s: completada.",
277 "Una aventura de Delvewright.",
278 "Aventura completada",
279 "Esperando al grupo — %s / %s",
280 "Elige tu clase",
281 "Elige el equipo que llevarás.",
282 "La marea te hace retroceder: la aventura queda a tus espaldas.",
283 "El paso está sellado.",
284 "Hoguera",
285 "Descansar y guardar",
286 "Solo guardar",
287];
288
289const PT_BR: Table = [
290 "Novo objetivo: %s",
291 "Objetivo concluído: %s",
292 "%s — concluída.",
293 "Uma aventura Delvewright.",
294 "Aventura concluída",
295 "Aguardando o grupo — %s / %s",
296 "Escolha sua classe",
297 "Escolha o equipamento que vai levar.",
298 "A maré te empurra de volta — a aventura ficou para trás.",
299 "A passagem está selada.",
300 "Fogueira",
301 "Descansar e salvar",
302 "Apenas salvar",
303];
304
305const PT_PT: Table = [
306 "Novo objetivo: %s",
307 "Objetivo concluído: %s",
308 "%s — concluída.",
309 "Uma aventura Delvewright.",
310 "Aventura concluída",
311 "À espera do grupo — %s / %s",
312 "Escolhe a tua classe",
313 "Escolhe o equipamento que vais levar.",
314 "A maré empurra-te de volta — a aventura ficou para trás.",
315 "A passagem está selada.",
316 "Fogueira",
317 "Descansar e guardar",
318 "Apenas guardar",
319];
320
321const RU: Table = [
322 "Новая цель: %s",
323 "Цель выполнена: %s",
324 "%s — пройдено.",
325 "Приключение Delvewright.",
326 "Приключение пройдено",
327 "Ожидание отряда — %s / %s",
328 "Выберите класс",
329 "Выберите снаряжение, которое возьмёте с собой.",
330 "Прилив относит вас назад — приключение позади.",
331 "Путь запечатан.",
332 "Костёр",
333 "Отдохнуть и сохранить",
334 "Только сохранить",
335];
336
337const IT: Table = [
338 "Nuovo obiettivo: %s",
339 "Obiettivo completato: %s",
340 "%s — completata.",
341 "Un'avventura Delvewright.",
342 "Avventura completata",
343 "In attesa del gruppo — %s / %s",
344 "Scegli la tua classe",
345 "Scegli l'equipaggiamento che porterai.",
346 "La marea ti respinge — l'avventura è alle tue spalle.",
347 "Il passaggio è sigillato.",
348 "Falò",
349 "Riposa e salva",
350 "Salva soltanto",
351];
352
353const NL: Table = [
354 "Nieuw doel: %s",
355 "Doel voltooid: %s",
356 "%s — voltooid.",
357 "Een Delvewright-avontuur.",
358 "Avontuur voltooid",
359 "Wachten op het gezelschap — %s / %s",
360 "Kies je klasse",
361 "Kies de uitrusting die je meeneemt.",
362 "Het tij drijft je terug — het avontuur ligt achter je.",
363 "De doorgang is verzegeld.",
364 "Kampvuur",
365 "Rusten en opslaan",
366 "Alleen opslaan",
367];
368
369const PL: Table = [
370 "Nowy cel: %s",
371 "Cel ukończony: %s",
372 "%s — ukończona.",
373 "Przygoda Delvewright.",
374 "Przygoda ukończona",
375 "Oczekiwanie na drużynę — %s / %s",
376 "Wybierz swoją klasę",
377 "Wybierz ekwipunek, który zabierzesz.",
378 "Przypływ zawraca cię — przygoda została za tobą.",
379 "Przejście jest zapieczętowane.",
380 "Ognisko",
381 "Odpocznij i zapisz",
382 "Tylko zapisz",
383];
384
385const TR: Table = [
386 "Yeni hedef: %s",
387 "Hedef tamamlandı: %s",
388 "%s — tamamlandı.",
389 "Bir Delvewright macerası.",
390 "Macera tamamlandı",
391 "Takım bekleniyor — %s / %s",
392 "Sınıfını seç",
393 "Yanına alacağın teçhizatı seç.",
394 "Gelgit seni geri getiriyor — macera arkanda kaldı.",
395 "Yol mühürlendi.",
396 "Kamp ateşi",
397 "Dinlen ve kaydet",
398 "Sadece kaydet",
399];
400
401const UK: Table = [
402 "Нова ціль: %s",
403 "Ціль виконано: %s",
404 "%s — пройдено.",
405 "Пригода Delvewright.",
406 "Пригоду пройдено",
407 "Очікування загону — %s / %s",
408 "Оберіть клас",
409 "Оберіть спорядження, яке візьмете з собою.",
410 "Приплив відносить вас назад — пригода позаду.",
411 "Шлях запечатано.",
412 "Багаття",
413 "Відпочити та зберегти",
414 "Лише зберегти",
415];
416
417const CS: Table = [
418 "Nový cíl: %s",
419 "Cíl splněn: %s",
420 "%s — dokončeno.",
421 "Dobrodružství Delvewright.",
422 "Dobrodružství dokončeno",
423 "Čekání na družinu — %s / %s",
424 "Vyber si třídu",
425 "Vyber si výbavu, kterou si vezmeš.",
426 "Příliv tě vrací zpět — dobrodružství leží za tebou.",
427 "Cesta je zapečetěná.",
428 "Táborák",
429 "Odpočinout a uložit",
430 "Jen uložit",
431];
432
433const SK: Table = [
434 "Nový cieľ: %s",
435 "Cieľ splnený: %s",
436 "%s — dokončené.",
437 "Dobrodružstvo Delvewright.",
438 "Dobrodružstvo dokončené",
439 "Čaká sa na družinu — %s / %s",
440 "Vyber si triedu",
441 "Vyber si výstroj, ktorú si vezmeš.",
442 "Príliv ťa vracia späť — dobrodružstvo je za tebou.",
443 "Cesta je zapečatená.",
444 "Táborák",
445 "Oddýchnuť si a uložiť",
446 "Len uložiť",
447];
448
449const SV: Table = [
450 "Nytt mål: %s",
451 "Mål avklarat: %s",
452 "%s — avklarat.",
453 "Ett Delvewright-äventyr.",
454 "Äventyret avklarat",
455 "Väntar på sällskapet — %s / %s",
456 "Välj din klass",
457 "Välj utrustningen du tar med dig.",
458 "Tidvattnet för dig tillbaka — äventyret ligger bakom dig.",
459 "Vägen är förseglad.",
460 "Lägereld",
461 "Vila och spara",
462 "Spara endast",
463];
464
465const DA: Table = [
466 "Nyt mål: %s",
467 "Mål fuldført: %s",
468 "%s — fuldført.",
469 "Et Delvewright-eventyr.",
470 "Eventyret fuldført",
471 "Venter på selskabet — %s / %s",
472 "Vælg din klasse",
473 "Vælg det udstyr, du tager med.",
474 "Tidevandet fører dig tilbage — eventyret ligger bag dig.",
475 "Vejen er forseglet.",
476 "Bål",
477 "Hvil og gem",
478 "Gem kun",
479];
480
481const NB: Table = [
482 "Nytt mål: %s",
483 "Mål fullført: %s",
484 "%s — fullført.",
485 "Et Delvewright-eventyr.",
486 "Eventyret fullført",
487 "Venter på følget — %s / %s",
488 "Velg klassen din",
489 "Velg utstyret du tar med deg.",
490 "Tidevannet fører deg tilbake — eventyret ligger bak deg.",
491 "Veien er forseglet.",
492 "Bål",
493 "Hvil og lagre",
494 "Bare lagre",
495];
496
497const FI: Table = [
498 "Uusi tavoite: %s",
499 "Tavoite suoritettu: %s",
500 "%s — suoritettu.",
501 "Delvewright-seikkailu.",
502 "Seikkailu suoritettu",
503 "Odotetaan ryhmää — %s / %s",
504 "Valitse luokkasi",
505 "Valitse mukaan otettavat varusteet.",
506 "Vuorovesi kääntää sinut takaisin — seikkailu jää taaksesi.",
507 "Tie on sinetöity.",
508 "Nuotio",
509 "Lepää ja tallenna",
510 "Vain tallennus",
511];
512
513const HU: Table = [
514 "Új cél: %s",
515 "Cél teljesítve: %s",
516 "%s — teljesítve.",
517 "Egy Delvewright-kaland.",
518 "A kaland teljesítve",
519 "Várakozás a csapatra — %s / %s",
520 "Válaszd ki a kasztodat",
521 "Válaszd ki a magaddal vitt felszerelést.",
522 "Az ár visszasodor — a kaland mögötted maradt.",
523 "Az út le van pecsételve.",
524 "Tábortűz",
525 "Pihenés és mentés",
526 "Csak mentés",
527];
528
529const RO: Table = [
530 "Obiectiv nou: %s",
531 "Obiectiv îndeplinit: %s",
532 "%s — încheiată.",
533 "O aventură Delvewright.",
534 "Aventură încheiată",
535 "Se așteaptă grupul — %s / %s",
536 "Alege-ți clasa",
537 "Alege echipamentul pe care îl vei lua.",
538 "Mareea te împinge înapoi — aventura a rămas în urmă.",
539 "Trecerea este pecetluită.",
540 "Foc de tabără",
541 "Odihnește-te și salvează",
542 "Doar salvează",
543];
544
545const EL: Table = [
546 "Νέος στόχος: %s",
547 "Ο στόχος ολοκληρώθηκε: %s",
548 "%s — ολοκληρώθηκε.",
549 "Μια περιπέτεια Delvewright.",
550 "Η περιπέτεια ολοκληρώθηκε",
551 "Αναμονή για την ομάδα — %s / %s",
552 "Διάλεξε την κλάση σου",
553 "Διάλεξε τον εξοπλισμό που θα πάρεις μαζί σου.",
554 "Η παλίρροια σε γυρίζει πίσω — η περιπέτεια είναι πίσω σου.",
555 "Το πέρασμα είναι σφραγισμένο.",
556 "Φωτιά",
557 "Ξεκουράσου και αποθήκευσε",
558 "Μόνο αποθήκευση",
559];
560
561const BG: Table = [
562 "Нова цел: %s",
563 "Целта е изпълнена: %s",
564 "%s — завършено.",
565 "Приключение на Delvewright.",
566 "Приключението е завършено",
567 "Изчакване на групата — %s / %s",
568 "Избери своя клас",
569 "Избери снаряжението, което ще носиш.",
570 "Приливът те връща назад — приключението остана зад теб.",
571 "Пътят е запечатан.",
572 "Огън",
573 "Почини си и запази",
574 "Само запази",
575];
576
577const TH: Table = [
578 "เป้าหมายใหม่: %s",
579 "ทำเป้าหมายสำเร็จ: %s",
580 "%s — จบแล้ว",
581 "การผจญภัยของ Delvewright",
582 "ผจญภัยสำเร็จ",
583 "กำลังรอกลุ่ม — %s / %s",
584 "เลือกคลาสของคุณ",
585 "เลือกอุปกรณ์ที่คุณจะนำติดตัวไป",
586 "กระแสน้ำพัดคุณกลับมา — การผจญภัยอยู่ข้างหลังคุณแล้ว",
587 "ทางนี้ถูกปิดผนึกไว้",
588 "กองไฟ",
589 "พักและบันทึก",
590 "บันทึกเท่านั้น",
591];
592
593const VI: Table = [
594 "Mục tiêu mới: %s",
595 "Hoàn thành mục tiêu: %s",
596 "%s — hoàn thành.",
597 "Một cuộc phiêu lưu Delvewright.",
598 "Hoàn thành cuộc phiêu lưu",
599 "Đang chờ cả nhóm — %s / %s",
600 "Chọn lớp nhân vật của bạn",
601 "Chọn trang bị bạn sẽ mang theo.",
602 "Thủy triều đẩy bạn trở lại — cuộc phiêu lưu ở phía sau bạn.",
603 "Lối đi đã bị niêm phong.",
604 "Lửa trại",
605 "Nghỉ ngơi và lưu",
606 "Chỉ lưu",
607];
608
609const ID: Table = [
610 "Tujuan baru: %s",
611 "Tujuan selesai: %s",
612 "%s — selesai.",
613 "Sebuah petualangan Delvewright.",
614 "Petualangan selesai",
615 "Menunggu rombongan — %s / %s",
616 "Pilih kelasmu",
617 "Pilih perlengkapan yang akan kamu bawa.",
618 "Air pasang membawamu kembali — petualangan ada di belakangmu.",
619 "Jalan ini tersegel.",
620 "Api unggun",
621 "Istirahat dan simpan",
622 "Simpan saja",
623];
624
625const MS: Table = [
626 "Objektif baharu: %s",
627 "Objektif selesai: %s",
628 "%s — selesai.",
629 "Sebuah pengembaraan Delvewright.",
630 "Pengembaraan selesai",
631 "Menunggu kumpulan — %s / %s",
632 "Pilih kelas anda",
633 "Pilih kelengkapan yang akan anda bawa.",
634 "Air pasang membawa anda kembali — pengembaraan berada di belakang anda.",
635 "Laluan ini telah dimeterai.",
636 "Unggun api",
637 "Berehat dan simpan",
638 "Simpan sahaja",
639];
640
641const AR: Table = [
642 "هدف جديد: %s",
643 "اكتمل الهدف: %s",
644 "%s — اكتملت.",
645 "مغامرة من Delvewright.",
646 "اكتملت المغامرة",
647 "في انتظار الفريق — %s / %s",
648 "اختر فئتك",
649 "اختر العتاد الذي ستحمله.",
650 "المد يعيدك — المغامرة خلفك.",
651 "الطريق مغلق.",
652 "نار المخيم",
653 "استرح واحفظ",
654 "احفظ فقط",
655];
656
657const HE: Table = [
658 "יעד חדש: %s",
659 "היעד הושלם: %s",
660 "%s — הושלמה.",
661 "הרפתקה של Delvewright.",
662 "ההרפתקה הושלמה",
663 "ממתינים לחבורה — %s / %s",
664 "בחר את המחלקה שלך",
665 "בחר את הציוד שתישא איתך.",
666 "הגאות מחזירה אותך — ההרפתקה מאחוריך.",
667 "הדרך חתומה.",
668 "מדורה",
669 "לנוח ולשמור",
670 "לשמור בלבד",
671];
672
673const HI: Table = [
674 "नया लक्ष्य: %s",
675 "लक्ष्य पूरा हुआ: %s",
676 "%s — पूर्ण।",
677 "एक Delvewright साहसिक यात्रा।",
678 "साहसिक यात्रा पूर्ण",
679 "दल की प्रतीक्षा — %s / %s",
680 "अपनी श्रेणी चुनें",
681 "वह सामान चुनें जो आप साथ ले जाएँगे।",
682 "ज्वार आपको वापस मोड़ देता है — यात्रा आपके पीछे रह गई।",
683 "यह रास्ता बंद है।",
684 "अलाव",
685 "विश्राम करें और सहेजें",
686 "केवल सहेजें",
687];
688
689const CA: Table = [
690 "Nou objectiu: %s",
691 "Objectiu completat: %s",
692 "%s — completada.",
693 "Una aventura de Delvewright.",
694 "Aventura completada",
695 "Esperant el grup — %s / %s",
696 "Tria la teva classe",
697 "Tria l'equipament que t'enduràs.",
698 "La marea et fa retrocedir: l'aventura queda enrere.",
699 "El pas està segellat.",
700 "Foguera",
701 "Descansa i desa",
702 "Només desa",
703];
704
705const FIL: Table = [
706 "Bagong layunin: %s",
707 "Natapos ang layunin: %s",
708 "%s — tapos na.",
709 "Isang pakikipagsapalaran ng Delvewright.",
710 "Tapos na ang pakikipagsapalaran",
711 "Naghihintay sa grupo — %s / %s",
712 "Piliin ang iyong klase",
713 "Piliin ang kagamitang dadalhin mo.",
714 "Ibinalik ka ng agos — nasa likuran mo na ang pakikipagsapalaran.",
715 "Nakasara ang daan.",
716 "Siga",
717 "Magpahinga at mag-save",
718 "Mag-save lamang",
719];
720
721const TABLES: &[(&str, &Table)] = &[
730 ("ar_sa", &AR),
731 ("bg_bg", &BG),
732 ("ca_es", &CA),
733 ("cs_cz", &CS),
734 ("da_dk", &DA),
735 ("de_at", &DE),
736 ("de_ch", &DE),
737 ("de_de", &DE),
738 ("el_gr", &EL),
739 ("es_ar", &ES),
740 ("es_cl", &ES),
741 ("es_ec", &ES),
742 ("es_es", &ES),
743 ("es_mx", &ES),
744 ("es_uy", &ES),
745 ("es_ve", &ES),
746 ("fi_fi", &FI),
747 ("fil_ph", &FIL),
748 ("fr_ca", &FR),
749 ("fr_ch", &FR),
750 ("fr_fr", &FR),
751 ("he_il", &HE),
752 ("hi_in", &HI),
753 ("hu_hu", &HU),
754 ("id_id", &ID),
755 ("it_it", &IT),
756 ("ja_jp", &JA),
757 ("ko_kr", &KO),
758 ("ms_my", &MS),
759 ("nl_be", &NL),
760 ("nl_nl", &NL),
761 ("no_no", &NB),
762 ("pl_pl", &PL),
763 ("pt_br", &PT_BR),
764 ("pt_pt", &PT_PT),
765 ("ro_ro", &RO),
766 ("ru_ru", &RU),
767 ("sk_sk", &SK),
768 ("sv_se", &SV),
769 ("th_th", &TH),
770 ("tl_ph", &FIL),
771 ("tr_tr", &TR),
772 ("uk_ua", &UK),
773 ("vi_vn", &VI),
774 ("zh_cn", &ZH_HANS),
775 ("zh_hk", &ZH_HANT),
776 ("zh_tw", &ZH_HANT),
777];
778
779fn table(mc_code: &str) -> Option<&'static Table> {
782 TABLES.iter().find(|(c, _)| *c == mc_code).map(|(_, t)| *t)
783}
784
785pub fn lang_entries(mc_code: &str) -> BTreeMap<String, String> {
790 let Some(t) = table(mc_code) else {
791 return BTreeMap::new();
792 };
793 ALL.iter()
794 .map(|c| (c.key.to_string(), t[c.slot].to_string()))
795 .collect()
796}
797
798pub fn english_entries() -> BTreeMap<String, String> {
800 ALL.iter()
801 .map(|c| (c.key.to_string(), c.en.to_string()))
802 .collect()
803}
804
805pub fn coverage(mc_code: &str) -> (usize, usize) {
809 (lang_entries(mc_code).len(), ALL.len())
810}
811
812pub fn validate_chrome_namespace(sidecars: &BTreeMap<String, L10nDoc>) -> Vec<Diagnostic> {
821 let mut d = Vec::new();
822 for (lang, doc) in sidecars {
823 for key in doc.content.keys() {
824 if !key.starts_with(RESERVED_PREFIX) {
825 continue;
826 }
827 d.push(Diagnostic::error(
828 codes::CHROME_RESERVED,
829 "l10n",
830 format!("l10n/{lang}.json#/content/{key}"),
831 format!(
832 "`{key}` is in the reserved `{RESERVED_PREFIX}` namespace, which the compiler \
833 owns: those are the engine's own on-screen strings (`New objective: `, \
834 `Choose your class`, …), they ship translated with the compiler, and no \
835 campaign authors or overrides them. Remove `{key}` from \
836 `l10n/{lang}.json` — to change what a campaign's own text says, translate \
837 that campaign's key instead"
838 ),
839 ));
840 }
841 }
842 d
843}
844
845pub const RESERVED_PREFIX: &str = "delvewright.";
848
849#[derive(Clone, Debug, Default)]
861pub struct Chrome {
862 baked: Option<&'static Table>,
864}
865
866impl Chrome {
867 pub fn for_build(language: Option<&str>) -> Self {
870 Self {
871 baked: language
872 .and_then(crate::mclang::mc_lang_code)
873 .and_then(table),
874 }
875 }
876
877 fn text(&self, c: ChromeString) -> &str {
879 match self.baked {
880 Some(t) => t[c.slot],
881 None => c.en,
882 }
883 }
884
885 pub fn get(&self, c: ChromeString) -> String {
887 c.tagged_as(self.text(c))
888 }
889
890 pub fn rebind(&self, s: &str) -> String {
896 let Some((key, _)) = crate::l10n::untag(s) else {
897 return s.to_string();
898 };
899 match ALL.iter().find(|c| c.key == key) {
900 Some(c) => self.get(*c),
901 None => s.to_string(),
902 }
903 }
904}
905
906#[cfg(test)]
907mod tests {
908 use super::*;
909
910 fn placeholders(s: &str) -> usize {
911 s.matches("%s").count()
912 }
913
914 #[test]
918 fn chrome_strings_are_well_formed() {
919 let mut seen = std::collections::BTreeSet::new();
920 for (i, c) in ALL.iter().enumerate() {
921 assert_eq!(c.slot, i, "`{}` declares the wrong slot", c.key);
922 assert!(
923 c.key.starts_with("delvewright.ui."),
924 "chrome key `{}` is outside the reserved namespace",
925 c.key
926 );
927 assert!(
928 c.key.starts_with(RESERVED_PREFIX),
929 "`{}` must live under the reserved prefix",
930 c.key
931 );
932 assert!(!c.en.is_empty(), "chrome key `{}` has no English", c.key);
933 assert_eq!(
934 placeholders(c.en),
935 c.args,
936 "`{}` declares {} args but its English has {}",
937 c.key,
938 c.args,
939 placeholders(c.en)
940 );
941 assert!(seen.insert(c.key), "duplicate chrome key `{}`", c.key);
942 }
943 assert_eq!(ALL.len(), 13, "the chrome inventory changed size");
944 }
945
946 #[test]
951 fn every_table_is_complete_and_placeholder_faithful() {
952 assert!(!TABLES.is_empty(), "no chrome table binds");
953 for (code, t) in TABLES {
954 assert!(
955 crate::mclang::CLIENT_LANGS.contains(code),
956 "`{code}` has a chrome table but is not a language the pinned client loads"
957 );
958 for c in ALL {
959 let s = t[c.slot];
960 assert!(!s.is_empty(), "`{code}` leaves `{}` empty", c.key);
961 assert_eq!(
962 placeholders(s),
963 c.args,
964 "`{code}` renders `{}` with {} placeholder(s), English has {}",
965 c.key,
966 placeholders(s),
967 c.args
968 );
969 }
970 }
971 }
972
973 #[test]
976 fn table_rows_are_sorted_and_unique() {
977 let codes: Vec<&str> = TABLES.iter().map(|(c, _)| *c).collect();
978 assert!(
979 codes.windows(2).all(|w| w[0] < w[1]),
980 "TABLES must stay sorted by language stem"
981 );
982 }
983
984 #[test]
987 fn zh_cn_is_complete() {
988 assert_eq!(coverage("zh_cn"), (13, 13));
989 assert_eq!(lang_entries("zh_cn")[CLASS_TITLE.key], "选择你的职业");
990 }
991
992 #[test]
995 fn untabled_language_ships_no_chrome() {
996 assert_eq!(coverage("tlh_aa"), (0, 13));
997 assert!(lang_entries("en_us").is_empty());
998 assert_eq!(english_entries().len(), 13);
999 }
1000
1001 #[test]
1004 fn build_carries_english_and_bake_carries_its_language() {
1005 let multi = Chrome::for_build(None);
1006 assert_eq!(multi.get(CLASS_TITLE), CLASS_TITLE.tagged());
1007 assert_eq!(multi.rebind(&CLASS_TITLE.tagged()), CLASS_TITLE.tagged());
1008
1009 let zh = Chrome::for_build(Some("zh-cn"));
1010 assert_eq!(zh.get(CLASS_TITLE), tag(CLASS_TITLE.key, "选择你的职业"));
1011 assert_eq!(
1012 zh.rebind(&CLASS_TITLE.tagged()),
1013 tag(CLASS_TITLE.key, "选择你的职业")
1014 );
1015
1016 let klingon = Chrome::for_build(Some("tlh-aa"));
1017 assert_eq!(klingon.get(CLASS_TITLE), CLASS_TITLE.tagged());
1018 }
1019
1020 #[test]
1023 fn rebind_leaves_authored_strings_alone() {
1024 let zh = Chrome::for_build(Some("zh-cn"));
1025 assert_eq!(zh.rebind("Shrine fire"), "Shrine fire");
1026 let authored = tag("fx.q.done.0.rest_prompt", "Shrine fire");
1027 assert_eq!(zh.rebind(&authored), authored);
1028 }
1029
1030 #[test]
1032 fn sidecar_may_not_shadow_chrome() {
1033 let mut content = BTreeMap::new();
1034 content.insert(CLASS_TITLE.key.to_string(), "俺の職業".to_string());
1035 content.insert("npc.keeper.name".to_string(), "門番".to_string());
1036 let doc = L10nDoc {
1037 dsl_version: crate::DSL_VERSION.to_string(),
1038 campaign_id: crate::ids::CampaignId("demo".to_string()),
1039 kind: crate::l10n::L10nKind::L10n,
1040 lang: "ja-jp".to_string(),
1041 content,
1042 source: BTreeMap::new(),
1043 };
1044 let d = validate_chrome_namespace(&BTreeMap::from([("ja-jp".to_string(), doc)]));
1045 assert_eq!(d.len(), 1, "exactly the chrome row is flagged");
1046 assert_eq!(d[0].code, codes::CHROME_RESERVED);
1047 assert!(d[0].message.contains(CLASS_TITLE.key));
1048 }
1049}