@@ -100,20 +90,13 @@ export class MainMenuState extends GameState {
${T.changelog.title}
-
- ${
- !G_IS_STANDALONE &&
- this.app.platformWrapper instanceof PlatformWrapperImplBrowser &&
- this.app.platformWrapper.embedProvider.iogLink
- ? `
.io games`
- : ""
- }
+
+
${T.mainMenu.helpTranslate}
- `
- );
+ `;
}
requestImportSavegame() {
@@ -228,6 +211,8 @@ export class MainMenuState extends GameState {
this.trackClicks(qs(".settingsButton"), this.onSettingsButtonClicked);
this.trackClicks(qs(".changelog"), this.onChangelogClicked);
+ this.trackClicks(qs(".languageChoose"), this.onLanguageChooseClicked);
+ this.trackClicks(qs(".helpTranslate"), this.onTranslationHelpLinkClicked);
const contestButton = qs(".participateContest");
if (contestButton) {
@@ -290,6 +275,41 @@ export class MainMenuState extends GameState {
);
}
+ onLanguageChooseClicked() {
+ this.app.analytics.trackUiClick("choose_language");
+ const setting = /** @type {EnumSetting} */ (getApplicationSettingById("language"));
+
+ const { optionSelected } = this.dialogs.showOptionChooser(T.settings.labels.language.title, {
+ active: this.app.settings.getLanguage(),
+ options: setting.options.map(option => ({
+ value: setting.valueGetter(option),
+ text: setting.textGetter(option),
+ desc: setting.descGetter(option),
+ iconPrefix: setting.iconPrefix,
+ })),
+ });
+
+ optionSelected.add(value => {
+ this.app.settings.updateLanguage(value);
+ if (setting.restartRequired) {
+ if (this.app.platformWrapper.getSupportsRestart()) {
+ this.app.platformWrapper.performRestart();
+ } else {
+ this.dialogs.showInfo(T.dialogs.restartRequired.title, T.dialogs.restartRequired.text, [
+ "ok:good",
+ ]);
+ }
+ }
+
+ if (setting.changeCb) {
+ setting.changeCb(this.app, value);
+ }
+
+ // Update current icon
+ this.htmlElement.querySelector("button.languageChoose").setAttribute("data-languageIcon", value);
+ }, this);
+ }
+
renderSavegames() {
const oldContainer = this.htmlElement.querySelector(".mainContainer .savegames");
if (oldContainer) {
@@ -398,6 +418,13 @@ export class MainMenuState extends GameState {
this.moveToState("SettingsState");
}
+ onTranslationHelpLinkClicked() {
+ this.app.analytics.trackUiClick("translation_help_link");
+ this.app.platformWrapper.openExternalLink(
+ "https://github.com/tobspr/shapez.io/blob/master/translations"
+ );
+ }
+
onPlayButtonClicked() {
if (
IS_DEMO &&
diff --git a/src/js/states/preload.js b/src/js/states/preload.js
index 6ef99bc0..535d7004 100644
--- a/src/js/states/preload.js
+++ b/src/js/states/preload.js
@@ -3,7 +3,7 @@ import { createLogger } from "../core/logging";
import { findNiceValue, waitNextFrame } from "../core/utils";
import { cachebust } from "../core/cachebust";
import { PlatformWrapperImplBrowser } from "../platform/browser/wrapper";
-import { T } from "../translations";
+import { T, autoDetectLanguageId, updateApplicationLanguage } from "../translations";
import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs";
import { CHANGELOG } from "../changelog";
import { globalConfig } from "../core/config";
@@ -68,38 +68,6 @@ export class PreloadState extends GameState {
startLoading() {
this.setStatus("Booting")
- .then(() => this.setStatus("Checking for updates"))
- .then(() => {
- if (G_IS_STANDALONE) {
- return Promise.race([
- new Promise(resolve => setTimeout(resolve, 10000)),
- fetch(
- "https://itch.io/api/1/x/wharf/latest?target=tobspr/shapezio&channel_name=windows",
- {
- cache: "no-cache",
- }
- )
- .then(res => res.json())
- .then(({ latest }) => {
- if (latest !== G_BUILD_VERSION) {
- const { ok } = this.dialogs.showInfo(
- T.dialogs.newUpdate.title,
- T.dialogs.newUpdate.desc,
- ["ok:good"]
- );
-
- return new Promise(resolve => {
- ok.add(resolve);
- });
- }
- })
- .catch(err => {
- logger.log("Failed to fetch version:", err);
- }),
- ]);
- }
- })
-
.then(() => this.setStatus("Creating platform wrapper"))
.then(() => this.app.platformWrapper.initialize())
@@ -143,6 +111,19 @@ export class PreloadState extends GameState {
}
})
+ .then(() => this.setStatus("Initializing language"))
+ .then(() => {
+ if (this.app.settings.getLanguage() === "auto-detect") {
+ const language = autoDetectLanguageId();
+ logger.log("Setting language to", language);
+ return this.app.settings.updateLanguage(language);
+ }
+ })
+ .then(() => {
+ const language = this.app.settings.getLanguage();
+ updateApplicationLanguage(language);
+ })
+
.then(() => this.setStatus("Initializing sounds"))
.then(() => {
// Notice: We don't await the sounds loading itself
diff --git a/src/js/translations.js b/src/js/translations.js
index e91f3a4c..1e517a3d 100644
--- a/src/js/translations.js
+++ b/src/js/translations.js
@@ -1,9 +1,13 @@
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 const T = baseTranslations;
+export let T = baseTranslations;
if (G_IS_DEV && globalConfig.debug.testTranslations) {
// Replaces all translations by fake translations to see whats translated and what not
@@ -19,3 +23,114 @@ if (G_IS_DEV && globalConfig.debug.testTranslations) {
};
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");
+ }
+ languages = ["de-De"];
+
+ 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/translations/README.md b/translations/README.md
new file mode 100644
index 00000000..c6fd8127
--- /dev/null
+++ b/translations/README.md
@@ -0,0 +1,46 @@
+# Translations
+
+The base translation is `base-en.yaml`. It will always contain the latest phrases and structure.
+
+## Languages
+
+- [English (Base Language, Source of Truth)](base-en.yaml)
+- [German](base-de.yaml)
+- [French](base-fr.yaml)
+- [Korean](base-kor.yaml)
+- [Dutch](base-nl.yaml)
+- [Polish](base-pl.yaml)
+- [Portuguese (Brazil)](base-pt-BR.yaml)
+- [Portuguese (Portugal)](base-pt-PT.yaml)
+- [Russian](base-ru.yaml)
+- [Greek](base-el.yaml)
+- [Italian](base-it.yaml)
+- [Romanian](base-ro.yaml)
+- [Swedish](base-sv.yaml)
+- [Chinese (Simplified)](base-zh-CN.yaml)
+- [Chinese (Traditional)](base-zh-TW.yaml)
+- [Spanish](base-es.yaml)
+- [Hungarian](base-hu.yaml)
+- [Turkish](base-tr.yaml)
+- [Japanese](base-ja.yaml)
+
+(If you want to translate into a new language, see below!)
+
+## Editing existing translations
+
+If you want to edit an existing translation (Fixing typos, Updating it to a newer version, etc), you can just use the github file editor to edit the file.
+
+- Find the file you want to edit (For example, `base-de.yaml` if you want to change the german translation)
+- Click on the file name on, there will be a small "edit" symbol on the top right
+- Do the changes you wish to do (Be sure **not** to translate placeholders!)
+- Click "Propose Changes"
+- I will review your changes and make comments, and eventually merge them so they will be in the next release!
+
+## Adding a new language
+
+Please DM me on discord (tobspr#5407), so I can add the language template for you. It is not as simple as creating a new file.
+PS: I'm super busy, but I'll give my best to do it quickly!
+
+## Updating a language to the latest version
+
+Right now there is no possibility to automatically update a translation to the latest version. It is required to manually check the base translation (`base-en.yaml`) and compare it to the other translations to remove unused keys and add new ones.
diff --git a/translations/base-cz.yaml b/translations/base-cz.yaml
new file mode 100644
index 00000000..286cff75
--- /dev/null
+++ b/translations/base-cz.yaml
@@ -0,0 +1,713 @@
+# Czech translation
+
+steamPage:
+ # This is the short text appearing on the steam page
+ shortText: shapez.io je hra o stavbě továren pro automatizaci výroby a kombinování čím dál složitějších tvarů na nekonečné mapě.
+
+ # This is the long description for the steam page - It is contained here so you can help to translate it, and I will regulary update the store page.
+ # NOTICE:
+ # - Do not translate the first line (This is the gif image at the start of the store)
+ # - Please keep the markup (Stuff like [b], [list] etc) in the same format
+ longText: >-
+ [img]{STEAM_APP_IMAGE}/extras/store_page_gif.gif[/img]
+
+ shapez.io je hra o stavbě továren pro automatizaci výroby a kombinování tvarů. Poskytněte vyžadované, stále složitější tvary, aby jste postoupili ve hře dále, a odemkněte vylepšení pro zrychlení vaší továrny.
+
+ Protože poptávka postupně roste, musíte svou továrnu rozšiřovat tak, aby vyhověla potřebám - Nové zdroje, najdete na [b]nekonečné mapě[/b]!
+
+ Jen tvary by byly nuda, proto máme pigmenty kterými musíte dílky obarvit - zkombinujte červené, zelené a modré barvivo pro vytvoření dalších odstínů a obarvěte s nimi tvary pro uspokojení poptávky.
+
+ Hra obsahuje 18 úrovní (což by vás mělo zaměstnat na spoustu hodin!), ale nový obsah je neustále přidáván - je toho hodně naplánováno!
+
+
+ [b]Výhody plné hry[/b]
+
+ [list]
+ [*] Označování pozic na mapě
+ [*] Neomezený počet uložených her
+ [*] Tmavý motiv
+ [*] Více nastavení
+ [*] Pomůžete mi dále vyvíjet shapez.io ❤️
+ [*] Více funkcí v budoucnu!
+ [/list]
+
+ [b]Plánované funkce a komunitní návrhy[/b]
+
+ Tato hra je open source - kdokoli může přispět! Kromě toho [b]hodně[/b] poslouchám komunitu! Snažím se přečíst si všechny návrhy a vzít v úvahu zpětnou vazbu.
+
+ [list]
+ [*] Mód s příběhem, kde stavba budov stojí tvary
+ [*] Více úrovní a budov (exkluzivní pro plnou verzi)
+ [*] Různé mapy a zábrany na mapě
+ [*] Konfigurace generátoru map (úprava počtu a velikosti nálezišť, seed map, a další)
+ [*] Více tvarů
+ [*] Další zlepšení výkonu (I když hra již běží docela dobře!)
+ [*] Režim pro barvoslepé
+ [*] A mnohem více!
+ [/list]
+
+ Nezapomeňte se podívat na moji Trello nástěnku pro úplný plán! https://trello.com/b/ISQncpJP/shapezio
+
+global:
+ loading: Načítám
+ error: Chyba
+
+ # How big numbers are rendered, e.g. "10,000"
+ thousandsDivider: " "
+
+ # The suffix for large numbers, e.g. 1.3k, 400.2M, etc.
+ suffix:
+ thousands: k
+ millions: M
+ billions: B
+ trillions: T
+
+ # Shown for infinitely big numbers
+ infinite: nekonečno
+
+ time:
+ # Used for formatting past time dates
+ oneSecondAgo: před sekundou
+ xSecondsAgo: před