Files
MagicMirror/js/translator.js
Kristjan ESPERANTO f29f424a62 [core] refactor: replace XMLHttpRequest with fetch and migrate e2e tests to Playwright (#3950)
### 1. Replace `XMLHttpRequest` with the modern `fetch` API for loading
translation files

#### Changes
- **translator.js**: Use `fetch` with `async/await` instead of XHR
callbacks
- **loader.js**: Align URL handling and add error handling (follow-up to
fetch migration)
- **Tests**: Update infrastructure for `fetch` compatibility

#### Benefits
- Modern standard API
- Cleaner, more readable code
- Better error handling and fallback mechanisms

### 2. Migrate e2e tests to Playwright

This wasn't originally planned for this PR, but is related. While
investigating suspicious log entries which surfaced after the fetch
migration I kept running into JSDOM’s limitations. That pushed me to
migrate the E2E suite to Playwright instead.

#### Changes
- switch e2e harness to Playwright (`tests/e2e/helpers/global-setup.js`)
- rewrite specs to use Playwright locators + shared `expectTextContent`
- install Chromium via `npx playwright install --with-deps` in CI

#### Benefits
- much closer to real browser behaviour
- and no more fighting JSDOM’s quirks
2025-11-08 21:59:05 +01:00

130 lines
4.2 KiB
JavaScript

/* global translations */
const Translator = (function () {
/**
* Load a JSON file via fetch.
* @param {string} file Path of the file we want to load.
* @returns {Promise<object>} the translations in the specified file
*/
async function loadJSON (file) {
const baseHref = document.baseURI;
const url = new URL(file, baseHref);
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Unexpected response status: ${response.status}`);
}
return await response.json();
} catch (exception) {
Log.error(`Loading json file =${file} failed`);
return null;
}
}
return {
coreTranslations: {},
coreTranslationsFallback: {},
translations: {},
translationsFallback: {},
/**
* Load a translation for a given key for a given module.
* @param {Module} module The module to load the translation for.
* @param {string} key The key of the text to translate.
* @param {object} variables The variables to use within the translation template (optional)
* @returns {string} the translated key
*/
translate (module, key, variables = {}) {
/**
* Combines template and variables like:
* template: "Please wait for {timeToWait} before continuing with {work}."
* variables: {timeToWait: "2 hours", work: "painting"}
* to: "Please wait for 2 hours before continuing with painting."
* @param {string} template Text with placeholder
* @param {object} variables Variables for the placeholder
* @returns {string} the template filled with the variables
*/
function createStringFromTemplate (template, variables) {
if (Object.prototype.toString.call(template) !== "[object String]") {
return template;
}
let templateToUse = template;
if (variables.fallback && !template.match(new RegExp("{.+}"))) {
templateToUse = variables.fallback;
}
return templateToUse.replace(new RegExp("{([^}]+)}", "g"), function (_unused, varName) {
return varName in variables ? variables[varName] : `{${varName}}`;
});
}
if (this.translations[module.name] && key in this.translations[module.name]) {
return createStringFromTemplate(this.translations[module.name][key], variables);
}
if (key in this.coreTranslations) {
return createStringFromTemplate(this.coreTranslations[key], variables);
}
if (this.translationsFallback[module.name] && key in this.translationsFallback[module.name]) {
return createStringFromTemplate(this.translationsFallback[module.name][key], variables);
}
if (key in this.coreTranslationsFallback) {
return createStringFromTemplate(this.coreTranslationsFallback[key], variables);
}
return key;
},
/**
* Load a translation file (json) and remember the data.
* @param {Module} module The module to load the translation file for.
* @param {string} file Path of the file we want to load.
* @param {boolean} isFallback Flag to indicate fallback translations.
*/
async load (module, file, isFallback) {
Log.log(`[translator] ${module.name} - Load translation${isFallback ? " fallback" : ""}: ${file}`);
if (this.translationsFallback[module.name]) {
return;
}
const json = await loadJSON(module.file(file));
const property = isFallback ? "translationsFallback" : "translations";
this[property][module.name] = json;
},
/**
* Load the core translations.
* @param {string} lang The language identifier of the core language.
*/
async loadCoreTranslations (lang) {
if (lang in translations) {
Log.log(`[translator] Loading core translation file: ${translations[lang]}`);
this.coreTranslations = await loadJSON(translations[lang]);
} else {
Log.log("[translator] Configured language not found in core translations.");
}
await this.loadCoreTranslationsFallback();
},
/**
* Load the core translations' fallback.
* The first language defined in translations.js will be used.
*/
async loadCoreTranslationsFallback () {
let first = Object.keys(translations)[0];
if (first) {
Log.log(`[translator] Loading core translation fallback file: ${translations[first]}`);
this.coreTranslationsFallback = await loadJSON(translations[first]);
}
}
};
}());
window.Translator = Translator;