From 5318d1833e3db246b65919e8a5a8472bfe0cfff6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niklas=20K=C3=B6hler?= Date: Wed, 18 Mar 2026 13:06:18 +0100 Subject: [PATCH 1/5] feat: add English and German translation files Add complete i18n JSON files for EN and DE with all UI strings, category names, event names, map names, and notes. German translations use official GW2 API-verified names. --- app/i18n/de.json | 147 +++++++++++++++++++++++++++++++++++++++++++++++ app/i18n/en.json | 147 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 294 insertions(+) create mode 100644 app/i18n/de.json create mode 100644 app/i18n/en.json diff --git a/app/i18n/de.json b/app/i18n/de.json new file mode 100644 index 0000000..41a2f0a --- /dev/null +++ b/app/i18n/de.json @@ -0,0 +1,147 @@ +{ + "ui": { + "siteName": "Simple Timer", + "browserSettingsAlert": "Abhängig von deinen Browsereinstellungen wird dies für zukünftige Sitzungen gespeichert.", + "donationAsk": "Willst du helfen, das Licht am Leuchten zu halten?", + "donationHint": "Eine Mystische Münze würde mir reichen. 😉", + "tooltipFast": "[fast] Farming Community besuchen", + "tooltipWiki": "Event im Guild Wiki öffnen", + "tooltipDone": "Als erledigt markieren!", + "tooltipAlert": "Erinnerung setzen!", + "tooltipWaypoint": "Wegmarke in Zwischenablage kopieren!" + }, + "categories": { + "general": "Allgemein", + "core": "Hauptspiel", + "ls1": "Lebendige Welt Staffel 1", + "ls2": "Lebendige Welt Staffel 2", + "hot": "Heart of Thorns", + "ls3": "Lebendige Welt Staffel 3", + "pof": "Path of Fire", + "ls4": "Lebendige Welt Staffel 4", + "ls5": "Die Eisbrut-Saga", + "eod": "End of Dragons", + "soto": "Secrets of the Obscure", + "janthir": "Janthir Wilds", + "Festivals": "Festivals" + }, + "events": { + "TyriaDayBreak": "Tyria Tagesanbruch", + "TyriaNightfall": "Tyria Einbruch der Nacht", + "ServerReset": "Server-Reset", + "ShadowBehemoth": "Schatten-Behemoth", + "FireElemental": "Feuerelementar", + "SvanirShamanChief": "Svanir-Schamane Häuptling", + "GreatJungleWurm": "Großer Dschungel-Wurm", + "GolemMarkII": "Inquest-Golem Mark II", + "ClawofJormag": "Klaue des Jormag", + "AdmiralTaidhaCovington": "Admiral Taidha Covington", + "Megadestroyer": "Megazerstörer", + "TheShatterer": "Der Zertrümmerer", + "ModniirUlgoth": "Modniir Ulgoth", + "Tequatl": "Tequatl der Sonnenlose", + "KarkaQueen": "Karka-Königin", + "TripleTrouble": "Dreifach-Unheil", + "LeyLineAnomaly": "Ley-Linien-Anomalie", + "TheTwistedMarionette": "Die Verdrehte Marionette", + "DefeatScarlet'sminions": "Scarlets Schergen besiegen", + "BattleForLion'sArch": "Schlacht um Löwenstein", + "TowerofNightmares": "Turm der Alpträume", + "Sandstorm": "Sandsturm", + "NightBosses": "Nacht-Bosse", + "Octovine": "Octovine", + "DefendingTarir": "Tarir verteidigen", + "ChakGerent": "Chak-Gerent", + "Dragon'sStand": "Widerstand des Drachen", + "Noran'sHomestead": "Norans Gehöft", + "NewLoamhurst": "Neu-Lehmfurt", + "Saidra'sHaven": "Saidras Zuflucht", + "CasinoBlitz": "Casino-Blitz", + "Pinata": "Piñata", + "BuriedTreasure": "Vergrabene Schätze", + "ThePathtoAscension": "Der Pfad zur Apotheose", + "Doppelganger": "Doppelgänger", + "MawsofTorment": "Schlünde der Qual", + "Junundu": "Junundu", + "ForgedwithFire": "Geschmiedet mit Feuer", + "Serpents'Ire": "Zorn der Schlangen", + "Palawadan": "Palawadan", + "Escorts": "Gefährliche Beute", + "Death-BrandedShatterer": "Todesgebrandmarkter Zertrümmerer", + "ThunderheadKeep": "Donnerkopf-Feste", + "TheOilFloes": "Die Öl-Schollen", + "SacredFlame": "Heilige Flamme", + "DoomloreShrine": "Verdammniswissen-Schrein", + "OozePit": "Schleim-Grube", + "MetalConcert": "Metal-Konzert", + "StormsofWinter": "Stürme des Winters", + "IcebroodChampions": "Eisbrut-Champions", + "Drakkar": "Drakkar", + "Dragonstorm": "Drachensturm", + "AetherbladeAssault": "Ätherklingen-Angriff", + "KainengBlackout": "Kaineng-Stromausfall", + "GangWar": "Bandenkrieg", + "Aspenwood": "Espenwald", + "Dragon'sEnd": "Drachen-Ende", + "JadeMaw": "Jade-Schlund", + "Wizard'sTower": "Den Turm des Zauberers freischalten", + "TargetPractice": "Zielübungen", + "FlybyNight": "Nachtflug", + "DefenseofAmnitas": "Verteidigung von Amnytas", + "Convergence:InnerNayos": "Konvergenz: Inneres Nayos", + "MistsandMonsters": "Von Nebeln und Monstern", + "Convergence:MountBalrior": "Konvergenz: Berg Balrior" + }, + "maps": { + "Tyria": "Tyria", + "Queensdale": "Königintal", + "Metrica Province": "Provinz Metrica", + "Wayfarer Foothills": "Wanderer-Hügel", + "Caledon Forest": "Caledon-Wald", + "Mount Maelstrom": "Mahlstromgipfel", + "Frostgorge Sound": "Eisklamm-Sund", + "Bloodtide Coast": "Blutstrom-Küste", + "Blazeridge Steppes": "Flammenkamm-Steppe", + "Harathi Hinterlands": "Harathi-Hinterland", + "Sparkfly Fen": "Funkenschwärmersumpf", + "Southsun Cove": "Südlicht-Bucht", + "Timberline Fals": "Baumgrenzen-Fälle", + "Gendaran Fields": "Gendarran-Felder", + "Gendarran Fields": "Gendarran-Felder", + "Iron Marches": "Eisenmark", + "Eye of the North": "Auge des Nordens", + "Dry Top": "Trockenkuppe", + "Verdant Brink": "Grasgrüne Schwelle", + "Auric Basin": "Güldener Talkessel", + "Tangled Depths": "Verschlungene Tiefen", + "Dragon's Stand": "Widerstand des Drachen", + "Lake Doric": "Doric-See", + "Crystal Oasis": "Kristalloase", + "Desert Highlands": "Wüsten-Hochland", + "Elon Riverlands": "Elon-Flusslande", + "The Desolation": "Das Ödland", + "Domain of Vabbi": "Domäne Vaabi", + "Domain of Istan": "Domäne Istan", + "Jahai Bluffs": "Jahai-Klippen", + "Thunderhead Peaks": "Donnerkopf-Gipfel", + "Grothmar Valley": "Grothmar-Tal", + "Bjora Marches": "Bjora-Sümpfe", + "Seitung Province": "Provinz Seitung", + "New Kaineng City": "Stadt Neu-Kaineng", + "The Echovald Wilds": "Die Echowald-Wildnis", + "Dragon's End": "Drachen-Ende", + "Skywatch Archipelago": "Himmelswacht-Archipel", + "Wizard's Tower": "Der Turm des Zauberers", + "Amnytas": "Amnytas", + "Janthir Syntri": "Janthir Syntri", + "Lowland Shore": "Tiefland-Küste" + }, + "notes": { + "Matriarch": "Matriarchin", + "Pylons": "Pylonen", + "Immelhoof": "Immelhuf", + "Piñata pre Meta": "Piñata Vor-Meta", + "DBS pre Meta": "TGZ Vor-Meta", + "Icebrood pre Meta": "Eisbrut Vor-Meta" + } +} diff --git a/app/i18n/en.json b/app/i18n/en.json new file mode 100644 index 0000000..6bde104 --- /dev/null +++ b/app/i18n/en.json @@ -0,0 +1,147 @@ +{ + "ui": { + "siteName": "Simple Timer", + "browserSettingsAlert": "Depending on your browser settings, this will be saved for your future sessions.", + "donationAsk": "Want to help keep the lights on?", + "donationHint": "A Mystic Coin would do it for me. 😉", + "tooltipFast": "Visit [fast] Farming Community", + "tooltipWiki": "Open Event in Guild Wiki", + "tooltipDone": "Mark as Done!", + "tooltipAlert": "Set an alert!", + "tooltipWaypoint": "Copy waypoint to clipboard!" + }, + "categories": { + "general": "General", + "core": "Core Game", + "ls1": "Living World Season 1", + "ls2": "Living World Season 2", + "hot": "Heart of Thorns", + "ls3": "Living World Season 3", + "pof": "Path of Fire", + "ls4": "Living World Season 4", + "ls5": "The Icebrood Saga", + "eod": "End of Dragons", + "soto": "Secrets of the Obscure", + "janthir": "Janthir Wilds", + "Festivals": "Festivals" + }, + "events": { + "TyriaDayBreak": "Tyria Day Break", + "TyriaNightfall": "Tyria Nightfall", + "ServerReset": "Server Reset", + "ShadowBehemoth": "Shadow Behemoth", + "FireElemental": "Fire Elemental", + "SvanirShamanChief": "Svanir Shaman Chief", + "GreatJungleWurm": "Great Jungle Wurm", + "GolemMarkII": "Golem Mark II", + "ClawofJormag": "Claw of Jormag", + "AdmiralTaidhaCovington": "Admiral Taidha Covington", + "Megadestroyer": "Megadestroyer", + "TheShatterer": "The Shatterer", + "ModniirUlgoth": "Modniir Ulgoth", + "Tequatl": "Tequatl", + "KarkaQueen": "Karka Queen", + "TripleTrouble": "Triple Trouble", + "LeyLineAnomaly": "Ley-Line Anomaly", + "TheTwistedMarionette": "The Twisted Marionette", + "DefeatScarlet'sminions": "Defeat Scarlet's Minions", + "BattleForLion'sArch": "Battle For Lion's Arch", + "TowerofNightmares": "Tower of Nightmares", + "Sandstorm": "Sandstorm", + "NightBosses": "Night Bosses", + "Octovine": "Octovine", + "DefendingTarir": "Defending Tarir", + "ChakGerent": "Chak Gerent", + "Dragon'sStand": "Dragon's Stand", + "Noran'sHomestead": "Noran's Homestead", + "NewLoamhurst": "New Loamhurst", + "Saidra'sHaven": "Saidra's Haven", + "CasinoBlitz": "Casino Blitz", + "Pinata": "Piñata", + "BuriedTreasure": "Buried Treasure", + "ThePathtoAscension": "The Path to Ascension", + "Doppelganger": "Doppelganger", + "MawsofTorment": "Maws of Torment", + "Junundu": "Junundu", + "ForgedwithFire": "Forged with Fire", + "Serpents'Ire": "Serpents' Ire", + "Palawadan": "Palawadan", + "Escorts": "Dangerous Prey", + "Death-BrandedShatterer": "Death-Branded Shatterer", + "ThunderheadKeep": "Thunderhead Keep", + "TheOilFloes": "The Oil Floes", + "SacredFlame": "Sacred Flame", + "DoomloreShrine": "Doomlore Shrine", + "OozePit": "Ooze Pit", + "MetalConcert": "Metal Concert", + "StormsofWinter": "Storms of Winter", + "IcebroodChampions": "Icebrood Champions", + "Drakkar": "Drakkar", + "Dragonstorm": "Dragonstorm", + "AetherbladeAssault": "Aetherblade Assault", + "KainengBlackout": "Kaineng Blackout", + "GangWar": "Gang War", + "Aspenwood": "Aspenwood", + "Dragon'sEnd": "Dragon's End", + "JadeMaw": "Jade Maw", + "Wizard'sTower": "Unlock the Wizard's Tower", + "TargetPractice": "Target Practice", + "FlybyNight": "Fly by Night", + "DefenseofAmnitas": "Defense of Amnytas", + "Convergence:InnerNayos": "Convergence: Inner Nayos", + "MistsandMonsters": "Of Mists and Monsters", + "Convergence:MountBalrior": "Convergence: Mount Balrior" + }, + "maps": { + "Tyria": "Tyria", + "Queensdale": "Queensdale", + "Metrica Province": "Metrica Province", + "Wayfarer Foothills": "Wayfarer Foothills", + "Caledon Forest": "Caledon Forest", + "Mount Maelstrom": "Mount Maelstrom", + "Frostgorge Sound": "Frostgorge Sound", + "Bloodtide Coast": "Bloodtide Coast", + "Blazeridge Steppes": "Blazeridge Steppes", + "Harathi Hinterlands": "Harathi Hinterlands", + "Sparkfly Fen": "Sparkfly Fen", + "Southsun Cove": "Southsun Cove", + "Timberline Fals": "Timberline Falls", + "Gendaran Fields": "Gendarran Fields", + "Gendarran Fields": "Gendarran Fields", + "Iron Marches": "Iron Marches", + "Eye of the North": "Eye of the North", + "Dry Top": "Dry Top", + "Verdant Brink": "Verdant Brink", + "Auric Basin": "Auric Basin", + "Tangled Depths": "Tangled Depths", + "Dragon's Stand": "Dragon's Stand", + "Lake Doric": "Lake Doric", + "Crystal Oasis": "Crystal Oasis", + "Desert Highlands": "Desert Highlands", + "Elon Riverlands": "Elon Riverlands", + "The Desolation": "The Desolation", + "Domain of Vabbi": "Domain of Vabbi", + "Domain of Istan": "Domain of Istan", + "Jahai Bluffs": "Jahai Bluffs", + "Thunderhead Peaks": "Thunderhead Peaks", + "Grothmar Valley": "Grothmar Valley", + "Bjora Marches": "Bjora Marches", + "Seitung Province": "Seitung Province", + "New Kaineng City": "New Kaineng City", + "The Echovald Wilds": "The Echovald Wilds", + "Dragon's End": "Dragon's End", + "Skywatch Archipelago": "Skywatch Archipelago", + "Wizard's Tower": "Wizard's Tower", + "Amnytas": "Amnytas", + "Janthir Syntri": "Janthir Syntri", + "Lowland Shore": "Lowland Shore" + }, + "notes": { + "Matriarch": "Matriarch", + "Pylons": "Pylons", + "Immelhoof": "Immelhoof", + "Piñata pre Meta": "Piñata pre Meta", + "DBS pre Meta": "DBS pre Meta", + "Icebrood pre Meta": "Icebrood pre Meta" + } +} From 9c9e3fdaa8145a77bc171427fc201a7f1f3fd0ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niklas=20K=C3=B6hler?= Date: Wed, 18 Mar 2026 13:06:25 +0100 Subject: [PATCH 2/5] feat: add i18n module for language switching Minimal translation module (~70 lines, zero dependencies) that loads JSON translation files, translates DOM elements via data-i18n attributes, and provides getTranslation() for dynamic content. Language preference is persisted in localStorage. --- app/i18n.js | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 app/i18n.js diff --git a/app/i18n.js b/app/i18n.js new file mode 100644 index 0000000..db92cea --- /dev/null +++ b/app/i18n.js @@ -0,0 +1,73 @@ +let translations = {}; +let currentLang = localStorage.getItem('lang') || 'en'; + +async function loadTranslations(lang) { + const response = await fetch(`/app/i18n/${lang}.json`); + translations = await response.json(); + currentLang = lang; +} + +function getCurrentLang() { + return currentLang; +} + +async function setLanguage(lang) { + localStorage.setItem('lang', lang); + await loadTranslations(lang); + translateDOM(); + updateLangButtons(); +} + +function translateDOM() { + document.querySelectorAll('[data-i18n]').forEach(el => { + const key = el.getAttribute('data-i18n'); + const value = resolve(key); + if (value) el.textContent = value; + }); + document.querySelectorAll('[data-i18n-tooltip]').forEach(el => { + const key = el.getAttribute('data-i18n-tooltip'); + const value = resolve(key); + if (value) el.setAttribute('tootltip', value); + }); +} + +function translateElement(el) { + el.querySelectorAll('[data-i18n]').forEach(child => { + const key = child.getAttribute('data-i18n'); + const value = resolve(key); + if (value) child.textContent = value; + }); + el.querySelectorAll('[data-i18n-tooltip]').forEach(child => { + const key = child.getAttribute('data-i18n-tooltip'); + const value = resolve(key); + if (value) child.setAttribute('tootltip', value); + }); +} + +function getTranslation(section, key) { + if (translations[section] && translations[section][key]) { + return translations[section][key]; + } + return key; +} + +function resolve(dotKey) { + const parts = dotKey.split('.'); + let obj = translations; + for (const p of parts) { + if (obj[p] === undefined) return null; + obj = obj[p]; + } + return obj; +} + +function updateLangButtons() { + document.querySelectorAll('.lang-btn').forEach(btn => { + btn.classList.toggle('lang-active', btn.dataset.lang === currentLang); + }); +} + +// Init +await loadTranslations(currentLang); + +export { setLanguage, getCurrentLang, getTranslation, translateElement, translateDOM, updateLangButtons, translations }; From d3d742d41043675661ad36156713137f3f66eff3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niklas=20K=C3=B6hler?= Date: Wed, 18 Mar 2026 13:06:30 +0100 Subject: [PATCH 3/5] feat: add data-i18n attributes and language switcher to HTML Add data-i18n attributes to static UI text (site name, browser alert, donation text, tooltips). Add EN/DE language switcher buttons in the header. Fix donation text typo. --- index.html | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/index.html b/index.html index 7f29e5b..38405a4 100644 --- a/index.html +++ b/index.html @@ -30,18 +30,23 @@
-

Simple Timer

+

Simple Timer

+
+ + | + +