Add chinese translation

Based on https://github.com/dyxang/Fantasy-Map-Generator-Chinese
This commit is contained in:
Blipz 2026-03-28 04:59:26 +01:00
parent 7eeb1f76c3
commit c5f1577c4b
12 changed files with 3359 additions and 980 deletions

View file

@ -8,7 +8,7 @@ const HTMLPlugin: Plugin = {
const content = await readFile("src/index.html", "utf-8"); const content = await readFile("src/index.html", "utf-8");
const matches = content.matchAll(/data-(?:html|text|tip)="([^"]+)"/g); const matches = content.matchAll(/data-(?:html|text|tip)="([^"]+)"/g);
for (const match of matches) { for (const match of matches) {
const key = match[1]; const key = match[1].replace(/"/g, '"');
keys.set(key, {key, defaultValue: key}); keys.set(key, {key, defaultValue: key});
} }
} }
@ -17,13 +17,15 @@ const HTMLPlugin: Plugin = {
export default defineConfig({ export default defineConfig({
locales: [ locales: [
"en", "en",
"fr" "fr",
"zh"
], ],
extract: { extract: {
input: "src/**/*.{js,ts}", input: "src/**/*.{js,ts}",
output: "public/locales/{{language}}/{{namespace}}.json", output: "public/locales/{{language}}/{{namespace}}.json",
defaultNS: "lang", defaultNS: "lang",
keySeparator: false keySeparator: "::",
nsSeparator: false,
}, },
plugins: [ plugins: [
HTMLPlugin HTMLPlugin

View file

@ -234,10 +234,10 @@
.icon-temperature-low:before {content:'\f76b';} /* '' */ .icon-temperature-low:before {content:'\f76b';} /* '' */
/* Amended FA icons */ /* Amended FA icons */
.icon-sort-name-up:after {font-size:.9em;content:'\f15d';} .icon-sort-name-up:after {font-size:.9em;content:'\00a0\f15d';}
.icon-sort-name-down:after {font-size:.9em;content:'\f15e';} .icon-sort-name-down:after {font-size:.9em;content:'\00a0\f15e';}
.icon-sort-number-up:after {font-size:.9em;content:'\f162';} .icon-sort-number-up:after {font-size:.9em;content:'\00a0\f162';}
.icon-sort-number-down:after {font-size:.9em;content:'\f163';} .icon-sort-number-down:after {font-size:.9em;content:'\00a0\f163';}
/* Custom icons */ /* Custom icons */
.icon-w:before {font-style:italic;content:'w:';} .icon-w:before {font-style:italic;content:'w:';}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

1620
public/locales/zh/lang.json Normal file

File diff suppressed because it is too large Load diff

View file

@ -523,8 +523,8 @@ function applyStoredOptions() {
if (stored("tooltipSize")) changeTooltipSize(stored("tooltipSize")); if (stored("tooltipSize")) changeTooltipSize(stored("tooltipSize"));
if (stored("regions")) changeStatesNumber(stored("regions")); if (stored("regions")) changeStatesNumber(stored("regions"));
if (stored("language")) changeLanguage(stored("language")); if (stored("language")) setLanguage(stored("language"));
else changeLanguage("en"); else setLanguage("en");
uiSize.max = uiSize.max = getUImaxSize(); uiSize.max = uiSize.max = getUImaxSize();
if (stored("uiSize")) changeUiSize(+stored("uiSize")); if (stored("uiSize")) changeUiSize(+stored("uiSize"));
@ -659,10 +659,15 @@ function changeEra() {
options.era = eraInput.value; options.era = eraInput.value;
} }
function changeLanguage(value) { function setLanguage(value) {
options.language = value; options.language = value;
} }
function changeLanguage(value) {
setLanguage(value);
changeLocale();
}
async function openTemplateSelectionDialog() { async function openTemplateSelectionDialog() {
const HeightmapSelectionDialog = await import("../dynamic/heightmap-selection.js?v=1.96.00"); const HeightmapSelectionDialog = await import("../dynamic/heightmap-selection.js?v=1.96.00");
HeightmapSelectionDialog.open(); HeightmapSelectionDialog.open();

File diff suppressed because it is too large Load diff

View file

@ -1,23 +1,49 @@
import i18next from "i18next"; import i18next from "i18next";
import i18nextHTTPBackend from "i18next-http-backend"; import i18nextHTTPBackend from "i18next-http-backend";
import { isVowel } from "../utils"; import { isVowel } from "../utils";
import { TOptions } from "i18next";
function initTooltips() {
for (const tip of document.querySelectorAll("[data-tip]")) {
tip.setAttribute(
"data-original-tip",
tip.getAttribute("data-tip") as string,
);
}
}
function updateLabels() { function updateLabels() {
for (const label of document.querySelectorAll("[data-html]")) { for (const label of document.querySelectorAll("[data-html]")) {
const translation = i18next.t(label.getAttribute("data-html") as string); let vars: TOptions = {
interpolation: { escapeValue: false },
};
for (const attr of label.attributes) {
if (attr.name.startsWith("data-var-")) {
vars[attr.name.slice(9)] = attr.value;
}
}
const translation = i18next.t(
label.getAttribute("data-html") as string,
vars,
);
if (translation) label.innerHTML = translation; if (translation) label.innerHTML = translation;
} }
for (const label of document.querySelectorAll("[data-text]")) { for (const label of document.querySelectorAll("[data-text]")) {
const translation = i18next.t(label.getAttribute("data-text") as string); const translation = i18next.t(label.getAttribute("data-text") as string);
if (translation) label.textContent = translation; if (translation) label.textContent = translation;
} }
for (const tip of document.querySelectorAll("[data-tip]")) { for (const tip of document.querySelectorAll("[data-original-tip]")) {
const translation = i18next.t(tip.getAttribute("data-tip") as string); const translation = i18next.t(
tip.getAttribute("data-original-tip") as string,
);
if (translation) tip.setAttribute("data-tip", translation); if (translation) tip.setAttribute("data-tip", translation);
} }
} }
function addFormatters() { function addFormatters() {
i18next.services.formatter?.add("link", (value, _lng, options) => {
return `<a href="${value}" target="blank">${options.text}</a>`;
});
i18next.services.formatter?.add("gender", (value, lng, options) => { i18next.services.formatter?.add("gender", (value, lng, options) => {
if (lng !== "fr") return value; if (lng !== "fr") return value;
else if (options.gender === "feminine") { else if (options.gender === "feminine") {
@ -43,13 +69,22 @@ window.initLocale = async () => {
await i18next.use(i18nextHTTPBackend).init( await i18next.use(i18nextHTTPBackend).init(
{ {
lng: options.language, lng: options.language,
fallbackLng: "en",
backend: { backend: {
loadPath: "locales/{{lng}}/lang.json", loadPath: "locales/{{lng}}/lang.json",
}, },
keySeparator: "::",
nsSeparator: false,
returnEmptyString: false,
}, },
() => { () => {
addFormatters(); addFormatters();
initTooltips();
updateLabels(); updateLabels();
}, },
); );
}; };
window.changeLocale = () => {
i18next.changeLanguage(options.language, updateLabels);
};

View file

@ -144,7 +144,7 @@ class ProvinceModule {
form[formName] += 10; form[formName] += 10;
const fullName = i18next.t("{{provinceName}} {{provinceForm}}", { const fullName = i18next.t("{{provinceName}} {{provinceForm}}", {
provinceName: name, provinceName: name,
provinceForm: i18next.t(formName), provinceForm: i18next.t(`provinceForm::${formName}`),
}); });
const color = getMixedColor(s.color!); const color = getMixedColor(s.color!);
const kinship = nameByBurg ? 0.8 : 0.4; const kinship = nameByBurg ? 0.8 : 0.4;
@ -327,7 +327,7 @@ class ProvinceModule {
const fullName = i18next.t("{{provinceName}} {{provinceForm}}", { const fullName = i18next.t("{{provinceName}} {{provinceForm}}", {
provinceName: name, provinceName: name,
provinceForm: i18next.t(formName), provinceForm: i18next.t(`provinceForm::${formName}`),
}); });
const dominion = colony const dominion = colony

View file

@ -64,6 +64,7 @@ gender.fr = {
Diarchy: "feminine", Diarchy: "feminine",
Horde: "feminine", Horde: "feminine",
League: "feminine", League: "feminine",
Marches: "feminine",
Oligarchy: "feminine", Oligarchy: "feminine",
Tetrarchy: "feminine", Tetrarchy: "feminine",
Theocracy: "feminine", Theocracy: "feminine",
@ -834,7 +835,7 @@ class StatesModule {
const stateGender = gender[options.language as string]?.[state.formName]; const stateGender = gender[options.language as string]?.[state.formName];
if (!state.name && state.formName) if (!state.name && state.formName)
return i18next.t("The {{noun}}", { return i18next.t("The {{noun}}", {
noun: i18next.t(state.formName), noun: i18next.t(`stateForm::${state.formName}`),
gender: stateGender, gender: stateGender,
}); });
const adjName = const adjName =
@ -842,13 +843,14 @@ class StatesModule {
return adjName return adjName
? i18next.t("{{adjective}} {{noun}}", { ? i18next.t("{{adjective}} {{noun}}", {
adjective: getAdjective(state.name), adjective: getAdjective(state.name),
noun: i18next.t(state.formName), noun: i18next.t(`stateForm::${state.formName}`),
gender: stateGender, gender: stateGender,
}) })
: i18next.t("{{noun}} of {{complement}}", { : i18next.t("{{noun}} of {{complement}}", {
noun: i18next.t(state.formName), noun: i18next.t(`stateForm::${state.formName}`),
complement: state.name, complement: state.name,
gender: stateGender, gender: stateGender,
interpolation: { escapeValue: false },
}); });
} }
} }

View file

@ -90,4 +90,5 @@ declare global {
var changeFont: () => void; var changeFont: () => void;
var getFriendlyHeight: (coords: [number, number]) => string; var getFriendlyHeight: (coords: [number, number]) => string;
var initLocale: () => void; var initLocale: () => void;
var changeLocale: () => void;
} }

View file

@ -329,7 +329,9 @@ export const getAdjective = (nounToBeAdjective: string) => {
}, },
], ],
}; };
for (const rule of adjectivizationRules[options.language]) { const rules =
adjectivizationRules[options.language] ?? adjectivizationRules.en;
for (const rule of rules) {
if (P(rule.probability) && rule.condition.test(nounToBeAdjective)) { if (P(rule.probability) && rule.condition.test(nounToBeAdjective)) {
return rule.action(nounToBeAdjective); return rule.action(nounToBeAdjective);
} }