Translate all static html files into English and Czech

This commit is contained in:
Jan Polák 2023-09-17 17:13:14 +02:00
parent e9b8e403da
commit d2ae0cc575
16 changed files with 274 additions and 80 deletions

71
bundle.js Executable file → Normal file
View File

@ -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);

View File

@ -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}`);

View File

@ -1,8 +1,8 @@
<footer>
<div class="flex align-center justify-center gap-2 pt-4">
<a class="font-medium" href="settings.html">Einstellungen</a>
<a class="font-medium" href="imprint.html">Impressum</a>
<a class="font-medium" href="data/log.txt">Logs</a>
<a class="font-medium" href="settings.html">__Einstellungen__</a>
<a class="font-medium" href="imprint.html">__Impressum__</a>
<a class="font-medium" href="data/log.txt">__Logs__</a>
<a href="https://twitter.com/badlogicgames" style="width: 24px;" aria-label="twitter.com/badlogicgames"><svg style="padding-top: 2px;" viewBox="328 355 335 276" xmlns="http://www.w3.org/2000/svg">
<path d="
M 630, 425
@ -25,11 +25,11 @@
<a href="https://github.com/badlogic/heissepreise" aria-label="github.com/badlogic/heissepreise"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg></a>
</div>
<div class="flex align-center justify-center gap-2 pt-4 pb-2">
<span>Historische Daten von <a href="http://h.43z.one" class="font-medium">@h43z</a> &amp; <a href="https://www.dossier.at/dossiers/supermaerkte/quellen/anatomie-eines-supermarkts-die-methodik/" class="font-medium">Dossier</a></span>
<span>__Historische Daten von__ <a href="http://h.43z.one" class="font-medium">@h43z</a> &amp; <a href="https://www.dossier.at/dossiers/supermaerkte/quellen/anatomie-eines-supermarkts-die-methodik/" class="font-medium">Dossier</a></span>
</div>
<small class="text-center mb-6">
<p>Alle Angaben ohne Gewähr, Irrtümer vorbehalten. <br />
Markennamen und Warenzeichen sind Eigentum der jeweiligen Inhaber.
<p>__Alle Angaben ohne Gewähr, Irrtümer vorbehalten.__<br />
__Markennamen und Warenzeichen sind Eigentum der jeweiligen Inhaber.__
</p>
</small>
</footer>

View File

@ -4,8 +4,8 @@
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Nicht-kommerzielles Open-Source-Projekt um KonsumentInnen es zu ermöglichen, die günstigste Variante eines Produktes im Handel ausfindig zu machen." />
<title>Heisse Preise</title>
<meta name="description" content="__page_description__" />
<title>__Heisse Preise__</title>
<link
rel="icon"
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%2210 0 100 100%22><text y=%22.90em%22 font-size=%2290%22>🔥</text></svg>"
@ -15,4 +15,4 @@
<body class="w-full min-h-full flex flex-col">
<div class="max-w-6xl mx-auto lg:py-4 flex flex-col flex-1 w-full">
<h2 class="bg-primary lg:rounded-t-xl text-center text-white p-3 uppercase text-2xl font-bold">🔥 Heisse Preise 🔥</h2>
<h2 class="bg-primary lg:rounded-t-xl text-center text-white p-3 uppercase text-2xl font-bold">🔥 __Heisse Preise__ 🔥</h2>

View File

@ -1,5 +1,5 @@
<div class="flex justify-center gap-4 py-2 font-bold text-primary border border-primary/50 lg:rounded-b-xl">
<a href="index.html">Suche</a>
<a href="changes.html">Preisänderungen</a>
<a href="carts.html">Warenkörbe</a>
<a href="index.html">__Suche__</a>
<a href="changes.html">__Preisänderungen__</a>
<a href="carts.html">__Warenkörbe__</a>
</div>

View File

@ -4,13 +4,20 @@
<cart-header x-id="cartHeader"></cart-header>
<div x-id="noItems" class="hidden block text-center my-3">
Noch keine Produkte im Warenkorb.<br />
Produkte suchen und mit '+' zum Warenkorb hinzufügen.
__Noch keine Produkte im Warenkorb.__<br />
__Produkte suchen und mit '+' zum Warenkorb hinzufügen.__
</div>
<items-filter x-id="cartFilter" stores nochartclear class="hidden" placeholder="Filtern... (min. 3 Zeichen)"></items-filter>
<items-filter x-id="cartFilter" stores nochartclear class="hidden" placeholder="__Filtern...__ __(min. 3 Zeichen)__"></items-filter>
<items-list x-id="linkedCartList" chart json nosort class="hidden"></items-list>
<items-list x-id="cartList" chart json nosort remove updown class="hidden"></items-list>
<items-filter x-id="productsFilter" class="hidden" stores misc nochartclear placeholder="Produkt hinzufügen... (min. 3 Zeichen)"></items-filter>
<items-filter
x-id="productsFilter"
class="hidden"
stores
misc
nochartclear
placeholder="__Produkt hinzufügen...__ (__min. 3 Zeichen__)"
></items-filter>
<items-list x-id="productsList" class="hidden" add></items-list>
%%_templates/_loader.html%%
</div>

View File

@ -2,11 +2,11 @@
<div class="w-full relative px-4 flex-1">
<div class="max-w-3xl mx-auto">
<h1 class="text-2xl font-bold pb-2 pt-8 text-center">Warenkörbe</h1>
<h1 class="text-2xl font-bold pb-2 pt-8 text-center">__Warenkörbe__</h1>
<div class="px-4 py-2 my-4 text-sm border rounded-xl md:mt-8 md:rounded-b-none md:mb-0 bg-gray-100 flex gap-4">
<input type="button" id="new" value="Neuer Warenkorb" class="text-primary font-medium hover:underline cursor-pointer" />
<input type="button" id="export" value="Exportieren" class="text-primary font-medium hover:underline cursor-pointer" />
<input type="button" id="import" value="Importieren" class="text-primary font-medium hover:underline cursor-pointer" />
<input type="button" id="new" value="__Neuer Warenkorb__" class="text-primary font-medium hover:underline cursor-pointer" />
<input type="button" id="export" value="__Exportieren__" class="text-primary font-medium hover:underline cursor-pointer" />
<input type="button" id="import" value="__Importieren__" class="text-primary font-medium hover:underline cursor-pointer" />
</div>
<carts-list id="carts" class="carts-list w-full"></carts-list>
%%_templates/_loader.html%%

View File

@ -1,8 +1,8 @@
%%_templates/_header.html%% %%_templates/_menu.html%%
<div class="w-full relative px-4 flex-1">
<h1 class="text-2xl font-bold pb-2 pt-8 text-center">Preisänderungen</h1>
<items-filter x-id="items-filter" pricechanges pricedirection stores misc placeholder="Filtern... (min. 3 Zeichen)"></items-filter>
<h1 class="text-2xl font-bold pb-2 pt-8 text-center">__Preisänderungen__</h1>
<items-filter x-id="items-filter" pricechanges pricedirection stores misc placeholder="__Filtern...__ __(min. 3 Zeichen)__"></items-filter>
<items-list chart></items-list>
%%_templates/_loader.html%%
</div>

44
site/i18n.js Executable file → Normal file
View File

@ -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.<string, Object.<string, string>>}
*/
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.<string, string>} [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.<string, string>} [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;

View File

@ -1,14 +1,16 @@
%%_templates/_header.html%% %%_templates/_menu.html%%
<div class="max-w-lg mx-auto my-8 px-4">
<h1 class="text-2xl font-bold">Impressum</h1>
<h1 class="text-2xl font-bold">__Impressum__</h1>
<p class="mt-4">
<strong>Angaben gemäß § 25 Mediengesetz (Österreich)</strong><br /><br />
<b>Medieninhaber:</b> Mario Zechner<br />
<b>Kontakt:</b> <a href="mailto:badlogicgames@gmail.com">badlogicgames@gmail.com</a><br />
<b>Adresse:</b> Schörgelgasse 3, 8010 Graz, Österreich
<b>__Medieninhaber__:</b> Mario Zechner<br />
<b>__Kontakt__:</b> <a href="mailto:badlogicgames@gmail.com">badlogicgames@gmail.com</a><br />
<b>__Adresse__:</b> Schörgelgasse 3, 8010 Graz, Österreich
</p>
<p class="mt-4">
__Diese nicht-kommerzielle Seite dient KonsumentInnen dazu, Preise von Produkten im Lebensmittelhandel vergleichen zu können.__
</p>
<p class="mt-4">Diese nicht-kommerzielle Seite dient KonsumentInnen dazu, Preise von Produkten im Lebensmittelhandel vergleichen zu können.</p>
</div>
%%_templates/_footer.html%%

View File

@ -2,11 +2,13 @@
<div class="w-full max-w-5-xl relative px-4 flex-1">
<div class="flex flex-row w-full justify-center font-bold text-primary gap-4 mt-4">
<a href="https://www.youtube.com/watch?v=2u-T85yMKGI">Video Anleitung</a>
<a href="https://docs.google.com/document/d/1Q5OWJOICXjSzTEIHBZgJl1p3FsiWFO0lzXIuwGbXBck/edit?usp=sharing" target="_blank">Text Anleitung</a>
<a href="./media.html">Medienberichte</a>
<a href="https://www.youtube.com/watch?v=2u-T85yMKGI">__Video Anleitung__</a>
<a href="https://docs.google.com/document/d/1Q5OWJOICXjSzTEIHBZgJl1p3FsiWFO0lzXIuwGbXBck/edit?usp=sharing" target="_blank"
>__Text Anleitung__</a
>
<a href="./media.html">__Medienberichte__</a>
</div>
<h1 class="text-2xl font-bold pb-2 pt-8 text-center">Produktsuche</h1>
<h1 class="text-2xl font-bold pb-2 pt-8 text-center">__Produktsuche__</h1>
<items-filter x-id="items-filter" emptyquery stores misc></items-filter>
<items-list share json chart></items-list>
%%_templates/_loader.html%%

36
site/locales/cs.json Executable file → Normal file
View File

@ -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"
}

36
site/locales/de.json Executable file → Normal file
View File

@ -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"
}

36
site/locales/en.json Executable file → Normal file
View File

@ -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"
}

View File

@ -1,9 +1,9 @@
%%_templates/_header.html%% %%_templates/_menu.html%%
<div class="mx-auto my-8 px-4">
<h2 class="text-2xl font-bold">Medienberichte</h2>
<h2 class="text-2xl font-bold">__Medienberichte__</h2>
<div class="flex flex-col mt-4 gap-4 text-primary">
<h3 class="text-xl font-bold text-black">Radio & Fernsehen</h3>
<h3 class="text-xl font-bold text-black">__Radio & Fernsehen__</h3>
<a href="https://twitter.com/badlogicgames/status/1661401581097570308"
>24.5.203 Österreichisches Parlament - Rede von Joachim Schnabel zum Thema Lebensmittelpreise, ÖVP</a
>
@ -42,7 +42,7 @@
>
<video src="https://marioslab.io/uploads/ZIB_2-REWE-%C3%96sterreich-Chef_%C3%BCber_hohe_Lebensmittelpreise-0220056522.mp4" controls></video>
<h3 class="text-xl font-bold text-black">Print & Online</h3>
<h3 class="text-xl font-bold text-black">__Print & Online__</h3>
<a href="https://www.puls24.at/news/politik/preisvergleich-politik-braucht-bis-herbst-twitter-user-2-stunden/297474"
>16.5.2023 PULS 24 - Preisvergleich: Politik braucht "bis Herbst", Twitter-User "2 Stunden"</a
>

View File

@ -1,7 +1,7 @@
%%_templates/_header.html%% %%_templates/_menu.html%%
<div class="max-w-lg mx-auto my-8 px-4">
<h2 class="text-2xl font-bold text-center">Einstellungen</h2>
<h2 class="text-2xl font-bold text-center">__Einstellungen__</h2>
<settings-view></settings-view>
</div>