Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
28debe6
feat(i18n): implement internationalization support with extraction sc…
elibosley Oct 3, 2025
67e424c
chore(extract-translations): remove unnecessary newline at the beginn…
elibosley Oct 3, 2025
3cfae32
feat(i18n): enhance internationalization support across API and UI co…
elibosley Oct 3, 2025
57c2dea
fix(extract-translations): add missing newline for improved readability
elibosley Oct 3, 2025
fb2856d
feat(i18n): enhance translation management with sorting script
elibosley Oct 3, 2025
2357031
feat(i18n): enhance localization in ConnectSettings and logging compo…
elibosley Oct 3, 2025
f74549d
feat(i18n): enhance localization in LogViewer and SingleLogViewer com…
elibosley Oct 3, 2025
eea5d4e
fix(i18n): update translation file paths and add new dependencies
elibosley Oct 3, 2025
7583331
feat/i18n (#1738)
elibosley Oct 3, 2025
2f7e64b
New Crowdin updates (#1740)
elibosley Oct 3, 2025
f48c85c
refactor(i18n): remove translations.php file and update related refer…
elibosley Oct 3, 2025
d81b5f5
feat(i18n): add i18n support for JSON schema and update OIDC configur…
elibosley Oct 3, 2025
1b27226
fix(i18n): refine i18n integration in JSON schema and OIDC configuration
elibosley Oct 3, 2025
c1bd053
fix(tests): correct mock function signature for window locale retrieval
elibosley Oct 3, 2025
0ee0da7
fix(i18n): simplify i18n key assignment in form-utils
elibosley Oct 3, 2025
dc8f720
feat(i18n): enhance i18n integration across components and locales
elibosley Oct 3, 2025
f25a468
feat(i18n): enhance i18n integration in test components
elibosley Oct 3, 2025
57bef96
New Crowdin updates (#1741)
elibosley Oct 5, 2025
e61ee74
refactor(tests): clean up unused imports in test files
elibosley Oct 6, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion api/.eslintrc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ export default tseslint.config(
'ignorePackages',
{
js: 'always',
ts: 'always',
mjs: 'always',
cjs: 'always',
ts: 'never',
tsx: 'never',
},
],
'no-restricted-globals': [
Expand Down
4 changes: 4 additions & 0 deletions api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ unraid-api report -vv

If you found this file you're likely a developer. If you'd like to know more about the API and when it's available please join [our discord](https://discord.unraid.net/).

## Internationalization

- Run `pnpm --filter @unraid/api i18n:extract` to scan the Nest.js source for translation helper usages and update `src/i18n/en.json` with any new keys. The extractor keeps existing translations intact and appends new keys with their English source text.

## License

Copyright Lime Technology Inc. All rights reserved.
2 changes: 1 addition & 1 deletion api/dev/configs/api.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "4.22.2",
"version": "4.25.2",
"extraOrigins": [],
"sandbox": true,
"ssoSubIds": [],
Expand Down
2 changes: 2 additions & 0 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
"// GraphQL Codegen": "",
"codegen": "graphql-codegen --config codegen.ts",
"codegen:watch": "graphql-codegen --config codegen.ts --watch",
"// Internationalization": "",
"i18n:extract": "node ./scripts/extract-translations.mjs",
"// Code Quality": "",
"lint": "eslint --config .eslintrc.ts src/",
"lint:fix": "eslint --fix --config .eslintrc.ts src/",
Expand Down
162 changes: 162 additions & 0 deletions api/scripts/extract-translations.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
#!/usr/bin/env node

import { readFile, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { glob } from 'glob';
import ts from 'typescript';

const projectRoot = process.cwd();
const sourcePatterns = 'src/**/*.{ts,js}';
const ignorePatterns = [
'**/__tests__/**',
'**/__test__/**',
'**/*.spec.ts',
'**/*.spec.js',
'**/*.test.ts',
'**/*.test.js',
];

const englishLocaleFile = path.resolve(projectRoot, 'src/i18n/en.json');

const identifierTargets = new Set(['t', 'translate']);
const propertyTargets = new Set([
'i18n.t',
'i18n.translate',
'ctx.t',
'this.translate',
'this.i18n.translate',
'this.i18n.t',
]);

function getPropertyChain(node) {
if (ts.isIdentifier(node)) {
return node.text;
}
if (ts.isPropertyAccessExpression(node)) {
const left = getPropertyChain(node.expression);
if (!left) return undefined;
return `${left}.${node.name.text}`;
}
return undefined;
}

function extractLiteral(node) {
if (ts.isStringLiteralLike(node)) {
return node.text;
}
if (ts.isNoSubstitutionTemplateLiteral(node)) {
return node.text;
}
return undefined;
}

function collectKeysFromSource(sourceFile) {
const keys = new Set();

function visit(node) {
if (ts.isCallExpression(node)) {
const expr = node.expression;
let matches = false;

if (ts.isIdentifier(expr) && identifierTargets.has(expr.text)) {
matches = true;
} else if (ts.isPropertyAccessExpression(expr)) {
const chain = getPropertyChain(expr);
if (chain && propertyTargets.has(chain)) {
matches = true;
}
}

if (matches) {
const [firstArg] = node.arguments;
if (firstArg) {
const literal = extractLiteral(firstArg);
if (literal) {
keys.add(literal);
}
}
}
}

ts.forEachChild(node, visit);
}

visit(sourceFile);
return keys;
}

async function loadEnglishCatalog() {
try {
const raw = await readFile(englishLocaleFile, 'utf8');
const parsed = raw.trim() ? JSON.parse(raw) : {};
if (typeof parsed !== 'object' || Array.isArray(parsed)) {
throw new Error('English locale file must contain a JSON object.');
}
return parsed;
} catch (error) {
if (error && error.code === 'ENOENT') {
return {};
}
throw error;
}
}

async function ensureEnglishCatalog(keys) {
const existingCatalog = await loadEnglishCatalog();
const existingKeys = new Set(Object.keys(existingCatalog));

let added = 0;
const combinedKeys = new Set([...existingKeys, ...keys]);
const sortedKeys = Array.from(combinedKeys).sort((a, b) => a.localeCompare(b));
const nextCatalog = {};

for (const key of sortedKeys) {
if (Object.prototype.hasOwnProperty.call(existingCatalog, key)) {
nextCatalog[key] = existingCatalog[key];
} else {
nextCatalog[key] = key;
added += 1;
}
}

const nextJson = `${JSON.stringify(nextCatalog, null, 2)}\n`;
const existingJson = JSON.stringify(existingCatalog, null, 2) + '\n';

if (nextJson !== existingJson) {
await writeFile(englishLocaleFile, nextJson, 'utf8');
}

return added;
}

async function main() {
const files = await glob(sourcePatterns, {
cwd: projectRoot,
ignore: ignorePatterns,
absolute: true,
});

const collectedKeys = new Set();

await Promise.all(
files.map(async (file) => {
const content = await readFile(file, 'utf8');
const sourceFile = ts.createSourceFile(file, content, ts.ScriptTarget.Latest, true);
const keys = collectKeysFromSource(sourceFile);
keys.forEach((key) => collectedKeys.add(key));
}),
);

const added = await ensureEnglishCatalog(collectedKeys);

if (added === 0) {
console.log('[i18n] No new backend translation keys detected.');
} else {
console.log(`[i18n] Added ${added} key(s) to src/i18n/en.json.`);
}
}

main().catch((error) => {
console.error('[i18n] Failed to extract backend translations.', error);
process.exitCode = 1;
});
1 change: 1 addition & 0 deletions api/src/i18n/ar.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
1 change: 1 addition & 0 deletions api/src/i18n/bn.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
1 change: 1 addition & 0 deletions api/src/i18n/ca.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
1 change: 1 addition & 0 deletions api/src/i18n/cs.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
1 change: 1 addition & 0 deletions api/src/i18n/da.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
1 change: 1 addition & 0 deletions api/src/i18n/de.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
1 change: 1 addition & 0 deletions api/src/i18n/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
1 change: 1 addition & 0 deletions api/src/i18n/es.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
1 change: 1 addition & 0 deletions api/src/i18n/fr.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
1 change: 1 addition & 0 deletions api/src/i18n/hi.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
1 change: 1 addition & 0 deletions api/src/i18n/hr.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
1 change: 1 addition & 0 deletions api/src/i18n/hu.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
1 change: 1 addition & 0 deletions api/src/i18n/it.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
1 change: 1 addition & 0 deletions api/src/i18n/ja.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
1 change: 1 addition & 0 deletions api/src/i18n/ko.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
1 change: 1 addition & 0 deletions api/src/i18n/lv.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
1 change: 1 addition & 0 deletions api/src/i18n/nl.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
1 change: 1 addition & 0 deletions api/src/i18n/no.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
1 change: 1 addition & 0 deletions api/src/i18n/pl.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
1 change: 1 addition & 0 deletions api/src/i18n/pt.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
1 change: 1 addition & 0 deletions api/src/i18n/ro.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
1 change: 1 addition & 0 deletions api/src/i18n/ru.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
1 change: 1 addition & 0 deletions api/src/i18n/sv.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
1 change: 1 addition & 0 deletions api/src/i18n/uk.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
1 change: 1 addition & 0 deletions api/src/i18n/zh.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
40 changes: 40 additions & 0 deletions api/src/types/jsonforms-i18n.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import '@jsonforms/core/lib/models/jsonSchema4';
import '@jsonforms/core/lib/models/jsonSchema7';
import '@jsonforms/core/src/models/jsonSchema4';
import '@jsonforms/core/src/models/jsonSchema7';
Comment on lines +1 to +4
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Verify necessity of side-effect imports for module augmentation.

In TypeScript declaration files, module augmentation via declare module typically does not require explicit side-effect imports. These imports may be redundant.

Additionally, per the coding guidelines, TypeScript imports should use .js extensions for ESM compatibility. If these imports are necessary, consider:

-import '@jsonforms/core/lib/models/jsonSchema4';
-import '@jsonforms/core/lib/models/jsonSchema7';
-import '@jsonforms/core/src/models/jsonSchema4';
-import '@jsonforms/core/src/models/jsonSchema7';
+import '@jsonforms/core/lib/models/jsonSchema4.js';
+import '@jsonforms/core/lib/models/jsonSchema7.js';
+import '@jsonforms/core/src/models/jsonSchema4.js';
+import '@jsonforms/core/src/models/jsonSchema7.js';

However, if these imports serve no purpose beyond TypeScript module resolution, they can likely be removed entirely.

As per coding guidelines.

Run the following script to check @jsonforms/core package structure and determine which module paths are actually exported:


🏁 Script executed:

#!/bin/bash
# Description: Examine @jsonforms/core package.json to identify exported module paths.

# Check if package.json exists and extract exports/main fields
fd -t f "package.json" api/node_modules/@jsonforms/core --exec cat {} \; | jq -r '.exports, .main, .module, .types'

Length of output: 119


🏁 Script executed:

#!/bin/bash
# Fetch package metadata for @jsonforms/core from npm registry
npm view @jsonforms/core --json | jq -r '.exports, .main, .module, .types'

Length of output: 61


Consolidate JSONForms module augmentations to .js ESM paths

  • Remove imports and declare module blocks for @jsonforms/core/src/models/* (these source‐path modules aren’t published).
  • Update remaining side-effect imports and declare module specifiers to use .js extensions, e.g.:
    • @jsonforms/core/lib/models/jsonSchema4.js
    • @jsonforms/core/lib/models/jsonSchema7.js
🤖 Prompt for AI Agents
In api/src/types/jsonforms-i18n.d.ts around lines 1 to 4, remove the side-effect
imports that reference non-published source paths
(@jsonforms/core/src/models/jsonSchema4 and jsonSchema7) and update the
remaining module augmentation imports and any declare module specifiers to use
the published ESM .js paths (e.g. @jsonforms/core/lib/models/jsonSchema4.js and
@jsonforms/core/lib/models/jsonSchema7.js); delete the two src/* import lines
and replace the lib/* import specifiers to include the .js extension so
TypeScript resolves the published ESM modules correctly.


declare module '@jsonforms/core/lib/models/jsonSchema4' {
interface JsonSchema4 {
i18n?: string;
}
}

declare module '@jsonforms/core/lib/models/jsonSchema7' {
interface JsonSchema7 {
i18n?: string;
}
}

declare module '@jsonforms/core/src/models/jsonSchema4' {
interface JsonSchema4 {
i18n?: string;
}
}

declare module '@jsonforms/core/src/models/jsonSchema7' {
interface JsonSchema7 {
i18n?: string;
}
}

declare module '@jsonforms/core/lib/models/jsonSchema4.js' {
interface JsonSchema4 {
i18n?: string;
}
}

declare module '@jsonforms/core/lib/models/jsonSchema7.js' {
interface JsonSchema7 {
i18n?: string;
}
}
Loading
Loading