Skip to content

Internationalization (i18n) – multi-language support, 15 locales, smart detection - #53

Closed
Nova0255 wants to merge 1 commit into
warframe-tools:mainfrom
Nova0255:i18n
Closed

Internationalization (i18n) – multi-language support, 15 locales, smart detection#53
Nova0255 wants to merge 1 commit into
warframe-tools:mainfrom
Nova0255:i18n

Conversation

@Nova0255

Copy link
Copy Markdown

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.

…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`.
@AnSq

AnSq commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

daily_kim_lettie is not corrupted. That's her actual handle in KIM (it's an ASCII art rose).

the_hex

@Nova0255

Copy link
Copy Markdown
Author

daily_kim_lettie is not corrupted. That's her actual handle in KIM (it's an ASCII art rose).
Huh I didn't know that. my bad... Happy to change it back.

Comment thread sources/js/i18n/i18n.js
Comment on lines +218 to +220
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>`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should probably live in icons.js

@AnSq

AnSq commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

I like the overlay system for tasks and applyStaticTranslations. I don't love the duplication of the English text for the keys in cycles. Seems like asking for trouble if we ever need to change the English version. Perhaps we could change cycles.json, so that instead of text and name with the actual English version, we instead had textKey and nameKey, with values that referenced keys in the locale files. So the beginning of cycles.json would instead look like this:

{
    "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 en.js would have a section like this:

    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 circuit_normal_week_1 instead of Blueprints for Excalibur, Trinity, or Ember, circuit_sp_week_1 instead of Incarnon Adapters for Braton, Kunai, Lato, Paris, and Skana, coda_weapons_batch_a instead of Hema, Sporothrix, Catabolyst, Pox, Dual Torxica, Mire, and Motovore

Comment on lines +9 to +12
function _factionIcon(name) {
const src = iconURL(`tasks/Syndicats/${name}`);
return `<img class="icon-filter inline-icon" src="${src}">`;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@AnSq

AnSq commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

If I were doing it, I'd probably put a language or globe icon in the language-select dropdown button.

Comment thread sources/css/critical.css
Comment on lines 292 to +298
#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;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#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

@HelpfulSoft1207

Copy link
Copy Markdown
Collaborator

I like the overlay system for tasks and applyStaticTranslations. I don't love the duplication of the English text for the keys in cycles. Seems like asking for trouble if we ever need to change the English version. Perhaps we could change cycles.json, so that instead of text and name with the actual English version, we instead had textKey and nameKey, with values that referenced keys in the locale files. So the beginning of cycles.json would instead look like this:

{
    "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 en.js would have a section like this:

    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 circuit_normal_week_1 instead of Blueprints for Excalibur, Trinity, or Ember, circuit_sp_week_1 instead of Incarnon Adapters for Braton, Kunai, Lato, Paris, and Skana, coda_weapons_batch_a instead of Hema, Sporothrix, Catabolyst, Pox, Dual Torxica, Mire, and Motovore

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.

Comment thread sources/js/app.js
// Add more IDs if you add more background image divs in HTML
];
const APP_VERSION = "5.0.1";
const APP_VERSION = "5.1.0";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

translation is a big enough feature to merit a major version bump, imo (6.0)

@Nova0255

Nova0255 commented Jun 19, 2026

Copy link
Copy Markdown
Author

Closing this to work on the mentioned issues and suggestions. Will resubmit once this has been worked on.

@Nova0255 Nova0255 closed this Jun 19, 2026
AnSq added a commit to AnSq/Warframe-Task-Checklist that referenced this pull request Jun 19, 2026
* 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
Comment thread sources/js/functions.js
Comment on lines +129 to +131
// 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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I cannot reproduce this bug. timeZoneName for Europe/London does indeed format to "GMT+0" or "GMT+1" for me.

HelpfulSoft1207 added a commit that referenced this pull request Jun 20, 2026
* 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants