This is an automated email from the ASF dual-hosted git repository.
tenthe pushed a commit to branch 4468-validate-translation-completeness-in-prs
in repository https://gitbox.apache.org/repos/asf/streampipes.git
The following commit(s) were added to
refs/heads/4468-validate-translation-completeness-in-prs by this push:
new 268e1d8886 ci(#4468): validate i18n catalogs in PRs
268e1d8886 is described below
commit 268e1d8886361ddab90b6b6f88e61e36a7ff3a21
Author: Philipp Zehnder <[email protected]>
AuthorDate: Tue May 12 21:13:56 2026 +0200
ci(#4468): validate i18n catalogs in PRs
---
.github/workflows/pr-validation.yml | 1 +
ui/deployment/i18n-validate.js | 112 ++++++++++++++++++++++++++++++++++++
ui/package.json | 6 +-
3 files changed, 117 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/pr-validation.yml
b/.github/workflows/pr-validation.yml
index 5f869ab425..9cec760642 100644
--- a/.github/workflows/pr-validation.yml
+++ b/.github/workflows/pr-validation.yml
@@ -65,6 +65,7 @@ jobs:
- name: Format and Lint all files
working-directory: ./ui
run: |
+ npm run i18n:check
npm run format
npm run lint
env:
diff --git a/ui/deployment/i18n-validate.js b/ui/deployment/i18n-validate.js
new file mode 100644
index 0000000000..3c9cbca156
--- /dev/null
+++ b/ui/deployment/i18n-validate.js
@@ -0,0 +1,112 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+const fs = require('fs');
+const path = require('path');
+
+/*
+ * Validates the tracked ngx-translate source catalogs before the UI is built.
+ *
+ * The runtime files in src/assets/i18n are generated from deployment/i18n by
+ * prebuild.js, so this script intentionally validates deployment/i18n only.
+ */
+const I18N_DIRECTORIES = ['deployment/i18n'];
+const SOURCE_LANGUAGE = 'en';
+const TARGET_LANGUAGES = ['de', 'pl'];
+const PLACEHOLDER_PATTERN = /\{\{\s*[\w.]+\s*\}\}/g;
+
+let hasError = false;
+
+function readTranslations(directory, language) {
+ const filePath = path.join(directory, `${language}.json`);
+ try {
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
+ } catch (error) {
+ reportError(`${filePath}: Could not read translation file.`);
+ return {};
+ }
+}
+
+function reportError(message) {
+ hasError = true;
+ console.error(`ERROR: ${message}`);
+}
+
+function getPlaceholders(value) {
+ // Treat {{name}} and {{ name }} as the same placeholder.
+ return new Set(
+ (String(value).match(PLACEHOLDER_PATTERN) || []).map(placeholder =>
+ placeholder.replace(/\s+/g, ''),
+ ),
+ );
+}
+
+function validateLanguage(directory, sourceTranslations, language) {
+ const translations = readTranslations(directory, language);
+ const sourceKeys = Object.keys(sourceTranslations);
+ const translationKeys = Object.keys(translations);
+ const sourceKeySet = new Set(sourceKeys);
+
+ for (const key of sourceKeys) {
+ // Every target catalog must contain all source keys.
+ if (!(key in translations)) {
+ reportError(`${directory}/${language}.json: Missing key
"${key}".`);
+ continue;
+ }
+
+ // Empty values render as missing translations in the UI.
+ const value = translations[key];
+ if (value === null || value === '') {
+ reportError(
+ `${directory}/${language}.json: Missing translation for
"${key}".`,
+ );
+ continue;
+ }
+
+ // Interpolation placeholders must survive translation unchanged.
+ const sourcePlaceholders = getPlaceholders(key);
+ const translationPlaceholders = getPlaceholders(value);
+ for (const placeholder of sourcePlaceholders) {
+ if (!translationPlaceholders.has(placeholder)) {
+ reportError(
+ `${directory}/${language}.json: Translation for "${key}"
is missing placeholder ${placeholder}.`,
+ );
+ }
+ }
+ }
+
+ // Extra keys indicate stale translations that extraction no longer finds.
+ for (const key of translationKeys) {
+ if (!sourceKeySet.has(key)) {
+ reportError(`${directory}/${language}.json: Extra key "${key}".`);
+ }
+ }
+}
+
+for (const directory of I18N_DIRECTORIES) {
+ const sourceTranslations = readTranslations(directory, SOURCE_LANGUAGE);
+ for (const language of TARGET_LANGUAGES) {
+ validateLanguage(directory, sourceTranslations, language);
+ }
+}
+
+if (hasError) {
+ process.exit(1);
+}
+
+console.log('All i18n translations are complete.');
diff --git a/ui/package.json b/ui/package.json
index 247317f172..461c1bdf3e 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -14,7 +14,7 @@
"start": "npm run build-libraries && node ./deployment/prebuild.js && ng
serve -o",
"test": "node ./deployment/prebuild.js && npm run build-libraries && ng
test",
"run-prebuild-script": "node ./deployment/prebuild.js",
- "build": "node ./deployment/prebuild.js && set
NODE_OPTIONS=--max-old-space-size=8192 && npm run build-libraries && ng build
--configuration production",
+ "build": "npm run i18n:validate && node ./deployment/prebuild.js && set
NODE_OPTIONS=--max-old-space-size=8192 && npm run build-libraries && ng build
--configuration production",
"build-dev": "node ./deployment/prebuild.js && set
NODE_OPTIONS=--max-old-space-size=8192 && npm run build-libraries && ng build",
"lint": "ng lint app && ng lint app-e2e && ng lint
@streampipes/platform-services && ng lint @streampipes/shared-ui",
"lint:fix": "ng lint app --fix && ng lint app-e2e --fix && ng lint
@streampipes/platform-services --fix && ng lint @streampipes/shared-ui --fix",
@@ -26,7 +26,9 @@
"test-cypress-all": "npx cypress run --spec 'cypress/tests/**/*.spec.ts'
--config baseUrl=http://localhost:8082",
"prepare": "cd ../ && husky install ./ui/.husky",
"i18n:extract": "ngx-translate-extract --input ./src --input ./projects
--output ./deployment/i18n/{en,de,pl}.json -n -s --clean --format json",
- "i18n:translate": "node ./deployment/i18n-translate.js"
+ "i18n:translate": "node ./deployment/i18n-translate.js",
+ "i18n:validate": "node ./deployment/i18n-validate.js",
+ "i18n:check": "npm run i18n:extract && prettier --write
./deployment/i18n/{en,de,pl}.json && npm run i18n:validate"
},
"dependencies": {
"@angular/animations": "^21.2.12",