mirror of
https://github.com/MichMich/MagicMirror.git
synced 2025-07-02 22:08:42 +00:00
## [2.32.0] - 2025-07-01 Thanks to: @bughaver, @bugsounet, @khassel, @KristjanESPERANTO, @plebcity, @rejas, @sdetweil. > ⚠️ This release needs nodejs version `v22.14.0 or higher` ### Added - [config] Allow to change module order for final renderer (or dynamically with CSS): Feature `order` in config (#3762) - [clock] Added option 'disableNextEvent' to hide next sun event (#3769) - [clock] Implement short syntax for clock week (#3775) ### Changed - [refactor] Simplify module loading process (#3766) - Use `node --run` instead of `npm run` (#3764) and adapt `start:dev` script (#3773) - [workflow] Run linter and spellcheck with LTS node version (#3767) - [workflow] Split "Run test" step into two steps for more clarity (#3767) - [linter] Review linter setup (#3783) - Fix command to lint markdown in `CONTRIBUTING.md` - Re-activate JSDoc linting and fix linting issues - Refactor ESLint config to use `defineConfig` and `globalIgnores` - Replace `eslint-plugin-import` with `eslint-plugin-import-x` - Switch Stylelint config to flat format and simplify Stylelint scripts - [workflow] Replace Node.js version v23 with v24 (#3770) - [refactor] Replace deprecated constants `fs.F_OK` and `fs.R_OK` (#3789) - [refactor] Replace `ansis` with built-in function `util.styleText` (#3793) - [core] Integrate stuff from `vendor` and `fonts` folders into main `package.json`, simplifies install and maintaining dependencies (#3795, #3805) - [l10n] Complete translations (with the help of translation tools) (#3794) - [refactor] Refactored `calendarfetcherutils` in Calendar module to handle timezones better (#3806) - Removed as many of the date conversions as possible - Use `moment-timezone` when calculating recurring events, this will fix problems from the past with offsets and DST not being handled properly - Added some tests to test the behavior of the refactored methods to make sure the correct event dates are returned - [linter] Enable ESLint rule `no-console` and replace `console` with `Log` in some files (#3810) - [tests] Review and refactor translation tests (#3792) ### Fixed - [fix] Handle spellcheck issues (#3783) - [calendar] fix fullday event rrule until with timezone offset (#3781) - [feat] Add rule `no-undef` in config file validation to fix #3785 (#3786) - [fonts] Fix `roboto.css` to avoid error message `Unknown descriptor 'var(' in @font-face rule.` in firefox console (#3787) - [tests] Fix and refactor e2e test `Same keys` in `translations_spec.js` (#3809) - [tests] Fix e2e tests newsfeed and calendar to exit without open handles (#3817) ### Updated - [core] Update dependencies including electron to v36 (#3774, #3788, #3811, #3804, #3815, #3823) - [core] Update package type to `commonjs` - [logger] Review factory code part: use `switch/case` instead of `if/else if` (#3812) --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Michael Teeuw <michael@xonaymedia.nl> Co-authored-by: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Ross Younger <crazyscot@gmail.com> Co-authored-by: Veeck <github@veeck.de> Co-authored-by: Bugsounet - Cédric <github@bugsounet.fr> Co-authored-by: jkriegshauser <joshuakr@nvidia.com> Co-authored-by: illimarkangur <116028111+illimarkangur@users.noreply.github.com> Co-authored-by: sam detweiler <sdetweil@gmail.com> Co-authored-by: vppencilsharpener <tim.pray@gmail.com> Co-authored-by: veeck <michael.veeck@nebenan.de> Co-authored-by: Paranoid93 <6515818+Paranoid93@users.noreply.github.com> Co-authored-by: Brian O'Connor <btoconnor@users.noreply.github.com> Co-authored-by: WallysWellies <59727507+WallysWellies@users.noreply.github.com> Co-authored-by: Jason Stieber <jrstieber@gmail.com> Co-authored-by: jargordon <50050429+jargordon@users.noreply.github.com> Co-authored-by: Daniel <32464403+dkallen78@users.noreply.github.com> Co-authored-by: Ryan Williams <65094007+ryan-d-williams@users.noreply.github.com> Co-authored-by: Panagiotis Skias <panagiotis.skias@gmail.com> Co-authored-by: Marc Landis <dirk.rettschlag@gmail.com> Co-authored-by: HeikoGr <20295490+HeikoGr@users.noreply.github.com> Co-authored-by: Pedro Lamas <pedrolamas@gmail.com> Co-authored-by: veeck <gitkraken@veeck.de> Co-authored-by: Magnus <34011212+MagMar94@users.noreply.github.com> Co-authored-by: Ikko Eltociear Ashimine <eltociear@gmail.com> Co-authored-by: DevIncomin <56730075+Developer-Incoming@users.noreply.github.com> Co-authored-by: Nathan <n8nyoung@gmail.com> Co-authored-by: mixasgr <mixasgr@users.noreply.github.com> Co-authored-by: Savvas Adamtziloglou <savvas-gr@greeklug.gr> Co-authored-by: Konstantinos <geraki@gmail.com> Co-authored-by: OWL4C <124401812+OWL4C@users.noreply.github.com> Co-authored-by: BugHaver <43462320+bughaver@users.noreply.github.com> Co-authored-by: BugHaver <43462320+lsaadeh@users.noreply.github.com> Co-authored-by: Koen Konst <koenspero@gmail.com> Co-authored-by: Koen Konst <c.h.konst@avisi.nl>
237 lines
7.3 KiB
JavaScript
237 lines
7.3 KiB
JavaScript
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
const helmet = require("helmet");
|
|
const { JSDOM } = require("jsdom");
|
|
const express = require("express");
|
|
const sinon = require("sinon");
|
|
const translations = require("../../translations/translations");
|
|
|
|
describe("translations", () => {
|
|
let server;
|
|
|
|
beforeAll(() => {
|
|
const app = express();
|
|
app.use(helmet());
|
|
app.use((req, res, next) => {
|
|
res.header("Access-Control-Allow-Origin", "*");
|
|
next();
|
|
});
|
|
app.use("/translations", express.static(path.join(__dirname, "..", "..", "translations")));
|
|
|
|
server = app.listen(3000);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await server.close();
|
|
});
|
|
|
|
it("should have a translation file in the specified path", () => {
|
|
for (const language in translations) {
|
|
const file = fs.statSync(translations[language]);
|
|
|
|
expect(file.isFile()).toBe(true);
|
|
}
|
|
});
|
|
|
|
describe("loadTranslations", () => {
|
|
let dom;
|
|
|
|
beforeEach(() => {
|
|
// Create a new JSDOM instance for each test
|
|
dom = new JSDOM("", { runScripts: "dangerously", resources: "usable" });
|
|
|
|
// Mock the necessary global objects
|
|
dom.window.Log = { log: jest.fn(), error: jest.fn() };
|
|
dom.window.Translator = {};
|
|
dom.window.config = { language: "de" };
|
|
|
|
// Load class.js and module.js content directly
|
|
const classJs = fs.readFileSync(path.join(__dirname, "..", "..", "js", "class.js"), "utf-8");
|
|
const moduleJs = fs.readFileSync(path.join(__dirname, "..", "..", "js", "module.js"), "utf-8");
|
|
|
|
// Execute the scripts in the JSDOM context
|
|
dom.window.eval(classJs);
|
|
dom.window.eval(moduleJs);
|
|
});
|
|
|
|
it("should load translation file", async () => {
|
|
await new Promise((resolve) => {
|
|
dom.window.onload = resolve;
|
|
});
|
|
|
|
const { Translator, Module, config } = dom.window;
|
|
config.language = "en";
|
|
Translator.load = sinon.stub().callsFake((_m, _f, _fb) => null);
|
|
|
|
Module.register("name", { getTranslations: () => translations });
|
|
const MMM = Module.create("name");
|
|
|
|
await MMM.loadTranslations();
|
|
|
|
expect(Translator.load.args).toHaveLength(1);
|
|
expect(Translator.load.calledWith(MMM, "translations/en.json", false)).toBe(true);
|
|
});
|
|
|
|
it("should load translation + fallback file", async () => {
|
|
await new Promise((resolve) => {
|
|
dom.window.onload = resolve;
|
|
});
|
|
|
|
const { Translator, Module } = dom.window;
|
|
Translator.load = sinon.stub().callsFake((_m, _f, _fb) => null);
|
|
|
|
Module.register("name", { getTranslations: () => translations });
|
|
const MMM = Module.create("name");
|
|
|
|
await MMM.loadTranslations();
|
|
|
|
expect(Translator.load.args).toHaveLength(2);
|
|
expect(Translator.load.calledWith(MMM, "translations/de.json", false)).toBe(true);
|
|
expect(Translator.load.calledWith(MMM, "translations/en.json", true)).toBe(true);
|
|
});
|
|
|
|
it("should load translation fallback file", async () => {
|
|
await new Promise((resolve) => {
|
|
dom.window.onload = resolve;
|
|
});
|
|
|
|
const { Translator, Module, config } = dom.window;
|
|
config.language = "--";
|
|
Translator.load = sinon.stub().callsFake((_m, _f, _fb) => null);
|
|
|
|
Module.register("name", { getTranslations: () => translations });
|
|
const MMM = Module.create("name");
|
|
|
|
await MMM.loadTranslations();
|
|
|
|
expect(Translator.load.args).toHaveLength(1);
|
|
expect(Translator.load.calledWith(MMM, "translations/en.json", true)).toBe(true);
|
|
});
|
|
|
|
it("should load no file", async () => {
|
|
await new Promise((resolve) => {
|
|
dom.window.onload = resolve;
|
|
});
|
|
|
|
const { Translator, Module } = dom.window;
|
|
Translator.load = sinon.stub();
|
|
|
|
Module.register("name", {});
|
|
const MMM = Module.create("name");
|
|
|
|
await MMM.loadTranslations();
|
|
|
|
expect(Translator.load.callCount).toBe(0);
|
|
});
|
|
});
|
|
|
|
const mmm = {
|
|
name: "TranslationTest",
|
|
file (file) {
|
|
return `http://localhost:3000/${file}`;
|
|
}
|
|
};
|
|
|
|
const translatorJs = fs.readFileSync(path.join(__dirname, "..", "..", "js", "translator.js"), "utf-8");
|
|
|
|
describe("parsing language files through the Translator class", () => {
|
|
for (const language in translations) {
|
|
it(`should parse ${language}`, async () => {
|
|
const dom = new JSDOM("", { runScripts: "dangerously", resources: "usable" });
|
|
dom.window.Log = { log: jest.fn() };
|
|
dom.window.translations = translations;
|
|
dom.window.eval(translatorJs);
|
|
|
|
await new Promise((resolve) => {
|
|
dom.window.onload = resolve;
|
|
});
|
|
|
|
const { Translator } = dom.window;
|
|
await Translator.load(mmm, translations[language], false);
|
|
|
|
expect(typeof Translator.translations[mmm.name]).toBe("object");
|
|
expect(Object.keys(Translator.translations[mmm.name]).length).toBeGreaterThanOrEqual(1);
|
|
});
|
|
}
|
|
});
|
|
|
|
describe("same keys", () => {
|
|
let base;
|
|
|
|
// Some expressions are not easy to translate automatically. For the sake of a working test, we filter them out.
|
|
const COMMON_EXCEPTIONS = ["WEEK_SHORT"];
|
|
|
|
// Some languages don't have certain words, so we need to filter those language specific exceptions.
|
|
const LANGUAGE_EXCEPTIONS = {
|
|
ca: ["DAYBEFOREYESTERDAY"],
|
|
cv: ["DAYBEFOREYESTERDAY"],
|
|
cy: ["DAYBEFOREYESTERDAY"],
|
|
en: ["DAYAFTERTOMORROW", "DAYBEFOREYESTERDAY"],
|
|
fy: ["DAYBEFOREYESTERDAY"],
|
|
gl: ["DAYBEFOREYESTERDAY"],
|
|
hu: ["DAYBEFOREYESTERDAY"],
|
|
id: ["DAYBEFOREYESTERDAY"],
|
|
it: ["DAYBEFOREYESTERDAY"],
|
|
"pt-br": ["DAYAFTERTOMORROW"],
|
|
tr: ["DAYBEFOREYESTERDAY"]
|
|
};
|
|
|
|
// Function to initialize JSDOM and load translations
|
|
const initializeTranslationDOM = (language) => {
|
|
const dom = new JSDOM("", { runScripts: "dangerously", resources: "usable" });
|
|
dom.window.Log = { log: jest.fn() };
|
|
dom.window.translations = translations;
|
|
dom.window.eval(translatorJs);
|
|
|
|
return new Promise((resolve) => {
|
|
dom.window.onload = async () => {
|
|
const { Translator } = dom.window;
|
|
await Translator.load(mmm, translations[language], false);
|
|
resolve(Translator.translations[mmm.name]);
|
|
};
|
|
});
|
|
};
|
|
|
|
beforeAll(async () => {
|
|
// Using German as the base rather than English, since
|
|
// some words do not have a direct translation in English.
|
|
const germanTranslations = await initializeTranslationDOM("de");
|
|
base = Object.keys(germanTranslations).sort();
|
|
});
|
|
|
|
for (const language in translations) {
|
|
if (language === "de") continue;
|
|
|
|
describe(`Translation keys of ${language}`, () => {
|
|
let keys;
|
|
|
|
beforeAll(async () => {
|
|
const languageTranslations = await initializeTranslationDOM(language);
|
|
keys = Object.keys(languageTranslations).sort();
|
|
});
|
|
|
|
it(`${language} should not contain keys that are not in base language`, () => {
|
|
keys.forEach((key) => {
|
|
expect(base).toContain(key, `Translation key '${key}' in language '${language}' is not present in base language`);
|
|
});
|
|
});
|
|
|
|
it(`${language} should contain all base keys (excluding defined exceptions)`, () => {
|
|
let filteredBase = base.filter((key) => !COMMON_EXCEPTIONS.includes(key));
|
|
let filteredKeys = keys.filter((key) => !COMMON_EXCEPTIONS.includes(key));
|
|
|
|
if (LANGUAGE_EXCEPTIONS[language]) {
|
|
const exceptions = LANGUAGE_EXCEPTIONS[language];
|
|
filteredBase = filteredBase.filter((key) => !exceptions.includes(key));
|
|
filteredKeys = filteredKeys.filter((key) => !exceptions.includes(key));
|
|
}
|
|
|
|
filteredBase.forEach((baseKey) => {
|
|
expect(filteredKeys).toContain(baseKey, `Translation key '${baseKey}' is missing in language '${language}'`);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
});
|
|
});
|