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",

Reply via email to