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