Skip to content

Add Swedish interface localization - #860

Open
yeager wants to merge 2 commits into
librespeed:masterfrom
yeager:i18n-swedish
Open

yeager wants to merge 2 commits into
librespeed:masterfrom
yeager:i18n-swedish

Conversation

@yeager

@yeager yeager commented Sep 28, 2026

Copy link
Copy Markdown

Adds dependency-free JSON localization for LibreSpeed interfaces, with complete English and Swedish catalogs. The browser language is selected automatically, and ?lang=sv explicitly selects Swedish.

The modern UI, classic UI, and stability test use the shared catalog; dynamic status messages are covered too.

Validation: browser-verified all three views with ?lang=sv; l10n-lint reports 0 errors; svlang writing rules pass; Gitleaks reports 0 findings.

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

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

Add shared English and Swedish localization across interfaces

✨ Enhancement 📝 Documentation 🕐 20-40 Minutes

Grey Divider

AI Description

• Add dependency-free English and Swedish catalogs with browser-language selection and a ?lang=sv
 override.
• Localize static labels and dynamic messages across the modern, classic, and stability interfaces.
• Document how to add catalogs and translate dynamic UI strings.
Diagram

graph TD
  P["Language preference"] --> I["Shared i18n"] --> C[("JSON catalogs")]
  I --> M["Modern pages"] & K["Classic page"] & S["Stability page"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Embed catalogs in a static script
  • ➕ Avoids a separate catalog request and asynchronous startup coordination.
  • ➖ Loads unused languages on every visit and couples translation content to JavaScript delivery.

Recommendation: Keep the shared JSON loader: it fits the static interfaces without adding a build step or dependency. Before treating localization as complete, bind the remaining English prose in the classic privacy policy and check behavior for unsupported ?lang= values.

Files changed (9) +408 / -165

Enhancement (8) +400 / -165
index.htmlBind standalone modern UI text to translation keys +44/-47

Bind standalone modern UI text to translation keys

• Loads the shared localization script and binds page metadata, controls, metrics, dialogs, and privacy text to catalog keys. Existing English text remains in the markup as a fallback.

frontend/index.html

i18n.jsAdd shared browser-side localization loader +56/-0

Add shared browser-side localization loader

• Selects English or Swedish from the query string or browser preferences, fetches a JSON catalog, and translates marked text and attributes. Exposes a readiness promise and translation function for dynamic messages.

frontend/javascript/i18n.js

index.jsTranslate dynamic modern-interface messages +11/-10

Translate dynamic modern-interface messages

• Waits for localization before initializing the modern UI. Routes button states, copy feedback, server errors, sponsor text, and connection text through the shared translator.

frontend/javascript/index.js

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

Define the English translation catalog

• Adds keys for shared metrics and privacy text plus modern, classic, and stability interface messages. Includes placeholders for dynamic values.

frontend/locales/en.json

sv.jsonAdd Swedish translations +91/-0

Add Swedish translations

• Provides Swedish values for the English catalog keys, including interface controls, privacy text, errors, and stability ratings.

frontend/locales/sv.json

index-classic.htmlConnect classic interface to shared localization +30/-29

Connect classic interface to shared localization

• Loads translations before server initialization and binds controls, metrics, links, and portions of the privacy policy. Localizes server-availability and copy feedback; several privacy-policy paragraphs remain unbound English text.

index-classic.html

index-modern.htmlBind root modern interface to translation keys +42/-45

Bind root modern interface to translation keys

• Loads the shared module and marks page metadata, labels, dialogs, and privacy content for translation. Mirrors the bindings added to the standalone modern page.

index-modern.html

stability.htmlLocalize stability-test controls and status text +35/-34

Localize stability-test controls and status text

• Binds headings, durations, controls, statistics, and button-state labels to the shared catalog. Translates generated server-discovery messages, ratings, and the disabled-alert label.

stability.html

Documentation (1) +8 / -0
README.mdDocument localization and catalog conventions +8/-0

Document localization and catalog conventions

• Explains where catalogs live, how browser selection and '?lang=sv' work, and how contributors should translate dynamic strings.

README.md

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

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (7) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. A language link executes attacker script 🐞 Bug ⛨ Security
Description
languageFromRequest accepts an arbitrary lang value, so one beginning with // makes the
catalog URL point to an attacker-hosted, CORS-enabled JSON file. When a sponsored server is
selected, startRenderingLoop inserts that catalog's server.sponsor value into innerHTML,
allowing markup from the crafted link to execute.
Code

frontend/javascript/i18n.js[R9-10]

+    const requested = new URLSearchParams(window.location.search).get("lang");
+    if (requested) return requested.toLowerCase().split("-")[0];
Evidence
The explicit language bypasses the supported-language set and becomes part of a URL resolved against
the catalog directory. The fetched JSON supplies translations, and the new sponsor rendering path
treats one translation as HTML; the supplied server list contains entries with sponsor URLs that
reach that path.

frontend/javascript/i18n.js[8-13]
frontend/javascript/i18n.js[39-48]
frontend/javascript/index.js[358-364]
server-list.json[2-11]

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 unvalidated `lang` parameter can load an attacker-controlled catalog, whose sponsor translation is inserted as HTML.
## Fix Focus Areas
- frontend/javascript/i18n.js[8-13]
- frontend/javascript/i18n.js[39-46]
- frontend/javascript/index.js[358-364]
## Recommended Fix
Accept only supported language codes before constructing the catalog URL, falling back to English for other values. Build the sponsor label and link with text nodes and DOM APIs rather than inserting translation text through `innerHTML`.

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


2. Configured stability tests fail to start 🐞 Bug ≡ Correctness
Description
initServers now calls LibreSpeedI18n.t() while the page's inline script can run before the
deferred localization script. When SPEEDTEST_SERVERS is configured as a nonempty array,
loadServers invokes initServers synchronously and the missing global throws before
initialization finishes.
Code

stability.html[488]

+          pendingOpt.textContent = window.LibreSpeedI18n.t("stability.finding-server", "Finding best server...");
Evidence
The array branch calls its callback immediately, the callback calls initServers from the inline
script, and initServers reaches the added translation call for a nonempty list. The deferred
script that defines LibreSpeedI18n appears after that inline script.

stability.html[408-419]
stability.html[485-490]
stability.html[985-991]
frontend/javascript/i18n.js[55-56]

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 supported array-based server configuration initializes the stability page before its deferred localization script has installed the translation API.
## Fix Focus Areas
- stability.html[408-419]
- stability.html[485-490]
- stability.html[985-991]
## Recommended Fix
Start server loading only after the localization script has executed, preferably after its `ready` promise settles. Preserve startup when catalog loading fails.

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



Remediation recommended

3. Slow translations block speed tests 🐞 Bug ☼ Reliability
Description
The modern bootstrap awaits LibreSpeedI18n.ready, and the classic bootstrap also waits for that
promise before initializing its test. If the catalog request remains pending, neither view reaches
its test controls or server initialization, even though the page has English fallback text.
Code

frontend/javascript/index.js[29]

+  await window.LibreSpeedI18n.ready;
Evidence
The catalog promise settles only after fetch and JSON processing or an error; it has no timeout.
Both startup paths are gated on that promise, so a request that does not settle prevents their
initialization.

frontend/javascript/i18n.js[39-53]
frontend/javascript/index.js[27-34]
index-classic.html[523-526]

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 catalog request now prevents modern and classic speed tests from initializing.
## Fix Focus Areas
- frontend/javascript/i18n.js[39-53]
- frontend/javascript/index.js[27-34]
- index-classic.html[523-526]
## Recommended Fix
Give catalog loading a bounded wait or allow test initialization with English fallback while it proceeds. Ensure late translation does not overwrite live UI state.

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


4. Late translations reset stability status 🐞 Bug ≡ Correctness
Description
translateDocument() unconditionally rewrites the mutable #thresholdValue text and #startBtn
title from their static translation bindings. If a threshold is adjusted or server discovery
finishes before the catalog loads, an active threshold can appear off and a ready or failed server
control can appear to be finding a server.
Code

stability.html[365]

+        <span id="thresholdValue" data-i18n="stability.off">Off</span>
Evidence
The new bindings target the same DOM values that updateStartButtonState and updateThreshold
change. Catalog completion later runs translateDocument, which writes the catalog's fixed finding
and off strings without checking those current states.

stability.html[296-297]
stability.html[365-367]
stability.html[555-560]
stability.html[942-946]
frontend/javascript/i18n.js[26-36]
frontend/javascript/i18n.js[45-49]

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 late catalog response overwrites stability controls whose text and title have already been updated to reflect live state.
## Fix Focus Areas
- stability.html[296-297]
- stability.html[365-367]
- stability.html[555-560]
- stability.html[942-946]
- frontend/javascript/i18n.js[26-36]
## Recommended Fix
Do not statically translate attributes or text owned by the stability state handlers. Translate their initial values and re-render the current server and threshold state once localization is ready.

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


5. Swedish selection resets across views 🐞 Bug ≡ Correctness
Description
The translated navigation links still point to URLs without the current lang parameter. A visitor
who selected ?lang=sv in a browser whose preferred language is not Swedish returns to another view
in that browser language or English.
Code

index-classic.html[R572-573]

+		<a href="index.html?design=new" data-i18n="classic.modern">Try the modern design</a><br>
+		<a href="stability.html" data-i18n="classic.stability">Stability Test</a> | <a href="https://github.com/librespeed/speedtest" data-i18n="classic.source">Source code</a>
Evidence
Language selection uses the query parameter only on the current page; otherwise it falls back to
browser preferences. The links between views do not include that parameter, although the root design
redirect preserves query parameters it receives.

frontend/javascript/i18n.js[8-14]
index-classic.html[572-573]
index-modern.html[80-82]
stability.html[279-280]
design-switch.js[58-69]

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

## Issue description
Navigation between localized views drops the visitor's explicit Swedish selection.
## Fix Focus Areas
- index-classic.html[572-573]
- index-modern.html[80-82]
- stability.html[279-280]
- frontend/javascript/i18n.js[8-14]
## Recommended Fix
Carry a supported, explicitly selected `lang` parameter into links between the classic, modern, and stability views, while retaining each link's existing design parameter where applicable.

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


View review recommended (2)
6. Classic privacy terms remain in English 🐞 Bug ≡ Correctness
Description
index-classic.html binds translations to the privacy headings and list items but leaves the
explanatory paragraphs, consent and removal terms, and close control as unbound English text. With
?lang=sv, those parts of the privacy policy remain English beside the translated sections; the
server and test-ID labels also remain English.
Code

index-classic.html[R576-578]

+		<h2 data-i18n="privacy.title">Privacy Policy</h2>
+		<p data-i18n="privacy.telemetry">This HTML5 speed test server is configured with telemetry enabled.</p>
+		<h4 data-i18n="privacy.collect-title">What data we collect</h4>
Evidence
The translator only visits elements with translation attributes. The cited classic markup adds
bindings to some policy elements but has none on the adjacent English paragraphs or other named
controls, while the catalogs already contain translations for several of those policy passages.

frontend/javascript/i18n.js[26-36]
index-classic.html[575-615]
index-classic.html[529-533]
index-classic.html[564-566]
frontend/locales/sv.json[24-42]

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

## Issue description
The classic view localizes selected headings and items but leaves substantial privacy-policy text and several interface labels in English.
## Fix Focus Areas
- index-classic.html[529-533]
- index-classic.html[564-566]
- index-classic.html[575-615]
- frontend/locales/en.json[21-42]
- frontend/locales/sv.json[21-42]
## Recommended Fix
Add bindings for the remaining privacy paragraphs, list text, close control, server label, and test-ID label. Reuse existing catalog keys where available and add matching English and Swedish entries for the rest.

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


7. Modern share control stays in English 🐞 Bug ≡ Correctness
Description
frontend/index.html adds a share.results binding to its button, but the corresponding button in
index-modern.html remains unbound. Visitors using the root modern view with ?lang=sv see “Share
results” after the other bound controls have been translated.
Code

frontend/index.html[74]

+        <span data-i18n="share.results">Share results</span>
Evidence
Only elements carrying data-i18n are translated. The PR binds the frontend button, the root modern
button still contains bare English text, and the Swedish catalog supplies share.results.

frontend/index.html[72-75]
index-modern.html[75-78]
frontend/javascript/i18n.js[26-30]
frontend/locales/sv.json[14-14]

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

## Issue description
The root modern page omits the share-button translation binding added to its frontend counterpart.
## Fix Focus Areas
- frontend/index.html[72-75]
- index-modern.html[75-78]
## Recommended Fix
Add the `share.results` binding to the button in `index-modern.html`, matching `frontend/index.html`.

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


Grey Divider

Context sources
Review mode: ⚖️ Balanced: This is a behavioral localization change spanning multiple HTML entry points and shared asynchronous JavaScript, creating several independently reviewable integration and fallback paths.

Grey Divider

Tip of the day
💡 Did you know, you can turn on the rule miner and Qodo learns your standards from review history

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment on lines +9 to +10
const requested = new URLSearchParams(window.location.search).get("lang");
if (requested) return requested.toLowerCase().split("-")[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.

Action required

1. A language link executes attacker script 🐞 Bug ⛨ Security

languageFromRequest accepts an arbitrary lang value, so one beginning with // makes the
catalog URL point to an attacker-hosted, CORS-enabled JSON file. When a sponsored server is
selected, startRenderingLoop inserts that catalog's server.sponsor value into innerHTML,
allowing markup from the crafted link to execute.
Agent Prompt
## Issue description
An unvalidated `lang` parameter can load an attacker-controlled catalog, whose sponsor translation is inserted as HTML.
## Fix Focus Areas
- frontend/javascript/i18n.js[8-13]
- frontend/javascript/i18n.js[39-46]
- frontend/javascript/index.js[358-364]
## Recommended Fix
Accept only supported language codes before constructing the catalog URL, falling back to English for other values. Build the sponsor label and link with text nodes and DOM APIs rather than inserting translation text through `innerHTML`.

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

Comment thread stability.html
var pendingOpt = document.createElement("option");
pendingOpt.value = "";
pendingOpt.textContent = "Finding best server...";
pendingOpt.textContent = window.LibreSpeedI18n.t("stability.finding-server", "Finding best server...");

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.

Action required

2. Configured stability tests fail to start 🐞 Bug ≡ Correctness

initServers now calls LibreSpeedI18n.t() while the page's inline script can run before the
deferred localization script. When SPEEDTEST_SERVERS is configured as a nonempty array,
loadServers invokes initServers synchronously and the missing global throws before
initialization finishes.
Agent Prompt
## Issue description
A supported array-based server configuration initializes the stability page before its deferred localization script has installed the translation API.
## Fix Focus Areas
- stability.html[408-419]
- stability.html[485-490]
- stability.html[985-991]
## Recommended Fix
Start server loading only after the localization script has executed, preferably after its `ready` promise settles. Preserve startup when catalog loading fails.

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


// Bootstrap the application when the DOM is ready
window.addEventListener("DOMContentLoaded", async () => {
await window.LibreSpeedI18n.ready;

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.

Remediation recommended

3. Slow translations block speed tests 🐞 Bug ☼ Reliability

The modern bootstrap awaits LibreSpeedI18n.ready, and the classic bootstrap also waits for that
promise before initializing its test. If the catalog request remains pending, neither view reaches
its test controls or server initialization, even though the page has English fallback text.
Agent Prompt
## Issue description
An indefinitely pending catalog request now prevents modern and classic speed tests from initializing.
## Fix Focus Areas
- frontend/javascript/i18n.js[39-53]
- frontend/javascript/index.js[27-34]
- index-classic.html[523-526]
## Recommended Fix
Give catalog loading a bounded wait or allow test initialization with English fallback while it proceeds. Ensure late translation does not overwrite live UI state.

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

Comment thread stability.html
oninput="updateThreshold(this.value)"
/>
<span id="thresholdValue">Off</span>
<span id="thresholdValue" data-i18n="stability.off">Off</span>

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.

Remediation recommended

4. Late translations reset stability status 🐞 Bug ≡ Correctness

translateDocument() unconditionally rewrites the mutable #thresholdValue text and #startBtn
title from their static translation bindings. If a threshold is adjusted or server discovery
finishes before the catalog loads, an active threshold can appear off and a ready or failed server
control can appear to be finding a server.
Agent Prompt
## Issue description
A late catalog response overwrites stability controls whose text and title have already been updated to reflect live state.
## Fix Focus Areas
- stability.html[296-297]
- stability.html[365-367]
- stability.html[555-560]
- stability.html[942-946]
- frontend/javascript/i18n.js[26-36]
## Recommended Fix
Do not statically translate attributes or text owned by the stability state handlers. Translate their initial values and re-render the current server and threshold state once localization is ready.

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

Comment thread index-classic.html
Comment on lines +572 to +573
<a href="index.html?design=new" data-i18n="classic.modern">Try the modern design</a><br>
<a href="stability.html" data-i18n="classic.stability">Stability Test</a> | <a href="https://github.com/librespeed/speedtest" data-i18n="classic.source">Source code</a>

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.

Remediation recommended

5. Swedish selection resets across views 🐞 Bug ≡ Correctness

The translated navigation links still point to URLs without the current lang parameter. A visitor
who selected ?lang=sv in a browser whose preferred language is not Swedish returns to another view
in that browser language or English.
Agent Prompt
## Issue description
Navigation between localized views drops the visitor's explicit Swedish selection.
## Fix Focus Areas
- index-classic.html[572-573]
- index-modern.html[80-82]
- stability.html[279-280]
- frontend/javascript/i18n.js[8-14]
## Recommended Fix
Carry a supported, explicitly selected `lang` parameter into links between the classic, modern, and stability views, while retaining each link's existing design parameter where applicable.

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

Comment thread index-classic.html
Comment on lines +576 to +578
<h2 data-i18n="privacy.title">Privacy Policy</h2>
<p data-i18n="privacy.telemetry">This HTML5 speed test server is configured with telemetry enabled.</p>
<h4 data-i18n="privacy.collect-title">What data we collect</h4>

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.

Remediation recommended

6. Classic privacy terms remain in english 🐞 Bug ≡ Correctness

index-classic.html binds translations to the privacy headings and list items but leaves the
explanatory paragraphs, consent and removal terms, and close control as unbound English text. With
?lang=sv, those parts of the privacy policy remain English beside the translated sections; the
server and test-ID labels also remain English.
Agent Prompt
## Issue description
The classic view localizes selected headings and items but leaves substantial privacy-policy text and several interface labels in English.
## Fix Focus Areas
- index-classic.html[529-533]
- index-classic.html[564-566]
- index-classic.html[575-615]
- frontend/locales/en.json[21-42]
- frontend/locales/sv.json[21-42]
## Recommended Fix
Add bindings for the remaining privacy paragraphs, list text, close control, server label, and test-ID label. Reuse existing catalog keys where available and add matching English and Swedish entries for the rest.

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

Comment thread frontend/index.html

<button class="small inverted hidden" id="share-results">
Share results
<span data-i18n="share.results">Share results</span>

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.

Remediation recommended

7. Modern share control stays in english 🐞 Bug ≡ Correctness

frontend/index.html adds a share.results binding to its button, but the corresponding button in
index-modern.html remains unbound. Visitors using the root modern view with ?lang=sv see “Share
results” after the other bound controls have been translated.
Agent Prompt
## Issue description
The root modern page omits the share-button translation binding added to its frontend counterpart.
## Fix Focus Areas
- frontend/index.html[72-75]
- index-modern.html[75-78]
## Recommended Fix
Add the `share.results` binding to the button in `index-modern.html`, matching `frontend/index.html`.

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

@HyakoV3

HyakoV3 commented Oct 6, 2026 •

Copy link
Copy Markdown

@yeager i have integrated your PR on my proposal: #863

I have been co-authored you there too.

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.

2 participants