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 @@ 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 @@
- Suche - Preisänderungen - Warenkörbe + __Suche__ + __Preisänderungen__ + __Warenkörbe__
\ 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 @@ - + - + %%_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__