mirror of
https://github.com/MichMich/MagicMirror.git
synced 2026-07-19 09:24:22 -07:00
refactor: convert Translator to ES module (#4202)
Another step towards ES modules: this converts `Translator` to a proper module and imports it where needed, instead of relying on it being globally available. The goal is to gradually get rid of the old global-script style in the core. It makes dependencies explicit, reduces hidden coupling and keeps the codebase easier to maintain going forward. The code change is tiny - the bigger and harder part was adapting the tests to the new loading behavior.
This commit is contained in:
committed by
GitHub
parent
a489d930e1
commit
ed95826d19
@@ -49,7 +49,6 @@
|
||||
<script type="text/javascript" src="defaultmodules/utils.js"></script>
|
||||
<script type="text/javascript" src="js/logger.js"></script>
|
||||
<script type="text/javascript" src="translations/translations.js"></script>
|
||||
<script type="text/javascript" src="js/translator.js"></script>
|
||||
<script type="text/javascript" src="config/basepath.js"></script>
|
||||
<script type="text/javascript" src="js/animateCSS.js"></script>
|
||||
<script type="text/javascript" src="js/positions.js"></script>
|
||||
|
||||
+3
-1
@@ -2,6 +2,8 @@
|
||||
|
||||
// eslint-disable-next-line import-x/extensions
|
||||
import { loadModules } from "./loader.js";
|
||||
// eslint-disable-next-line import-x/extensions
|
||||
import { Translator } from "./translator.js";
|
||||
|
||||
let modules = [];
|
||||
|
||||
@@ -571,7 +573,7 @@ export const MM = {
|
||||
|
||||
Log.setLogLevel(config.logLevel);
|
||||
|
||||
await globalThis.Translator.loadCoreTranslations(config.language);
|
||||
await Translator.loadCoreTranslations(config.language);
|
||||
await loadModules();
|
||||
},
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
import { loadFileForModule } from "./loader.js";
|
||||
// eslint-disable-next-line import-x/extensions
|
||||
import { MMSocket } from "./socketclient.js";
|
||||
// eslint-disable-next-line import-x/extensions
|
||||
import { Translator } from "./translator.js";
|
||||
|
||||
/*
|
||||
* Module Blueprint.
|
||||
|
||||
+1
-3
@@ -1,6 +1,6 @@
|
||||
/* global translations */
|
||||
|
||||
const Translator = (function () {
|
||||
export const Translator = (function () {
|
||||
|
||||
/**
|
||||
* Load a JSON file via fetch.
|
||||
@@ -125,5 +125,3 @@ const Translator = (function () {
|
||||
}
|
||||
};
|
||||
}());
|
||||
|
||||
window.Translator = Translator;
|
||||
|
||||
@@ -2,27 +2,23 @@ const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const { pathToFileURL } = require("node:url");
|
||||
const helmet = require("helmet");
|
||||
const { JSDOM } = require("jsdom");
|
||||
const express = require("express");
|
||||
const translations = require("../../translations/translations");
|
||||
const {
|
||||
setupTranslationTestEnvironment,
|
||||
TRANSLATOR_MODULE_URL,
|
||||
resetTranslatorState
|
||||
} = require("../utils/translation_test_environment");
|
||||
|
||||
/**
|
||||
* Helper function to create a fresh Translator instance with DOM environment.
|
||||
* @returns {object} Object containing window and Translator
|
||||
* Create a fresh Translator state for each test.
|
||||
* @returns {Promise<object>} Shared Translator singleton with cleared state
|
||||
*/
|
||||
function createTranslationTestEnvironment () {
|
||||
// Setup DOM environment with Translator
|
||||
const translatorJs = fs.readFileSync(path.join(__dirname, "..", "..", "js", "translator.js"), "utf-8");
|
||||
const dom = new JSDOM("", { url: "http://localhost:3000", runScripts: "outside-only" });
|
||||
|
||||
dom.window.Log = { log: vi.fn(), error: vi.fn() };
|
||||
dom.window.translations = translations;
|
||||
dom.window.fetch = fetch;
|
||||
dom.window.eval(translatorJs);
|
||||
|
||||
const window = dom.window;
|
||||
|
||||
return { window, Translator: window.Translator };
|
||||
async function getFreshTranslator () {
|
||||
setupTranslationTestEnvironment(3000);
|
||||
const { Translator } = await import(TRANSLATOR_MODULE_URL);
|
||||
resetTranslatorState(Translator);
|
||||
return Translator;
|
||||
}
|
||||
|
||||
describe("translations", () => {
|
||||
@@ -40,6 +36,13 @@ describe("translations", () => {
|
||||
server = app.listen(3000);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
delete global.document;
|
||||
delete global.Log;
|
||||
delete global.config;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await server.close();
|
||||
});
|
||||
@@ -53,101 +56,70 @@ describe("translations", () => {
|
||||
});
|
||||
|
||||
describe("loadTranslations", () => {
|
||||
let dom;
|
||||
let Translator;
|
||||
let Module;
|
||||
let config;
|
||||
|
||||
beforeEach(async () => {
|
||||
// Create a new translation test environment for each test
|
||||
const env = createTranslationTestEnvironment();
|
||||
const window = env.window;
|
||||
global.Log = { log: vi.fn(), error: vi.fn(), warn: vi.fn() };
|
||||
config = { language: "de" };
|
||||
global.config = config;
|
||||
|
||||
// Bridge JSDOM globals to Node.js so module.js (ES module) can access them
|
||||
global.Log = window.Log;
|
||||
global.Translator = window.Translator;
|
||||
global.config = { language: "de" };
|
||||
global.window = { name: "", mmVersion: "2.0.0" };
|
||||
global.MM = { hideModule: () => {}, showModule: () => {}, sendNotification: () => {}, updateDom: () => {} };
|
||||
global.nunjucks = {
|
||||
Environment () {
|
||||
this.addFilter = () => {};
|
||||
this.renderString = () => "";
|
||||
this.render = (_t, _d, cb) => cb(null, "");
|
||||
},
|
||||
WebLoader () {},
|
||||
runtime: { markSafe: (str) => str }
|
||||
};
|
||||
// module.js and translator.js are ES modules that read these globals at call time.
|
||||
Translator = await getFreshTranslator();
|
||||
|
||||
// Import Module directly — eval can't handle ES module syntax
|
||||
const modulePath = pathToFileURL(path.join(__dirname, "..", "..", "js", "module.js")).href;
|
||||
const { Module } = await import(`${modulePath}?test=${Date.now()}`);
|
||||
window.Module = Module;
|
||||
|
||||
// Expose config on window so tests can modify dom.window.config
|
||||
window.config = global.config;
|
||||
|
||||
dom = { window };
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete global.Log;
|
||||
delete global.Translator;
|
||||
delete global.config;
|
||||
delete global.window;
|
||||
delete global.MM;
|
||||
delete global.nunjucks;
|
||||
({ Module } = await import(modulePath));
|
||||
});
|
||||
|
||||
it("should load translation file", async () => {
|
||||
const { Translator, Module, config } = dom.window;
|
||||
config.language = "en";
|
||||
Translator.load = vi.fn().mockImplementation(() => null);
|
||||
const loadSpy = vi.spyOn(Translator, "load").mockResolvedValue(null);
|
||||
|
||||
Module.register("name", { getTranslations: () => translations });
|
||||
const MMM = Module.create("name");
|
||||
|
||||
await MMM.loadTranslations();
|
||||
|
||||
expect(Translator.load.mock.calls).toHaveLength(1);
|
||||
expect(Translator.load).toHaveBeenCalledWith(MMM, "translations/en.json", false);
|
||||
expect(loadSpy.mock.calls).toHaveLength(1);
|
||||
expect(loadSpy).toHaveBeenCalledWith(MMM, "translations/en.json", false);
|
||||
});
|
||||
|
||||
it("should load translation + fallback file", async () => {
|
||||
const { Translator, Module } = dom.window;
|
||||
Translator.load = vi.fn().mockImplementation(() => null);
|
||||
const loadSpy = vi.spyOn(Translator, "load").mockResolvedValue(null);
|
||||
|
||||
Module.register("name", { getTranslations: () => translations });
|
||||
const MMM = Module.create("name");
|
||||
|
||||
await MMM.loadTranslations();
|
||||
|
||||
expect(Translator.load.mock.calls).toHaveLength(2);
|
||||
expect(Translator.load).toHaveBeenCalledWith(MMM, "translations/de.json", false);
|
||||
expect(Translator.load).toHaveBeenCalledWith(MMM, "translations/en.json", true);
|
||||
expect(loadSpy.mock.calls).toHaveLength(2);
|
||||
expect(loadSpy).toHaveBeenCalledWith(MMM, "translations/de.json", false);
|
||||
expect(loadSpy).toHaveBeenCalledWith(MMM, "translations/en.json", true);
|
||||
});
|
||||
|
||||
it("should load translation fallback file", async () => {
|
||||
const { Translator, Module, config } = dom.window;
|
||||
config.language = "--";
|
||||
Translator.load = vi.fn().mockImplementation(() => null);
|
||||
const loadSpy = vi.spyOn(Translator, "load").mockResolvedValue(null);
|
||||
|
||||
Module.register("name", { getTranslations: () => translations });
|
||||
const MMM = Module.create("name");
|
||||
|
||||
await MMM.loadTranslations();
|
||||
|
||||
expect(Translator.load.mock.calls).toHaveLength(1);
|
||||
expect(Translator.load).toHaveBeenCalledWith(MMM, "translations/en.json", true);
|
||||
expect(loadSpy.mock.calls).toHaveLength(1);
|
||||
expect(loadSpy).toHaveBeenCalledWith(MMM, "translations/en.json", true);
|
||||
});
|
||||
|
||||
it("should load no file", async () => {
|
||||
const { Translator, Module } = dom.window;
|
||||
Translator.load = vi.fn();
|
||||
const loadSpy = vi.spyOn(Translator, "load").mockResolvedValue(null);
|
||||
|
||||
Module.register("name", {});
|
||||
const MMM = Module.create("name");
|
||||
|
||||
await MMM.loadTranslations();
|
||||
|
||||
expect(Translator.load.mock.calls).toHaveLength(0);
|
||||
expect(loadSpy.mock.calls).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -161,7 +133,7 @@ describe("translations", () => {
|
||||
describe("parsing language files through the Translator class", () => {
|
||||
for (const language in translations) {
|
||||
it(`should parse ${language}`, async () => {
|
||||
const { Translator } = createTranslationTestEnvironment();
|
||||
const Translator = await getFreshTranslator();
|
||||
await Translator.load(mmm, translations[language], false);
|
||||
|
||||
expect(typeof Translator.translations[mmm.name]).toBe("object");
|
||||
@@ -193,7 +165,7 @@ describe("translations", () => {
|
||||
|
||||
// Function to initialize JSDOM and load translations
|
||||
const initializeTranslationDOM = async (language) => {
|
||||
const { Translator } = createTranslationTestEnvironment();
|
||||
const Translator = await getFreshTranslator();
|
||||
await Translator.load(mmm, translations[language], false);
|
||||
return Translator.translations[mmm.name];
|
||||
};
|
||||
|
||||
@@ -9,7 +9,6 @@ describe("File js/module (cloneObject)", () => {
|
||||
let originalLog;
|
||||
let originalConfig;
|
||||
let originalMM;
|
||||
let originalTranslator;
|
||||
let originalNunjucks;
|
||||
|
||||
beforeAll(async () => {
|
||||
@@ -17,7 +16,6 @@ describe("File js/module (cloneObject)", () => {
|
||||
originalLog = global.Log;
|
||||
originalConfig = global.config;
|
||||
originalMM = global.MM;
|
||||
originalTranslator = global.Translator;
|
||||
originalNunjucks = global.nunjucks;
|
||||
|
||||
global.window = { mmVersion: "2.0.0" };
|
||||
@@ -29,10 +27,6 @@ describe("File js/module (cloneObject)", () => {
|
||||
sendNotification: () => {},
|
||||
updateDom: () => {}
|
||||
};
|
||||
global.Translator = {
|
||||
load: () => Promise.resolve(),
|
||||
translate: () => ""
|
||||
};
|
||||
global.nunjucks = {
|
||||
Environment () {
|
||||
this.addFilter = () => {};
|
||||
@@ -57,7 +51,6 @@ describe("File js/module (cloneObject)", () => {
|
||||
global.Log = originalLog;
|
||||
global.config = originalConfig;
|
||||
global.MM = originalMM;
|
||||
global.Translator = originalTranslator;
|
||||
global.nunjucks = originalNunjucks;
|
||||
});
|
||||
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const helmet = require("helmet");
|
||||
const { JSDOM } = require("jsdom");
|
||||
const express = require("express");
|
||||
const {
|
||||
setupTranslationTestEnvironment,
|
||||
TRANSLATOR_MODULE_URL,
|
||||
resetTranslatorState
|
||||
} = require("../../utils/translation_test_environment");
|
||||
|
||||
/**
|
||||
* Helper function to create a fresh Translator instance with DOM environment.
|
||||
* @returns {object} Object containing window and Translator
|
||||
* Create a fresh Translator state for each test.
|
||||
* @returns {Promise<object>} Shared Translator singleton with cleared state
|
||||
*/
|
||||
function createTranslationTestEnvironment () {
|
||||
const translatorJs = fs.readFileSync(path.join(__dirname, "..", "..", "..", "js", "translator.js"), "utf-8");
|
||||
const dom = new JSDOM("", { url: "http://localhost:3001", runScripts: "outside-only" });
|
||||
|
||||
dom.window.Log = { log: vi.fn(), error: vi.fn() };
|
||||
dom.window.fetch = fetch;
|
||||
dom.window.eval(translatorJs);
|
||||
|
||||
return { window: dom.window, Translator: dom.window.Translator };
|
||||
async function getFreshTranslator () {
|
||||
setupTranslationTestEnvironment(3001);
|
||||
const { Translator } = await import(TRANSLATOR_MODULE_URL);
|
||||
resetTranslatorState(Translator);
|
||||
return Translator;
|
||||
}
|
||||
|
||||
describe("Translator", () => {
|
||||
@@ -49,6 +49,13 @@ describe("Translator", () => {
|
||||
await server.close();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
delete global.document;
|
||||
delete global.Log;
|
||||
delete global.translations;
|
||||
});
|
||||
|
||||
describe("translate", () => {
|
||||
const translations = {
|
||||
"MMM-Module": {
|
||||
@@ -93,8 +100,8 @@ describe("Translator", () => {
|
||||
Translator.coreTranslationsFallback = coreTranslationsFallback;
|
||||
};
|
||||
|
||||
it("should return custom module translation", () => {
|
||||
const { Translator } = createTranslationTestEnvironment();
|
||||
it("should return custom module translation", async () => {
|
||||
const Translator = await getFreshTranslator();
|
||||
setTranslations(Translator);
|
||||
|
||||
let translation = Translator.translate({ name: "MMM-Module" }, "Hello");
|
||||
@@ -104,8 +111,8 @@ describe("Translator", () => {
|
||||
expect(translation).toBe("Hallo fewieden");
|
||||
});
|
||||
|
||||
it("should return core translation", () => {
|
||||
const { Translator } = createTranslationTestEnvironment();
|
||||
it("should return core translation", async () => {
|
||||
const Translator = await getFreshTranslator();
|
||||
setTranslations(Translator);
|
||||
let translation = Translator.translate({ name: "MMM-Module" }, "FOO");
|
||||
expect(translation).toBe("Foo");
|
||||
@@ -113,29 +120,29 @@ describe("Translator", () => {
|
||||
expect(translation).toBe("Bar Lorem Ipsum");
|
||||
});
|
||||
|
||||
it("should return custom module translation fallback", () => {
|
||||
const { Translator } = createTranslationTestEnvironment();
|
||||
it("should return custom module translation fallback", async () => {
|
||||
const Translator = await getFreshTranslator();
|
||||
setTranslations(Translator);
|
||||
const translation = Translator.translate({ name: "MMM-Module" }, "A key");
|
||||
expect(translation).toBe("A translation");
|
||||
});
|
||||
|
||||
it("should return core translation fallback", () => {
|
||||
const { Translator } = createTranslationTestEnvironment();
|
||||
it("should return core translation fallback", async () => {
|
||||
const Translator = await getFreshTranslator();
|
||||
setTranslations(Translator);
|
||||
const translation = Translator.translate({ name: "MMM-Module" }, "Fallback");
|
||||
expect(translation).toBe("core fallback");
|
||||
});
|
||||
|
||||
it("should return translation with placeholder for missing variables", () => {
|
||||
const { Translator } = createTranslationTestEnvironment();
|
||||
it("should return translation with placeholder for missing variables", async () => {
|
||||
const Translator = await getFreshTranslator();
|
||||
setTranslations(Translator);
|
||||
const translation = Translator.translate({ name: "MMM-Module" }, "Hello {username}");
|
||||
expect(translation).toBe("Hallo {username}");
|
||||
});
|
||||
|
||||
it("should return key if no translation was found", () => {
|
||||
const { Translator } = createTranslationTestEnvironment();
|
||||
it("should return key if no translation was found", async () => {
|
||||
const Translator = await getFreshTranslator();
|
||||
setTranslations(Translator);
|
||||
const translation = Translator.translate({ name: "MMM-Module" }, "MISSING");
|
||||
expect(translation).toBe("MISSING");
|
||||
@@ -151,7 +158,7 @@ describe("Translator", () => {
|
||||
};
|
||||
|
||||
it("should load translations", async () => {
|
||||
const { Translator } = createTranslationTestEnvironment();
|
||||
const Translator = await getFreshTranslator();
|
||||
const file = "translation_test.json";
|
||||
|
||||
await Translator.load(mmm, file, false);
|
||||
@@ -160,7 +167,7 @@ describe("Translator", () => {
|
||||
});
|
||||
|
||||
it("should load translation fallbacks", async () => {
|
||||
const { Translator } = createTranslationTestEnvironment();
|
||||
const Translator = await getFreshTranslator();
|
||||
const file = "translation_test.json";
|
||||
|
||||
await Translator.load(mmm, file, true);
|
||||
@@ -169,7 +176,7 @@ describe("Translator", () => {
|
||||
});
|
||||
|
||||
it("should not load translations, if module fallback exists", async () => {
|
||||
const { Translator } = createTranslationTestEnvironment();
|
||||
const Translator = await getFreshTranslator();
|
||||
const file = "translation_test.json";
|
||||
|
||||
Translator.translationsFallback[mmm.name] = {
|
||||
@@ -186,8 +193,8 @@ describe("Translator", () => {
|
||||
|
||||
describe("loadCoreTranslations", () => {
|
||||
it("should load core translations and fallback", async () => {
|
||||
const { window, Translator } = createTranslationTestEnvironment();
|
||||
window.translations = { en: "http://localhost:3001/translations/translation_test.json" };
|
||||
const Translator = await getFreshTranslator();
|
||||
global.translations = { en: "http://localhost:3001/translations/translation_test.json" };
|
||||
await Translator.loadCoreTranslations("en");
|
||||
|
||||
const en = translationTestData;
|
||||
@@ -197,8 +204,8 @@ describe("Translator", () => {
|
||||
});
|
||||
|
||||
it("should load core fallback if language cannot be found", async () => {
|
||||
const { window, Translator } = createTranslationTestEnvironment();
|
||||
window.translations = { en: "http://localhost:3001/translations/translation_test.json" };
|
||||
const Translator = await getFreshTranslator();
|
||||
global.translations = { en: "http://localhost:3001/translations/translation_test.json" };
|
||||
await Translator.loadCoreTranslations("MISSINGLANG");
|
||||
|
||||
const en = translationTestData;
|
||||
@@ -210,8 +217,8 @@ describe("Translator", () => {
|
||||
|
||||
describe("loadCoreTranslationsFallback", () => {
|
||||
it("should load core translations fallback", async () => {
|
||||
const { window, Translator } = createTranslationTestEnvironment();
|
||||
window.translations = { en: "http://localhost:3001/translations/translation_test.json" };
|
||||
const Translator = await getFreshTranslator();
|
||||
global.translations = { en: "http://localhost:3001/translations/translation_test.json" };
|
||||
await Translator.loadCoreTranslationsFallback();
|
||||
|
||||
const en = translationTestData;
|
||||
@@ -220,8 +227,8 @@ describe("Translator", () => {
|
||||
});
|
||||
|
||||
it("should load core fallback if language cannot be found", async () => {
|
||||
const { window, Translator } = createTranslationTestEnvironment();
|
||||
window.translations = {};
|
||||
const Translator = await getFreshTranslator();
|
||||
global.translations = {};
|
||||
await Translator.loadCoreTranslations();
|
||||
|
||||
expect(Translator.coreTranslationsFallback).toEqual({});
|
||||
|
||||
@@ -7,7 +7,6 @@ describe("Test function cmpVersions in js/module.js", () => {
|
||||
let originalLog;
|
||||
let originalConfig;
|
||||
let originalMM;
|
||||
let originalTranslator;
|
||||
let originalNunjucks;
|
||||
|
||||
beforeAll(async () => {
|
||||
@@ -15,7 +14,6 @@ describe("Test function cmpVersions in js/module.js", () => {
|
||||
originalLog = global.Log;
|
||||
originalConfig = global.config;
|
||||
originalMM = global.MM;
|
||||
originalTranslator = global.Translator;
|
||||
originalNunjucks = global.nunjucks;
|
||||
|
||||
global.window = { mmVersion: "2.0.0" };
|
||||
@@ -27,10 +25,6 @@ describe("Test function cmpVersions in js/module.js", () => {
|
||||
sendNotification: () => {},
|
||||
updateDom: () => {}
|
||||
};
|
||||
global.Translator = {
|
||||
load: () => Promise.resolve(),
|
||||
translate: () => ""
|
||||
};
|
||||
global.nunjucks = {
|
||||
Environment () {
|
||||
this.addFilter = () => {};
|
||||
@@ -53,7 +47,6 @@ describe("Test function cmpVersions in js/module.js", () => {
|
||||
global.Log = originalLog;
|
||||
global.config = originalConfig;
|
||||
global.MM = originalMM;
|
||||
global.Translator = originalTranslator;
|
||||
global.nunjucks = originalNunjucks;
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
const path = require("node:path");
|
||||
const { pathToFileURL } = require("node:url");
|
||||
const { JSDOM } = require("jsdom");
|
||||
|
||||
const TRANSLATOR_MODULE_URL = pathToFileURL(path.join(__dirname, "..", "..", "js", "translator.js")).href;
|
||||
|
||||
/**
|
||||
* Reset mutable Translator state between tests.
|
||||
* Clears all non-function plain-object properties to remain resilient
|
||||
* when additional translation stores are introduced.
|
||||
* @param {object} Translator The shared Translator module instance.
|
||||
*/
|
||||
function resetTranslatorState (Translator) {
|
||||
for (const [key, value] of Object.entries(Translator)) {
|
||||
if (typeof value === "function") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Object.prototype.toString.call(value) === "[object Object]") {
|
||||
Translator[key] = {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up DOM globals used by translation tests.
|
||||
* @param {number} [port] Base URI port used to resolve relative translation paths.
|
||||
* @returns {void}
|
||||
*/
|
||||
function setupTranslationTestEnvironment (port = 3000) {
|
||||
const dom = new JSDOM("", { url: `http://localhost:${port}` });
|
||||
|
||||
global.document = dom.window.document;
|
||||
if (!global.Log) {
|
||||
global.Log = { log: vi.fn(), error: vi.fn() };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
setupTranslationTestEnvironment,
|
||||
TRANSLATOR_MODULE_URL,
|
||||
resetTranslatorState
|
||||
};
|
||||
Reference in New Issue
Block a user