Repository: UniversalViewer/universalviewer
Pull request #1336: Locale check script
Author: login: 404mike
Created: 2025-03-11T11:21:19Z | Merged: 2025-03-14T09:10:13Z

A script to validate locales within the UV.

Using English as the primary language the script can be run on two tasks.

### Checking for unused locales

Check if the locale is used anywhere within the UV modules?

```
node scripts/validate_locale.ts checkLocaleUsage
```

Will produce:

```
Final locale keys count: {
  '$collapse': 13,
  ....
  '$pause': 1,
  '$play': 1,
  '$brightness': 8,
  '$contrast': 8,
  '$saturation': 8,
  '$reset': 8,
  '$remember': 8
}
Unused locale keys: [
  [ '$entireFileX', 0 ],
  [ '$wholeImageHighResExplanation', 0 ],
  [ '$wholeImageLowResAsJpgExplanation', 0 ],
  [ '$log_in', 0 ],
  [ '$pleaseLogInToViewAtFullQuality', 0 ]
]
```

This works by loading the English locale file and checking each of the module configs for any usage, incrementing the usage through each iteration.

The script doesn't check any .tsx files.

### Check for missing locales

Does the English locale contain strings that are missing from the other locales?

```
node scripts/validate_locale.ts compareLocales  
```

Will produce:

```
Key "$reset" missing in locale "cy-GB"
Key "$remember" missing in locale "cy-GB"
Key "$brightness" missing in locale "fr-FR"
Key "$contrast" missing in locale "fr-FR"
Key "$saturation" missing in locale "fr-FR"
Key "$reset" missing in locale "fr-FR"
Key "$remember" missing in locale "fr-FR"
Key "$brightness" missing in locale "pl-PL"
Key "$contrast" missing in locale "pl-PL"
Key "$saturation" missing in locale "pl-PL"
Key "$reset" missing in locale "pl-PL"
Key "$remember" missing in locale "pl-PL"
```

All outputs above were ran against main branch.


Diff:
diff --git a/package.json b/package.json
index 9baf0c0df..43cffeea5 100644
--- a/package.json
+++ b/package.json
@@ -25,7 +25,10 @@
     "copy-files": "copyfiles -u 1 src/**/*.svg dist/cjs && copyfiles -u 1 src/**/*.gif dist/cjs && copyfiles -u 1 src/**/*.less dist/cjs && copyfiles -u 1 src/extensions/**/*.less dist/cjs && copyfiles -u 1 src/**/*.css dist/cjs",
     "docs": "typedoc --plugin typedoc-plugin-missing-exports ; echo docs.universalviewer.io > docs/CNAME",
     "test": "jest",
-    "prepublishOnly": "npm run build && npm run build-tsc && npm run build-es"
+    "prepublishOnly": "npm run build && npm run build-tsc && npm run build-es",
+    "checkLocaleUsage": "node scripts/validate_locale.ts checkLocaleUsage",
+    "findMissingTranslations": "node scripts/validate_locale.ts missingTranslations",
+    "findHardCodedLocaleStrings": "node scripts/validate_locale.ts hardCodedStrings"
   },
   "repository": {
     "type": "git",
diff --git a/scripts/validate_locale.ts b/scripts/validate_locale.ts
new file mode 100644
index 000000000..486762ff3
--- /dev/null
+++ b/scripts/validate_locale.ts
@@ -0,0 +1,171 @@
+const fs = require('fs');
+const path = require('path');
+
+// Read in parameters to choose which type of validation to run
+let args = process.argv.slice(2);
+let runType = args[0];
+
+// Define primary locale
+const primaryLocale = 'en-GB.json';
+
+const checkLocaleUsage = () => {
+    // get a list of all UV extensions
+    const uvExtensions = getUVExtensions();
+
+    // Read primary locale file.
+    let localeData = readLocaleFile(primaryLocale);
+    // replace all values with a 0
+    // we will increment the value for each key found in the extension config files
+    let localeKeys = Object.keys(localeData).reduce((acc, key) => {
+        acc[key] = 0;
+        return acc;
+    }, {});
+
+    // Loop through all UV extensions.
+    uvExtensions.forEach(extension => {
+        let config = readExtensionConfig(extension);
+        // Recursively extract all locale values from any "content" objects.
+        let contentLocaleValues = extractContentLocaleValues(config);
+        // For each locale value found, if it exists in the primary locale keys, increment its count.
+        contentLocaleValues.forEach(val => {
+            if (val in localeKeys) {
+                localeKeys[val]++;
+            }
+        });
+    });
+
+    console.log("Final locale keys count:", localeKeys);
+    console.log("Unused locale keys:", getUnusedLocaleKeys(localeKeys));
+};
+
+
+const checkHardCodedStrings = () => {
+    let missing = [];
+    // get a list of all UV extensions
+    const uvExtensions = getUVExtensions();
+
+    // Loop through all UV extensions.
+    uvExtensions.forEach(extension => {
+        let config = readExtensionConfig(extension);
+        // Recursively extract all locale values from any "content" objects.
+        let contentLocaleValues = extractContentLocaleValues(config);
+        // For each locale value found, if it exists in the primary locale keys, increment its count.
+        contentLocaleValues.forEach(val => {
+            // if val does not start with $
+            let hasStartingDelimiter = keyStartsWithDollar(val);
+            if (!hasStartingDelimiter) {
+                missing.push({ extension: extension, key: val });
+            }
+        });
+    });
+
+    console.log("missing locale keys:", missing);
+};
+
+const keyStartsWithDollar = (val) => {
+    if (val.substring(0, 1) !== '$') {
+        return false
+    }
+
+    return true;
+};
+
+const getUVExtensions = () => {
+    let extensions = [];
+    // get a list of all UV extensions
+    const directoryPath = path.join(__dirname, '../src/content-handlers/iiif/extensions/');
+
+    try {
+        const files = fs.readdirSync(directoryPath);
+
+        files.forEach(function (file) {
+            let dir = file.substring(0, 2);
+            // only get directories that start with 'uv'
+            if (dir === 'uv') {
+                extensions.push(file);
+            }
+        });
+    } catch (err) {
+        console.log('Unable to scan directory: ' + err);
+    }
+
+    return extensions;
+}
+
+const getUVLocales = () => {
+    const localeDir = path.join(__dirname, '../src/locales');
+    const localeFiles = fs.readdirSync(localeDir);
+    return localeFiles;
+};
+
+const getUnusedLocaleKeys = (localeKeys) => {
+    return Object.entries(localeKeys).filter(([key, value]) => value === 0);
+}
+
+// Recursively extract all values in any "content" object that are strings starting with "$"
+const extractContentLocaleValues = (obj) => {
+    let values = [];
+    for (const [prop, value] of Object.entries(obj)) {
+        if (prop === 'content' && typeof value === 'object' && value !== null) {
+            Object.values(value).forEach(item => {
+                if (typeof item === 'string') {
+                    values.push(item);
+                }
+            });
+        } else if (typeof value === 'object' && value !== null) {
+            values = values.concat(extractContentLocaleValues(value));
+        }
+    }
+    return values;
+};
+
+const readLocaleFile = (lang) => {
+    const localeDir = path.join(__dirname, '../src/locales');
+    const localeFiles = fs.readdirSync(localeDir).filter(file => file.endsWith(`${lang}`));
+    if (localeFiles.length === 0) {
+        throw new Error(`No locale file found for language ${lang}`);
+    }
+    const filePath = path.join(localeDir, localeFiles[0]);
+    const fileContent = fs.readFileSync(filePath, 'utf8');
+    return JSON.parse(fileContent);
+};
+
+const readExtensionConfig = (extension) => {
+    const configPath = path.join(__dirname, `../src/content-handlers/iiif/extensions/${extension}/config/config.json`);
+    const configContent = fs.readFileSync(configPath, 'utf8');
+    return JSON.parse(configContent);
+};
+
+const missingTranslations = () => {
+    const primaryLocaleData = readLocaleFile(primaryLocale);
+    const allLocales = getUVLocales();
+
+    for (let locale of allLocales) {
+        if (locale === primaryLocale) {
+            continue;
+        }
+        const localeData = readLocaleFile(locale);
+        // Compare primary locale keys with each other locale's keys.
+        // If a key is missing in the other locale, log an error.
+        for (let key of Object.keys(primaryLocaleData)) {
+            if (!(key in localeData)) {
+                console.error(`Key "${key}" missing in locale "${locale}"`);
+            }
+        }
+    }
+};
+
+switch (runType) {
+    case 'checkLocaleUsage':
+        checkLocaleUsage();
+        break;
+    case 'missingTranslations':
+        missingTranslations();
+        break;
+    case 'hardCodedStrings':
+        checkHardCodedStrings();
+        break;
+    default:
+        console.error('No parameter or no valid run type. Please use checkLocaleUsage, missingTranslations or hardCodedStrings');
+        break;
+}
\ No newline at end of file
diff --git a/src/locales/en-GB.json b/src/locales/en-GB.json
index 987cb42e9..74e085e0f 100644
--- a/src/locales/en-GB.json
+++ b/src/locales/en-GB.json
@@ -118,7 +118,7 @@
   "$navigatorEnabled": "Navigator Enabled",
   "$clickToZoomEnabled": "Mouse Click To Zoom",
   "$reducedMotion": "Reduce motion (disables animations)",
-  "truncateThumbnailLabels": "Truncate Thumbnail Labels",
+  "$truncateThumbnailLabels": "Truncate Thumbnail Labels",
   "$preserveViewport": "Preserve Zoom",
   "$uvWebsite": "<a href='https://github.com/UniversalViewer/universalviewer/wiki/About'>More info about the Universal Viewer</a>",
   "$custom": "custom",


Reviews:
user: login: demiankatz
body: I haven't had a chance for a full review yet, but see below for one idea that might help make this more future-proof.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T15:04:48Z
user: login: 404mike
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T15:51:55Z
user: login: demiankatz
body: Thanks, @404mike -- I took a closer look at the rest of this. See below for a couple more thoughts.

Also, should we wire this up in the "scripts" section of package.json, so it can be executed with `npm run`? That might help make it more visible as an option for people new to the project.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T16:20:04Z
user: login: LlGC-mij
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T14:46:13Z
user: login: LlGC-mij
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T14:50:20Z
user: login: demiankatz
body: Thanks, @LlGC-mij for the progress here. See below for a few finishing touches (and feel free to modify or ignore my naming suggestions if you don't like them).
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T15:50:32Z
user: login: LlGC-mij
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T16:07:41Z
user: login: LlGC-mij
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T16:07:56Z
user: login: LlGC-mij
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T16:08:01Z
user: login: demiankatz
body: This looks good to me now! Thanks, @404mike and @LlGC-mij!

Please feel free to merge if you're both happy with the state of things.

Should someone do a follow-up PR to address the hard-coded strings detected by the tool?
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T17:43:38Z

Review comments:
diff_hunk: @@ -0,0 +1,115 @@
+const fs = require('fs');
+const path = require('path');
+
+// Read in parameters to choose which type of validation to run
+let args = process.argv.slice(2);
+let runType = args[0];
+if (!['checkLocaleUsage', 'compareLocales'].includes(runType)) {
+    console.log('No parameter or no valid run type. Please use checkLocaleUsage or compareLocales');
+}
+
+// Define all locales and primary locale
+const allLocales = ['cy-GB', 'en-GB', 'fr-FR', 'pl-PL', 'sv-SE'];
+const primaryLocale = 'en-GB';
+
+// List of all UV modules
+const uvModules = [
path: scripts/validate_locale.ts
user: login: demiankatz
body: Could we replace this hard-coded list with an operation to read the subdirectories starting with uv out of the extension directory?
created_at: 2025-03-11T15:04:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,115 @@
+const fs = require('fs');
+const path = require('path');
+
+// Read in parameters to choose which type of validation to run
+let args = process.argv.slice(2);
+let runType = args[0];
+if (!['checkLocaleUsage', 'compareLocales'].includes(runType)) {
+    console.log('No parameter or no valid run type. Please use checkLocaleUsage or compareLocales');
+}
+
+// Define all locales and primary locale
+const allLocales = ['cy-GB', 'en-GB', 'fr-FR', 'pl-PL', 'sv-SE'];
+const primaryLocale = 'en-GB';
+
+// List of all UV modules
+const uvModules = [
path: scripts/validate_locale.ts
user: login: 404mike
body: @demiankatz makes sense. Code updated to get all 'UV-' extensions.
created_at: 2025-03-11T15:51:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,128 @@
+const fs = require('fs');
+const path = require('path');
+
+// Read in parameters to choose which type of validation to run
+let args = process.argv.slice(2);
+let runType = args[0];
+if (!['checkLocaleUsage', 'compareLocales'].includes(runType)) {
+    console.log('No parameter or no valid run type. Please use checkLocaleUsage or compareLocales');
+}
+
+// Define all locales and primary locale
+const allLocales = ['cy-GB', 'en-GB', 'fr-FR', 'pl-PL', 'sv-SE'];
path: scripts/validate_locale.ts
user: login: demiankatz
body: This could also be programmatically determined by reading .json files from src/locales.
created_at: 2025-03-11T16:17:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,128 @@
+const fs = require('fs');
+const path = require('path');
+
+// Read in parameters to choose which type of validation to run
+let args = process.argv.slice(2);
+let runType = args[0];
+if (!['checkLocaleUsage', 'compareLocales'].includes(runType)) {
+    console.log('No parameter or no valid run type. Please use checkLocaleUsage or compareLocales');
+}
+
+// Define all locales and primary locale
+const allLocales = ['cy-GB', 'en-GB', 'fr-FR', 'pl-PL', 'sv-SE'];
+const primaryLocale = 'en-GB';
+
+const checkLocaleUsage = () => {
+    // get a list of all UV extensions
+    const uvExtensions = getUVExtensions();
+
+    // Read primary locale file.
+    let localeData = readLocaleFile(primaryLocale);
+    // replace all values with a 0
+    // we will increment the value for each key found in the extension config files
+    let localeKeys = Object.keys(localeData).reduce((acc, key) => {
+        acc[key] = 0;
+        return acc;
+    }, {});
+
+    // Loop through all UV extensions.
+    uvExtensions.forEach(extension => {
+        let config = readExtensionConfig(extension);
+        // Recursively extract all locale values from any "content" objects.
+        let contentLocaleValues = extractContentLocaleValues(config);
+        // For each locale value found, if it exists in the primary locale keys, increment its count.
+        contentLocaleValues.forEach(val => {
+            if (val in localeKeys) {
+                localeKeys[val]++;
+            }
+        });
+    });
+
+    console.log("Final locale keys count:", localeKeys);
+    console.log("Unused locale keys:", getUnusedLocaleKeys(localeKeys));
+};
+
+const getUVExtensions = () => {
+    let extensions = [];
+    // get a list of all UV extensions
+    const directoryPath = path.join(__dirname, '../src/content-handlers/iiif/extensions/');
+
+    try {
+        const files = fs.readdirSync(directoryPath);
+
+        files.forEach(function (file) {
+            let dir = file.substring(0, 2);
+            // only get directories that start with 'uv'
+            if (dir === 'uv') {
+                extensions.push(file);
+            }
+        });
+    } catch (err) {
+        console.log('Unable to scan directory: ' + err);
+    }
+
+    return extensions;
+}
+
+const getUnusedLocaleKeys = (localeKeys) => {
+    return Object.entries(localeKeys).filter(([key, value]) => value === 0);
+}
+
+// Recursively extract all values in any "content" object that are strings starting with "$"
path: scripts/validate_locale.ts
user: login: demiankatz
body: Should we also warn about finding content keys that do NOT start with $? I think this may be a sign of configuration errors in most cases, unless I'm overlooking something.
created_at: 2025-03-11T16:18:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,128 @@
+const fs = require('fs');
+const path = require('path');
+
+// Read in parameters to choose which type of validation to run
+let args = process.argv.slice(2);
+let runType = args[0];
+if (!['checkLocaleUsage', 'compareLocales'].includes(runType)) {
+    console.log('No parameter or no valid run type. Please use checkLocaleUsage or compareLocales');
+}
+
+// Define all locales and primary locale
+const allLocales = ['cy-GB', 'en-GB', 'fr-FR', 'pl-PL', 'sv-SE'];
+const primaryLocale = 'en-GB';
+
+const checkLocaleUsage = () => {
+    // get a list of all UV extensions
+    const uvExtensions = getUVExtensions();
+
+    // Read primary locale file.
+    let localeData = readLocaleFile(primaryLocale);
+    // replace all values with a 0
+    // we will increment the value for each key found in the extension config files
+    let localeKeys = Object.keys(localeData).reduce((acc, key) => {
+        acc[key] = 0;
+        return acc;
+    }, {});
+
+    // Loop through all UV extensions.
+    uvExtensions.forEach(extension => {
+        let config = readExtensionConfig(extension);
+        // Recursively extract all locale values from any "content" objects.
+        let contentLocaleValues = extractContentLocaleValues(config);
+        // For each locale value found, if it exists in the primary locale keys, increment its count.
+        contentLocaleValues.forEach(val => {
+            if (val in localeKeys) {
+                localeKeys[val]++;
+            }
+        });
+    });
+
+    console.log("Final locale keys count:", localeKeys);
+    console.log("Unused locale keys:", getUnusedLocaleKeys(localeKeys));
+};
+
+const getUVExtensions = () => {
+    let extensions = [];
+    // get a list of all UV extensions
+    const directoryPath = path.join(__dirname, '../src/content-handlers/iiif/extensions/');
+
+    try {
+        const files = fs.readdirSync(directoryPath);
+
+        files.forEach(function (file) {
+            let dir = file.substring(0, 2);
+            // only get directories that start with 'uv'
+            if (dir === 'uv') {
+                extensions.push(file);
+            }
+        });
+    } catch (err) {
+        console.log('Unable to scan directory: ' + err);
+    }
+
+    return extensions;
+}
+
+const getUnusedLocaleKeys = (localeKeys) => {
+    return Object.entries(localeKeys).filter(([key, value]) => value === 0);
+}
+
+// Recursively extract all values in any "content" object that are strings starting with "$"
path: scripts/validate_locale.ts
user: login: LlGC-mij
body: This is a good idea.
created_at: 2025-03-12T14:46:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,128 @@
+const fs = require('fs');
+const path = require('path');
+
+// Read in parameters to choose which type of validation to run
+let args = process.argv.slice(2);
+let runType = args[0];
+if (!['checkLocaleUsage', 'compareLocales'].includes(runType)) {
+    console.log('No parameter or no valid run type. Please use checkLocaleUsage or compareLocales');
+}
+
+// Define all locales and primary locale
+const allLocales = ['cy-GB', 'en-GB', 'fr-FR', 'pl-PL', 'sv-SE'];
+const primaryLocale = 'en-GB';
+
+const checkLocaleUsage = () => {
+    // get a list of all UV extensions
+    const uvExtensions = getUVExtensions();
+
+    // Read primary locale file.
+    let localeData = readLocaleFile(primaryLocale);
+    // replace all values with a 0
+    // we will increment the value for each key found in the extension config files
+    let localeKeys = Object.keys(localeData).reduce((acc, key) => {
+        acc[key] = 0;
+        return acc;
+    }, {});
+
+    // Loop through all UV extensions.
+    uvExtensions.forEach(extension => {
+        let config = readExtensionConfig(extension);
+        // Recursively extract all locale values from any "content" objects.
+        let contentLocaleValues = extractContentLocaleValues(config);
+        // For each locale value found, if it exists in the primary locale keys, increment its count.
+        contentLocaleValues.forEach(val => {
+            if (val in localeKeys) {
+                localeKeys[val]++;
+            }
+        });
+    });
+
+    console.log("Final locale keys count:", localeKeys);
+    console.log("Unused locale keys:", getUnusedLocaleKeys(localeKeys));
+};
+
+const getUVExtensions = () => {
+    let extensions = [];
+    // get a list of all UV extensions
+    const directoryPath = path.join(__dirname, '../src/content-handlers/iiif/extensions/');
+
+    try {
+        const files = fs.readdirSync(directoryPath);
+
+        files.forEach(function (file) {
+            let dir = file.substring(0, 2);
+            // only get directories that start with 'uv'
+            if (dir === 'uv') {
+                extensions.push(file);
+            }
+        });
+    } catch (err) {
+        console.log('Unable to scan directory: ' + err);
+    }
+
+    return extensions;
+}
+
+const getUnusedLocaleKeys = (localeKeys) => {
+    return Object.entries(localeKeys).filter(([key, value]) => value === 0);
+}
+
+// Recursively extract all values in any "content" object that are strings starting with "$"
path: scripts/validate_locale.ts
user: login: LlGC-mij
body: I've updated the script. It now includes additional functionality to check for this.

`npm run hardCodedStrings`

Now outputs a object of extensions with hard coded strings:

```
[
  { extension: 'uv-mediaelement-extension', key: 'Size:' },
  { extension: 'uv-mediaelement-extension', key: 'Width' },
  { extension: 'uv-model-viewer-extension', key: 'custom' },
  { extension: 'uv-model-viewer-extension', key: 'Embed' },
  {
    extension: 'uv-model-viewer-extension',
    key: 'To embed this item in your own website, copy and paste the code below.'
  },
  { extension: 'uv-model-viewer-extension', key: 'Height' },
  { extension: 'uv-model-viewer-extension', key: 'IIIF Manifest' },
  { extension: 'uv-model-viewer-extension', key: 'Share' }
]
```
created_at: 2025-03-12T14:50:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -25,7 +25,10 @@
     "copy-files": "copyfiles -u 1 src/**/*.svg dist/cjs && copyfiles -u 1 src/**/*.gif dist/cjs && copyfiles -u 1 src/**/*.less dist/cjs && copyfiles -u 1 src/extensions/**/*.less dist/cjs && copyfiles -u 1 src/**/*.css dist/cjs",
     "docs": "typedoc --plugin typedoc-plugin-missing-exports ; echo docs.universalviewer.io > docs/CNAME",
     "test": "jest",
-    "prepublishOnly": "npm run build && npm run build-tsc && npm run build-es"
+    "prepublishOnly": "npm run build && npm run build-tsc && npm run build-es",
+    "checkLocaleUsage": "node scripts/validate_locale.ts checkLocaleUsage",
+    "missingTranslations": "node scripts/validate_locale.ts missingTranslations",
+    "hardCodedStrings": "node scripts/validate_locale.ts hardCodedStrings"
path: package.json
user: login: demiankatz
body: Maybe for a little extra context, `findMissingTranslations` and `findHardCodedLocaleStrings` might be slightly better names?
created_at: 2025-03-12T15:45:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,173 @@
+const fs = require('fs');
+const path = require('path');
+
+// Read in parameters to choose which type of validation to run
+let args = process.argv.slice(2);
+let runType = args[0];
+
+// Define primary locale
+const primaryLocale = 'en-GB.json';
+
+const checkLocaleUsage = () => {
+    // get a list of all UV extensions
+    const uvExtensions = getUVExtensions();
+
+    // Read primary locale file.
+    let localeData = readLocaleFile(primaryLocale);
+    // replace all values with a 0
+    // we will increment the value for each key found in the extension config files
+    let localeKeys = Object.keys(localeData).reduce((acc, key) => {
+        acc[key] = 0;
+        return acc;
+    }, {});
+
+    // Loop through all UV extensions.
+    uvExtensions.forEach(extension => {
+        let config = readExtensionConfig(extension);
+        // Recursively extract all locale values from any "content" objects.
+        let contentLocaleValues = extractContentLocaleValues(config);
+        // For each locale value found, if it exists in the primary locale keys, increment its count.
+        contentLocaleValues.forEach(val => {
+            if (val in localeKeys) {
+                localeKeys[val]++;
+            }
+        });
+    });
+
+    console.log("Final locale keys count:", localeKeys);
+    console.log("Unused locale keys:", getUnusedLocaleKeys(localeKeys));
+};
+
+
+const checkHardCodedStrings = () => {
+    let missing = [];
+    // get a list of all UV extensions
+    const uvExtensions = getUVExtensions();
+
+    // Loop through all UV extensions.
+    uvExtensions.forEach(extension => {
+        let config = readExtensionConfig(extension);
+        // Recursively extract all locale values from any "content" objects.
+        let contentLocaleValues = extractContentLocaleValues(config);
+        // For each locale value found, if it exists in the primary locale keys, increment its count.
+        contentLocaleValues.forEach(val => {
+            // if val does not start with $
+            let hasStartingDelimiter = keyStartsWithDollar(val, extension);
+            if (!hasStartingDelimiter) {
+                // console.log(`Key ${val} in extension ${extension} does not start with $`);
+                missing.push({ extension: extension, key: val });
+            }
+        });
+    });
+
+    console.log("missing locale keys:", missing);
+};
+
+const keyStartsWithDollar = (val, extension) => {
+    console.log('val', val, 'extension', extension);
path: scripts/validate_locale.ts
user: login: demiankatz
body: Is this a debug statement that should be removed?
created_at: 2025-03-12T15:47:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,173 @@
+const fs = require('fs');
+const path = require('path');
+
+// Read in parameters to choose which type of validation to run
+let args = process.argv.slice(2);
+let runType = args[0];
+
+// Define primary locale
+const primaryLocale = 'en-GB.json';
+
+const checkLocaleUsage = () => {
+    // get a list of all UV extensions
+    const uvExtensions = getUVExtensions();
+
+    // Read primary locale file.
+    let localeData = readLocaleFile(primaryLocale);
+    // replace all values with a 0
+    // we will increment the value for each key found in the extension config files
+    let localeKeys = Object.keys(localeData).reduce((acc, key) => {
+        acc[key] = 0;
+        return acc;
+    }, {});
+
+    // Loop through all UV extensions.
+    uvExtensions.forEach(extension => {
+        let config = readExtensionConfig(extension);
+        // Recursively extract all locale values from any "content" objects.
+        let contentLocaleValues = extractContentLocaleValues(config);
+        // For each locale value found, if it exists in the primary locale keys, increment its count.
+        contentLocaleValues.forEach(val => {
+            if (val in localeKeys) {
+                localeKeys[val]++;
+            }
+        });
+    });
+
+    console.log("Final locale keys count:", localeKeys);
+    console.log("Unused locale keys:", getUnusedLocaleKeys(localeKeys));
+};
+
+
+const checkHardCodedStrings = () => {
+    let missing = [];
+    // get a list of all UV extensions
+    const uvExtensions = getUVExtensions();
+
+    // Loop through all UV extensions.
+    uvExtensions.forEach(extension => {
+        let config = readExtensionConfig(extension);
+        // Recursively extract all locale values from any "content" objects.
+        let contentLocaleValues = extractContentLocaleValues(config);
+        // For each locale value found, if it exists in the primary locale keys, increment its count.
+        contentLocaleValues.forEach(val => {
+            // if val does not start with $
+            let hasStartingDelimiter = keyStartsWithDollar(val, extension);
+            if (!hasStartingDelimiter) {
+                // console.log(`Key ${val} in extension ${extension} does not start with $`);
path: scripts/validate_locale.ts
user: login: demiankatz
body: Is it safe to remove this, or should it be uncommented?
created_at: 2025-03-12T15:47:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -25,7 +25,10 @@
     "copy-files": "copyfiles -u 1 src/**/*.svg dist/cjs && copyfiles -u 1 src/**/*.gif dist/cjs && copyfiles -u 1 src/**/*.less dist/cjs && copyfiles -u 1 src/extensions/**/*.less dist/cjs && copyfiles -u 1 src/**/*.css dist/cjs",
     "docs": "typedoc --plugin typedoc-plugin-missing-exports ; echo docs.universalviewer.io > docs/CNAME",
     "test": "jest",
-    "prepublishOnly": "npm run build && npm run build-tsc && npm run build-es"
+    "prepublishOnly": "npm run build && npm run build-tsc && npm run build-es",
+    "checkLocaleUsage": "node scripts/validate_locale.ts checkLocaleUsage",
+    "missingTranslations": "node scripts/validate_locale.ts missingTranslations",
+    "hardCodedStrings": "node scripts/validate_locale.ts hardCodedStrings"
path: package.json
user: login: LlGC-mij
body: No objections to these.
created_at: 2025-03-12T16:07:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,173 @@
+const fs = require('fs');
+const path = require('path');
+
+// Read in parameters to choose which type of validation to run
+let args = process.argv.slice(2);
+let runType = args[0];
+
+// Define primary locale
+const primaryLocale = 'en-GB.json';
+
+const checkLocaleUsage = () => {
+    // get a list of all UV extensions
+    const uvExtensions = getUVExtensions();
+
+    // Read primary locale file.
+    let localeData = readLocaleFile(primaryLocale);
+    // replace all values with a 0
+    // we will increment the value for each key found in the extension config files
+    let localeKeys = Object.keys(localeData).reduce((acc, key) => {
+        acc[key] = 0;
+        return acc;
+    }, {});
+
+    // Loop through all UV extensions.
+    uvExtensions.forEach(extension => {
+        let config = readExtensionConfig(extension);
+        // Recursively extract all locale values from any "content" objects.
+        let contentLocaleValues = extractContentLocaleValues(config);
+        // For each locale value found, if it exists in the primary locale keys, increment its count.
+        contentLocaleValues.forEach(val => {
+            if (val in localeKeys) {
+                localeKeys[val]++;
+            }
+        });
+    });
+
+    console.log("Final locale keys count:", localeKeys);
+    console.log("Unused locale keys:", getUnusedLocaleKeys(localeKeys));
+};
+
+
+const checkHardCodedStrings = () => {
+    let missing = [];
+    // get a list of all UV extensions
+    const uvExtensions = getUVExtensions();
+
+    // Loop through all UV extensions.
+    uvExtensions.forEach(extension => {
+        let config = readExtensionConfig(extension);
+        // Recursively extract all locale values from any "content" objects.
+        let contentLocaleValues = extractContentLocaleValues(config);
+        // For each locale value found, if it exists in the primary locale keys, increment its count.
+        contentLocaleValues.forEach(val => {
+            // if val does not start with $
+            let hasStartingDelimiter = keyStartsWithDollar(val, extension);
+            if (!hasStartingDelimiter) {
+                // console.log(`Key ${val} in extension ${extension} does not start with $`);
+                missing.push({ extension: extension, key: val });
+            }
+        });
+    });
+
+    console.log("missing locale keys:", missing);
+};
+
+const keyStartsWithDollar = (val, extension) => {
+    console.log('val', val, 'extension', extension);
path: scripts/validate_locale.ts
user: login: LlGC-mij
body: Removed
created_at: 2025-03-12T16:07:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,173 @@
+const fs = require('fs');
+const path = require('path');
+
+// Read in parameters to choose which type of validation to run
+let args = process.argv.slice(2);
+let runType = args[0];
+
+// Define primary locale
+const primaryLocale = 'en-GB.json';
+
+const checkLocaleUsage = () => {
+    // get a list of all UV extensions
+    const uvExtensions = getUVExtensions();
+
+    // Read primary locale file.
+    let localeData = readLocaleFile(primaryLocale);
+    // replace all values with a 0
+    // we will increment the value for each key found in the extension config files
+    let localeKeys = Object.keys(localeData).reduce((acc, key) => {
+        acc[key] = 0;
+        return acc;
+    }, {});
+
+    // Loop through all UV extensions.
+    uvExtensions.forEach(extension => {
+        let config = readExtensionConfig(extension);
+        // Recursively extract all locale values from any "content" objects.
+        let contentLocaleValues = extractContentLocaleValues(config);
+        // For each locale value found, if it exists in the primary locale keys, increment its count.
+        contentLocaleValues.forEach(val => {
+            if (val in localeKeys) {
+                localeKeys[val]++;
+            }
+        });
+    });
+
+    console.log("Final locale keys count:", localeKeys);
+    console.log("Unused locale keys:", getUnusedLocaleKeys(localeKeys));
+};
+
+
+const checkHardCodedStrings = () => {
+    let missing = [];
+    // get a list of all UV extensions
+    const uvExtensions = getUVExtensions();
+
+    // Loop through all UV extensions.
+    uvExtensions.forEach(extension => {
+        let config = readExtensionConfig(extension);
+        // Recursively extract all locale values from any "content" objects.
+        let contentLocaleValues = extractContentLocaleValues(config);
+        // For each locale value found, if it exists in the primary locale keys, increment its count.
+        contentLocaleValues.forEach(val => {
+            // if val does not start with $
+            let hasStartingDelimiter = keyStartsWithDollar(val, extension);
+            if (!hasStartingDelimiter) {
+                // console.log(`Key ${val} in extension ${extension} does not start with $`);
path: scripts/validate_locale.ts
user: login: LlGC-mij
body: Removed
created_at: 2025-03-12T16:08:01Z
author_association: CONTRIBUTOR

PR comments:
user: login: vercel[bot]
created_at: 2025-03-11T11:21:24Z
body: [vc]: #GNXHaRqpNlgU8LQU80byu5nq8gcOuEtSU//o8VfULlg=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJ1bml2ZXJzYWx2aWV3ZXIiLCJyb290RGlyZWN0b3J5IjpudWxsLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vYmwtdXYtdGVhbS91bml2ZXJzYWx2aWV3ZXIvR2ZXODJWS1k5U2NzdVNVZFphRUZ0a2hUWTgySiIsInByZXZpZXdVcmwiOiJ1bml2ZXJzYWx2aWV3ZXItZ2l0LWZvcmstNDA0bWlrZS1sb2NhbGUtY2hlY2stYmwtdXYtdGVhbS52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6InVuaXZlcnNhbHZpZXdlci1naXQtZm9yay00MDRtaWtlLWxvY2FsZS1jaGVjay1ibC11di10ZWFtLnZlcmNlbC5hcHAifX1dfQ==
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **universalviewer** | ✅ Ready ([Inspect](https://vercel.com/bl-uv-team/universalviewer/GfW82VKY9ScsuSUdZaEFtkhTY82J)) | [Visit Preview](https://universalviewer-git-fork-404mike-locale-check-bl-uv-team.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/universalviewer-git-fork-404mike-locale-check-bl-uv-team.vercel.app?via=pr-comment-feedback-link) | Mar 12, 2025 4:11pm |


author_association: NONE
user: login: LlGC-mij
created_at: 2025-03-12T14:52:59Z
body: @demiankatz thank you for the feedback.

I've updated the script to cover all the feedback so far. I've also renamed a few things to hopefully make things a bit more clear as to what they do?

The latest change includes the ability to run the different tests from npm:
```
  checkLocaleUsage
    node scripts/validate_locale.ts checkLocaleUsage
  missingTranslations
    node scripts/validate_locale.ts missingTranslations
  hardCodedStrings
    node scripts/validate_locale.ts hardCodedStrings
 ```
author_association: CONTRIBUTOR
user: login: LlGC-mij
created_at: 2025-03-14T09:14:05Z
body: Thank you @demiankatz. Both @404mike and I are the same person 😆 (I forgot to update my keys!).

I'm happy to look at fixing any issues found by the tool. I'll take a look next week.
author_association: CONTRIBUTOR
user: login: demiankatz
created_at: 2025-03-14T17:32:45Z
body: Ha, thanks, @LlGC-mij. You and yourself make an excellent team. ;-)
author_association: CONTRIBUTOR