First commit, done on M6 from the PLAN.MD

This commit is contained in:
gauvainboiche
2026-10-25 17:34:44 +01:00
parent d5dc2a3901
commit 04193e2013
86 changed files with 8396 additions and 0 deletions
+189
View File
@@ -0,0 +1,189 @@
{
"app.title": "The Desk",
"dash.title": "Ministerial Briefing",
"dash.confidential": "Strictly confidential",
"dash.econ": "Economic figures",
"dash.intel": "Intelligence summary",
"dash.classified": "Classified",
"ind.order": "Order",
"ind.stability": "Stability",
"ind.civilRights": "Civil rights",
"ind.opinion": "Public opinion",
"ind.economy": "Economy",
"ind.hardPower": "Hard power",
"ind.softPower": "Soft power",
"ind.unrest": "Unrest",
"ind.armyLoyalty": "Army loyalty",
"opinion.0": "Angry",
"opinion.1": "Hostile",
"opinion.2": "Uneasy",
"opinion.3": "Neutral",
"opinion.4": "Content",
"opinion.5": "Happy",
"opinion.6": "Ecstatic",
"unrest.0": "Calm",
"unrest.1": "Restless",
"unrest.2": "Tense",
"unrest.3": "Volatile",
"unrest.4": "Explosive",
"army.0": "Mutinous",
"army.1": "Wavering",
"army.2": "Steady",
"army.3": "Loyal",
"econ.gdp": "GDP",
"econ.growth": "Growth",
"econ.unemployment": "Unemployment",
"econ.inflation": "Inflation",
"econ.debt": "Debt / GDP",
"econ.deficit": "Deficit / GDP",
"econ.rate": "Interest rate",
"econ.gdpUnit": "bn {currency}",
"cal.dayCount": "Day {day} of {total}",
"cal.briefing": "Next briefing",
"cal.review": "Budget review",
"cal.inDays": "in {n} days",
"cal.today": "today",
"cal.tomorrow": "tomorrow",
"cal.pause": "Pause",
"cal.speed": "Speed {n}×",
"inbox.title": "In-tray",
"inbox.idle": "Nothing awaits your signature. The country carries on.",
"inbox.overTitle": "End of term",
"inbox.overBody": "Your term has ended. (The final review arrives in a later build.)",
"news.edition": "Edition of {date}",
"sector.education": "Education",
"sector.health": "Health",
"sector.social": "Social & pensions",
"sector.police": "Police",
"sector.army": "Army",
"sector.justice": "Justice",
"sector.industry": "Industry & energy",
"sector.transport": "Public transport",
"sector.infrastructure": "Infrastructure",
"sector.housing": "Housing",
"sector.research": "Research",
"sector.culture": "Culture & media",
"sector.foreignAffairs": "Foreign affairs",
"sector.environment": "Environment",
"sector.administration": "Administration",
"tax.income": "Income tax",
"tax.corporate": "Corporate tax",
"tax.vat": "VAT",
"ledger.title": "Treasury ledger",
"ledger.spending": "Spending, % of GDP",
"ledger.spendingHint": "The tick on each line marks what the sector needs.",
"ledger.totalAlloc": "Allocated to sectors",
"ledger.revenue": "Revenue: tax rates",
"ledger.revenueHint": "The tick marks the rate where revenue peaks. Beyond it, you collect less.",
"ledger.yield": "{n} % of GDP",
"ledger.quality": "Quality {now} → {target}",
"ledger.summary": "Treasury summary",
"ledger.sumRevenue": "Revenue",
"ledger.sumSectors": "Sector spending",
"ledger.sumInterest": "Debt interest",
"ledger.sumSpending": "Total spending",
"ledger.sumSurplus": "Surplus",
"ledger.sumDeficit": "Deficit",
"ledger.perYear": "{n} bn {currency} per year",
"ledger.projection": "Next four quarters, if nothing else changes",
"ledger.now": "Now",
"ledger.months": "+{n} mo",
"ledger.reset": "Reset",
"ledger.confirm": "Sign and file",
"ledger.error": "The Treasury rejected this budget.",
"kind.memo": "Memorandum",
"kind.telegram": "Telegram",
"kind.petition": "Petition",
"kind.report": "Report",
"kind.letter": "Letter",
"card.approved": "Approved",
"card.rejected": "Rejected",
"card.keys": "Keys: Y approve · N reject",
"card.from": "From",
"profile.title": "Sworn in",
"profile.lead": "Before you take office, the Press Office needs one thing for the official records.",
"profile.body": "How should the press refer to you?",
"profile.he": "He / Him",
"profile.she": "She / Her",
"profile.they": "They / Them",
"profile.note": "This only changes how you are named in the history books.",
"alert.urgent": "Urgent telegram",
"alert.ack": "Understood",
"ind.regime": "Hold on power",
"regime.0": "Secure",
"regime.1": "Shaky",
"regime.2": "Critical",
"end.verdict": "The historians' verdict",
"end.score": "Score {n} / 100",
"end.grade": "Grade",
"end.served": "Served {days} days",
"end.decisions": "Decisions that defined the term",
"end.noDecisions": "No decision was signed.",
"end.chose": "You chose: {label}",
"end.again": "Play again",
"end.averages": "avg",
"end.final": "end",
"cal.menu": "Menu",
"menu.secret": "Top secret",
"menu.tagline": "A country. Five years. Every decision carries your signature.",
"menu.paused": "Game paused",
"menu.continue": "Continue",
"menu.resume": "Resume",
"menu.new": "New game",
"menu.load": "Load game",
"menu.save": "Save game",
"menu.export": "Export",
"menu.import": "Import",
"menu.settings": "Settings",
"menu.credits": "Credits",
"menu.quit": "Main menu",
"menu.back": "Back",
"menu.info": "{country} · day {day}",
"new.title": "New game",
"new.name": "Name of the country",
"new.scenario": "Scenario",
"new.pronouns": "How should the press refer to you?",
"new.start": "Take office",
"load.title": "Load game",
"load.auto": "Autosave",
"load.slot": "Slot {n}",
"load.empty": "Empty",
"load.info": "{country} · day {day}{date}",
"load.finished": "finished",
"load.unreadable": "Unreadable save",
"load.load": "Load",
"load.delete": "Delete",
"load.sure": "Sure?",
"load.replace": "Loading replaces the game in progress.",
"save.title": "Save game",
"save.here": "Save here",
"save.overwrite": "Overwrite",
"save.done": "Saved to slot {n}.",
"save.failed": "The browser refused the save. Use Export to keep your game.",
"export.title": "Export",
"export.hint": "Copy this text or download the file. You can import it later, on any device.",
"export.copy": "Copy",
"export.copied": "Copied.",
"export.copyFailed": "Select the text and copy it by hand.",
"export.download": "Download .sav",
"import.title": "Import",
"import.hint": "Paste a save string, or choose a .sav file.",
"import.file": "Choose a file",
"import.go": "Import",
"import.error.format": "That does not look like a save string.",
"import.error.checksum": "The save is damaged or incomplete (checksum mismatch).",
"import.error.tooNew": "This save comes from a newer version of the game.",
"import.error.version": "This save is from a version that cannot be converted.",
"import.error.invalid": "The contents of this save are not valid.",
"import.error.scenario": "This save uses a scenario that is not installed.",
"import.error.empty": "Nothing to import yet.",
"settings.title": "Settings",
"settings.language": "Language",
"settings.slow": "Slow time",
"settings.slowHint": "One in-game day takes 60 seconds instead of 10.",
"settings.storage": "This browser is not keeping saves between visits (private window or blocked storage). Use Export to keep your game.",
"credits.title": "Credits",
"credits.fiction": "Valdoria and everyone in it are fictional.",
"credits.tech": "Built with Go and plain JavaScript, with no third-party libraries.",
"credits.fonts": "Fonts: Special Elite (Apache License 2.0), IM Fell English and Courier Prime (SIL Open Font License). The licence files are shipped with the game."
}
+189
View File
@@ -0,0 +1,189 @@
{
"app.title": "Le Bureau",
"dash.title": "Note ministérielle",
"dash.confidential": "Strictement confidentiel",
"dash.econ": "Chiffres économiques",
"dash.intel": "Synthèse du renseignement",
"dash.classified": "Classifié",
"ind.order": "Ordre public",
"ind.stability": "Stabilité",
"ind.civilRights": "Droits civiques",
"ind.opinion": "Opinion publique",
"ind.economy": "Économie",
"ind.hardPower": "Puissance militaire",
"ind.softPower": "Influence",
"ind.unrest": "Agitation",
"ind.armyLoyalty": "Loyauté de l'armée",
"opinion.0": "En colère",
"opinion.1": "Hostile",
"opinion.2": "Inquiète",
"opinion.3": "Neutre",
"opinion.4": "Satisfaite",
"opinion.5": "Heureuse",
"opinion.6": "Extatique",
"unrest.0": "Calme",
"unrest.1": "Agité",
"unrest.2": "Tendu",
"unrest.3": "Volatil",
"unrest.4": "Explosif",
"army.0": "Mutinée",
"army.1": "Hésitante",
"army.2": "Stable",
"army.3": "Loyale",
"econ.gdp": "PIB",
"econ.growth": "Croissance",
"econ.unemployment": "Chômage",
"econ.inflation": "Inflation",
"econ.debt": "Dette / PIB",
"econ.deficit": "Déficit / PIB",
"econ.rate": "Taux d'intérêt",
"econ.gdpUnit": "Md {currency}",
"cal.dayCount": "Jour {day} sur {total}",
"cal.briefing": "Prochain dossier",
"cal.review": "Revue budgétaire",
"cal.inDays": "dans {n} jours",
"cal.today": "aujourd'hui",
"cal.tomorrow": "demain",
"cal.pause": "Pause",
"cal.speed": "Vitesse {n}×",
"inbox.title": "Corbeille d'arrivée",
"inbox.idle": "Rien n'attend votre signature. Le pays poursuit sa route.",
"inbox.overTitle": "Fin de mandat",
"inbox.overBody": "Votre mandat est terminé. (Le bilan final arrive dans une prochaine version.)",
"news.edition": "Édition du {date}",
"sector.education": "Éducation",
"sector.health": "Santé",
"sector.social": "Social, retraites",
"sector.police": "Police",
"sector.army": "Armée",
"sector.justice": "Justice",
"sector.industry": "Industrie, énergie",
"sector.transport": "Transports publics",
"sector.infrastructure": "Infrastructures",
"sector.housing": "Logement",
"sector.research": "Recherche",
"sector.culture": "Culture, médias",
"sector.foreignAffairs": "Diplomatie",
"sector.environment": "Environnement",
"sector.administration": "Administration",
"tax.income": "Impôt revenu",
"tax.corporate": "Impôt sociétés",
"tax.vat": "TVA",
"ledger.title": "Registre du Trésor",
"ledger.spending": "Dépenses, % du PIB",
"ledger.spendingHint": "Le repère sur chaque ligne marque le besoin du secteur.",
"ledger.totalAlloc": "Alloué aux secteurs",
"ledger.revenue": "Recettes : taux d'imposition",
"ledger.revenueHint": "Le repère marque le taux où les recettes sont maximales. Au-delà, vous collectez moins.",
"ledger.yield": "{n} % du PIB",
"ledger.quality": "Qualité {now} → {target}",
"ledger.summary": "Synthèse du Trésor",
"ledger.sumRevenue": "Recettes",
"ledger.sumSectors": "Dépenses sectorielles",
"ledger.sumInterest": "Intérêts de la dette",
"ledger.sumSpending": "Dépenses totales",
"ledger.sumSurplus": "Excédent",
"ledger.sumDeficit": "Déficit",
"ledger.perYear": "{n} Md {currency} par an",
"ledger.projection": "Quatre prochains trimestres, sans autre changement",
"ledger.now": "Auj.",
"ledger.months": "+{n} m",
"ledger.reset": "Réinitialiser",
"ledger.confirm": "Signer et classer",
"ledger.error": "Le Trésor a rejeté ce budget.",
"kind.memo": "Mémorandum",
"kind.telegram": "Télégramme",
"kind.petition": "Pétition",
"kind.report": "Rapport",
"kind.letter": "Lettre",
"card.approved": "Approuvé",
"card.rejected": "Rejeté",
"card.keys": "Touches : Y approuver · N rejeter",
"card.from": "De la part de",
"profile.title": "Prestation de serment",
"profile.lead": "Avant votre entrée en fonction, le service de presse a besoin d'une précision pour les registres officiels.",
"profile.body": "Comment la presse doit-elle s'adresser à vous ?",
"profile.he": "Il / Lui",
"profile.she": "Elle / Elle",
"profile.they": "Iel / Ielle",
"profile.note": "Cela ne change que la manière dont vous serez nommé dans les livres d'histoire.",
"alert.urgent": "Télégramme urgent",
"alert.ack": "Compris",
"ind.regime": "Emprise sur le pouvoir",
"regime.0": "Solide",
"regime.1": "Fragile",
"regime.2": "Critique",
"end.verdict": "Le verdict des historiens",
"end.score": "Score {n} / 100",
"end.grade": "Note",
"end.served": "{days} jours au pouvoir",
"end.decisions": "Les décisions qui ont marqué le mandat",
"end.noDecisions": "Aucune décision n'a été signée.",
"end.chose": "Votre choix : {label}",
"end.again": "Rejouer",
"end.averages": "moy.",
"end.final": "fin",
"cal.menu": "Menu",
"menu.secret": "Secret défense",
"menu.tagline": "Un pays. Cinq ans. Chaque décision porte votre signature.",
"menu.paused": "Partie en pause",
"menu.continue": "Continuer",
"menu.resume": "Reprendre",
"menu.new": "Nouvelle partie",
"menu.load": "Charger une partie",
"menu.save": "Sauvegarder",
"menu.export": "Exporter",
"menu.import": "Importer",
"menu.settings": "Réglages",
"menu.credits": "Crédits",
"menu.quit": "Menu principal",
"menu.back": "Retour",
"menu.info": "{country} · jour {day}",
"new.title": "Nouvelle partie",
"new.name": "Nom du pays",
"new.scenario": "Scénario",
"new.pronouns": "Comment la presse doit-elle s'adresser à vous ?",
"new.start": "Prendre ses fonctions",
"load.title": "Charger une partie",
"load.auto": "Sauvegarde auto",
"load.slot": "Emplacement {n}",
"load.empty": "Vide",
"load.info": "{country} · jour {day}{date}",
"load.finished": "terminée",
"load.unreadable": "Sauvegarde illisible",
"load.load": "Charger",
"load.delete": "Supprimer",
"load.sure": "Sûr ?",
"load.replace": "Charger remplace la partie en cours.",
"save.title": "Sauvegarder",
"save.here": "Sauvegarder ici",
"save.overwrite": "Écraser",
"save.done": "Sauvegardé dans l'emplacement {n}.",
"save.failed": "Le navigateur a refusé la sauvegarde. Utilisez Exporter pour garder votre partie.",
"export.title": "Exporter",
"export.hint": "Copiez ce texte ou téléchargez le fichier. Vous pourrez l'importer plus tard, sur n'importe quel appareil.",
"export.copy": "Copier",
"export.copied": "Copié.",
"export.copyFailed": "Sélectionnez le texte et copiez-le à la main.",
"export.download": "Télécharger le .sav",
"import.title": "Importer",
"import.hint": "Collez une chaîne de sauvegarde, ou choisissez un fichier .sav.",
"import.file": "Choisir un fichier",
"import.go": "Importer",
"import.error.format": "Cela ne ressemble pas à une chaîne de sauvegarde.",
"import.error.checksum": "La sauvegarde est endommagée ou incomplète (somme de contrôle invalide).",
"import.error.tooNew": "Cette sauvegarde vient d'une version plus récente du jeu.",
"import.error.version": "Cette sauvegarde vient d'une version qui ne peut pas être convertie.",
"import.error.invalid": "Le contenu de cette sauvegarde n'est pas valide.",
"import.error.scenario": "Cette sauvegarde utilise un scénario qui n'est pas installé.",
"import.error.empty": "Rien à importer pour l'instant.",
"settings.title": "Réglages",
"settings.language": "Langue",
"settings.slow": "Temps ralenti",
"settings.slowHint": "Un jour en jeu dure 60 secondes au lieu de 10.",
"settings.storage": "Ce navigateur ne conserve pas les sauvegardes entre deux visites (fenêtre privée ou stockage bloqué). Utilisez Exporter pour garder votre partie.",
"credits.title": "Crédits",
"credits.fiction": "La Valdorie et tous ses habitants sont fictifs.",
"credits.tech": "Réalisé avec Go et du JavaScript pur, sans bibliothèque tierce.",
"credits.fonts": "Polices : Special Elite (licence Apache 2.0), IM Fell English et Courier Prime (licence SIL Open Font). Les fichiers de licence sont fournis avec le jeu."
}
+72
View File
@@ -0,0 +1,72 @@
// UI strings live in en.json / fr.json. Content fields (cards, newspaper) are
// { en, fr } objects read through tx().
const dict = {};
let lang = "en";
export async function initI18n(initial = "en") {
for (const l of ["en", "fr"]) {
dict[l] = await (await fetch(`js/i18n/${l}.json`)).json();
}
setLang(initial);
}
export function setLang(l) {
lang = dict[l] ? l : "en";
document.documentElement.lang = lang;
}
export const getLang = () => lang;
export function t(key, vars) {
let s = dict[lang]?.[key] ?? dict.en?.[key] ?? key;
if (vars) for (const [k, v] of Object.entries(vars)) s = s.replaceAll(`{${k}}`, v);
return s;
}
// Pick the current language from a { en, fr } content field.
export const tx = (obj) => obj?.[lang] ?? obj?.en ?? "";
// Pronoun tokens for text that refers to the player: {sub} {Sub} {obj} {pos} {is} {has} {was}
// in English, {sub} {Sub} {tonic} {e} (agreement ending) in French. Text that needs none of
// them should avoid pronouns altogether.
const PRONOUN_FORMS = {
en: {
he: { sub: "he", Sub: "He", obj: "him", pos: "his", is: "is", has: "has", was: "was" },
she: { sub: "she", Sub: "She", obj: "her", pos: "her", is: "is", has: "has", was: "was" },
they: { sub: "they", Sub: "They", obj: "them", pos: "their", is: "are", has: "have", was: "were" },
},
fr: {
he: { sub: "il", Sub: "Il", tonic: "lui", e: "" },
she: { sub: "elle", Sub: "Elle", tonic: "elle", e: "e" },
they: { sub: "iel", Sub: "Iel", tonic: "ielle", e: "·e" },
},
};
export function tp(text, pronouns = "they") {
const forms = PRONOUN_FORMS[lang]?.[pronouns] ?? PRONOUN_FORMS[lang]?.they ?? {};
return text.replace(/\{(\w+)\}/g, (match, key) => forms[key] ?? match);
}
// A historian's title: English is one string, French has a form per pronoun set.
export function tt(title, pronouns = "they") {
const v = title?.[lang] ?? title?.en ?? "";
return typeof v === "string" ? v : (v[pronouns] ?? v.they);
}
export function fmt(n, digits = 1) {
return new Intl.NumberFormat(lang, { minimumFractionDigits: digits, maximumFractionDigits: digits }).format(n);
}
// ISO date (YYYY-MM-DD, treated as UTC) → long localized parts.
export function dateParts(iso) {
const d = new Date(`${iso}T00:00:00Z`);
const f = (opts) => new Intl.DateTimeFormat(lang, { timeZone: "UTC", ...opts }).format(d);
return {
weekday: f({ weekday: "long" }),
day: f({ day: "numeric" }),
month: f({ month: "long" }),
year: f({ year: "numeric" }),
full: f({ dateStyle: "full" }),
};
}