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