Convert V2 translations to Toml

This commit is contained in:
James Brunton
2025-11-26 14:13:54 +00:00
parent f4c9becce2
commit cddd5e7d15
49 changed files with 239623 additions and 80233 deletions
+13 -61
View File
@@ -53,7 +53,6 @@
"globals": "^16.4.0",
"i18next": "^25.5.2",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.0.2",
"jszip": "^3.10.1",
"license-report": "^6.8.0",
"pdf-lib": "^1.17.1",
@@ -65,6 +64,7 @@
"react-rnd": "^10.5.2",
"react-router-dom": "^7.9.1",
"signature_pad": "^5.0.4",
"smol-toml": "^1.4.2",
"tailwindcss": "^4.1.13",
"web-vitals": "^5.1.0"
},
@@ -6453,15 +6453,6 @@
"node": ">= 6"
}
},
"node_modules/cross-fetch": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz",
"integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==",
"license": "MIT",
"dependencies": {
"node-fetch": "^2.6.12"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -8893,15 +8884,6 @@
"@babel/runtime": "^7.23.2"
}
},
"node_modules/i18next-http-backend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-3.0.2.tgz",
"integrity": "sha512-PdlvPnvIp4E1sYi46Ik4tBYh/v/NbYfFFgTjkwFl0is8A18s7/bx9aXqsrOax9WUbeNS6mD2oix7Z0yGGf6m5g==",
"license": "MIT",
"dependencies": {
"cross-fetch": "4.0.0"
}
},
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@@ -10717,48 +10699,6 @@
"node": ">= 0.4.0"
}
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/node-fetch/node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/node-fetch/node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
"node_modules/node-fetch/node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/node-releases": {
"version": "2.0.27",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
@@ -13111,6 +13051,18 @@
"npm": ">= 3.0.0"
}
},
"node_modules/smol-toml": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.4.2.tgz",
"integrity": "sha512-rInDH6lCNiEyn3+hH8KVGFdbjc099j47+OSgbMrfDYX1CmXLfdKd7qi6IfcWj2wFxvSVkuI46M+wPGYfEOEj6g==",
"license": "BSD-3-Clause",
"engines": {
"node": ">= 18"
},
"funding": {
"url": "https://github.com/sponsors/cyyynthia"
}
},
"node_modules/socks": {
"version": "2.8.7",
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz",
+1 -1
View File
@@ -49,7 +49,6 @@
"globals": "^16.4.0",
"i18next": "^25.5.2",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.0.2",
"jszip": "^3.10.1",
"license-report": "^6.8.0",
"pdf-lib": "^1.17.1",
@@ -61,6 +60,7 @@
"react-rnd": "^10.5.2",
"react-router-dom": "^7.9.1",
"signature_pad": "^5.0.4",
"smol-toml": "^1.4.2",
"tailwindcss": "^4.1.13",
"web-vitals": "^5.1.0"
},
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -1,7 +1,7 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import Backend from 'i18next-http-backend';
import TomlBackend from '@app/i18n/tomlBackend';
// Define supported languages (based on your existing translations)
export const supportedLanguages = {
@@ -51,7 +51,7 @@ export const supportedLanguages = {
export const rtlLanguages = ['ar-AR', 'fa-IR'];
i18n
.use(Backend)
.use(TomlBackend)
.use(LanguageDetector)
.use(initReactI18next)
.init({
@@ -73,7 +73,7 @@ i18n
const lng = lngs[0];
const basePath = import.meta.env.BASE_URL || '/';
const cleanBasePath = basePath.endsWith('/') ? basePath.slice(0, -1) : basePath;
return `${cleanBasePath}/locales/${lng}/${namespaces[0]}.json`;
return `${cleanBasePath}/locales/${lng}/${namespaces[0]}.toml`;
},
},
+3 -3
View File
@@ -1,16 +1,16 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import Backend from 'i18next-http-backend';
import TomlBackend from '@app/i18n/tomlBackend';
i18n
.use(Backend)
.use(TomlBackend)
.use(initReactI18next)
.init({
lng: 'en',
fallbackLng: 'en',
debug: false,
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json',
loadPath: '/locales/{{lng}}/{{ns}}.toml',
},
interpolation: {
escapeValue: false,
+51
View File
@@ -0,0 +1,51 @@
import { BackendModule, ReadCallback } from 'i18next';
import { parse } from 'smol-toml';
export interface TomlBackendOptions {
loadPath: string | ((lngs: string[], namespaces: string[]) => string);
}
class TomlBackend implements BackendModule<TomlBackendOptions> {
static type = 'backend' as const;
type = 'backend' as const;
constructor(services?: unknown, options?: TomlBackendOptions) {
this.init(services, options);
}
init(_services?: unknown, options?: TomlBackendOptions): void {
this.options = options;
}
read(language: string, namespace: string, callback: ReadCallback): void {
const loadPath = this.options?.loadPath;
if (!loadPath) {
callback(new Error('loadPath is not configured'), null);
return;
}
const url = typeof loadPath === 'function'
? loadPath([language], [namespace])
: loadPath.replace('{{lng}}', language).replace('{{ns}}', namespace);
fetch(url)
.then((response) => {
if (!response.ok) {
throw new Error(`Failed to load translation file: ${url} (${response.status})`);
}
return response.text();
})
.then((tomlContent) => {
const parsed = parse(tomlContent);
callback(null, parsed);
})
.catch((error) => {
callback(error, null);
});
}
private options?: TomlBackendOptions;
}
export default TomlBackend;
@@ -2,10 +2,11 @@ import fs from 'fs';
import path from 'path';
import ts from 'typescript';
import { describe, expect, test } from 'vitest';
import { parse } from 'smol-toml';
const REPO_ROOT = path.join(__dirname, '../../../..');
const SRC_ROOT = path.join(__dirname, '../..');
const EN_GB_FILE = path.join(__dirname, '../../../public/locales/en-GB/translation.json');
const EN_GB_FILE = path.join(__dirname, '../../../public/locales/en-GB/translation.toml');
const IGNORED_DIRS = new Set([
'tests',
@@ -150,7 +151,7 @@ describe('Missing translation coverage', () => {
expect(fs.existsSync(EN_GB_FILE)).toBe(true);
const localeContent = fs.readFileSync(EN_GB_FILE, 'utf8');
const enGb = JSON.parse(localeContent);
const enGb = parse(localeContent);
const availableKeys = flattenKeys(enGb);
const usedKeys = listSourceFiles()
+10 -9
View File
@@ -1,6 +1,7 @@
import { describe, test, expect } from 'vitest';
import fs from 'fs';
import path from 'path';
import { parse } from 'smol-toml';
const LOCALES_DIR = path.join(__dirname, '../../../public/locales');
@@ -17,7 +18,7 @@ const getLocaleDirectories = () => {
const localeDirectories = getLocaleDirectories();
describe('Translation JSON Validation', () => {
describe('Translation TOML Validation', () => {
test('should find the locales directory', () => {
expect(fs.existsSync(LOCALES_DIR)).toBe(true);
});
@@ -26,8 +27,8 @@ describe('Translation JSON Validation', () => {
expect(localeDirectories.length).toBeGreaterThan(0);
});
test.each(localeDirectories)('should have valid JSON in %s/translation.json', (localeDir) => {
const translationFile = path.join(LOCALES_DIR, localeDir, 'translation.json');
test.each(localeDirectories)('should have valid TOML in %s/translation.toml', (localeDir) => {
const translationFile = path.join(LOCALES_DIR, localeDir, 'translation.toml');
// Check if file exists
expect(fs.existsSync(translationFile)).toBe(true);
@@ -36,15 +37,15 @@ describe('Translation JSON Validation', () => {
const content = fs.readFileSync(translationFile, 'utf8');
expect(content.trim()).not.toBe('');
// Parse JSON - this will throw if invalid JSON
let jsonData;
// Parse TOML - this will throw if invalid TOML
let tomlData;
expect(() => {
jsonData = JSON.parse(content);
tomlData = parse(content);
}).not.toThrow();
// Ensure it's an object at root level
expect(typeof jsonData).toBe('object');
expect(jsonData).not.toBeNull();
expect(Array.isArray(jsonData)).toBe(false);
expect(typeof tomlData).toBe('object');
expect(tomlData).not.toBeNull();
expect(Array.isArray(tomlData)).toBe(false);
});
});
@@ -1,6 +1,7 @@
import { describe, test, expect } from 'vitest';
import fs from 'fs';
import path from 'path';
import { parse } from 'smol-toml';
const LOCALES_DIR = path.join(__dirname, '../../../public/locales');
@@ -40,11 +41,11 @@ describe('Translation key structure', () => {
expect(localeDirectories.length).toBeGreaterThan(0);
});
test.each(localeDirectories)('should not contain dotted keys in %s/translation.json', (localeDir) => {
const translationFile = path.join(LOCALES_DIR, localeDir, 'translation.json');
test.each(localeDirectories)('should not contain dotted keys in %s/translation.toml', (localeDir) => {
const translationFile = path.join(LOCALES_DIR, localeDir, 'translation.toml');
expect(fs.existsSync(translationFile)).toBe(true);
const data = JSON.parse(fs.readFileSync(translationFile, 'utf8'));
const data = parse(fs.readFileSync(translationFile, 'utf8'));
const dottedKeys = findDottedKeys(data);
expect(dottedKeys, `Dotted keys found in ${localeDir}: ${dottedKeys.join(', ')}`).toHaveLength(0);
});