🌐(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:
@@ -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,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
11
src/frontend/packages/i18n/.eslintrc.js
Normal file
11
src/frontend/packages/i18n/.eslintrc.js
Normal 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
1
src/frontend/packages/i18n/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
locales
|
||||||
104
src/frontend/packages/i18n/__tests__/i18n.test.ts
Normal file
104
src/frontend/packages/i18n/__tests__/i18n.test.ts
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
47
src/frontend/packages/i18n/__tests__/translations.test.ts
Normal file
47
src/frontend/packages/i18n/__tests__/translations.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
56
src/frontend/packages/i18n/format-deploy.mjs
Normal file
56
src/frontend/packages/i18n/format-deploy.mjs
Normal 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!`);
|
||||||
10
src/frontend/packages/i18n/i18next-parser.config.jest.mjs
Normal file
10
src/frontend/packages/i18n/i18next-parser.config.jest.mjs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
const config = {
|
||||||
|
customValueTemplate: {
|
||||||
|
message: '${key}',
|
||||||
|
description: '${description}',
|
||||||
|
},
|
||||||
|
keepRemoved: false,
|
||||||
|
keySeparator: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
10
src/frontend/packages/i18n/i18next-parser.config.mjs
Normal file
10
src/frontend/packages/i18n/i18next-parser.config.mjs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
const config = {
|
||||||
|
customValueTemplate: {
|
||||||
|
message: '${key}',
|
||||||
|
description: '${description}',
|
||||||
|
},
|
||||||
|
keepRemoved: false,
|
||||||
|
keySeparator: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
7
src/frontend/packages/i18n/jest.config.ts
Normal file
7
src/frontend/packages/i18n/jest.config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export default {
|
||||||
|
rootDir: './',
|
||||||
|
testEnvironment: 'node',
|
||||||
|
transform: {
|
||||||
|
'^.+\\.(ts)$': 'ts-jest',
|
||||||
|
},
|
||||||
|
};
|
||||||
24
src/frontend/packages/i18n/package.json
Normal file
24
src/frontend/packages/i18n/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/frontend/packages/i18n/tsconfig.json
Normal file
21
src/frontend/packages/i18n/tsconfig.json
Normal 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"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user