From e9b8e403daf7373e11b29fd56096bf77dfabe223 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Pol=C3=A1k?= <456647@muni.cz>
Date: Sun, 17 Sep 2023 15:08:42 +0200
Subject: [PATCH 1/4] Initial draft of i18n API
---
bundle.js | 1 +
site/i18n.js | 71 ++++++++++++++++++++++++++++++++++++++++++++
site/index.js | 13 ++++++++
site/locales/cs.json | 0
site/locales/de.json | 0
site/locales/en.json | 0
6 files changed, 85 insertions(+)
create mode 100755 site/i18n.js
create mode 100755 site/locales/cs.json
create mode 100755 site/locales/de.json
create mode 100755 site/locales/en.json
diff --git a/bundle.js b/bundle.js
index 76af6a0..b33f61f 100755
--- a/bundle.js
+++ b/bundle.js
@@ -23,6 +23,7 @@ function replaceFileContents(string, fileDir) {
const pattern = /%%([^%]+)%%|\/\/\s*include\s*"([^"]+)"/g;
return string.replace(pattern, (_, filename1, filename2) => {
+ if (filename2 != null) console.error("include is used!!!!!");
const filename = filename1 || filename2;
const filenamePath = path.join(fileDir, filename);
try {
diff --git a/site/i18n.js b/site/i18n.js
new file mode 100755
index 0000000..ee2e330
--- /dev/null
+++ b/site/i18n.js
@@ -0,0 +1,71 @@
+// Internationalization support
+// This file is used both serverside and clientside.
+
+/**
+ * Supported languages and their translations.
+ * Each file contains an object with key-value pairs.
+ *
+ * Argument substitution is supported through named arguments: translate("Hello {{name}}", {name: "World"})
+ *
+ * Plurals and arrays are currently not supported.
+ *
+ * If the key is missing in the current localization, it falls back to default localization
+ * and then to treating the key itself as the localization.
+ */
+
+const translations = {
+ cs: require("./locales/cs.json"),
+ de: require("./locales/de.json"),
+ en: require("./locales/en.json"),
+};
+/**
+ * @type {string[]}
+ */
+const locales = Object.keys(locales);
+const defaultLocale = "de";
+
+/** The currently selected locale. */
+var currentLocale = defaultLocale;
+
+/**
+ * Set the globally used locale.
+ * Expects a 2 character language code string, one from locales.
+ */
+function setLocale(locale) {
+ if (Object.hasOwn(locales, locale)) {
+ currentLocale = locale;
+ return true;
+ }
+ console.error("Attempted to setLocale to unsupported language: ", locale);
+ return false;
+}
+
+/**
+ * @param {string} key to translate
+ * @param {Object?} args arguments to substitute into the translated key
+ * @returns translated string
+ */
+function translate(key, args) {
+ let translation = locales[currentLocale][key];
+ if (translation === undefined) {
+ console.error("Untranslated key in ", currentLocale, ": ", key);
+ if (currentLocale != defaultLocale) {
+ translation = locales[defaultLocale][key] || key;
+ } else {
+ translation = key;
+ }
+ }
+
+ if (typeof args === "object") {
+ // Do argument substitution
+ for (const arg in args) {
+ translation = translation.replaceAll("{{" + arg + "}}", args[arg]);
+ }
+ }
+
+ return translation;
+}
+
+exports.setLocale = setLocale;
+exports.locales = locales;
+exports.translate = translate;
diff --git a/site/index.js b/site/index.js
index db47adf..b2315ec 100644
--- a/site/index.js
+++ b/site/index.js
@@ -6,11 +6,24 @@ const { STORE_KEYS } = require("./model/stores");
const { ProgressBar } = require("./views/progress-bar");
const progressBar = new ProgressBar(STORE_KEYS.length);
+const i18n = require("./i18n");
+
(async () => {
if (location.href.includes("heissepreise.github.io")) {
location.href = "https://heisse-preise.io";
}
+ // Find the most preferred supported language
+ for (const langCode in navigator.languages) {
+ // We don't do regional codes, so take just the language code
+ let lang = langCode.length >= 2 ? langCode.substring(0, 2) : null;
+ if (lang == null) continue;
+ if (i18n.locales.indexOf(lang) != -1) {
+ i18n.setLocale(lang);
+ break;
+ }
+ }
+
await model.load(() => progressBar.addStep());
const itemsFilter = document.querySelector("items-filter");
const itemsList = document.querySelector("items-list");
diff --git a/site/locales/cs.json b/site/locales/cs.json
new file mode 100755
index 0000000..e69de29
diff --git a/site/locales/de.json b/site/locales/de.json
new file mode 100755
index 0000000..e69de29
diff --git a/site/locales/en.json b/site/locales/en.json
new file mode 100755
index 0000000..e69de29
From d2ae0cc5759ad26023ea18cc675b816137f6a8c0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Pol=C3=A1k?= <456647@muni.cz>
Date: Sun, 17 Sep 2023 17:13:14 +0200
Subject: [PATCH 2/4] Translate all static html files into English and Czech
---
bundle.js | 71 ++++++++++++++++++++++++++----------
server.js | 50 ++++++++++++++++++-------
site/_templates/_footer.html | 12 +++---
site/_templates/_header.html | 6 +--
site/_templates/_menu.html | 6 +--
site/cart.html | 15 ++++++--
site/carts.html | 8 ++--
site/changes.html | 4 +-
site/i18n.js | 44 ++++++++++++++++------
site/imprint.html | 12 +++---
site/index.html | 10 +++--
site/locales/cs.json | 36 ++++++++++++++++++
site/locales/de.json | 36 ++++++++++++++++++
site/locales/en.json | 36 ++++++++++++++++++
site/media.html | 6 +--
site/settings.html | 2 +-
16 files changed, 274 insertions(+), 80 deletions(-)
mode change 100755 => 100644 bundle.js
mode change 100755 => 100644 site/i18n.js
mode change 100755 => 100644 site/locales/cs.json
mode change 100755 => 100644 site/locales/de.json
mode change 100755 => 100644 site/locales/en.json
diff --git a/bundle.js b/bundle.js
old mode 100755
new mode 100644
index b33f61f..fbe5dc7
--- a/bundle.js
+++ b/bundle.js
@@ -4,6 +4,7 @@ const chokidar = require("chokidar");
const esbuild = require("esbuild");
const { exec } = require("child_process");
const { promisify } = require("util");
+const i18n = require("./site/i18n");
function deleteDirectory(directory) {
if (fs.existsSync(directory)) {
@@ -19,39 +20,69 @@ function deleteDirectory(directory) {
}
}
-function replaceFileContents(string, fileDir) {
- const pattern = /%%([^%]+)%%|\/\/\s*include\s*"([^"]+)"/g;
+/**
+ * Process file content by resolving includes and translation placeholders.
+ *
+ * @param {string} fileContent original file content
+ * @param {string} fileDir path of the directory of the file
+ * @param {string} locale language code which should be used to resolve translation placeholders
+ * @returns {string}
+ */
+function replaceFileContents(fileContent, fileDir, locale) {
+ const pattern = /%%([^%]+)%%|__(.+?)__/g;
- return string.replace(pattern, (_, filename1, filename2) => {
- if (filename2 != null) console.error("include is used!!!!!");
- const filename = filename1 || filename2;
- const filenamePath = path.join(fileDir, filename);
- try {
- const data = fs.readFileSync(filenamePath, "utf8");
- const replacedData = replaceFileContents(data, path.dirname(filenamePath));
- return replacedData;
- } catch (error) {
- console.error(`Error reading file "${filenamePath}":`, error);
- return "";
+ return fileContent.replace(pattern, (_, filename, translationKey) => {
+ if (filename != undefined) {
+ const filenamePath = path.join(fileDir, filename);
+ try {
+ const data = fs.readFileSync(filenamePath, "utf8");
+ const replacedData = replaceFileContents(data, path.dirname(filenamePath), locale);
+ return replacedData;
+ } catch (error) {
+ console.error(`Error reading file "${filenamePath}":`, error);
+ return "";
+ }
+ } else if (translationKey != undefined) {
+ return i18n.translateWithLocale(locale, translationKey);
}
});
}
+/**
+ * Copy inputFile to outputFile, possibly modifying it in the process.
+ *
+ * @param {string} inputFile path
+ * @param {string} outputFile path
+ * @param {function(string, boolean, string):boolean} filter takes path, whether it is a directory and data (or null if directory), returns true if the file should be left out
+ */
function processFile(inputFile, outputFile, filter) {
- const fileDir = path.dirname(inputFile);
- if (inputFile.includes(".mp3")) {
+ let extension = path.extname(inputFile);
+ if (extension == ".html") {
+ const data = fs.readFileSync(inputFile, "utf8");
+ if (filter(inputFile, false, data)) return;
+ for (const locale of i18n.locales) {
+ const replacedData = replaceFileContents(data, path.dirname(inputFile), locale);
+ if (locale == i18n.defaultLocale) {
+ fs.writeFileSync(outputFile, replacedData);
+ }
+ let pathWithLanguageCode = outputFile.substring(0, outputFile.length - extension.length) + "." + locale + extension;
+ fs.writeFileSync(pathWithLanguageCode, replacedData);
+ }
+ } else {
const data = fs.readFileSync(inputFile);
if (filter(inputFile, false, data)) return;
fs.writeFileSync(outputFile, data);
- } else {
- const data = fs.readFileSync(inputFile, "utf8");
- if (filter(inputFile, false, data)) return;
- const replacedData = replaceFileContents(data, fileDir);
- fs.writeFileSync(outputFile, replacedData);
}
console.log(`${inputFile} -> ${outputFile}`);
}
+/**
+ *
+ * @param {string} inputDir path to the input directory, traversed recursively, outputDir and files/directories starting with _ are automatically skipped
+ * @param {string} outputDir path to the output directory
+ * @param {boolean} deleteOutput whether the contents of output directory should be deleted first
+ * @param {function(string, boolean, string):boolean} filter takes path, whether it is a directory and data (or null if directory), returns true if the file should be left out
+ */
function generateSite(inputDir, outputDir, deleteOutput, filter) {
if (deleteOutput) {
deleteDirectory(outputDir);
diff --git a/server.js b/server.js
index dec91c0..701ac38 100644
--- a/server.js
+++ b/server.js
@@ -7,6 +7,7 @@ const csv = require("./site/js/misc");
const chokidar = require("chokidar");
const express = require("express");
const compression = require("compression");
+const i18n = require("./site/i18n");
function copyItemsToSite(dataDir) {
const items = analysis.readJSON(`${dataDir}/latest-canonical.json.${analysis.FILE_COMPRESSOR}`);
@@ -45,6 +46,7 @@ function parseArguments() {
const args = process.argv.slice(2);
let port = process.env.PORT !== undefined && process.env.PORT != "" ? parseInt(process.env.PORT) : 3000;
let liveReload = process.env.NODE_ENV === "development" || false;
+ let skipDataUpdate = false;
for (let i = 0; i < args.length; i++) {
if (args[i] === "-p" || args[i] === "--port") {
port = parseInt(args[i + 1]);
@@ -54,6 +56,8 @@ function parseArguments() {
throw new Error("Live reload is only supported in development mode");
}
liveReload = true;
+ } else if (args[i] === "--skip-data-update") {
+ skipDataUpdate = true;
} else if (args[i] === "-h" || args[i] === "--help") {
console.log("Usage: node server.js [-p|--port PORT] [-l|--live-reload]");
console.log();
@@ -64,7 +68,7 @@ function parseArguments() {
}
}
- return { port, liveReload };
+ return { port, liveReload, skipDataUpdate };
}
function setupLogging() {
@@ -81,7 +85,7 @@ function setupLogging() {
(async () => {
const dataDir = "data";
- const { port, liveReload } = parseArguments();
+ const { port, liveReload, skipDataUpdate } = parseArguments();
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir);
@@ -102,25 +106,43 @@ function setupLogging() {
setupLogging();
bundle.bundle("site", outputDir, liveReload);
- analysis.migrateCompression(dataDir, ".json", ".json.br");
- analysis.migrateCompression(dataDir, ".json.gz", ".json.br");
+ if (!skipDataUpdate) {
+ analysis.migrateCompression(dataDir, ".json", ".json.br");
+ analysis.migrateCompression(dataDir, ".json.gz", ".json.br");
- if (fs.existsSync(`${dataDir}/latest-canonical.json.${analysis.FILE_COMPRESSOR}`)) {
- copyItemsToSite(dataDir);
- analysis.updateData(dataDir, (_newItems) => {
+ if (fs.existsSync(`${dataDir}/latest-canonical.json.${analysis.FILE_COMPRESSOR}`)) {
+ copyItemsToSite(dataDir);
+ analysis.updateData(dataDir, (_newItems) => {
+ copyItemsToSite(dataDir);
+ });
+ } else {
+ await analysis.updateData(dataDir);
+ copyItemsToSite(dataDir);
+ }
+ scheduleFunction(5, 0, 0, async () => {
+ items = await analysis.updateData(dataDir);
copyItemsToSite(dataDir);
});
- } else {
- await analysis.updateData(dataDir);
- copyItemsToSite(dataDir);
}
- scheduleFunction(5, 0, 0, async () => {
- items = await analysis.updateData(dataDir);
- copyItemsToSite(dataDir);
- });
const app = express();
app.use(compression());
+ app.use(function (req, res, next) {
+ if (req.method == "GET") {
+ if (req.path == "/") {
+ req.url = "/index.html";
+ }
+ if (req.path.endsWith(".html")) {
+ // Only html files are translated
+ let pickedLanguage = req.acceptsLanguages(i18n.locales);
+ if (pickedLanguage) {
+ let translatedPath = req.path.substring(0, req.path.length - "html".length) + pickedLanguage + ".html";
+ req.url = translatedPath;
+ } // otherwise use default, untranslated file
+ }
+ }
+ next();
+ });
app.use(express.static("site/output"));
const server = http.createServer(app).listen(port, () => {
console.log(`App listening on port ${port}`);
diff --git a/site/_templates/_footer.html b/site/_templates/_footer.html
index fcb0f58..2b355a6 100644
--- a/site/_templates/_footer.html
+++ b/site/_templates/_footer.html
@@ -1,8 +1,8 @@
- Alle Angaben ohne Gewähr, Irrtümer vorbehalten.
- Markennamen und Warenzeichen sind Eigentum der jeweiligen Inhaber.
+
__Alle Angaben ohne Gewähr, Irrtümer vorbehalten.__
+ __Markennamen und Warenzeichen sind Eigentum der jeweiligen Inhaber.__
diff --git a/site/_templates/_header.html b/site/_templates/_header.html
index d6f1a23..9b38ae8 100644
--- a/site/_templates/_header.html
+++ b/site/_templates/_header.html
@@ -4,8 +4,8 @@
-
-
Heisse Preise
+
+ __Heisse Preise__
-
🔥 Heisse Preise 🔥
+
🔥 __Heisse Preise__ 🔥
diff --git a/site/_templates/_menu.html b/site/_templates/_menu.html
index 2ad5fc1..db3d19d 100644
--- a/site/_templates/_menu.html
+++ b/site/_templates/_menu.html
@@ -1,5 +1,5 @@
\ No newline at end of file
diff --git a/site/cart.html b/site/cart.html
index 93beef9..ec150e4 100644
--- a/site/cart.html
+++ b/site/cart.html
@@ -4,13 +4,20 @@
- Noch keine Produkte im Warenkorb.
- Produkte suchen und mit '+' zum Warenkorb hinzufügen.
+ __Noch keine Produkte im Warenkorb.__
+ __Produkte suchen und mit '+' zum Warenkorb hinzufügen.__
-
+
-
+
%%_templates/_loader.html%%
diff --git a/site/carts.html b/site/carts.html
index 6c283a7..bd0c59f 100644
--- a/site/carts.html
+++ b/site/carts.html
@@ -2,11 +2,11 @@
-
Warenkörbe
+
__Warenkörbe__
-
-
-
+
+
+
%%_templates/_loader.html%%
diff --git a/site/changes.html b/site/changes.html
index 6306604..68d50a9 100644
--- a/site/changes.html
+++ b/site/changes.html
@@ -1,8 +1,8 @@
%%_templates/_header.html%% %%_templates/_menu.html%%
-
Preisänderungen
-
+ __Preisänderungen__
+
%%_templates/_loader.html%%
diff --git a/site/i18n.js b/site/i18n.js
old mode 100755
new mode 100644
index ee2e330..8e01aeb
--- a/site/i18n.js
+++ b/site/i18n.js
@@ -11,8 +11,9 @@
*
* If the key is missing in the current localization, it falls back to default localization
* and then to treating the key itself as the localization.
+ *
+ * @type {Object.
>}
*/
-
const translations = {
cs: require("./locales/cs.json"),
de: require("./locales/de.json"),
@@ -21,18 +22,25 @@ const translations = {
/**
* @type {string[]}
*/
-const locales = Object.keys(locales);
+const locales = Object.keys(translations);
+/**
+ * @type {string}
+ */
const defaultLocale = "de";
-/** The currently selected locale. */
+/**
+ * The currently selected locale.
+ * @type {string}
+ */
var currentLocale = defaultLocale;
/**
* Set the globally used locale.
* Expects a 2 character language code string, one from locales.
+ * @param {string} locale
*/
function setLocale(locale) {
- if (Object.hasOwn(locales, locale)) {
+ if (locales.indexOf(locale) != -1) {
currentLocale = locale;
return true;
}
@@ -41,16 +49,28 @@ function setLocale(locale) {
}
/**
- * @param {string} key to translate
- * @param {Object?} args arguments to substitute into the translated key
- * @returns translated string
+ * Translates the key using the current global locale.
+ *
+ * @param {!string} key to translate
+ * @param {!Object.} [args] arguments to substitute into the translated key
+ * @returns {string} translated string
*/
function translate(key, args) {
- let translation = locales[currentLocale][key];
+ return translateWithLocale(currentLocale, key, args);
+}
+
+/**
+ * @param {!string} locale name of the language to use for translation, MUST be one of the supported languages
+ * @param {!string} key to translate
+ * @param {!Object.} [args] arguments to substitute into the translated key
+ * @returns {string} translated string
+ */
+function translateWithLocale(locale, key, args) {
+ let translation = translations[locale][key];
if (translation === undefined) {
- console.error("Untranslated key in ", currentLocale, ": ", key);
- if (currentLocale != defaultLocale) {
- translation = locales[defaultLocale][key] || key;
+ console.error("Untranslated key in ", locale, ": ", key);
+ if (locale != defaultLocale) {
+ translation = translations[defaultLocale][key] || key;
} else {
translation = key;
}
@@ -67,5 +87,7 @@ function translate(key, args) {
}
exports.setLocale = setLocale;
+exports.defaultLocale = defaultLocale;
exports.locales = locales;
exports.translate = translate;
+exports.translateWithLocale = translateWithLocale;
diff --git a/site/imprint.html b/site/imprint.html
index 7e51149..609a403 100644
--- a/site/imprint.html
+++ b/site/imprint.html
@@ -1,14 +1,16 @@
%%_templates/_header.html%% %%_templates/_menu.html%%
-
Impressum
+
__Impressum__
Angaben gemäß § 25 Mediengesetz (Österreich)
- Medieninhaber: Mario Zechner
- Kontakt: badlogicgames@gmail.com
- Adresse: Schörgelgasse 3, 8010 Graz, Österreich
+ __Medieninhaber__: Mario Zechner
+ __Kontakt__: badlogicgames@gmail.com
+ __Adresse__: Schörgelgasse 3, 8010 Graz, Österreich
+
+
+ __Diese nicht-kommerzielle Seite dient KonsumentInnen dazu, Preise von Produkten im Lebensmittelhandel vergleichen zu können.__
-
Diese nicht-kommerzielle Seite dient KonsumentInnen dazu, Preise von Produkten im Lebensmittelhandel vergleichen zu können.
%%_templates/_footer.html%%
diff --git a/site/index.html b/site/index.html
index ad4e74b..82ebfc1 100644
--- a/site/index.html
+++ b/site/index.html
@@ -2,11 +2,13 @@
-
Produktsuche
+
__Produktsuche__
%%_templates/_loader.html%%
diff --git a/site/locales/cs.json b/site/locales/cs.json
old mode 100755
new mode 100644
index e69de29..e216620
--- a/site/locales/cs.json
+++ b/site/locales/cs.json
@@ -0,0 +1,36 @@
+{
+ "Heisse Preise": "Zlevněno?",
+ "__page_description__": "Nekomerční open source projekt umožňující spotřebitelům najít nejlevnější verzi produktu v obchodech.",
+ "Einstellungen": "Nastavení",
+ "Impressum": "Impressum",
+ "Logs": "Logy",
+ "Historische Daten von": "Historická data od",
+ "Alle Angaben ohne Gewähr, Irrtümer vorbehalten.": "Veškeré informace jsou poskytovány bez záruky, chyby vyhrazeny.",
+ "Markennamen und Warenzeichen sind Eigentum der jeweiligen Inhaber.": "Názvy značek a ochranné známky jsou majetkem příslušných vlastníků.",
+ "Suche": "Vyhledávání",
+ "Preisänderungen": "Změny cen",
+ "Warenkörbe": "Nákupní vozíky",
+
+ "Noch keine Produkte im Warenkorb.": "Nákupní košík je prázdný.",
+ "Produkte suchen und mit '+' zum Warenkorb hinzufügen.": "Vyhledejte produkty a přidejte je do nákupního košíku pomocí '+'.",
+ "Filtern...": "Filtr...",
+ "(min. 3 Zeichen)": "(alespoň 3 znaky)",
+ "Produkt hinzufügen...": "Přidat produkt...",
+
+ "Neuer Warenkorb": "Nový nákupní košík",
+ "Exportieren": "Export",
+ "Importieren": "Import",
+
+ "Medieninhaber": "Majitel",
+ "Kontakt": "Kontakt",
+ "Adresse": "Adresa",
+ "Diese nicht-kommerzielle Seite dient KonsumentInnen dazu, Preise von Produkten im Lebensmittelhandel vergleichen zu können.": "Tato nekomerční stránka umožňuje spotřebitelům porovnávat ceny produktů v obchodě s potravinami.",
+
+ "Video Anleitung": "Video instrukce (německy)",
+ "Text Anleitung": "Textové instrukce (německy)",
+ "Medienberichte": "Napsali o nás",
+ "Produktsuche": "Vyhledávání produktů",
+
+ "Radio & Fernsehen": "Rádio & Televize",
+ "Print & Online": "Tisk & Online"
+}
diff --git a/site/locales/de.json b/site/locales/de.json
old mode 100755
new mode 100644
index e69de29..07015b9
--- a/site/locales/de.json
+++ b/site/locales/de.json
@@ -0,0 +1,36 @@
+{
+ "Heisse Preise": "Heisse Preise",
+ "__page_description__": "Nicht-kommerzielles Open-Source-Projekt um KonsumentInnen es zu ermöglichen, die günstigste Variante eines Produktes im Handel ausfindig zu machen.",
+ "Einstellungen": "Einstellungen",
+ "Impressum": "Impressum",
+ "Logs": "Logs",
+ "Historische Daten von": "Historische Daten von",
+ "Alle Angaben ohne Gewähr, Irrtümer vorbehalten.": "Alle Angaben ohne Gewähr, Irrtümer vorbehalten.",
+ "Markennamen und Warenzeichen sind Eigentum der jeweiligen Inhaber.": "Markennamen und Warenzeichen sind Eigentum der jeweiligen Inhaber.",
+ "Suche": "Suche",
+ "Preisänderungen": "Preisänderungen",
+ "Warenkörbe": "Warenkörbe",
+
+ "Noch keine Produkte im Warenkorb.": "Noch keine Produkte im Warenkorb.",
+ "Produkte suchen und mit '+' zum Warenkorb hinzufügen.": "Produkte suchen und mit '+' zum Warenkorb hinzufügen.",
+ "Filtern...": "Filtern...",
+ "(min. 3 Zeichen)": "(min. 3 Zeichen)",
+ "Produkt hinzufügen...": "Produkt hinzufügen...",
+
+ "Neuer Warenkorb": "Neuer Warenkorb",
+ "Exportieren": "Exportieren",
+ "Importieren": "Importieren",
+
+ "Medieninhaber": "Medieninhaber",
+ "Kontakt": "Kontakt",
+ "Adresse": "Adresse",
+ "Diese nicht-kommerzielle Seite dient KonsumentInnen dazu, Preise von Produkten im Lebensmittelhandel vergleichen zu können.": "Diese nicht-kommerzielle Seite dient KonsumentInnen dazu, Preise von Produkten im Lebensmittelhandel vergleichen zu können.",
+
+ "Video Anleitung": "Video Anleitung",
+ "Text Anleitung": "Text Anleitung",
+ "Medienberichte": "Medienberichte",
+ "Produktsuche": "Produktsuche",
+
+ "Radio & Fernsehen": "Radio & Fernsehen",
+ "Print & Online": "Print & Online"
+}
diff --git a/site/locales/en.json b/site/locales/en.json
old mode 100755
new mode 100644
index e69de29..acc6f27
--- a/site/locales/en.json
+++ b/site/locales/en.json
@@ -0,0 +1,36 @@
+{
+ "Heisse Preise": "Hot Prices",
+ "__page_description__": "Non-commercial open source project to enable consumers to find the cheapest version of a product in stores.",
+ "Einstellungen": "Settings",
+ "Impressum": "Imprint",
+ "Logs": "Logs",
+ "Historische Daten von": "Histoic data from",
+ "Alle Angaben ohne Gewähr, Irrtümer vorbehalten.": "All information provided without guarantee, errors excepted.",
+ "Markennamen und Warenzeichen sind Eigentum der jeweiligen Inhaber.": "Brand names and trademarks are the property of their respective owners.",
+ "Suche": "Search",
+ "Preisänderungen": "Price changes",
+ "Warenkörbe": "Shopping carts",
+
+ "Noch keine Produkte im Warenkorb.": "No products in your shopping cart yet.",
+ "Produkte suchen und mit '+' zum Warenkorb hinzufügen.": "Search for products and add them to the shopping cart with '+'.",
+ "Filtern...": "Filter...",
+ "(min. 3 Zeichen)": "(at least 3 characters)",
+ "Produkt hinzufügen...": "Add product...",
+
+ "Neuer Warenkorb": "New shopping cart",
+ "Exportieren": "Export",
+ "Importieren": "Import",
+
+ "Medieninhaber": "Owner",
+ "Kontakt": "Contact",
+ "Adresse": "Address",
+ "Diese nicht-kommerzielle Seite dient KonsumentInnen dazu, Preise von Produkten im Lebensmittelhandel vergleichen zu können.": "This non-commercial site allows consumers to compare prices of products in the grocery store.",
+
+ "Video Anleitung": "Video instructions (in German)",
+ "Text Anleitung": "Text instructions (in German)",
+ "Medienberichte": "Media reports",
+ "Produktsuche": "Product search",
+
+ "Radio & Fernsehen": "Radio & Television",
+ "Print & Online": "Print & Online"
+}
diff --git a/site/media.html b/site/media.html
index e837a55..cc2199c 100644
--- a/site/media.html
+++ b/site/media.html
@@ -1,9 +1,9 @@
%%_templates/_header.html%% %%_templates/_menu.html%%
-
Medienberichte
+
__Medienberichte__
-
Radio & Fernsehen
+
__Radio & Fernsehen__
24.5.203 Österreichisches Parlament - Rede von Joachim Schnabel zum Thema Lebensmittelpreise, ÖVP
@@ -42,7 +42,7 @@
>
-
Print & Online
+
__Print & Online__
16.5.2023 PULS 24 - Preisvergleich: Politik braucht "bis Herbst", Twitter-User "2 Stunden"
diff --git a/site/settings.html b/site/settings.html
index 2821573..1332ea9 100644
--- a/site/settings.html
+++ b/site/settings.html
@@ -1,7 +1,7 @@
%%_templates/_header.html%% %%_templates/_menu.html%%
-
Einstellungen
+ __Einstellungen__
From f36b44fc42f227bbcbe88255cbd6e0f8a95bb947 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Pol=C3=A1k?= <456647@muni.cz>
Date: Sun, 17 Sep 2023 19:21:42 +0200
Subject: [PATCH 3/4] Translate strings in JavaScript
---
server.js | 2 ++
site/cart.js | 19 +++++++----
site/carts.js | 9 +++--
site/i18n.js | 5 ++-
site/index.js | 38 +++++++++++----------
site/js/misc.js | 6 +++-
site/locales/cs.json | 67 ++++++++++++++++++++++++++++++++++--
site/locales/de.json | 67 ++++++++++++++++++++++++++++++++++--
site/locales/en.json | 69 ++++++++++++++++++++++++++++++++++++--
site/settings.js | 23 ++++++++-----
site/views/carts-list.js | 19 ++++++-----
site/views/items-chart.js | 19 ++++++-----
site/views/items-filter.js | 29 +++++++++-------
site/views/items-list.js | 41 +++++++++++-----------
14 files changed, 321 insertions(+), 92 deletions(-)
diff --git a/server.js b/server.js
index 701ac38..4fe21d0 100644
--- a/server.js
+++ b/server.js
@@ -123,6 +123,8 @@ function setupLogging() {
items = await analysis.updateData(dataDir);
copyItemsToSite(dataDir);
});
+ } else {
+ copyItemsToSite(dataDir);
}
const app = express();
diff --git a/site/cart.js b/site/cart.js
index c906ca6..85763aa 100644
--- a/site/cart.js
+++ b/site/cart.js
@@ -6,6 +6,7 @@ const { STORE_KEYS, stores } = require("./model/stores");
require("./views");
const { ProgressBar } = require("./views/progress-bar");
const progressBar = new ProgressBar(STORE_KEYS.length);
+const { __ } = require("./i18n");
let carts = null;
@@ -35,8 +36,12 @@ class CartHeader extends View {
-
Teilen
-
+
${__(
+ "Cart_Teilen"
+ )}
+
`;
const elements = this.elements;
@@ -47,7 +52,9 @@ class CartHeader extends View {
let newName = cart.name;
while (true) {
newName = prompt(
- "Warenkorb '" + cart.name + " existiert bereits. Bitte einen anderen Namen für den zu speichernden Warenkorb eingeben",
+ __("Cart_Warenkorb '{{name}}' existiert bereits. Bitte einen anderen Namen für den zu speichernden Warenkorb eingeben", {
+ name: cart.name,
+ }),
cart.name + today()
);
if (!newName || newName.trim().length == 0) return;
@@ -69,7 +76,7 @@ class CartHeader extends View {
render() {
const cart = this.model.cart;
const elements = this.elements;
- elements.name.innerText = `Warenkorb '${cart.name}'`;
+ elements.name.innerText = __("Cart_Warenkorb {{name}}", { name: cart.name });
if (this.model.linked) {
elements.save.classList.remove("hidden");
} else {
@@ -112,7 +119,7 @@ function loadCart() {
}
if (cart == null) {
- alert("Warenkorb '" + cartName + "' existiert nicht.");
+ alert(__("Cart_Warenkorb '{{name}}' existiert nicht.", { name: cartName }));
location.href = "carts.html";
}
@@ -133,7 +140,7 @@ function loadCart() {
STORE_KEYS.forEach((store) => {
cartFilter.elements[store].checked = true;
});
- cartList.elements.numItemsLabel.innerHTML = "
Artikel: ";
+ cartList.elements.numItemsLabel.innerHTML = `
${__("Cart_Artikel")}: `;
cartList.elements.enableChart.checked = models.items.length < 2000;
cartList.elements.chart.elements.sumStores.checked = models.items.length < 2000;
diff --git a/site/carts.js b/site/carts.js
index 1152333..50877a3 100644
--- a/site/carts.js
+++ b/site/carts.js
@@ -1,17 +1,18 @@
const { downloadJSON, today } = require("./js/misc");
const model = require("./model");
require("./views");
+const { __ } = require("./i18n");
const { STORE_KEYS } = require("./model/stores");
const { ProgressBar } = require("./views/progress-bar");
const progressBar = new ProgressBar(STORE_KEYS.length);
function newCart() {
- let name = prompt("Name für Warenkorb eingeben:");
+ let name = prompt(__("Carts_Name für Warenkorb eingeben:"));
if (!name || name.trim().length == 0) return;
name = name.trim();
if (model.carts.carts.some((cart) => cart.name === name)) {
- alert("Warenkorb mit Namen '" + name + "' existiert bereits");
+ alert(__("Carts_Warenkorb mit Namen '{{name}}' existiert bereits", { name: name }));
return;
}
model.carts.add(name);
@@ -36,7 +37,9 @@ function importCart(importedCart) {
let newName = importedCart.name;
while (true) {
newName = prompt(
- "Warenkorb '" + importedCart.name + " existiert bereits. Bitte einen anderen Namen für den zu importierenden Warenkorb eingeben",
+ __("Carts_Warenkorb '{{name}}' existiert bereits. Bitte einen anderen Namen für den zu importierenden Warenkorb eingeben", {
+ name: importedCart.name,
+ }),
importedCart.name + today()
);
if (!newName || newName.trim().length == 0) return;
diff --git a/site/i18n.js b/site/i18n.js
index 8e01aeb..7930972 100644
--- a/site/i18n.js
+++ b/site/i18n.js
@@ -15,6 +15,7 @@
* @type {Object.
>}
*/
const translations = {
+ // sorted alphabetically
cs: require("./locales/cs.json"),
de: require("./locales/de.json"),
en: require("./locales/en.json"),
@@ -40,7 +41,8 @@ var currentLocale = defaultLocale;
* @param {string} locale
*/
function setLocale(locale) {
- if (locales.indexOf(locale) != -1) {
+ if (locales.includes(locale)) {
+ console.log("Locale changed to " + locale);
currentLocale = locale;
return true;
}
@@ -90,4 +92,5 @@ exports.setLocale = setLocale;
exports.defaultLocale = defaultLocale;
exports.locales = locales;
exports.translate = translate;
+exports.__ = translate;
exports.translateWithLocale = translateWithLocale;
diff --git a/site/index.js b/site/index.js
index b2315ec..dc4b4d3 100644
--- a/site/index.js
+++ b/site/index.js
@@ -1,3 +1,24 @@
+const i18n = require("./i18n");
+
+// Process redirects and localization before running anything else
+(() => {
+ if (location.href.includes("heissepreise.github.io")) {
+ location.href = "https://heisse-preise.io";
+ return;
+ }
+
+ // Find the most preferred supported language
+ for (const langCode of navigator.languages) {
+ // We don't do regional codes, so take just the language code
+ let lang = langCode.length >= 2 ? langCode.substring(0, 2) : null;
+ if (lang == null) continue;
+ if (i18n.locales.includes(lang)) {
+ i18n.setLocale(lang);
+ break;
+ }
+ }
+})();
+
const { getQueryParameter } = require("./js/misc");
const model = require("./model");
require("./views");
@@ -6,24 +27,7 @@ const { STORE_KEYS } = require("./model/stores");
const { ProgressBar } = require("./views/progress-bar");
const progressBar = new ProgressBar(STORE_KEYS.length);
-const i18n = require("./i18n");
-
(async () => {
- if (location.href.includes("heissepreise.github.io")) {
- location.href = "https://heisse-preise.io";
- }
-
- // Find the most preferred supported language
- for (const langCode in navigator.languages) {
- // We don't do regional codes, so take just the language code
- let lang = langCode.length >= 2 ? langCode.substring(0, 2) : null;
- if (lang == null) continue;
- if (i18n.locales.indexOf(lang) != -1) {
- i18n.setLocale(lang);
- break;
- }
- }
-
await model.load(() => progressBar.addStep());
const itemsFilter = document.querySelector("items-filter");
const itemsList = document.querySelector("items-list");
diff --git a/site/js/misc.js b/site/js/misc.js
index c226a94..045cc69 100644
--- a/site/js/misc.js
+++ b/site/js/misc.js
@@ -305,7 +305,11 @@ exports.itemsToCSV = (items) => {
*/
exports.numberToLocale = (number) => {
try {
- return number.toLocaleString("at-DE", { minimumFractionDigits: 0, maximumFractionDigits: 2 });
+ let locale = "at-DE";
+ if (navigator) {
+ locale = navigator.language;
+ }
+ return number.toLocaleString(locale, { minimumFractionDigits: 0, maximumFractionDigits: 2 });
} catch (e) {
return number;
}
diff --git a/site/locales/cs.json b/site/locales/cs.json
index e216620..8cbd8d2 100644
--- a/site/locales/cs.json
+++ b/site/locales/cs.json
@@ -1,6 +1,6 @@
{
"Heisse Preise": "Zlevněno?",
- "__page_description__": "Nekomerční open source projekt umožňující spotřebitelům najít nejlevnější verzi produktu v obchodech.",
+ "page_description": "Nekomerční open source projekt umožňující spotřebitelům najít nejlevnější verzi produktu v obchodech.",
"Einstellungen": "Nastavení",
"Impressum": "Impressum",
"Logs": "Logy",
@@ -32,5 +32,68 @@
"Produktsuche": "Vyhledávání produktů",
"Radio & Fernsehen": "Rádio & Televize",
- "Print & Online": "Tisk & Online"
+ "Print & Online": "Tisk & Online",
+
+ "CartsList_Name": "Název",
+ "CartsList_Produkte": "Produkt",
+ "CartsList_Preis": "Cena",
+
+ "CartsList_Preisänderungen": "Změny cen",
+ "CartsList_Teilen": "Sdílet",
+ "CartsList_JSON": "JSON",
+ "CartsList_Löschen": "Smazat",
+
+ "ItemsChart_Keine Daten ausgewählt": "Nejsou vybrána žádná data",
+ "ItemsChart_Preissumme Gesamt": "Celková cena",
+ "ItemsChart_Preissumme Ketten": "Cena řetězce",
+ "ItemsChart_Nur heutige Preise": "Cena dnes",
+ "ItemsChart_Änderung in % seit": "Změna v % od",
+ "ItemsChart_Änderung in % seit {{date}}": "Změna v % od {{date}}",
+ "ItemsChart_Preissumme {{s}}": "Cena {{s}}",
+
+ "ItemsFilter_Produkte suchen...": "Vyhledávání produktů...",
+ "ItemsFilter_Filter anzeigen/ausblenden": "Zobrazit/skrýt filtry",
+ "ItemsFilter_Alle": "Vše",
+ "ItemsFilter_Datum": "Datum",
+ "ItemsFilter_Billiger seit letzter Änderung": "Levnější od poslední změny",
+ "ItemsFilter_Nur Diskont-Eigenmarken": "Pouze privátní značky",
+ "ItemsFilter_Nur Bio": "Pouze bio",
+ "ItemsFilter_Exaktes Wort": "Přesná slova",
+ "ItemsFilter_Preis €": "Cena €",
+ "ItemsFilter_Teurer": "Dražší",
+ "ItemsFilter_Billiger": "Levnější",
+
+ "ItemsList_Resultate": "Výsledky",
+ "ItemsList_Diagramm": "Graf",
+ "ItemsList_Verkaufspreis": "Prodejní cena",
+ "ItemsList_Mengenpreis": "Měrná cena",
+ "ItemsList_Sortieren": "Seřadit podle",
+ "ItemsList_Preis aufsteigend": "Cena vzestupně",
+ "ItemsList_Preis absteigend": "Cena sestupně",
+ "ItemsList_Menge aufsteigend": "Množství vzsetupně",
+ "ItemsList_Menge absteigend": "Množství sestupně",
+ "ItemsList_Kette & Name": "Řetězec & název",
+ "ItemsList_Namensähnlichkeit": "Podobnost jména",
+ "ItemsList_Kette": "Řetězec",
+ "ItemsList_Name": "Název",
+ "ItemsList_Preis": "Cena",
+
+ "Cart_Teilen": "Sdílet",
+ "Cart_Speichern": "Uložit",
+ "Cart_Warenkorb '{{name}}' existiert bereits. Bitte einen anderen Namen für den zu speichernden Warenkorb eingeben": "Nákupní košík '{{name}}' již existuje. Zadejte prosím jiný název",
+ "Cart_Warenkorb '{{name}}' existiert nicht.": "Nákupní košík '{{name}}' neexistuje.",
+ "Cart_Artikel": "Položka",
+
+ "Carts_Name für Warenkorb eingeben:": "Zadejte název nákupního košíku:",
+ "Carts_Warenkorb mit Namen '{{name}}' existiert bereits": "Nákupní košík se jménem '{{name}}' již existuje",
+ "Carts_Warenkorb '{{name}}' existiert bereits. Bitte einen anderen Namen für den zu importierenden Warenkorb eingeben": "Nákupní košík '{{name}}' již existuje. Zadejte prosím jiný název pod který se nákupní košík importuje",
+
+ "Settings_Vorselektierte Ketten": "Předvybrané řetězce",
+ "Settings_Start-Datum für Diagramme": "Počáteční datum pro grafy",
+ "Settings_Diagramm Typ": "Typ grafu",
+ "Settings_Stufen": "Schodový",
+ "Settings_Linien": "Čárový",
+ "Settings_Nur verfügbare Produkte anzeigen": "Zobrazit pouze dostupné produkty",
+ "Settings_Diagramm immer anzeigen (wenn verfügbar)": "Vždy zobrazovat graf (pokud je k dispozici)",
+ "Settings_Suche immer anzeigen (wenn verfügbar)": "Vždy zobrazit vyhledávání (je-li k dispozici)"
}
diff --git a/site/locales/de.json b/site/locales/de.json
index 07015b9..8db43b5 100644
--- a/site/locales/de.json
+++ b/site/locales/de.json
@@ -1,6 +1,6 @@
{
"Heisse Preise": "Heisse Preise",
- "__page_description__": "Nicht-kommerzielles Open-Source-Projekt um KonsumentInnen es zu ermöglichen, die günstigste Variante eines Produktes im Handel ausfindig zu machen.",
+ "page_description": "Nicht-kommerzielles Open-Source-Projekt um KonsumentInnen es zu ermöglichen, die günstigste Variante eines Produktes im Handel ausfindig zu machen.",
"Einstellungen": "Einstellungen",
"Impressum": "Impressum",
"Logs": "Logs",
@@ -32,5 +32,68 @@
"Produktsuche": "Produktsuche",
"Radio & Fernsehen": "Radio & Fernsehen",
- "Print & Online": "Print & Online"
+ "Print & Online": "Print & Online",
+
+ "CartsList_Name": "Name",
+ "CartsList_Produkte": "Produkte",
+ "CartsList_Preis": "Preis",
+
+ "CartsList_Preisänderungen": "Preisänderungen",
+ "CartsList_Teilen": "Teilen",
+ "CartsList_JSON": "JSON",
+ "CartsList_Löschen": "Löschen",
+
+ "ItemsChart_Keine Daten ausgewählt": "Keine Daten ausgewählt",
+ "ItemsChart_Preissumme Gesamt": "Preissumme Gesamt",
+ "ItemsChart_Preissumme Ketten": "Preissumme Ketten",
+ "ItemsChart_Nur heutige Preise": "Nur heutige Preise",
+ "ItemsChart_Änderung in % seit": "Änderung in % seit",
+ "ItemsChart_Änderung in % seit {{date}}": "Änderung in % seit {{date}}",
+ "ItemsChart_Preissumme {{s}}": "Preissumme {{s}}",
+
+ "ItemsFilter_Produkte suchen...": "Produkte suchen...",
+ "ItemsFilter_Filter anzeigen/ausblenden": "Filter anzeigen/ausblenden",
+ "ItemsFilter_Alle": "Alle",
+ "ItemsFilter_Datum": "Datum",
+ "ItemsFilter_Billiger seit letzter Änderung": "Billiger seit letzter Änderung",
+ "ItemsFilter_Nur Diskont-Eigenmarken": "Nur Diskont-Eigenmarken",
+ "ItemsFilter_Nur Bio": "Nur Bio",
+ "ItemsFilter_Exaktes Wort": "Exaktes Wort",
+ "ItemsFilter_Preis €": "Preis €",
+ "ItemsFilter_Teurer": "Teurer",
+ "ItemsFilter_Billiger": "Billiger",
+
+ "ItemsList_Resultate": "Resultate",
+ "ItemsList_Diagramm": "Diagramm",
+ "ItemsList_Verkaufspreis": "Verkaufspreis",
+ "ItemsList_Mengenpreis": "Mengenpreis",
+ "ItemsList_Sortieren": "Sortieren",
+ "ItemsList_Preis aufsteigend": "Preis aufsteigend",
+ "ItemsList_Preis absteigend": "Preis absteigend",
+ "ItemsList_Menge aufsteigend": "Menge aufsteigend",
+ "ItemsList_Menge absteigend": "Menge absteigend",
+ "ItemsList_Kette & Name": "Kette & Name",
+ "ItemsList_Namensähnlichkeit": "Namensähnlichkeit",
+ "ItemsList_Kette": "Kette",
+ "ItemsList_Name": "Name",
+ "ItemsList_Preis": "Preis",
+
+ "Cart_Teilen": "Teilen",
+ "Cart_Speichern": "Speichern",
+ "Cart_Warenkorb '{{name}}' existiert bereits. Bitte einen anderen Namen für den zu speichernden Warenkorb eingeben": "Warenkorb '{{name}}' existiert bereits. Bitte einen anderen Namen für den zu speichernden Warenkorb eingeben",
+ "Cart_Warenkorb '{{name}}' existiert nicht.": "Warenkorb '{{name}}' existiert nicht.",
+ "Cart_Artikel": "Artikel",
+
+ "Carts_Name für Warenkorb eingeben:": "Name für Warenkorb eingeben:",
+ "Carts_Warenkorb mit Namen '{{name}}' existiert bereits": "Warenkorb mit Namen '{{name}}' existiert bereits",
+ "Carts_Warenkorb '{{name}}' existiert bereits. Bitte einen anderen Namen für den zu importierenden Warenkorb eingeben": "Warenkorb '{{name}}' existiert bereits. Bitte einen anderen Namen für den zu importierenden Warenkorb eingeben",
+
+ "Settings_Vorselektierte Ketten": "Vorselektierte Ketten",
+ "Settings_Start-Datum für Diagramme": "Start-Datum für Diagramme",
+ "Settings_Diagramm Typ": "Diagramm Typ",
+ "Settings_Stufen": "Stufen",
+ "Settings_Linien": "Linien",
+ "Settings_Nur verfügbare Produkte anzeigen": "Nur verfügbare Produkte anzeigen",
+ "Settings_Diagramm immer anzeigen (wenn verfügbar)": "Diagramm immer anzeigen (wenn verfügbar)",
+ "Settings_Suche immer anzeigen (wenn verfügbar)": "Suche immer anzeigen (wenn verfügbar)"
}
diff --git a/site/locales/en.json b/site/locales/en.json
index acc6f27..fd808cd 100644
--- a/site/locales/en.json
+++ b/site/locales/en.json
@@ -1,10 +1,10 @@
{
"Heisse Preise": "Hot Prices",
- "__page_description__": "Non-commercial open source project to enable consumers to find the cheapest version of a product in stores.",
+ "page_description": "Non-commercial open source project to enable consumers to find the cheapest version of a product in stores.",
"Einstellungen": "Settings",
"Impressum": "Imprint",
"Logs": "Logs",
- "Historische Daten von": "Histoic data from",
+ "Historische Daten von": "Historic data from",
"Alle Angaben ohne Gewähr, Irrtümer vorbehalten.": "All information provided without guarantee, errors excepted.",
"Markennamen und Warenzeichen sind Eigentum der jeweiligen Inhaber.": "Brand names and trademarks are the property of their respective owners.",
"Suche": "Search",
@@ -32,5 +32,68 @@
"Produktsuche": "Product search",
"Radio & Fernsehen": "Radio & Television",
- "Print & Online": "Print & Online"
+ "Print & Online": "Print & Online",
+
+ "CartsList_Name": "Name",
+ "CartsList_Produkte": "Product",
+ "CartsList_Preis": "Price",
+
+ "CartsList_Preisänderungen": "Price changes",
+ "CartsList_Teilen": "Share",
+ "CartsList_JSON": "JSON",
+ "CartsList_Löschen": "Delete",
+
+ "ItemsChart_Keine Daten ausgewählt": "No data selected",
+ "ItemsChart_Preissumme Gesamt": "Total price",
+ "ItemsChart_Preissumme Ketten": "Store price",
+ "ItemsChart_Nur heutige Preise": "Price today",
+ "ItemsChart_Änderung in % seit": "Change in % since",
+ "ItemsChart_Änderung in % seit {{date}}": "Change in % since {{date}}",
+ "ItemsChart_Preissumme {{s}}": "Price {{s}}",
+
+ "ItemsFilter_Produkte suchen...": "Product search...",
+ "ItemsFilter_Filter anzeigen/ausblenden": "Show/hide filters",
+ "ItemsFilter_Alle": "All",
+ "ItemsFilter_Datum": "Date",
+ "ItemsFilter_Billiger seit letzter Änderung": "Cheaper since last change",
+ "ItemsFilter_Nur Diskont-Eigenmarken": "Private brands only",
+ "ItemsFilter_Nur Bio": "Only bio",
+ "ItemsFilter_Exaktes Wort": "Exact word",
+ "ItemsFilter_Preis €": "Price €",
+ "ItemsFilter_Teurer": "More expensive",
+ "ItemsFilter_Billiger": "Cheaper",
+
+ "ItemsList_Resultate": "Results",
+ "ItemsList_Diagramm": "Chart",
+ "ItemsList_Verkaufspreis": "Unit price",
+ "ItemsList_Mengenpreis": "Bulk price",
+ "ItemsList_Sortieren": "Sort by",
+ "ItemsList_Preis aufsteigend": "Price ascending",
+ "ItemsList_Preis absteigend": "Price descending",
+ "ItemsList_Menge aufsteigend": "Quantity ascending",
+ "ItemsList_Menge absteigend": "Quantity descending",
+ "ItemsList_Kette & Name": "Store chain & name",
+ "ItemsList_Namensähnlichkeit": "Name similarity",
+ "ItemsList_Kette": "Store chain",
+ "ItemsList_Name": "Name",
+ "ItemsList_Preis": "Price",
+
+ "Cart_Teilen": "Share",
+ "Cart_Speichern": "Save",
+ "Cart_Warenkorb '{{name}}' existiert bereits. Bitte einen anderen Namen für den zu speichernden Warenkorb eingeben": "Shopping cart '{{name}}' already exists. Please enter a different name for the shopping cart to be saved",
+ "Cart_Warenkorb '{{name}}' existiert nicht.": "Shopping cart '{{name}}' does not exist.",
+ "Cart_Artikel": "Item",
+
+ "Carts_Name für Warenkorb eingeben:": "Enter name for shopping cart:",
+ "Carts_Warenkorb mit Namen '{{name}}' existiert bereits": "Shopping cart with name '{{name}}' already exists",
+ "Carts_Warenkorb '{{name}}' existiert bereits. Bitte einen anderen Namen für den zu importierenden Warenkorb eingeben": "Shopping cart '{{name}}' already exists. Please enter a different name for the shopping cart to be imported",
+
+ "Settings_Vorselektierte Ketten": "Pre-selected store chains",
+ "Settings_Start-Datum für Diagramme": "Start date for charts",
+ "Settings_Diagramm Typ": "Chart type",
+ "Settings_Stufen": "Stepped",
+ "Settings_Linien": "Lines",
+ "Settings_Nur verfügbare Produkte anzeigen": "Show only available products",
+ "Settings_Diagramm immer anzeigen (wenn verfügbar)": "Always show chart (when available)",
+ "Settings_Suche immer anzeigen (wenn verfügbar)": "Always show search (when available)"
}
diff --git a/site/settings.js b/site/settings.js
index bfaccd3..7dfe840 100644
--- a/site/settings.js
+++ b/site/settings.js
@@ -3,13 +3,14 @@ const { View } = require("./views/view");
const { Settings } = require("./model/settings");
require("./js/misc");
require("./views/custom-checkbox");
+const { __ } = require("./i18n");
class SettingsView extends View {
constructor() {
super();
this.innerHTML = /*html*/ `
-
Vorselektierte Ketten
+
${__("Settings_Vorselektierte Ketten")}
${STORE_KEYS.map(
(store) => /*html*/ `
@@ -22,7 +23,7 @@ class SettingsView extends View {
).join("")}
- Start-Datum für Diagramme
+ ${__("Settings_Start-Datum für Diagramme")}
- Diagramm Typ
+ ${__("Settings_Diagramm Typ")}
- Stufen
- Linien
+ ${__("Settings_Stufen")}
+ ${__("Settings_Linien")}
-
-
-
+
+
+
`;
this.setupEventHandlers();
diff --git a/site/views/carts-list.js b/site/views/carts-list.js
index 75f9c67..6f1735d 100644
--- a/site/views/carts-list.js
+++ b/site/views/carts-list.js
@@ -1,5 +1,6 @@
const { downloadJSON, dom } = require("../js/misc");
const { View } = require("./view");
+const { __ } = require("../i18n");
class CartsList extends View {
constructor() {
@@ -8,9 +9,9 @@ class CartsList extends View {
- Name
- Produkte
- Preis
+ ${__("CartsList_Name")}
+ ${__("CartsList_Produkte")}
+ ${__("CartsList_Preis")}
@@ -26,18 +27,20 @@ class CartsList extends View {
- Produkte:
+ ${__("CartsList_Produkte")}:
- Preisänderungen:
+ ${__("CartsList_Preisänderungen")}:
`
diff --git a/site/views/items-chart.js b/site/views/items-chart.js
index 7750e3a..83c9dcb 100644
--- a/site/views/items-chart.js
+++ b/site/views/items-chart.js
@@ -1,6 +1,7 @@
const { STORE_KEYS } = require("../model/stores");
const { settings } = require("../model");
const { today, log, deltaTime, uniqueDates, calculateItemPriceTimeSeries } = require("../js/misc");
+const { __ } = require("../i18n");
const { View } = require("./view");
require("./custom-checkbox");
const moment = require("moment");
@@ -19,13 +20,13 @@ class ItemsChart extends View {
}">
-
Keine Daten ausgewählt
+
${__("ItemsChart_Keine Daten ausgewählt")}
-
-
-
-
+
+
+
+
@@ -137,7 +138,7 @@ class ItemsChart extends View {
let yAxis = {
ticks: {
callback: function (value, index, ticks) {
- return value.toLocaleString("de-DE", {
+ return value.toLocaleString(navigator.language || "de-DE", {
minimumFractionDigits: 2,
style: "currency",
currency: "EUR",
@@ -149,7 +150,7 @@ class ItemsChart extends View {
yAxis = {
title: {
display: true,
- text: "Änderung in % seit " + startDate,
+ text: __("ItemsChart_Änderung in % seit {{date}}", { date: startDate }),
},
ticks: {
callback: (value) => {
@@ -212,7 +213,7 @@ class ItemsChart extends View {
if (elements.sumTotal.checked && items.length > 0) {
const now = performance.now();
itemsToShow.push({
- name: "Preissumme Gesamt",
+ name: __("ItemsChart_Preissumme Gesamt"),
priceHistory: this.calculateOverallPriceChanges(items, onlyToday, percentageChange, startDate, endDate),
});
log("ItemsChart - Calculating overall sum total " + ((performance.now() - now) / 1000).toFixed(2) + " secs");
@@ -224,7 +225,7 @@ class ItemsChart extends View {
const storeItems = items.filter((item) => item.store === store);
if (storeItems.length > 0) {
itemsToShow.push({
- name: "Preissumme " + store,
+ name: __("ItemsChart_Preissumme {{s}}", { s: store }),
priceHistory: this.calculateOverallPriceChanges(storeItems, onlyToday, percentageChange, startDate, endDate),
});
}
diff --git a/site/views/items-filter.js b/site/views/items-filter.js
index 7cc669f..c42f2a4 100644
--- a/site/views/items-filter.js
+++ b/site/views/items-filter.js
@@ -3,6 +3,7 @@ const { stores, STORE_KEYS, BUDGET_BRANDS } = require("../model/stores");
const { fromCategoryCode, categories } = require("../model/categories");
const { settings } = require("../model");
const { View } = require("./view");
+const { __ } = require("../i18n");
class ItemsFilter extends View {
constructor() {
@@ -21,7 +22,9 @@ class ItemsFilter extends View {
const hideStores = this._filterByStores ? "" : "hidden";
const hideMisc = this._filterByMisc ? "" : "hidden";
const hideAvailableObption = this._availableOption ? "" : "hidden";
- const placeholder = this.hasAttribute("placeholder") ? this.getAttribute("placeholder") : "Produkte suchen... (min. 3 Zeichen)";
+ const placeholder = this.hasAttribute("placeholder")
+ ? this.getAttribute("placeholder")
+ : __("ItemsFilter_Produkte suchen...") + " " + __("(min. 3 Zeichen)");
this.innerHTML = /*html*/ `
${
@@ -29,7 +32,7 @@ class ItemsFilter extends View {
? `
@@ -43,9 +46,9 @@ class ItemsFilter extends View {
- store.defaultChecked) ? "checked" : ""
- }>
+ store.defaultChecked) ? "checked" : ""
+ }>
${STORE_KEYS.map(
(store) => /*html*/ `
- Datum
+ ${__("ItemsFilter_Datum")}
${today()}
- Billiger seit letzter Änderung
+ ${__("ItemsFilter_Billiger seit letzter Änderung")}
-
-
+
+
- Preis €
+ ${__("ItemsFilter_Preis €")}
-
-
-
+
+
diff --git a/site/views/items-list.js b/site/views/items-list.js
index 31d9538..551111d 100644
--- a/site/views/items-list.js
+++ b/site/views/items-list.js
@@ -14,6 +14,7 @@ const { vectorizeItems, similaritySortItems } = require("../js/knn");
const { stores } = require("../model/stores");
const { View } = require("./view");
const { ItemsChart } = require("./items-chart");
+const { __ } = require("../i18n");
class ItemsList extends View {
static priceTypeId = 0;
@@ -34,32 +35,34 @@ class ItemsList extends View {
- Sortieren
+ ${__("ItemsList_Sortieren")}
- Preis aufsteigend
- Preis absteigend
- Menge aufsteigend
- Menge absteigend
- Kette & Name
- Namensähnlichkeit
+ ${__("ItemsList_Preis aufsteigend")}
+ ${__("ItemsList_Preis absteigend")}
+ ${__("ItemsList_Menge aufsteigend")}
+ ${__("ItemsList_Menge absteigend")}
+ ${__("ItemsList_Kette & Name")}
+ ${__("ItemsList_Namensähnlichkeit")}
@@ -67,9 +70,9 @@ class ItemsList extends View {
- Kette
- Name
- Preis ▼
+ ${__("ItemsList_Kette")}
+ ${__("ItemsList_Name")}
+ ${__("ItemsList_Preis")} ▼
@@ -106,7 +109,7 @@ class ItemsList extends View {
this._showAllPriceHistories = false;
elements.expandPriceHistories.addEventListener("click", () => {
const showAll = (this._showAllPriceHistories = !this._showAllPriceHistories);
- elements.expandPriceHistories.innerText = showAll ? "Preis ▲" : "Preis ▼";
+ elements.expandPriceHistories.innerText = __("ItemsList_Preis") + (showAll ? " ▲" : " ▼");
elements.tableBody.querySelectorAll(".priceinfo").forEach((el) => (showAll ? el.classList.remove("hidden") : el.classList.add("hidden")));
});
From 3b9fc43161b8b3a15dc46db754b54aa64fc3cd2f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Pol=C3=A1k?= <456647@muni.cz>
Date: Sun, 17 Sep 2023 19:48:29 +0200
Subject: [PATCH 4/4] Localization fixes
---
bundle.js | 7 +++--
site/i18n.js => i18n.js | 35 -----------------------
{site/locales => locales}/cs.json | 3 +-
{site/locales => locales}/de.json | 1 +
{site/locales => locales}/en.json | 3 +-
server.js | 2 +-
site/browser_i18n.js | 46 +++++++++++++++++++++++++++++++
site/cart.js | 2 +-
site/carts.js | 2 +-
site/index.js | 25 ++++-------------
site/settings.js | 2 +-
site/views/carts-list.js | 2 +-
site/views/items-chart.js | 2 +-
site/views/items-filter.js | 2 +-
site/views/items-list.js | 2 +-
15 files changed, 69 insertions(+), 67 deletions(-)
rename site/i18n.js => i18n.js (67%)
rename {site/locales => locales}/cs.json (98%)
rename {site/locales => locales}/de.json (98%)
rename {site/locales => locales}/en.json (98%)
create mode 100644 site/browser_i18n.js
diff --git a/bundle.js b/bundle.js
index fbe5dc7..c33c3f1 100644
--- a/bundle.js
+++ b/bundle.js
@@ -4,7 +4,7 @@ const chokidar = require("chokidar");
const esbuild = require("esbuild");
const { exec } = require("child_process");
const { promisify } = require("util");
-const i18n = require("./site/i18n");
+const i18n = require("./i18n");
function deleteDirectory(directory) {
if (fs.existsSync(directory)) {
@@ -64,16 +64,18 @@ function processFile(inputFile, outputFile, filter) {
const replacedData = replaceFileContents(data, path.dirname(inputFile), locale);
if (locale == i18n.defaultLocale) {
fs.writeFileSync(outputFile, replacedData);
+ console.log(`${inputFile} -> ${outputFile}`);
}
let pathWithLanguageCode = outputFile.substring(0, outputFile.length - extension.length) + "." + locale + extension;
fs.writeFileSync(pathWithLanguageCode, replacedData);
+ console.log(`${inputFile} -> ${pathWithLanguageCode}`);
}
} else {
const data = fs.readFileSync(inputFile);
if (filter(inputFile, false, data)) return;
fs.writeFileSync(outputFile, data);
+ console.log(`${inputFile} -> ${outputFile}`);
}
- console.log(`${inputFile} -> ${outputFile}`);
}
/**
@@ -167,6 +169,7 @@ async function bundle(inputDir, outputDir, watch) {
bundleHTML(inputDir, outputDir, false, watch, (filePath, isDir, data) => {
if (isDir) return false;
if (filePath.endsWith("style.css")) return true;
+ if (filePath.includes("/locales/")) return true;
if (filePath.endsWith(".js") && !filePath.includes("socket.io.js")) return true;
if (data.includes(`require("`)) return true;
return false;
diff --git a/site/i18n.js b/i18n.js
similarity index 67%
rename from site/i18n.js
rename to i18n.js
index 7930972..7427e13 100644
--- a/site/i18n.js
+++ b/i18n.js
@@ -29,38 +29,6 @@ const locales = Object.keys(translations);
*/
const defaultLocale = "de";
-/**
- * The currently selected locale.
- * @type {string}
- */
-var currentLocale = defaultLocale;
-
-/**
- * Set the globally used locale.
- * Expects a 2 character language code string, one from locales.
- * @param {string} locale
- */
-function setLocale(locale) {
- if (locales.includes(locale)) {
- console.log("Locale changed to " + locale);
- currentLocale = locale;
- return true;
- }
- console.error("Attempted to setLocale to unsupported language: ", locale);
- return false;
-}
-
-/**
- * Translates the key using the current global locale.
- *
- * @param {!string} key to translate
- * @param {!Object.} [args] arguments to substitute into the translated key
- * @returns {string} translated string
- */
-function translate(key, args) {
- return translateWithLocale(currentLocale, key, args);
-}
-
/**
* @param {!string} locale name of the language to use for translation, MUST be one of the supported languages
* @param {!string} key to translate
@@ -88,9 +56,6 @@ function translateWithLocale(locale, key, args) {
return translation;
}
-exports.setLocale = setLocale;
exports.defaultLocale = defaultLocale;
exports.locales = locales;
-exports.translate = translate;
-exports.__ = translate;
exports.translateWithLocale = translateWithLocale;
diff --git a/site/locales/cs.json b/locales/cs.json
similarity index 98%
rename from site/locales/cs.json
rename to locales/cs.json
index 8cbd8d2..7c20ee3 100644
--- a/site/locales/cs.json
+++ b/locales/cs.json
@@ -78,8 +78,9 @@
"ItemsList_Name": "Název",
"ItemsList_Preis": "Cena",
- "Cart_Teilen": "Sdílet",
+ "Cart_Teilen": "Detail",
"Cart_Speichern": "Uložit",
+ "Cart_Warenkorb {{name}}": "Nákupní košík {{name}}",
"Cart_Warenkorb '{{name}}' existiert bereits. Bitte einen anderen Namen für den zu speichernden Warenkorb eingeben": "Nákupní košík '{{name}}' již existuje. Zadejte prosím jiný název",
"Cart_Warenkorb '{{name}}' existiert nicht.": "Nákupní košík '{{name}}' neexistuje.",
"Cart_Artikel": "Položka",
diff --git a/site/locales/de.json b/locales/de.json
similarity index 98%
rename from site/locales/de.json
rename to locales/de.json
index 8db43b5..5ef9db8 100644
--- a/site/locales/de.json
+++ b/locales/de.json
@@ -80,6 +80,7 @@
"Cart_Teilen": "Teilen",
"Cart_Speichern": "Speichern",
+ "Cart_Warenkorb {{name}}": "Warenkorb {{name}}",
"Cart_Warenkorb '{{name}}' existiert bereits. Bitte einen anderen Namen für den zu speichernden Warenkorb eingeben": "Warenkorb '{{name}}' existiert bereits. Bitte einen anderen Namen für den zu speichernden Warenkorb eingeben",
"Cart_Warenkorb '{{name}}' existiert nicht.": "Warenkorb '{{name}}' existiert nicht.",
"Cart_Artikel": "Artikel",
diff --git a/site/locales/en.json b/locales/en.json
similarity index 98%
rename from site/locales/en.json
rename to locales/en.json
index fd808cd..fa2d33c 100644
--- a/site/locales/en.json
+++ b/locales/en.json
@@ -78,8 +78,9 @@
"ItemsList_Name": "Name",
"ItemsList_Preis": "Price",
- "Cart_Teilen": "Share",
+ "Cart_Teilen": "Detail",
"Cart_Speichern": "Save",
+ "Cart_Warenkorb {{name}}": "Shopping cart {{name}}",
"Cart_Warenkorb '{{name}}' existiert bereits. Bitte einen anderen Namen für den zu speichernden Warenkorb eingeben": "Shopping cart '{{name}}' already exists. Please enter a different name for the shopping cart to be saved",
"Cart_Warenkorb '{{name}}' existiert nicht.": "Shopping cart '{{name}}' does not exist.",
"Cart_Artikel": "Item",
diff --git a/server.js b/server.js
index 4fe21d0..985fcd9 100644
--- a/server.js
+++ b/server.js
@@ -7,7 +7,7 @@ const csv = require("./site/js/misc");
const chokidar = require("chokidar");
const express = require("express");
const compression = require("compression");
-const i18n = require("./site/i18n");
+const i18n = require("./i18n");
function copyItemsToSite(dataDir) {
const items = analysis.readJSON(`${dataDir}/latest-canonical.json.${analysis.FILE_COMPRESSOR}`);
diff --git a/site/browser_i18n.js b/site/browser_i18n.js
new file mode 100644
index 0000000..539e4a3
--- /dev/null
+++ b/site/browser_i18n.js
@@ -0,0 +1,46 @@
+const i18n = require("../i18n");
+
+/**
+ * The currently selected locale.
+ * @type {?string}
+ */
+var currentLocale = i18n.defaultLocale;
+
+/**
+ * Set the globally used locale.
+ * Expects a 2 character language code string, one from locales.
+ * @param {string} locale
+ */
+function setLocale(locale) {
+ if (i18n.locales.includes(locale)) {
+ console.log("Locale changed to " + locale);
+ currentLocale = locale;
+ return true;
+ }
+ console.error("Attempted to setLocale to unsupported language: ", locale);
+ return false;
+}
+
+/**
+ * Translates the key using the current global locale.
+ *
+ * @param {!string} key to translate
+ * @param {!Object.} [args] arguments to substitute into the translated key
+ * @returns {string} translated string
+ */
+function translate(key, args) {
+ return i18n.translateWithLocale(currentLocale, key, args);
+}
+
+// Find the most preferred supported language
+for (const langCode of navigator.languages) {
+ // We don't do regional codes, so take just the language code
+ let lang = langCode.length >= 2 ? langCode.substring(0, 2) : null;
+ if (lang == null) continue;
+ if (i18n.locales.includes(lang)) {
+ setLocale(lang);
+ break;
+ }
+}
+
+exports.__ = translate;
diff --git a/site/cart.js b/site/cart.js
index 85763aa..64253f6 100644
--- a/site/cart.js
+++ b/site/cart.js
@@ -6,7 +6,7 @@ const { STORE_KEYS, stores } = require("./model/stores");
require("./views");
const { ProgressBar } = require("./views/progress-bar");
const progressBar = new ProgressBar(STORE_KEYS.length);
-const { __ } = require("./i18n");
+const { __ } = require("./browser_i18n");
let carts = null;
diff --git a/site/carts.js b/site/carts.js
index 50877a3..d137926 100644
--- a/site/carts.js
+++ b/site/carts.js
@@ -1,7 +1,7 @@
const { downloadJSON, today } = require("./js/misc");
const model = require("./model");
require("./views");
-const { __ } = require("./i18n");
+const { __ } = require("./browser_i18n");
const { STORE_KEYS } = require("./model/stores");
const { ProgressBar } = require("./views/progress-bar");
diff --git a/site/index.js b/site/index.js
index dc4b4d3..f384e24 100644
--- a/site/index.js
+++ b/site/index.js
@@ -1,23 +1,8 @@
-const i18n = require("./i18n");
-
-// Process redirects and localization before running anything else
-(() => {
- if (location.href.includes("heissepreise.github.io")) {
- location.href = "https://heisse-preise.io";
- return;
- }
-
- // Find the most preferred supported language
- for (const langCode of navigator.languages) {
- // We don't do regional codes, so take just the language code
- let lang = langCode.length >= 2 ? langCode.substring(0, 2) : null;
- if (lang == null) continue;
- if (i18n.locales.includes(lang)) {
- i18n.setLocale(lang);
- break;
- }
- }
-})();
+// Process redirects before running anything else
+if (location.href.includes("heissepreise.github.io")) {
+ location.href = "https://heisse-preise.io";
+ return;
+}
const { getQueryParameter } = require("./js/misc");
const model = require("./model");
diff --git a/site/settings.js b/site/settings.js
index 7dfe840..778007c 100644
--- a/site/settings.js
+++ b/site/settings.js
@@ -3,7 +3,7 @@ const { View } = require("./views/view");
const { Settings } = require("./model/settings");
require("./js/misc");
require("./views/custom-checkbox");
-const { __ } = require("./i18n");
+const { __ } = require("./browser_i18n");
class SettingsView extends View {
constructor() {
diff --git a/site/views/carts-list.js b/site/views/carts-list.js
index 6f1735d..9ccfd6c 100644
--- a/site/views/carts-list.js
+++ b/site/views/carts-list.js
@@ -1,6 +1,6 @@
const { downloadJSON, dom } = require("../js/misc");
const { View } = require("./view");
-const { __ } = require("../i18n");
+const { __ } = require("../browser_i18n");
class CartsList extends View {
constructor() {
diff --git a/site/views/items-chart.js b/site/views/items-chart.js
index 83c9dcb..3d5f2fe 100644
--- a/site/views/items-chart.js
+++ b/site/views/items-chart.js
@@ -1,7 +1,7 @@
const { STORE_KEYS } = require("../model/stores");
const { settings } = require("../model");
const { today, log, deltaTime, uniqueDates, calculateItemPriceTimeSeries } = require("../js/misc");
-const { __ } = require("../i18n");
+const { __ } = require("../browser_i18n");
const { View } = require("./view");
require("./custom-checkbox");
const moment = require("moment");
diff --git a/site/views/items-filter.js b/site/views/items-filter.js
index c42f2a4..6c9377c 100644
--- a/site/views/items-filter.js
+++ b/site/views/items-filter.js
@@ -3,7 +3,7 @@ const { stores, STORE_KEYS, BUDGET_BRANDS } = require("../model/stores");
const { fromCategoryCode, categories } = require("../model/categories");
const { settings } = require("../model");
const { View } = require("./view");
-const { __ } = require("../i18n");
+const { __ } = require("../browser_i18n");
class ItemsFilter extends View {
constructor() {
diff --git a/site/views/items-list.js b/site/views/items-list.js
index 551111d..84999d3 100644
--- a/site/views/items-list.js
+++ b/site/views/items-list.js
@@ -14,7 +14,7 @@ const { vectorizeItems, similaritySortItems } = require("../js/knn");
const { stores } = require("../model/stores");
const { View } = require("./view");
const { ItemsChart } = require("./items-chart");
-const { __ } = require("../i18n");
+const { __ } = require("../browser_i18n");
class ItemsList extends View {
static priceTypeId = 0;