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%%