🌐(i18n) create package i18n

The package i18n will handle the
internationalization of the applications.
It will parse the frontend code and extract
the translations to be send to the crowdin
platform.
This commit is contained in:
Anthony LC
2024-04-02 16:19:35 +02:00
committed by Anthony LC
parent 3098b9f4fc
commit dfafd335f4
11 changed files with 297 additions and 0 deletions

View File

@@ -20,4 +20,10 @@ files: [
dest: "/backend.pot", dest: "/backend.pot",
translation : "/backend/locale/%locale_with_underscore%/LC_MESSAGES/django.po" translation : "/backend/locale/%locale_with_underscore%/LC_MESSAGES/django.po"
}, },
{
source: "/frontend/packages/i18n/locales/impress/translations-crowdin.json",
dest: "/impress.json",
translation: "/frontend/packages/i18n/locales/impress/%two_letters_code%/translations.json",
skip_untranslated_strings: true,
},
] ]

View File

@@ -0,0 +1,11 @@
module.exports = {
root: true,
extends: ['impress/jest', 'plugin:import/recommended'],
parserOptions: {
sourceType: 'module',
ecmaVersion: 'latest',
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
},
ignorePatterns: ['node_modules'],
};

1
src/frontend/packages/i18n/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
locales

View File

@@ -0,0 +1,104 @@
import { execSync } from 'child_process';
import fs from 'fs';
import path from 'path';
describe('integration testing on i18n package', () => {
afterAll(() => {
fs.rmSync('./locales/tests', { recursive: true, force: true });
});
test('cmd extract-translation:impress', () => {
// To be sure the file is not here
fs.rmSync('./locales/impress/translations-crowdin.json', {
recursive: true,
force: true,
});
expect(
fs.existsSync('./locales/impress/translations-crowdin.json'),
).toBeFalsy();
// Generate the file
execSync('yarn extract-translation:impress');
expect(
fs.existsSync('./locales/impress/translations-crowdin.json'),
).toBeTruthy();
});
test('cmd format-deploy', () => {
// To be sure the tests folder is not here
fs.rmSync('./locales/tests', { recursive: true, force: true });
expect(fs.existsSync('./locales/tests')).toBeFalsy();
// Generate english json file
fs.mkdirSync('./locales/tests/en/', { recursive: true });
fs.writeFileSync(
'./locales/tests/en/translations.json',
JSON.stringify({ test: { message: 'My test' } }),
'utf8',
);
expect(fs.existsSync('./locales/tests/en/translations.json')).toBeTruthy();
fs.mkdirSync('./locales/tests/fr/', { recursive: true });
fs.writeFileSync(
'./locales/tests/fr/translations.json',
JSON.stringify({ test: { message: 'Mon test' } }),
'utf8',
);
expect(fs.existsSync('./locales/tests/fr/translations.json')).toBeTruthy();
// Execute format-deploy command
const output = './locales/tests/translations.json';
execSync(`node ./format-deploy.mjs --app=tests --output=${output}`);
const json = JSON.parse(fs.readFileSync(output, 'utf8'));
expect(json).toEqual({
en: {
translation: { test: 'My test' },
},
fr: {
translation: { test: 'Mon test' },
},
});
});
test('cmd format-deploy throws an error when translation file is not found', () => {
// To be sure the tests folder is not here
fs.rmSync('./locales/tests', { recursive: true, force: true });
expect(fs.existsSync('./locales/tests')).toBeFalsy();
// Generate english json file
fs.mkdirSync('./locales/tests/en/', { recursive: true });
// Execute format-deploy command
const output = './locales/tests/translations.json';
const cmd = () => {
execSync(`node ./format-deploy.mjs --app=tests --output=${output}`, {
stdio: 'pipe',
});
};
expect(cmd).toThrow(
`Error: File locales${path.sep}tests${path.sep}en${path.sep}translations.json not found!`,
);
});
test('cmd format-deploy throws an error when no translation to deploy', () => {
// To be sure the tests folder is not here
fs.rmSync('./locales/tests', { recursive: true, force: true });
expect(fs.existsSync('./locales/tests')).toBeFalsy();
// Generate english json file
fs.mkdirSync('./locales/tests/', { recursive: true });
// Execute format-deploy command
const output = './locales/tests/translations.json';
const cmd = () => {
execSync(`node ./format-deploy.mjs --app=tests --output=${output}`, {
stdio: 'pipe',
});
};
expect(cmd).toThrow('Error: No translation to deploy');
});
});

View File

@@ -0,0 +1,47 @@
import { execSync } from 'child_process';
import fs from 'fs';
describe('checks all the frontend translation are made', () => {
it('checks missing translation. If this test fails, go to https://crowdin.com/', () => {
// Extract the translations
execSync(
'yarn extract-translation:impress -c ./i18next-parser.config.jest.mjs',
);
const outputCrowdin = './locales/impress/translations-crowdin.json';
const jsonCrowdin = JSON.parse(fs.readFileSync(outputCrowdin, 'utf8'));
const listKeysCrowdin = Object.keys(jsonCrowdin).sort();
// Check the translations in the app impress
const outputimpress = '../../apps/impress/src/i18n/translations.json';
const jsonimpress = JSON.parse(fs.readFileSync(outputimpress, 'utf8'));
// Our keys are in english, so we don't need to check the english translation
Object.keys(jsonimpress)
.filter((key) => key !== 'en')
.forEach((key) => {
const listKeysimpress = Object.keys(jsonimpress[key].translation).sort();
const missingKeys = listKeysCrowdin.filter(
(element) => !listKeysimpress.includes(element),
);
const additionalKeys = listKeysimpress.filter(
(element) => !listKeysCrowdin.includes(element),
);
if (missingKeys.length > 0) {
console.log(
`Missing keys in impress translations that should be translated in Crowdin, got to https://crowdin.com/ :`,
missingKeys,
);
}
if (additionalKeys.length > 0) {
console.log(
`Additional keys in impress translations that seems not present in this branch:`,
additionalKeys,
);
}
expect(missingKeys.length).toBe(0);
});
});
});

View File

@@ -0,0 +1,56 @@
import fs from 'fs';
import path from 'path';
import { hideBin } from 'yargs/helpers';
import yargs from 'yargs/yargs';
// Get our args
const argv = yargs(hideBin(process.argv)).argv;
const { app, output } = argv;
const folderPath = './locales/' + app;
const namefile = 'translations.json';
const jsonI18n = {};
// Fetch the files in the locales folder
fs.readdirSync(folderPath).map((language) => {
const languagePath = path.join(folderPath, path.sep, language);
// Crowdin output file in folder, we want to treat only these ones
if (!fs.lstatSync(languagePath).isDirectory()) {
return;
}
jsonI18n[language] = {
translation: {},
};
// Get the json file generated by crowdin
const pathTranslateFile = path.join(languagePath, path.sep, namefile);
if (!fs.existsSync(pathTranslateFile)) {
throw new Error(`File ${pathTranslateFile} not found!`);
}
const json = JSON.parse(fs.readFileSync(pathTranslateFile, 'utf8'));
// Transform the json file to the format expected by i18next
const jsonKeyMessage = {};
Object.keys(json)
.sort()
.forEach((key) => {
jsonKeyMessage[key] = json[key].message;
});
jsonI18n[language] = {
translation: jsonKeyMessage,
};
});
if (!Object.keys(jsonI18n).length) {
throw new Error(`No translation to deploy`);
}
// Write the file to the output
fs.writeFileSync(output, JSON.stringify(jsonI18n), 'utf8');
console.log(`${app} translations deployed!`);

View File

@@ -0,0 +1,10 @@
const config = {
customValueTemplate: {
message: '${key}',
description: '${description}',
},
keepRemoved: false,
keySeparator: false,
};
export default config;

View File

@@ -0,0 +1,10 @@
const config = {
customValueTemplate: {
message: '${key}',
description: '${description}',
},
keepRemoved: false,
keySeparator: false,
};
export default config;

View File

@@ -0,0 +1,7 @@
export default {
rootDir: './',
testEnvironment: 'node',
transform: {
'^.+\\.(ts)$': 'ts-jest',
},
};

View File

@@ -0,0 +1,24 @@
{
"name": "packages-i18n",
"version": "0.1.0",
"private": true,
"scripts": {
"extract-translation": "yarn extract-translation:impress",
"extract-translation:impress": "yarn i18next ../../apps/impress/**/*.{ts,tsx} -c ./i18next-parser.config.mjs -o ./locales/impress/translations-crowdin.json",
"format-deploy": "yarn format-deploy:impress",
"format-deploy:impress": "node ./format-deploy.mjs --app=impress --output=../../apps/impress/src/i18n/translations.json",
"lint": "eslint --ext .js,.ts,.mjs .",
"test": "jest"
},
"dependencies": {
"@types/jest": "29.5.12",
"@types/node": "*",
"eslint-config-impress": "*",
"eslint-plugin-import": "2.29.1",
"i18next-parser": "8.8.0",
"jest": "29.7.0",
"ts-jest": "29.1.2",
"typescript": "*",
"yargs": "17.7.2"
}
}

View File

@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
},
"include": [
"**/*.ts",
],
"exclude": ["node_modules"]
}