diff --git a/package.json b/package.json index 1228ead9..b364e2f8 100644 --- a/package.json +++ b/package.json @@ -94,6 +94,7 @@ "sass-unused": "^0.3.0", "strip-json-comments": "^3.0.1", "trim": "^0.0.1", + "yaml-loader": "^0.6.0", "yarn": "^1.22.4" } } diff --git a/src/js/languages.js b/src/js/languages.js deleted file mode 100644 index c46c3e88..00000000 --- a/src/js/languages.js +++ /dev/null @@ -1,113 +0,0 @@ -/** - * @type {Object} - */ -export const LANGUAGES = { - "en": { - name: "English", - data: null, - code: "en", - region: "", - }, - "de": { - name: "Deutsch", - data: require("./built-temp/base-de.json"), - code: "de", - region: "", - }, - "fr": { - name: "Français", - data: require("./built-temp/base-fr.json"), - code: "fr", - region: "", - }, - "ja": { - name: "日本語", - data: require("./built-temp/base-ja.json"), - code: "ja", - region: "", - }, - "pt-PT": { - name: "Português (Portugal)", - data: require("./built-temp/base-pt-PT.json"), - code: "pt", - region: "PT", - }, - "pt-BR": { - name: "Português (Brasil)", - data: require("./built-temp/base-pt-BR.json"), - code: "pt", - region: "BR", - }, - "ru": { - name: "Русский", - data: require("./built-temp/base-ru.json"), - code: "ru", - region: "", - }, - "cs": { - name: "Čeština", - data: require("./built-temp/base-cz.json"), - code: "cs", - region: "", - }, - "es-419": { - name: "Español", - data: require("./built-temp/base-es.json"), - code: "es", - region: "", - }, - "pl": { - name: "Polski", - data: require("./built-temp/base-pl.json"), - code: "pl", - region: "", - }, - "kor": { - name: "한국어", - data: require("./built-temp/base-kor.json"), - code: "kor", - region: "", - }, - "nl": { - name: "Nederlands", - data: require("./built-temp/base-nl.json"), - code: "nl", - region: "", - }, - "no": { - name: "Norsk", - data: require("./built-temp/base-no.json"), - code: "no", - region: "", - }, - - "tr": { - name: "Türkçe", - data: require("./built-temp/base-tr.json"), - code: "tr", - region: "", - }, - - "zh-CN": { - // simplified - name: "中文简体", - data: require("./built-temp/base-zh-CN.json"), - code: "zh", - region: "CN", - }, - - "zh-TW": { - // traditional - name: "中文繁體", - data: require("./built-temp/base-zh-TW.json"), - code: "zh", - region: "TW", - }, - - "sv": { - name: "Svenska", - data: require("./built-temp/base-sv.json"), - code: "sv", - region: "", - }, -}; diff --git a/src/js/profile/application_settings.js b/src/js/profile/application_settings.js index 2167c77c..0c026152 100644 --- a/src/js/profile/application_settings.js +++ b/src/js/profile/application_settings.js @@ -6,10 +6,14 @@ import { ReadWriteProxy } from "../core/read_write_proxy"; import { BoolSetting, EnumSetting, BaseSetting } from "./setting_types"; import { createLogger } from "../core/logging"; import { ExplainedResult } from "../core/explained_result"; -import { THEMES, THEME, applyGameTheme } from "../game/theme"; +import { THEMES, applyGameTheme } from "../game/theme"; import { IS_DEMO } from "../core/config"; import { T } from "../translations"; -import { LANGUAGES } from "../languages"; +import languageMap, { isLanguageTag, languageTags } from "../translations/languages"; + +/** + * @typedef {import("../translations/languages").LanguageTag} LanguageTag + **/ const logger = createLogger("application_settings"); @@ -125,9 +129,9 @@ export const autosaveIntervals = [ /** @type {Array} */ export const allApplicationSettings = [ new EnumSetting("language", { - options: Object.keys(LANGUAGES), - valueGetter: key => key, - textGetter: key => LANGUAGES[key].name, + options: languageTags, + valueGetter: languageTag => languageTag, + textGetter: languageTag => languageMap[languageTag].name, category: enumCategories.general, restartRequired: true, changeCb: (app, id) => null, @@ -275,6 +279,7 @@ class SettingsStorage { this.refreshRate = "60"; this.scrollWheelSensitivity = "regular"; this.movementSpeed = "regular"; + /** @type {LanguageTag | "auto-detect"} **/ this.language = "auto-detect"; this.autosaveInterval = "two_minutes"; @@ -405,10 +410,12 @@ export class ApplicationSettings extends ReadWriteProxy { } // Setters - - updateLanguage(id) { - assert(LANGUAGES[id], "Language not known: " + id); - return this.updateSetting("language", id); + /** + * @param {LanguageTag} languageTag + **/ + updateLanguage(languageTag) { + assert(isLanguageTag(languageTag), "Language not known: " + languageTag); + return this.updateSetting("language", languageTag); } /** diff --git a/src/js/savegame/schemas/1000.js b/src/js/savegame/schemas/1000.js index 29cd0b1a..4e50c357 100644 --- a/src/js/savegame/schemas/1000.js +++ b/src/js/savegame/schemas/1000.js @@ -1,4 +1,4 @@ -import { BaseSavegameInterface } from "../savegame_interface.js"; +import { BaseSavegameInterface } from "../savegame_interface"; const schema = require("./1000.json"); diff --git a/src/js/savegame/schemas/1001.js b/src/js/savegame/schemas/1001.js index af86b09d..45ffc3ac 100644 --- a/src/js/savegame/schemas/1001.js +++ b/src/js/savegame/schemas/1001.js @@ -1,7 +1,7 @@ -import { SavegameInterface_V1000 } from "./1000.js"; -import { createLogger } from "../../core/logging.js"; -import { T } from "../../translations.js"; -import { TypeVector, TypeNumber, TypeString, TypeNullable } from "../serialization_data_types.js"; +import { SavegameInterface_V1000 } from "./1000"; +import { createLogger } from "../../core/logging"; +import { T } from "../../translations"; +import { TypeVector, TypeNumber } from "../serialization_data_types"; const schema = require("./1001.json"); diff --git a/src/js/savegame/schemas/1002.js b/src/js/savegame/schemas/1002.js index 866bc1e8..30f406b5 100644 --- a/src/js/savegame/schemas/1002.js +++ b/src/js/savegame/schemas/1002.js @@ -1,6 +1,5 @@ -import { createLogger } from "../../core/logging.js"; -import { T } from "../../translations.js"; -import { SavegameInterface_V1001 } from "./1001.js"; +import { createLogger } from "../../core/logging"; +import { SavegameInterface_V1001 } from "./1001"; const schema = require("./1002.json"); const logger = createLogger("savegame_interface/1002"); diff --git a/src/js/savegame/schemas/1003.js b/src/js/savegame/schemas/1003.js index 4a4a3ee3..a22237f8 100644 --- a/src/js/savegame/schemas/1003.js +++ b/src/js/savegame/schemas/1003.js @@ -1,5 +1,5 @@ -import { createLogger } from "../../core/logging.js"; -import { SavegameInterface_V1002 } from "./1002.js"; +import { createLogger } from "../../core/logging"; +import { SavegameInterface_V1002 } from "./1002"; const schema = require("./1003.json"); const logger = createLogger("savegame_interface/1003"); diff --git a/src/js/savegame/schemas/1004.js b/src/js/savegame/schemas/1004.js index c9feda1a..357bcae7 100644 --- a/src/js/savegame/schemas/1004.js +++ b/src/js/savegame/schemas/1004.js @@ -1,5 +1,5 @@ -import { createLogger } from "../../core/logging.js"; -import { SavegameInterface_V1003 } from "./1003.js"; +import { createLogger } from "../../core/logging"; +import { SavegameInterface_V1003 } from "./1003"; const schema = require("./1004.json"); const logger = createLogger("savegame_interface/1004"); diff --git a/src/js/savegame/schemas/1005.js b/src/js/savegame/schemas/1005.js index 0380f8eb..4ad38ba8 100644 --- a/src/js/savegame/schemas/1005.js +++ b/src/js/savegame/schemas/1005.js @@ -1,5 +1,5 @@ -import { createLogger } from "../../core/logging.js"; -import { SavegameInterface_V1004 } from "./1004.js"; +import { createLogger } from "../../core/logging"; +import { SavegameInterface_V1004 } from "./1004"; const schema = require("./1005.json"); const logger = createLogger("savegame_interface/1005"); diff --git a/src/js/states/preload.js b/src/js/states/preload.js index 0f47e8d6..476e5db9 100644 --- a/src/js/states/preload.js +++ b/src/js/states/preload.js @@ -4,6 +4,7 @@ import { findNiceValue } from "../core/utils"; import { cachebust } from "../core/cachebust"; import { PlatformWrapperImplBrowser } from "../platform/browser/wrapper"; import { T, autoDetectLanguageId, updateApplicationLanguage } from "../translations"; +import { isLanguageTag } from "../translations/languages"; import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs"; import { CHANGELOG } from "../changelog"; import { globalConfig } from "../core/config"; @@ -125,7 +126,9 @@ export class PreloadState extends GameState { }) .then(() => { const language = this.app.settings.getLanguage(); - updateApplicationLanguage(language); + if (isLanguageTag(language)) { + updateApplicationLanguage(language); + } }) .then(() => this.setStatus("Initializing sounds")) diff --git a/src/js/translations.js b/src/js/translations.js deleted file mode 100644 index 6ad926bc..00000000 --- a/src/js/translations.js +++ /dev/null @@ -1,135 +0,0 @@ -import { globalConfig } from "./core/config"; -import { createLogger } from "./core/logging"; -import { LANGUAGES } from "./languages"; - -const logger = createLogger("translations"); - -// @ts-ignore -const baseTranslations = require("./built-temp/base-en.json"); - -export let T = baseTranslations; - -if (G_IS_DEV && globalConfig.debug.testTranslations) { - // Replaces all translations by fake translations to see whats translated and what not - const mapTranslations = obj => { - for (const key in obj) { - const value = obj[key]; - if (typeof value === "string") { - obj[key] = value.replace(/[a-z]/gi, "x"); - } else { - mapTranslations(value); - } - } - }; - mapTranslations(T); -} - -export function applyLanguage(languageCode) { - logger.log("Applying language:", languageCode); - const data = LANGUAGES[languageCode]; - if (!data) { - logger.error("Language not found:", languageCode); - return false; - } -} - -// Language key is something like de-DE or en or en-US -function mapLanguageCodeToId(languageKey) { - const key = languageKey.toLowerCase(); - const shortKey = key.split("-")[0]; - - // Try to match by key or short key - for (const id in LANGUAGES) { - const data = LANGUAGES[id]; - const code = data.code.toLowerCase(); - if (code === key) { - console.log("-> Match", languageKey, "->", id); - return id; - } - if (code === shortKey) { - console.log("-> Match by short key", languageKey, "->", id); - return id; - } - } - - // If none found, try to find a better alternative by using the base language at least - for (const id in LANGUAGES) { - const data = LANGUAGES[id]; - const code = data.code.toLowerCase(); - const shortCode = code.split("-")[0]; - - if (shortCode === key) { - console.log("-> Desperate Match", languageKey, "->", id); - return id; - } - if (shortCode === shortKey) { - console.log("-> Desperate Match by short key", languageKey, "->", id); - return id; - } - } - - return null; -} - -/** - * Tries to auto-detect a language - * @returns {string} - */ -export function autoDetectLanguageId() { - let languages = []; - if (navigator.languages) { - languages = navigator.languages.slice(); - } else if (navigator.language) { - languages = [navigator.language]; - } else { - logger.warn("Navigator has no languages prop"); - } - - for (let i = 0; i < languages.length; ++i) { - logger.log("Trying to find language target for", languages[i]); - const trans = mapLanguageCodeToId(languages[i]); - if (trans) { - return trans; - } - } - - // Fallback - return "en"; -} - -function matchDataRecursive(dest, src) { - if (typeof dest !== "object" || typeof src !== "object") { - return; - } - - for (const key in dest) { - if (src[key]) { - // console.log("copy", key); - const data = dest[key]; - if (typeof data === "object") { - matchDataRecursive(dest[key], src[key]); - } else if (typeof data === "string" || typeof data === "number") { - // console.log("match string", key); - dest[key] = src[key]; - } else { - logger.log("Unknown type:", typeof data, "in key", key); - } - } - } -} - -export function updateApplicationLanguage(id) { - logger.log("Setting application language:", id); - - const data = LANGUAGES[id]; - - if (!data) { - logger.error("Unknown language:", id); - return; - } - - if (data.data) { - logger.log("Applying translations ..."); - matchDataRecursive(T, data.data); - } -} diff --git a/src/js/translations/index.js b/src/js/translations/index.js new file mode 100644 index 00000000..6cdca291 --- /dev/null +++ b/src/js/translations/index.js @@ -0,0 +1,115 @@ +import { globalConfig } from "../core/config"; +import { createLogger } from "../core/logging"; +import languageMap, { isLanguageTag, languageTags } from "./languages"; + +/** + * @typedef {import("./languages").LanguageTag} LanguageTag + **/ + +const logger = createLogger("translations"); + +export const T = languageMap.en.data; + +if (G_IS_DEV && globalConfig.debug.testTranslations) { + // Replaces all translations by fake translations to see whats translated and what not + const mapTranslations = obj => { + for (const key in obj) { + const value = obj[key]; + if (typeof value === "string") { + obj[key] = value.replace(/[a-z]/gi, "x"); + } else { + mapTranslations(value); + } + } + }; + mapTranslations(T); +} + +/** tag should be a language code, like de-DE or en or en-US + * @param {string} tag + * @returns {LanguageTag | null} + **/ +function mapLanguageCodeToId(tag) { + if (isLanguageTag(tag)) { + return tag; + } + + const [code] = tag.split("-"); + + // Try to match by tag or code + for (const languageTag of languageTags) { + const { code: c } = languageMap[languageTag]; + if (tag === c) { + console.log("-> Match", tag, "->", languageTag); + return languageTag; + } + if (code === c) { + console.log("-> Match by short key", tag, "->", languageTag); + return languageTag; + } + } + + return null; +} + +/** + * Tries to auto-detect a language + * @returns {LanguageTag} + */ +export function autoDetectLanguageId() { + let languages = []; + if (navigator.languages) { + languages = navigator.languages.slice(); + } else if (navigator.language) { + languages = [navigator.language]; + } else { + logger.warn("Navigator has no languages prop"); + } + + for (let i = 0; i < languages.length; ++i) { + logger.log("Trying to find language target for", languages[i]); + const trans = mapLanguageCodeToId(languages[i]); + if (trans) { + return trans; + } + } + + // Fallback + return "en"; +} + +function matchDataRecursive(dest, src) { + if (typeof dest !== "object" || typeof src !== "object") { + return; + } + + for (const key in dest) { + if (src[key]) { + const data = dest[key]; + if (typeof data === "object") { + matchDataRecursive(dest[key], src[key]); + } else if (typeof data === "string" || typeof data === "number") { + dest[key] = src[key]; + } else { + logger.log("Unknown type:", typeof data, "in key", key); + } + } + } +} + +/** + * @param {LanguageTag} languageTag + **/ +export function updateApplicationLanguage(languageTag) { + logger.log("Setting application language:", languageTag); + + if (!languageMap.hasOwnProperty(languageTag)) { + logger.error("Unknown language:", languageTag); + return; + } + + const { data } = languageMap[languageTag]; + + logger.log("Applying translations ..."); + matchDataRecursive(T, data); +} diff --git a/src/js/translations/languages/cs.js b/src/js/translations/languages/cs.js new file mode 100644 index 00000000..b5de7815 --- /dev/null +++ b/src/js/translations/languages/cs.js @@ -0,0 +1,7 @@ +import data from "yaml-loader!../../../../translations/base-cs.yaml"; + +/** @type {"cs"} **/ +const code = "cs"; +const name = "Čeština"; + +export default { code, data, name }; diff --git a/src/js/translations/languages/de.js b/src/js/translations/languages/de.js new file mode 100644 index 00000000..98fada16 --- /dev/null +++ b/src/js/translations/languages/de.js @@ -0,0 +1,7 @@ +import data from "yaml-loader!../../../../translations/base-de.yaml"; + +/** @type {"de"} **/ +const code = "de"; +const name = "Deutsch"; + +export default { code, data, name }; diff --git a/src/js/translations/languages/en.js b/src/js/translations/languages/en.js new file mode 100644 index 00000000..d441b4c5 --- /dev/null +++ b/src/js/translations/languages/en.js @@ -0,0 +1,7 @@ +import data from "yaml-loader!../../../../translations/base-en.yaml"; + +/** @type {"en"} **/ +const code = "en"; +const name = "English"; + +export default { code, data, name }; diff --git a/src/js/translations/languages/es-419.js b/src/js/translations/languages/es-419.js new file mode 100644 index 00000000..453ba94b --- /dev/null +++ b/src/js/translations/languages/es-419.js @@ -0,0 +1,7 @@ +import data from "yaml-loader!../../../../translations/base-es-419.yaml"; + +/** @type {"es"} **/ +const code = "es"; +const name = "Español"; + +export default { code, data, name }; diff --git a/src/js/translations/languages/fr.js b/src/js/translations/languages/fr.js new file mode 100644 index 00000000..9c9491d2 --- /dev/null +++ b/src/js/translations/languages/fr.js @@ -0,0 +1,7 @@ +import data from "yaml-loader!../../../../translations/base-fr.yaml"; + +/** @type {"fr"} **/ +const code = "fr"; +const name = "Français"; + +export default { code, data, name }; diff --git a/src/js/translations/languages/index.js b/src/js/translations/languages/index.js new file mode 100644 index 00000000..80c2a461 --- /dev/null +++ b/src/js/translations/languages/index.js @@ -0,0 +1,99 @@ +import cs from "./cs"; +import de from "./de"; +import en from "./en"; +import es419 from "./es-419"; +import fr from "./fr"; +import ja from "./ja"; +import kor from "./kor"; +import nl from "./nl"; +import no from "./no"; +import pl from "./pl"; +import ptPT from "./pt-PT"; +import ptBR from "./pt-BR"; +import ru from "./ru"; +import sv from "./sv"; +import tr from "./tr"; +import zhCN from "./zh-CN"; +import zhTW from "./zh-TW"; + +/** + * @typedef {"en" | "de" | "fr" | "ja" | "pt-PT" | "pt-BR" | "ru" | "cs" | "es-419" | "pl" | "kor" | "nl" | "no" | "tr" | "zh-CN" | "zh-TW" | "sv"} LanguageTag + */ + +/** @type {LanguageTag[]} **/ +export const languageTags = [ + "en", + "de", + "fr", + "ja", + "pt-PT", + "pt-BR", + "ru", + "cs", + "es-419", + "pl", + "kor", + "nl", + "no", + "tr", + "zh-CN", + "zh-TW", + "sv", +]; + +/** + * @param {unknown} value + * @returns {value is LanguageTag} + **/ +export function isLanguageTag(value) { + return languageTags.includes(/** @type {LanguageTag} **/ (value)); +} + +/** @typedef {"en" | "de" | "fr" | "ja" | "pt" | "ru" | "cs" | "es" | "pl" | "kor" | "nl" | "no" | "tr" | "zh" | "sv"} LanguageCode + */ + +/** @type {LanguageCode[]} **/ +const languageCodes = [ + "en", + "de", + "fr", + "ja", + "pt", + "ru", + "cs", + "es", + "pl", + "kor", + "nl", + "no", + "tr", + "zh", + "sv", +]; + +/** @typedef {"PT" | "BR" | "CN" | "TW"} LanguageRegion **/ +/** @type {LanguageRegion[]} **/ +const languageRegions = ["PT", "BR", "CN", "TW"]; + +/** + * @type {Record} + */ +export default { + cs, + de, + en, + "es-419": es419, + fr, + ja, + kor, + nl, + no, + pl, + "pt-BR": ptBR, + "pt-PT": ptPT, + ru, + sv, + tr, + "zh-CN": zhCN, + "zh-TW": zhTW, +}; diff --git a/src/js/translations/languages/ja.js b/src/js/translations/languages/ja.js new file mode 100644 index 00000000..92a73b6d --- /dev/null +++ b/src/js/translations/languages/ja.js @@ -0,0 +1,7 @@ +import data from "yaml-loader!../../../../translations/base-ja.yaml"; + +/** @type {"ja"} **/ +const code = "ja"; +const name = "日本語"; + +export default { code, data, name }; diff --git a/src/js/translations/languages/kor.js b/src/js/translations/languages/kor.js new file mode 100644 index 00000000..7933e007 --- /dev/null +++ b/src/js/translations/languages/kor.js @@ -0,0 +1,7 @@ +import data from "yaml-loader!../../../../translations/base-kor.yaml"; + +/** @type {"kor"} **/ +const code = "kor"; +const name = "한국어"; + +export default { code, data, name }; diff --git a/src/js/translations/languages/nl.js b/src/js/translations/languages/nl.js new file mode 100644 index 00000000..6eeeb8d0 --- /dev/null +++ b/src/js/translations/languages/nl.js @@ -0,0 +1,7 @@ +import data from "yaml-loader!../../../../translations/base-nl.yaml"; + +/** @type {"nl"} **/ +const code = "nl"; +const name = "Nederlands"; + +export default { code, data, name }; diff --git a/src/js/translations/languages/no.js b/src/js/translations/languages/no.js new file mode 100644 index 00000000..3ae507cc --- /dev/null +++ b/src/js/translations/languages/no.js @@ -0,0 +1,7 @@ +import data from "yaml-loader!../../../../translations/base-no.yaml"; + +/** @type {"no"} **/ +const code = "no"; +const name = "Norsk"; + +export default { code, data, name }; diff --git a/src/js/translations/languages/pl.js b/src/js/translations/languages/pl.js new file mode 100644 index 00000000..86b19a70 --- /dev/null +++ b/src/js/translations/languages/pl.js @@ -0,0 +1,7 @@ +import data from "yaml-loader!../../../../translations/base-pl.yaml"; + +/** @type {"pl"} **/ +const code = "pl"; +const name = "Polski"; + +export default { code, data, name }; diff --git a/src/js/translations/languages/pt-BR.js b/src/js/translations/languages/pt-BR.js new file mode 100644 index 00000000..5bac3e80 --- /dev/null +++ b/src/js/translations/languages/pt-BR.js @@ -0,0 +1,8 @@ +import data from "yaml-loader!../../../../translations/base-pt-BR.yaml"; + +/** @type {"pt"} **/ +const code = "pt"; +const name = "Português (Brasil)"; +const region = "BR"; + +export default { code, data, name, region }; diff --git a/src/js/translations/languages/pt-PT.js b/src/js/translations/languages/pt-PT.js new file mode 100644 index 00000000..41295401 --- /dev/null +++ b/src/js/translations/languages/pt-PT.js @@ -0,0 +1,8 @@ +import data from "yaml-loader!../../../../translations/base-pt-PT.yaml"; + +/** @type {"pt"} **/ +const code = "pt"; +const name = "Português (Portugal)"; +const region = "PT"; + +export default { code, data, name, region }; diff --git a/src/js/translations/languages/ru.js b/src/js/translations/languages/ru.js new file mode 100644 index 00000000..4dc57b0b --- /dev/null +++ b/src/js/translations/languages/ru.js @@ -0,0 +1,7 @@ +import data from "yaml-loader!../../../../translations/base-ru.yaml"; + +/** @type {"ru"} **/ +const code = "ru"; +const name = "Русский"; + +export default { code, data, name }; diff --git a/src/js/translations/languages/sv.js b/src/js/translations/languages/sv.js new file mode 100644 index 00000000..2bba1e8d --- /dev/null +++ b/src/js/translations/languages/sv.js @@ -0,0 +1,7 @@ +import data from "yaml-loader!../../../../translations/base-sv.yaml"; + +/** @type {"sv"} **/ +const code = "sv"; +const name = "Svenska"; + +export default { code, data, name }; diff --git a/src/js/translations/languages/tr.js b/src/js/translations/languages/tr.js new file mode 100644 index 00000000..15ef6a5e --- /dev/null +++ b/src/js/translations/languages/tr.js @@ -0,0 +1,7 @@ +import data from "yaml-loader!../../../../translations/base-tr.yaml"; + +/** @type {"tr"} **/ +const code = "tr"; +const name = "Türkçe"; + +export default { code, data, name }; diff --git a/src/js/translations/languages/zh-CN.js b/src/js/translations/languages/zh-CN.js new file mode 100644 index 00000000..b8fa0bb7 --- /dev/null +++ b/src/js/translations/languages/zh-CN.js @@ -0,0 +1,8 @@ +import data from "yaml-loader!../../../../translations/base-zh-CN.yaml"; + +/** @type {"zh"} **/ +const code = "zh"; +const name = "中文简体"; +const region = "CN"; + +export default { code, data, name, region }; diff --git a/src/js/translations/languages/zh-TW.js b/src/js/translations/languages/zh-TW.js new file mode 100644 index 00000000..7bcffa78 --- /dev/null +++ b/src/js/translations/languages/zh-TW.js @@ -0,0 +1,8 @@ +import data from "yaml-loader!../../../../translations/base-zh-TW.yaml"; + +/** @type {"zh"} **/ +const code = "zh"; +const name = "中文繁體"; +const region = "TW"; + +export default { code, data, name, region }; diff --git a/src/js/translations/types.d.ts b/src/js/translations/types.d.ts new file mode 100644 index 00000000..d261723c --- /dev/null +++ b/src/js/translations/types.d.ts @@ -0,0 +1,344 @@ +declare type Translation = Record; + +declare type BeltVariant = "default"; +declare type WireVariant = "default" | "chainable"; +declare type UndergroundBeltVariant = "default" | "tier2"; +declare type SplitterVariant = "default" | "compact" | "compact-inverse"; +declare type CutterVariant = "default" | "quad"; +declare type AdvancedProcessorVariant = "default"; +declare type RotaterVariant = "default" | "ccw" | "fl"; +declare type StackerVariant = "default"; +declare type MixerVariant = "default"; +declare type PainterVariant = "default" | "mirrored" | "double" | "quad"; +declare type TrashVariant = "default" | "storage"; +declare type EnergyGeneratorVariant = "default"; +declare type WireCrossingsVariant = "default" | "merger"; + +declare type BuildingVariantMap = { + belt: BeltVariant; + wire: WireVariant; + underground_belt: UndergroundBeltVariant; + splitter: SplitterVariant; + cutter: CutterVariant; + advanced_processor: AdvancedProcessorVariant; + rotater: RotaterVariant; + stacker: StackerVariant; + mixer: MixerVariant; + painter: PainterVariant; + trash: TrashVariant; + energy_generator: EnergyGeneratorVariant; + wire_crossings: WireCrossingsVariant; +}; + +declare type BuildingVariant = BuildingVariantMap[V]; + +declare type Building = keyof BuildingVariantMap; +declare type Color = + | "red" + | "green" + | "blue" + | "yellow" + | "purple" + | "cyan" + | "white" + | "uncolored" + | "black"; +declare type Environment = "dev" | "prod" | "staging"; +declare type Interval = + | "one_minute" + | "two_minutes" + | "five_minutes" + | "ten_minutes" + | "twenty_minutes" + | "disabled"; +declare type Scale = "super_small" | "small" | "regular" | "large" | "huge"; +declare type Sensitivity = "super_slow" | "slow" | "regular" | "fast" | "super_fast"; +declare type Speed = "super_slow" | "slow" | "regular" | "fast" | "super_fast" | "extremely_fast"; +declare type Theme = "dark" | "light"; + +declare type Rewards = + | "reward_cutter_and_trash" + | "reward_rotater" + | "reward_painter" + | "reward_mixer" + | "reward_stacker" + | "reward_splitter" + | "reward_tunnel" + | "reward_rotater_ccw" + | "reward_miner_chainable" + | "reward_underground_belt_tier_2" + | "reward_splitter_compact" + | "reward_cutter_quad" + | "reward_painter_double" + | "reward_painter_quad" + | "reward_storage" + | "reward_freeplay" + | "reward_blueprints" + | "no_reward" + | "no_reward_freeplay"; + +declare type TitleAndText = Translation<"title" | "text">; +declare type TitleAndDesc = Translation<"title" | "desc">; +declare type TitleAndDescription = Translation<"title" | "description">; +declare type NameAndDescription = Translation<"name" | "description">; + +declare type SteamPage = Translation<"shortText" | "discordLink" | "longText">; + +declare type Global = Translation< + "loading" | "error" | "thousandsDivider" | "decimalSeparator" | "infinite" +> & { + suffix: Translation<"thousands" | "millions" | "billions" | "trillions">; + time: Translation< + | "oneSecondAgo" + | "xSecondsAgo" + | "oneMinuteAgo" + | "xMinutesAgo" + | "oneHourAgo" + | "xHoursAgo" + | "oneDayAgo" + | "xDaysAgo" + | "secondsShort" + | "minutesAndSecondsShort" + | "hoursAndMinutesShort" + | "xMinutes" + >; + keys: Translation<"tab" | "control" | "alt" | "escape" | "shift" | "space">; +}; + +declare type DemoBanners = Translation<"title" | "intro">; + +declare type MainMenu = Translation< + | "play" + | "continue" + | "newGame" + | "changelog" + | "subreddit" + | "importSavegame" + | "openSourceHint" + | "discordLink" + | "helpTranslate" + | "madeBy" + | "browserWarning" + | "savegameLevel" + | "savegameLevelUnknown" +>; + +declare type Dialogs = Translation< + | "importSavegameError" + | "importSavegameSuccess" + | "gameLoadFailure" + | "confirmSavegameDelete" + | "savegameDeletionError" + | "restartRequired", + TitleAndText +> & + Translation< + | "editKeybinding" + | "resetKeybindingsConfirmation" + | "keybindingsResetOk" + | "featureRestriction" + | "oneSavegameLimit" + | "updateSummary" + | "upgradesIntroduction" + | "massDeleteConfirm" + | "massCutConfirm" + | "massCutInsufficientConfirm" + | "blueprintsNotUnlocked" + | "keybindingsIntroduction" + | "exportScreenshotWarning", + TitleAndDesc + > & { + createMarker: TitleAndDesc & Translation<"titleEdit">; + markerDemoLimit: Translation<"desc">; + } & { + buttons: Translation< + | "ok" + | "delete" + | "cancel" + | "later" + | "restart" + | "reset" + | "getStandalone" + | "deleteGame" + | "viewUpdate" + | "showUpgrades" + | "showKeybindings" + >; + }; + +declare type Ingame = { + keybindingsOverlay: Translation< + | "moveMap" + | "selectBuildings" + | "stopPlacement" + | "rotateBuilding" + | "placeMultiple" + | "reverseOrientation" + | "disableAutoOrientation" + | "toggleHud" + | "placeBuilding" + | "createMarker" + | "delete" + | "pasteLastBlueprint" + | "lockBeltDirection" + | "plannerSwitchSide" + | "cutSelection" + | "copySelection" + | "clearSelection" + | "pipette" + | "switchLayers" + >; + colors: Translation; + buildingPlacement: Translation<"cycleBuildingVariants" | "hotkeyLabel"> & { + infoTexts: Translation< + | "speed" + | "range" + | "storage" + | "oneItemPerSecond" + | "itemsPerSecond" + | "itemsPerSecondDouble" + | "tiles" + >; + }; + levelCompleteNotification: Translation<"levelTitle" | "completed" | "unlockText" | "buttonNextLevel">; + notifications: Translation<"newUpgrade" | "gameSaved">; + shop: Translation<"title" | "buttonUnlock" | "tier" | "maximumLevel"> & { + tierLabels: [string, string, string, string, string, string, string, string, string, string]; + }; + statistics: Translation<"title" | "noShapesProduced" | "shapesPerMinute"> & { + dataSources: Translation<"stored" | "produced" | "delivered", TitleAndDescription>; + }; + settingsMenu: Translation<"playtime" | "buildingsPlaced" | "beltsPlaced"> & { + buttons: Translation<"continue" | "settings" | "menu">; + }; + tutorialHints: Translation<"title" | "showHint" | "hideHint">; + blueprintPlacer: Translation<"cost">; + waypoints: Translation<"waypoints" | "hub" | "description" | "creationSuccessNotification">; + shapeViewer: Translation<"title" | "empty" | "copyKey">; + interactiveTutorial: Translation<"title"> & { + hints: Translation<"1_1_extractor" | "1_2_conveyor" | "1_3_expand">; + }; +}; + +declare type ShopUpgrades = Record<"belt" | "miner" | "processors" | "painting", NameAndDescription>; + +declare type Buildings = { [Key in Building]: Translation, NameAndDescription> } & { + hub: Translation<"deliver" | "toUnlock" | "levelShortcut">; + energy_generator: Translation<"deliver" | "toGenerateEnergy">; +}; + +declare type StoryRewards = Translation; + +declare type Settings = Translation<"title" | "buildDate"> & { + categories: Translation<"general" | "userInterface" | "advanced">; + versionBadges: Translation; + labels: Translation< + | "language" + | "enableColorBlindHelper" + | "fullscreen" + | "soundsMuted" + | "musicMuted" + | "refreshRate" + | "alwaysMultiplace" + | "offerHints" + | "enableTunnelSmartplace" + | "vignette" + | "rotationByBuilding" + | "compactBuildingInfo" + | "disableCutDeleteWarnings", + TitleAndDescription + > & { + autosaveInterval: TitleAndDescription & { intervals: Translation }; + movementSpeed: TitleAndDescription & { speeds: Translation }; + scrollWheelSensitivity: TitleAndDescription & { sensitivity: Translation }; + theme: TitleAndDescription & { themes: Translation }; + uiScale: TitleAndDescription & { scales: Translation }; + }; +}; + +declare type KeyBindings = Translation<"title" | "hint" | "resetKeybindings" | "categoryLabels"> & { + categoryLabels: Translation< + "general" | "ingame" | "navigation" | "placement" | "massSelect" | "buildings" | "placementModifiers" + >; + mappings: Translation< + | "confirm" + | "back" + | "mapMoveUp" + | "mapMoveRight" + | "mapMoveDown" + | "mapMoveLeft" + | "mapMoveFaster" + | "centerMap" + | "mapZoomIn" + | "mapZoomOut" + | "createMarker" + | "menuOpenShop" + | "menuOpenStats" + | "menuClose" + | "toggleHud" + | "toggleFPSInfo" + | "switchLayers" + | "exportScreenshot" + | "belt" + | "splitter" + | "underground_belt" + | "miner" + | "cutter" + | "advanced_processor" + | "rotater" + | "stacker" + | "mixer" + | "energy_generator" + | "painter" + | "trash" + | "wire" + | "pipette" + | "rotateWhilePlacing" + | "rotateInverseModifier" + | "cycleBuildingVariants" + | "confirmMassDelete" + | "pasteLastBlueprint" + | "cycleBuildings" + | "lockBeltDirection" + | "switchDirectionLockSide" + | "massSelectStart" + | "massSelectSelectMultiple" + | "massSelectCopy" + | "massSelectCut" + | "placementDisableAutoOrientation" + | "placeMultiple" + | "placeInverse" + >; +}; + +declare type About = Translation<"title" | "body">; + +declare type Changelog = Translation<"title">; + +declare type Demo = Translation<"settingNotAvailable"> & { + features: Translation< + "restoringGames" | "importingGames" | "oneGameLimit" | "customizeKeybindings" | "exportingBase" + >; +}; + +declare interface Translations { + steamPage: SteamPage; + global: Global; + demoBanners: DemoBanners; + mainMenu: MainMenu; + dialogs: Dialogs; + ingame: Ingame; + shopUpgrades: ShopUpgrades; + buildings: Buildings; + storyRewards: StoryRewards; + settings: Settings; + keybindings: KeyBindings; + about: About; + changelog: Changelog; + demo: Demo; +} + +declare module "yaml-loader!*" { + const content: Translations; + export default content; +} diff --git a/translations/base-cz.yaml b/translations/base-cs.yaml similarity index 100% rename from translations/base-cz.yaml rename to translations/base-cs.yaml diff --git a/translations/base-es.yaml b/translations/base-es-419.yaml similarity index 100% rename from translations/base-es.yaml rename to translations/base-es-419.yaml diff --git a/yarn.lock b/yarn.lock index a67c83c2..2f5b60cb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8555,7 +8555,15 @@ yaml-js@^0.1.3: resolved "https://registry.yarnpkg.com/yaml-js/-/yaml-js-0.1.5.tgz#a01369010b3558d8aaed2394615dfd0780fd8fac" integrity sha1-oBNpAQs1WNiq7SOUYV39B4D9j6w= -yaml@^1.10.0: +yaml-loader@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/yaml-loader/-/yaml-loader-0.6.0.tgz#fe1c48b9f4803dace55a59a1474e790ba6ab1b48" + integrity sha512-1bNiLelumURyj+zvVHOv8Y3dpCri0F2S+DCcmps0pA1zWRLjS+FhZQg4o3aUUDYESh73+pKZNI18bj7stpReow== + dependencies: + loader-utils "^1.4.0" + yaml "^1.8.3" + +yaml@^1.10.0, yaml@^1.8.3: version "1.10.0" resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e" integrity sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg==