Internationalization (i18n) – multi-language support, 15 locales, smart detection - #53
Internationalization (i18n) – multi-language support, 15 locales, smart detection#53Nova0255 wants to merge 1 commit into
Conversation
…rt detection
## New Features
* **Internationalization (i18n):** The whole app can now be shown in other
languages. A new engine (`sources/js/i18n/`) handles locale detection, string
lookup, and fallback, and a **language switcher** now lives in the header.
* **15 languages:** English and French are fully translated; ready-to-fill
scaffolds are included for Italian, German, Spanish, Brazilian Portuguese,
Russian, Polish, Ukrainian, Turkish, Japanese, Simplified Chinese, Traditional
Chinese, Korean, and Thai.
* **Region-aware detection:** Browser language is matched smartly — `pt-BR` →
Brazilian Portuguese, and any Chinese region (`zh-CN`, `zh-TW`, `zh-HK`, …)
resolves to Simplified/Traditional via `Intl.Locale`.
## How It Works
* **English is canonical** — every other locale falls back to it, so anything
untranslated still appears (in English) instead of blank.
* **Empty `""` = not translated yet**, falling back to English/source. Partial
translations are safe to ship.
* **`meta.ready` gates visibility** — a language only shows in the switcher once
it's marked ready (English always available), so nobody lands on an empty UI.
## Enhancements
* **"Help translate on GitHub"** link in the switcher, plus a contributor guide
(`sources/js/i18n/README.md`) on how to add or finish a language.
* **Automated tests** (`sources/tests/i18n.test.js`): each locale is checked for
key parity, preserved `{placeholders}`, valid task ids, and base-of-operations
consistency — plus a table testing browser-tag → locale resolution.
* **Header layout:** a long translated description now wraps to a second line
instead of pushing the theme/menu controls below it.
* **Disabled browser auto-translation** so tools like Google Translate don't
fight the app's own live DOM updates.
## Fixes
* **DST detection for GMT±0 zones** (e.g. Europe/London): a zero UTC offset
formats as the bare string `"GMT"`, which parsed to `NaN` and broke `isDst`
during summer time. Caught by the test suite.
* Fixed corrupted `daily_kim_lettie` text (`Belladona ~{@ (Lettie)` →
`Belladona (Lettie)`).
## Version
* Bump version to 5.1.0 in `app.js` and `package.json`.
|
| const CHEVRON_SVG = `<svg class="lang-chevron" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" /></svg>`; | ||
| const CHECK_SVG = `<svg class="lang-check" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" /></svg>`; | ||
| const EXTERNAL_SVG = `<svg class="lang-ext" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" /></svg>`; |
There was a problem hiding this comment.
These should probably live in icons.js
|
I like the overlay system for tasks and {
"weekly_ayatan": {
"ref": "2026-01-26",
"columns": [
{
"nameKey": "item",
"order": [
{ "textKey": "ayatan_sah", "icon": "OroFusexA.png" },
{ "textKey": "ayatan_ayr", "icon": "OroFusexB.png" },
{ "textKey": "ayatan_orta", "icon": "OroFusexC.png" },
{ "textKey": "ayatan_vaya", "icon": "OroFusexD.png" },
{ "textKey": "ayatan_piv", "icon": "OroFusexE.png" },
{ "textKey": "ayatan_valana", "icon": "OroFusexG.png" }
]
},
{
"nameKey": "tileset",
"order": [
{ "textKey": "orokin_derelict", "icon": "FactionInfested.png" },
{ "textKey": "orokin_tower", "icon": "FactionOrokin.png" },
{ "textKey": "orokin_derelict", "icon": "FactionInfested.png" },
{ "textKey": "orokin_tower", "icon": "FactionOrokin.png" },
{ "textKey": "orokin_derelict", "icon": "FactionInfested.png" },
{ "textKey": "orokin_tower", "icon": "FactionOrokin.png" }
]
}
]
},
"weekly_kahl_garrison": {
"ref": "2026-01-12",
"columns": [
{
"nameKey": "mission",
"order": [
{ "textKey": "sneaky_sabotage", "icon": "FactionCorpus.png" },
{ "textKey": "junk_run", "icon": "FactionGrineer.png" },
{ "textKey": "prison_break", "icon": "SentientSymbol.png" }
]
},
{
"nameKey": "tileset",
"order": [
{ "textKey": "spaceport_venus" },
{ "textKey": "grineer_forest" },
{ "textKey": "murex" }
]
}
]
},
...and then cycles: {
// column names
"item": "Item",
"tileset": "Tileset",
"mission": "Mission",
//etc...
// weekly_ayatan items
"ayatan_sah": "Ayatan Sah Sculpture",
"ayatan_ayr": "Ayatan Ayr Sculpture",
"ayatan_orta": "Ayatan Orta Sculpture",
"ayatan_vaya": "Ayatan Vaya Sculpture",
"ayatan_piv": "Ayatan Piv Sculpture",
"ayatan_valana": "Ayatan Valana Sculpture",
// weekly_ayatan tileset
"orokin_derelict": "Orokin Derelict (Deimos)",
"orokin_tower": "Orokin Tower (Void)",
// weekly_kahl_garrison mission
"sneaky_sabotage": "Sneaky Sabotage",
"junk_run": "Junk Run",
"prison_break": "Prison Break",
// weekly_kahl_garrison tileset
"spaceport_venus": "Spaceport (Orb Vallis, Venus)",
"grineer_forest": "Grineer Forest (Earth)",
"murex": "Murex",
...and you could have keys like |
| function _factionIcon(name) { | ||
| const src = iconURL(`tasks/Syndicats/${name}`); | ||
| return `<img class="icon-filter inline-icon" src="${src}">`; | ||
| } |
There was a problem hiding this comment.
surely this should just be imported from moreInfo.js, right? Or possibly even moved to functions.js and imported from there. It's going to be needed in every language. Should probably rename it to factionIcon (no underscore), since it'll no longer be a semi-private implementation detail of moreInfo.
| #site-header button:not(:last-of-type) { margin-right: 0.5rem; } | ||
| #site-header button:not(:first-of-type) { margin-left: 0.5rem; } | ||
| /* language switcher: custom dropdown (native <select> popups can't be themed) */ | ||
| #language-switcher { | ||
| position: relative; | ||
| margin-right: 0.5rem; | ||
| } |
There was a problem hiding this comment.
| #site-header button:not(:last-of-type) { margin-right: 0.5rem; } | |
| #site-header button:not(:first-of-type) { margin-left: 0.5rem; } | |
| /* language switcher: custom dropdown (native <select> popups can't be themed) */ | |
| #language-switcher { | |
| position: relative; | |
| margin-right: 0.5rem; | |
| } | |
| #site-controls > :not(:last-child) { margin-right: 0.5rem; } | |
| #site-controls > :not(:first-child) { margin-left: 0.5rem; } | |
| /* language switcher: custom dropdown (native <select> popups can't be themed) */ | |
| #language-switcher { | |
| position: relative; | |
| } |
for consistent spacing of header controls
Just now looking at this, and will have more time to go through it all later, but the less places we need to change things the better. As we all know, this game constantly adds new checklist items lol. |
| // Add more IDs if you add more background image divs in HTML | ||
| ]; | ||
| const APP_VERSION = "5.0.1"; | ||
| const APP_VERSION = "5.1.0"; |
There was a problem hiding this comment.
translation is a big enough feature to merit a major version bump, imo (6.0)
|
Closing this to work on the mentioned issues and suggestions. Will resubmit once this has been worked on. |
* added weekly Hunhow Kuva * added week 9 incarnons * added Clan Weekly Initiatives (closes #46) * added weekly Kaya vendor (closes #55) * improved the documentation for icons * added a script to invert icons that come as white * adapted some changes from/for warframe-tools#53 * improved the placement and wrapping of site-controls * future-proofed the CSS for site-controls button spacing * `moreInfo.js` `_factionIcon()` is now `functions.js` `factionIcon()` * bump version to 5.1 * bump game version to 43.0.2
| // a zero offset formats as bare "GMT" (no "+0"), which parses to NaN — treat it as 0 | ||
| const offset = parseInt(timeZoneName.replace("GMT", "").split(":")[0], 10); | ||
| return Number.isNaN(offset) ? 0 : offset; |
There was a problem hiding this comment.
I cannot reproduce this bug. timeZoneName for Europe/London does indeed format to "GMT+0" or "GMT+1" for me.
* added weekly Hunhow Kuva * added week 9 incarnons * closes #54 * added Clan Weekly Initiatives (closes #46) * added weekly Kaya vendor (closes #55) * improved the documentation for icons [as discussed](https://github.com/warframe-tools/Task-Checklist/discussions/46#discussioncomment-17356429) * added a script to invert icons that come as white * adapted some changes from/for #53 * improved the placement and wrapping of site-controls * future-proofed the CSS for site-controls button spacing * `moreInfo.js` `_factionIcon()` is now `functions.js` `factionIcon()` * bump version to 5.1 * bump game version to 43.0.2

New Features
sources/js/i18n/) handles locale detection, string lookup, and fallback, and a language switcher now lives in the header.pt-BR→ Brazilian Portuguese, and any Chinese region (zh-CN,zh-TW,zh-HK, …) resolves to Simplified/Traditional viaIntl.Locale.How It Works
""= not translated yet, falling back to English/source. Partial translations are safe to ship.meta.readygates visibility — a language only shows in the switcher once it's marked ready (English always available), so nobody lands on an empty UI.Enhancements
sources/js/i18n/README.md) on how to add or finish a language.sources/tests/i18n.test.js): each locale is checked for key parity, preserved{placeholders}, valid task ids, and base-of-operations consistency — plus a table testing browser-tag → locale resolution.Fixes
"GMT", which parsed toNaNand brokeisDstduring summer time. Caught by the test suite.daily_kim_lettietext (Belladona ~{@ (Lettie)→Belladona (Lettie)).Version
app.jsandpackage.json.