Skip to content

Localization of the modern page (en, sv, pt, es) - #873

Open
HyakoV3 wants to merge 3 commits into
librespeed:masterfrom
HyakoV3:i18n-modern-lean
Open

HyakoV3 wants to merge 3 commits into
librespeed:masterfrom
HyakoV3:i18n-modern-lean

Conversation

@HyakoV3

@HyakoV3 HyakoV3 commented Oct 25, 2026

Copy link
Copy Markdown
Contributor

Localization of the modern page (English, Swedish, Portuguese, Spanish)

Follow-up to the discussion in #863 and to the Swedish localization in #860 by @yeager. @sstidl found the localization
worth having but kept it apart from theming, and #860 bigger than the purpose needs. This is a smaller version of the
same idea, with @yeager's catalog format (flat JSON, dotted keys, data-i18n bindings, English text kept in the HTML as
the fallback). Theming stays out of this PR.

What it does

  • frontend/javascript/i18n.js (about 85 lines of code) loads frontend/locales/<language>.json and fills the elements marked
    data-i18n / data-i18n-attr. Scripts use LibreSpeedI18n.t(key, fallback, { name: value }).
  • Language: saved choice, then the browser language, then English. A selector in the footer changes it and saves the
    choice in localStorage, so there is a way to pick a language without a URL parameter.
  • index.js draws its own texts (start button, sponsor, "connected through", the alert, copy link) through t(), and
    draws them again on the i18nchange event.
  • Catalogs: en, sv, pt, es, with the strings of the modern page and its privacy policy.

What was left out on purpose, compared to #860

  • No ?lang= parameter and no rewriting of links (the language is the browser's or the saved choice).
  • No catalog timeout and no waiting for the catalog before the test starts: the English text is already in the page and
    the dynamic texts are drawn from t() on every frame, so a slow catalog cannot hold the test.
  • Only the modern page (index-modern.html and frontend/index.html). The classic page and the stability page can
    follow with the same files if this shape is acceptable.
  • The sponsor line is built with DOM nodes (a text node and an <a>) instead of innerHTML, which was needed anyway to
    put a translated label in front of the link.

Docker

TITLE and TAGLINE replace the translated title, heading and tagline by plain text, as before; the sed patterns in
docker/entrypoint.sh were updated for the new data-i18n attributes.

Tests

tests/e2e/i18n.spec.js (browser language, selector, saved choice, back to English, unsupported language, failed
catalog, alert, and that every catalog has the keys of the English one and the pages only use keys that exist). The
existing title-special-chars, modes, design-switch, static-repository-assets and mobile-gauge-visibility
specs pass with the Docker setup (26 tests).

@qodo-free-for-open-source-projects

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

Localize the modern speedtest page in four languages

✨ Enhancement 🧪 Tests 🕐 20-40 Minutes

Grey Divider

AI Description

• Add English, Swedish, Portuguese, and Spanish translations to the modern page and privacy policy.
• Choose languages from saved preferences or browser settings, with a persistent footer selector.
• Cover language switching and catalog completeness while preserving Docker title and tagline
 overrides.
Diagram

graph TD
  P["Saved or browser locale"] --> I["i18n runtime"] --> C["Locale catalogs"]
  F["Footer selector"] --> I
  C -->|translations| I
  I --> H["Modern page DOM"]
  I -->|i18nchange| J["Dynamic UI renderer"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Translate without hiding the page
  • ➕ Content stays visible if a catalog request stalls.
  • ➕ No loading state blocks access to the page.
  • ➖ Visitors may briefly see English before their translation loads.

Recommendation: The small, page-scoped catalog runtime fits four static languages without adding a dependency. The main tradeoff is hiding the page during a non-English catalog request: consider a bounded fallback if avoiding an indefinitely blank page matters more than preventing an English flash.

Files changed (14) +647 / -100

Enhancement (10) +470 / -98
index.htmlBind standalone frontend text to translation keys +47/-43

Bind standalone frontend text to translation keys

• Adds the localization script, translation bindings across the interface and privacy policy, and a footer language selector. English text remains in the markup.

frontend/index.html

i18n.jsAdd the modern-page localization runtime +110/-0

Add the modern-page localization runtime

• Chooses a supported language from saved or browser preferences, loads its JSON catalog, and translates bound text and attributes. Exposes a translation function, persists selector changes, and emits i18nchange for script-rendered text.

frontend/javascript/i18n.js

index.jsTranslate text generated by the speedtest UI +34/-13

Translate text generated by the speedtest UI

• Routes button labels, alerts, connection text, copy feedback, and sponsor labels through the localization runtime. Marks rendered content for refresh after language changes and builds the sponsor link with DOM nodes.

frontend/javascript/index.js

en.jsonDefine the English translation catalog +52/-0

Define the English translation catalog

• Provides the reference keys and English text for the modern interface, accessibility labels, and privacy policy.

frontend/locales/en.json

es.jsonAdd Spanish translations +52/-0

Add Spanish translations

• Translates the modern interface, accessibility labels, and privacy policy using the English catalog's keys.

frontend/locales/es.json

pt.jsonAdd Portuguese translations +52/-0

Add Portuguese translations

• Translates the modern interface, accessibility labels, and privacy policy using the English catalog's keys.

frontend/locales/pt.json

sv.jsonAdd Swedish translations +52/-0

Add Swedish translations

• Translates the modern interface, accessibility labels, and privacy policy using the English catalog's keys.

frontend/locales/sv.json

index.cssStyle the selector and pending translation state +21/-0

Style the selector and pending translation state

• Styles the footer language selector and hides the body while an initial non-English catalog loads to avoid a flash of English.

frontend/styling/index.css

main.cssPreserve privacy-warning styling +4/-0

Preserve privacy-warning styling

• Makes the new translatable agreement span inherit its parent warning's typography and color.

frontend/styling/main.css

index-modern.htmlLocalize the root modern page +46/-42

Localize the root modern page

• Adds the localization script, translation bindings for page and privacy-policy content, and a footer selector. Retains English markup as the default display text.

index-modern.html

Tests (2) +161 / -0
i18n.spec.jsExercise language selection and catalog coverage +156/-0

Exercise language selection and catalog coverage

• Tests browser-language detection, switching and persistence, English fallback, translated UI and alerts, and failed catalog requests. Checks that catalogs share the English keys and that page bindings reference existing keys.

tests/e2e/i18n.spec.js

static-repository-assets.spec.jsVerify localization assets are served +5/-0

Verify localization assets are served

• Adds the localization script and four catalogs to the expected static repository assets.

tests/e2e/static-repository-assets.spec.js

Documentation (1) +14 / -0
README.mdDocument modern-page localization +14/-0

Document modern-page localization

• Explains language selection, translation bindings, catalog extension, and the behavior of Docker branding overrides.

frontend/README.md

Other (1) +2 / -2
entrypoint.shPreserve modern-page branding overrides +2/-2

Preserve modern-page branding overrides

• Adjusts TITLE and TAGLINE replacements to match the newly annotated modern-page markup. The replacements remove translation bindings so configured branding remains plain text.

docker/entrypoint.sh

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Docker users cannot translate the header ✓ Resolved
Description
The updated TITLE and TAGLINE substitutions replace marked-up elements with versions that omit
their data-i18n attributes. Both variables have non-empty Docker defaults, so even an uncustomized
Docker deployment leaves the page title, heading, and tagline in English when a visitor chooses
another language.
Code

docker/entrypoint.sh[127]

+    sed -i "s/<title data-i18n=\"page.title\">LibreSpeed - Free and Open Source Speedtest<\\/title>/<title>$TITLE_ESCAPED - Free and Open Source Speedtest<\\/title>/g; s/<h1 data-i18n=\"brand.title\">Free and Open Source Speedtest\\.<\\/h1>/<h1>$TITLE_ESCAPED<\\/h1>/g" /var/www/html/index-modern.html
Evidence
The Docker defaults cause both substitution branches to run; their replacement markup drops the
attributes that the translation function requires.

Dockerfile[40-41]
Dockerfile.alpine[59-60]
docker/entrypoint.sh[123-136]
frontend/javascript/i18n.js[50-53]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Docker's default TITLE and TAGLINE values trigger substitutions that remove the modern page's translation bindings, preventing its header text from changing language.
## Fix Focus Areas
- docker/entrypoint.sh[124-136]
- Dockerfile[40-41]
- Dockerfile.alpine[59-60]
## Recommended Fix
Distinguish default values from explicit overrides and leave the marked-up modern-page elements intact for defaults. Continue replacing the elements with unbound plain text for explicit custom values.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. A stalled catalog hides the test forever ✓ Resolved
Description
html[data-i18n-pending] body hides the entire page, and set() removes that attribute only after
its fetch settles. When an initial non-English catalog request remains pending, the English fallback
and the running test remain invisible with no timeout or usable language selector.
Code

frontend/styling/index.css[R104-106]

+html[data-i18n-pending] body {
+  visibility: hidden;
+}
Evidence
Every non-English initial choice sets the pending attribute, the stylesheet hides the body while it
exists, and the unbounded fetch removes it only in finally().

frontend/javascript/i18n.js[65-87]
frontend/javascript/i18n.js[90-100]
frontend/styling/index.css[102-106]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
An indefinitely pending initial locale request leaves the entire modern page invisible because the pending attribute is removed only when the request settles.
## Fix Focus Areas
- frontend/javascript/i18n.js[65-100]
- frontend/styling/index.css[102-106]
## Recommended Fix
Put a finite bound on the initial hidden state so the English page becomes visible even if the locale request never settles. Ensure a late successful response can still apply its translation safely.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Rapid changes restore the wrong language ✓ Resolved
Description
set() commits each fetched catalog without checking whether a newer language choice has been made.
If an earlier request finishes after a later one, it overwrites the visible translation, selector,
document language, and saved preference with the obsolete choice.
Code

frontend/javascript/i18n.js[R72-75]

+      .then(catalog => {
+        language = id;
+        messages = catalog;
+        if (save) {
Evidence
Each change starts a separate fetch, while every successful response unconditionally commits its
state and resets the selector.

frontend/javascript/i18n.js[60-84]
frontend/javascript/i18n.js[94-100]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Overlapping catalog requests commit in response order rather than user-selection order, allowing an older response to replace the latest choice.
## Fix Focus Areas
- frontend/javascript/i18n.js[65-100]
## Recommended Fix
Track the most recent language request with a generation token or cancel superseded requests. Before updating language, messages, storage, or the DOM, confirm that the response belongs to the latest choice.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Failed changes leave a misleading choice ✓ Resolved
Description
set() logs a failed catalog request without restoring the language selector to the language still
shown on the page. If someone using Portuguese selects English while en.json is unavailable, the
selector says English but the text, document language, and saved preference remain Portuguese.
Code

frontend/javascript/i18n.js[R86-87]

+      .catch(error => console.warn("LibreSpeed localization unavailable:", error))
+      .finally(() => document.documentElement.removeAttribute("data-i18n-pending"));
Evidence
The selector changes natively before its handler runs, but state updates and selector
synchronization occur only after a successful response; the failure handler only warns.

frontend/javascript/i18n.js[60-87]
frontend/javascript/i18n.js[94-100]
index-modern.html[84-85]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A failed language change leaves the selector displaying the requested language while the page continues using the previous one.
## Fix Focus Areas
- frontend/javascript/i18n.js[65-100]
## Recommended Fix
On catalog failure, restore each selector to the active language. If English should remain selectable without its catalog, explicitly restore the original English text and attributes before committing that choice.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

5. Missing keys keep old translations ✓ Resolved
Description
translateDocument() takes fallback text and attributes from the current DOM, which a previous
catalog has already overwritten. If a later catalog lacks a key, switching languages retains the
prior language's text for that element instead of using the English fallback supplied in the HTML.
Code

frontend/javascript/i18n.js[R51-57]

+    document.querySelectorAll("[data-i18n]").forEach(element => {
+      element.textContent = translate(element.dataset.i18n, element.textContent.trim());
+    });
+    document.querySelectorAll("[data-i18n-attr]").forEach(element => {
+      element.dataset.i18nAttr.split(",").forEach(binding => {
+        const [attribute, key] = binding.split(":");
+        element.setAttribute(attribute, translate(key, element.getAttribute(attribute)));
Evidence
Each translation overwrites the DOM and then uses that same mutable DOM value as the next
translation's fallback. The bundled catalogs currently have matching keys, so this arises with an
incomplete catalog rather than the supplied files.

frontend/javascript/i18n.js[44-59]
frontend/javascript/i18n.js[72-84]
tests/e2e/i18n.spec.js[133-154]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
After one translation has replaced the markup, a missing key in a later catalog falls back to the previous translation rather than the original English text or attribute.
## Fix Focus Areas
- frontend/javascript/i18n.js[44-59]
## Recommended Fix
Capture each bound element's original English text or attribute value before its first translation, and use that preserved value for every later fallback.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can enable the Remediation agent and Qodo fixes findings in a dedicated fix PR

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread docker/entrypoint.sh Outdated
Comment thread frontend/styling/index.css
Comment thread frontend/javascript/i18n.js
Comment thread frontend/javascript/i18n.js Outdated
Comment thread frontend/javascript/i18n.js Outdated
HyakoV3 and others added 2 commits October 25, 2026 10:49
…anish

A small i18n.js loads flat JSON catalogs (frontend/locales/<language>.json) and
fills the elements marked with data-i18n and data-i18n-attr. The language is the
saved choice, then the browser language, then English; a selector in the footer
changes it and saves the choice. The texts that index.js draws go through
LibreSpeedI18n.t and are drawn again when the language changes.

Scope is the modern page only. It builds on the catalog format of the
localization proposed by yeager in librespeed#860, without the ?lang= parameter, the
link rewriting, the catalog timeout or the sponsor markup rework.

The entrypoint seds for TITLE and TAGLINE match the new markup.

Co-authored-by: Daniel Nylander <1206564+yeager@users.noreply.github.com>
- Docker: leave the translated title, heading and tagline alone when TITLE and
  TAGLINE have their default values, which the images always set; only a custom
  value replaces them.
- A catalog that never answers no longer hides the page: the page shows in English
  after 3 s and still translates if the catalog arrives later.
- The latest choice of language wins when the catalogs arrive out of order.
- When a change of language fails, the selector goes back to the language shown.
- A key missing from a catalog falls back to the English text of the page, not
  to the previous language.

Co-authored-by: Daniel Nylander <1206564+yeager@users.noreply.github.com>
…er selector

The selector used the native arrow, a light font and no spacing. It now uses the
bold accent color of the footer links, the rounded border and the chevron of the
server selector, and turns pink on hover like the links.

Co-authored-by: Daniel Nylander <1206564+yeager@users.noreply.github.com>
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.

1 participant