diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 8010240..e3261b4 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -24,3 +24,4 @@ jobs: cache: 'npm' - run: npm ci - run: npm run lint + - run: npm run test diff --git a/backend/customWords.txt b/customWords.txt similarity index 100% rename from backend/customWords.txt rename to customWords.txt diff --git a/eslint.config.js b/eslint.config.js index 9addf75..400a703 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -20,7 +20,7 @@ export default defineConfig([ import: cspellImport, caseSensitive: false, }, - customWordListFile: "./backend/customWords.txt", // add custom words, that spellchecker doesn't know + customWordListFile: "./customWords.txt", // add custom words, that spellchecker doesn't know }], }, }, diff --git a/frontend/localization.js b/frontend/localization.js index 04c5212..ffff149 100644 --- a/frontend/localization.js +++ b/frontend/localization.js @@ -17,65 +17,16 @@ */ import * as Blockly from 'blockly/core' -import * as En from 'blockly/msg/en' -import * as De from 'blockly/msg/de' -import { RailBlocksEnTable } from '../backend/tokens_en.js' -import { RailBlocksDeTable } from '../backend/tokens_de.js' +import { en_locale } from '../locales/en/en_locale' +import { de_locale } from '../locales/de/de_locale' + const LANGUAGE_STORAGE_KEY = 'railblocks.language' // This file contains constants related to localization, such as retrieving the correct labels for the current language and applying the selected language to the Blockly editor and the page. const LANGUAGE_CONFIGS = { - de: { - label: 'Deutsch', - locale: De, - tokens: RailBlocksDeTable, - htmlLabels: { - title: 'RailBlocks', - simulationTitle: 'Simulieren', - deployTitle: 'Auf der Anlage ausführen', - optionsTitle: 'Entwicklerinformationen anzeigen', - languageButton: 'Sprache', - languageMenuLabel: 'Editorsprache', - generatedCodeTitle: 'Generierter RailSL', - logsTitle: 'Protokolle', - deployConfirm: 'Wirklich auf der Anlage ausführen?', - saveTitle: 'Aktuellen Arbeitsbereich auf der Festplatte speichern', - loadTitle: 'Arbeitsbereich von der Festplatte laden', - attributionsTitle: 'Danksagungen', - attributionsText: 'Bilder erstellt und verteilt von user jucy_fish (Zugsymbol), Freepik (Sanduhr-GIF), Flaticon und svgrepo.' - }, - toolboxLabels: { - setStatements: 'Setzanweisungen', - waitStatements: 'Warteanweisungen', - controlFlow: 'Kontrollfluss' - } - }, - en: { - label: 'English', - locale: En, - tokens: RailBlocksEnTable, - htmlLabels: { - title: 'RailBlocks', - simulationTitle: 'Simulate', - deployTitle: 'Deploy on railway', - optionsTitle: 'View developer information', - languageButton: 'Language', - languageMenuLabel: 'Editor language', - generatedCodeTitle: 'Generated RailSL', - logsTitle: 'Logs', - deployConfirm: 'Really deploy on the railway?', - saveTitle: 'Save current workspace to disk', - loadTitle: 'Load workspace from disk', - attributionsTitle: 'Attributions', - attributionsText: 'Images created and distributed by user jucy_fish (train icon), Freepik (hourglass gif), Flaticon and svgrepo.' - }, - toolboxLabels: { - setStatements: 'Set Statements', - waitStatements: 'Wait Statements', - controlFlow: 'Control Flow' - } - } + en: en_locale, + de: de_locale } /** diff --git a/locales/de/de_locale.js b/locales/de/de_locale.js new file mode 100644 index 0000000..6927ae0 --- /dev/null +++ b/locales/de/de_locale.js @@ -0,0 +1,45 @@ +/* + * RailBlocks - A Blockly RailSL Implementation + * + * https://github.com/kieler/RailBlocks + * + * Copyright 2026 by + * + Kiel University and others + * + Department of Computer Science + * + Real-Time and Embedded Systems Group + * + * This program and the accompanying materials are made + * available under the terms of the MIT License which + * is available at https://opensource.org/license/MIT. + * + * SPDX-License-Identifier: MIT + */ + +import * as De from 'blockly/msg/en' +import { de_tokens } from './de_tokens' + +export const de_locale = { + label: 'Deutsch', + locale: De, + tokens: de_tokens, + htmlLabels: { + title: 'RailBlocks', + simulationTitle: 'Simulieren', + deployTitle: 'Auf der Anlage ausführen', + optionsTitle: 'Entwicklerinformationen anzeigen', + languageButton: 'Sprache', + languageMenuLabel: 'Editorsprache', + generatedCodeTitle: 'Generierter RailSL', + logsTitle: 'Protokolle', + deployConfirm: 'Wirklich auf der Anlage ausführen?', + saveTitle: 'Aktuellen Arbeitsbereich auf der Festplatte speichern', + loadTitle: 'Arbeitsbereich von der Festplatte laden', + attributionsTitle: 'Danksagungen', + attributionsText: 'Bilder erstellt und verteilt von user jucy_fish (Zugsymbol), Freepik (Sanduhr-GIF), Flaticon und svgrepo.' + }, + toolboxLabels: { + setStatements: 'Setzanweisungen', + waitStatements: 'Warteanweisungen', + controlFlow: 'Kontrollfluss' + } +}; diff --git a/backend/tokens_de.js b/locales/de/de_tokens.js similarity index 99% rename from backend/tokens_de.js rename to locales/de/de_tokens.js index 4304a17..0f34655 100644 --- a/backend/tokens_de.js +++ b/locales/de/de_tokens.js @@ -17,7 +17,7 @@ */ // This file contains the German translations for the block labels, tooltips and warnings. -export const RailBlocksDeTable = { +export const de_tokens = { RAILBLOCKS_PROGRAM_TEXT: 'Programm', RAILBLOCKS_PROGRAM_TOOLTIP: 'Das Programm, das ausgeführt werden soll.\n Alle weiteren Befehle sollten innerhalb dieses Blocks stehen.', diff --git a/locales/en/en_locale.js b/locales/en/en_locale.js new file mode 100644 index 0000000..4347367 --- /dev/null +++ b/locales/en/en_locale.js @@ -0,0 +1,45 @@ +/* + * RailBlocks - A Blockly RailSL Implementation + * + * https://github.com/kieler/RailBlocks + * + * Copyright 2026 by + * + Kiel University and others + * + Department of Computer Science + * + Real-Time and Embedded Systems Group + * + * This program and the accompanying materials are made + * available under the terms of the MIT License which + * is available at https://opensource.org/license/MIT. + * + * SPDX-License-Identifier: MIT + */ + +import * as En from 'blockly/msg/en' +import { en_tokens } from './en_tokens' + +export const en_locale = { + label: 'English', + locale: En, + tokens: en_tokens, + htmlLabels: { + title: 'RailBlocks', + simulationTitle: 'Simulate', + deployTitle: 'Deploy on railway', + optionsTitle: 'View developer information', + languageButton: 'Language', + languageMenuLabel: 'Editor language', + generatedCodeTitle: 'Generated RailSL', + logsTitle: 'Logs', + deployConfirm: 'Really deploy on the railway?', + saveTitle: 'Save current workspace to disk', + loadTitle: 'Load workspace from disk', + attributionsTitle: 'Attributions', + attributionsText: 'Images created and distributed by user jucy_fish (train icon), Freepik (hourglass gif), Flaticon and svgrepo.' + }, + toolboxLabels: { + setStatements: 'Set Statements', + waitStatements: 'Wait Statements', + controlFlow: 'Control Flow' + } +}; diff --git a/backend/tokens_en.js b/locales/en/en_tokens.js similarity index 99% rename from backend/tokens_en.js rename to locales/en/en_tokens.js index 74125e7..eb49cdd 100644 --- a/backend/tokens_en.js +++ b/locales/en/en_tokens.js @@ -17,7 +17,7 @@ */ // This file contains the English translations for the block labels, tooltips and warnings. -export const RailBlocksEnTable = { +export const en_tokens = { RAILBLOCKS_PROGRAM_TEXT: 'Program', RAILBLOCKS_PROGRAM_TOOLTIP: 'The program that should be executed.\nAll further commands should be inside this block.', diff --git a/package-lock.json b/package-lock.json index 24a7a09..779c0d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,10 +21,8 @@ "@eslint/js": "^10.0.1", "eslint": "^10.5.0", "globals": "^17.6.0", - "typescript-eslint": "^8.34.1" - }, - "engines": { - "node": "^24.0.0" + "typescript-eslint": "^8.34.1", + "vitest": "^4.1.9" } }, "node_modules/@asamuzakjp/css-color": { @@ -1389,6 +1387,12 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true + }, "node_modules/@pkgr/core": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.3.6.tgz", @@ -1487,9 +1491,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1503,9 +1504,6 @@ "cpu": [ "arm" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1519,9 +1517,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1535,9 +1530,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1551,9 +1543,6 @@ "cpu": [ "loong64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1567,9 +1556,6 @@ "cpu": [ "loong64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1583,9 +1569,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1599,9 +1582,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1615,9 +1595,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1631,9 +1608,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1647,9 +1621,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1663,9 +1634,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1679,9 +1647,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1766,6 +1731,28 @@ "win32" ] }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true + }, "node_modules/@types/esrecurse": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", @@ -2017,6 +2004,112 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@vitest/expect": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz", + "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==", + "dev": true, + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz", + "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==", + "dev": true, + "dependencies": { + "@vitest/spy": "4.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz", + "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==", + "dev": true, + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz", + "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==", + "dev": true, + "dependencies": { + "@vitest/utils": "4.1.9", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz", + "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==", + "dev": true, + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "@vitest/utils": "4.1.9", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz", + "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==", + "dev": true, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz", + "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==", + "dev": true, + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -2110,6 +2203,15 @@ "dev": true, "license": "MIT" }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -2207,6 +2309,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -2327,6 +2438,12 @@ "node": ">= 0.6" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, "node_modules/cookie": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", @@ -2649,6 +2766,12 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -2889,6 +3012,15 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -2908,6 +3040,15 @@ "node": ">= 0.6" } }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/express": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", @@ -3568,6 +3709,15 @@ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "license": "ISC" }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -3706,6 +3856,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obug": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", + "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -3828,6 +3991,12 @@ "url": "https://opencollective.com/express" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -4234,6 +4403,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true + }, "node_modules/smol-toml": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.1.tgz", @@ -4256,6 +4431,12 @@ "node": ">=0.10.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -4265,6 +4446,12 @@ "node": ">= 0.8" } }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -4328,6 +4515,21 @@ "url": "https://opencollective.com/synckit" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.16", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", @@ -4344,6 +4546,15 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tldts": { "version": "6.1.86", "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", @@ -4608,6 +4819,95 @@ } } }, + "node_modules/vitest": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz", + "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==", + "dev": true, + "dependencies": { + "@vitest/expect": "4.1.9", + "@vitest/mocker": "4.1.9", + "@vitest/pretty-format": "4.1.9", + "@vitest/runner": "4.1.9", + "@vitest/snapshot": "4.1.9", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.9", + "@vitest/browser-preview": "4.1.9", + "@vitest/browser-webdriverio": "4.1.9", + "@vitest/coverage-istanbul": "4.1.9", + "@vitest/coverage-v8": "4.1.9", + "@vitest/ui": "4.1.9", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, "node_modules/vscode-languageserver-textdocument": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", @@ -4706,6 +5006,22 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", diff --git a/package.json b/package.json index 75f7cc2..99f78c4 100644 --- a/package.json +++ b/package.json @@ -3,14 +3,18 @@ "version": "0.1.1", "description": "A Blockly RailSL implementation", "author": "Henri Heyden", - "contributors": ["Tokessa Hamann"], + "contributors": [ + "Tokessa Hamann" + ], "license": "MIT", "main": "main.js", "scripts": { "start": "concurrently \"npm:frontend\" \"npm:backend\"", "frontend": "vite ./frontend/", "backend": "cd backend && node backend.js", - "lint": "eslint ./" + "lint": "eslint ./", + "test": "vitest run", + "test:watch": "vitest" }, "type": "module", "dependencies": { @@ -26,6 +30,7 @@ "@eslint/js": "^10.0.1", "eslint": "^10.5.0", "globals": "^17.6.0", - "typescript-eslint": "^8.34.1" + "typescript-eslint": "^8.34.1", + "vitest": "^4.1.9" } } diff --git a/test/validate_translation_consistency.test.js b/test/validate_translation_consistency.test.js new file mode 100644 index 0000000..37d6504 --- /dev/null +++ b/test/validate_translation_consistency.test.js @@ -0,0 +1,83 @@ +import { describe, test, expect } from 'vitest'; + +import { en_locale } from '../locales/en/en_locale.js'; +import { de_locale } from '../locales/de/de_locale.js'; + +const translations = { en_locale, de_locale }; + +function compare(reference, translation, path = '') { + const missing = []; + const extra = []; + + for (const key of Object.keys(reference)) { + const current = path ? `${path}.${key}` : key; + + if (!(key in translation)) { + missing.push(current); + continue; + } + + if ( + reference[key] && + typeof reference[key] === 'object' && + !Array.isArray(reference[key]) + ) { + const result = compare(reference[key], translation[key], current); + missing.push(...result.missing); + extra.push(...result.extra); + } + } + + for (const key of Object.keys(translation)) { + if (!(key in reference)) { + extra.push(path ? `${path}.${key}` : key); + } + } + + return { missing, extra }; +} + +function findNonStrings(obj, path = '') { + const errors = []; + + for (const [key, value] of Object.entries(obj)) { + const current = path ? `${path}.${key}` : key; + + if ( + value && + typeof value === 'object' && + !Array.isArray(value) + ) { + errors.push(...findNonStrings(value, current)); + } else if (typeof value !== 'string') { + errors.push(`${current}: ${typeof value}`); + } + } + + return errors; +} + +describe('Localization', () => { + test('all localization files contain the same keys', () => { + for (const [name, translation] of Object.entries(translations)) { + const { missing, extra } = compare(en_locale, translation); + + expect( + { missing, extra }, + `${name} differs from English` + ).toEqual({ + missing: [], + extra: [], + }); + } + }); + + test('all translation values are strings', () => { + for (const [name, translation] of Object.entries(translations)) { + expect( + findNonStrings(translation), + `${name} contains non-string values` + ).toEqual([]); + } + }); +});