Convert V2 translations to Toml

This commit is contained in:
James Brunton
2025-11-05 16:01:09 +00:00
parent f4c9becce2
commit cddd5e7d15
49 changed files with 239623 additions and 80233 deletions

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`;
},
},

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,
@@ -29,7 +29,7 @@ i18n
'convert.singleOrMultiple': 'Output',
'convert.emailNote': 'Email attachments and embedded images will be included',
'common.color': 'Color',
'common.grayscale': 'Grayscale',
'common.grayscale': 'Grayscale',
'common.blackWhite': 'Black & White',
'common.single': 'Single Image',
'common.multiple': 'Multiple Images',
@@ -45,4 +45,4 @@ i18n
}
});
export default i18n;
export default i18n;

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;

View File

@@ -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()

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);
});
});

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');
@@ -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);
});