Skip to content

Commit

Permalink
🌐(i18n) create package i18n
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
AntoLC committed Apr 2, 2024
1 parent 3098b9f commit dfafd33
Show file tree
Hide file tree
Showing 11 changed files with 297 additions and 0 deletions.
6 changes: 6 additions & 0 deletions crowdin/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,10 @@ files: [
dest: "/backend.pot",
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 changes: 11 additions & 0 deletions src/frontend/packages/i18n/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -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 change: 1 addition & 0 deletions src/frontend/packages/i18n/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
locales
104 changes: 104 additions & 0 deletions src/frontend/packages/i18n/__tests__/i18n.test.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 47 additions & 0 deletions src/frontend/packages/i18n/__tests__/translations.test.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 56 additions & 0 deletions src/frontend/packages/i18n/format-deploy.mjs
Original file line number Diff line number Diff line change
@@ -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 changes: 10 additions & 0 deletions src/frontend/packages/i18n/i18next-parser.config.jest.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const config = {
customValueTemplate: {
message: '${key}',
description: '${description}',
},
keepRemoved: false,
keySeparator: false,
};

export default config;
10 changes: 10 additions & 0 deletions src/frontend/packages/i18n/i18next-parser.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const config = {
customValueTemplate: {
message: '${key}',
description: '${description}',
},
keepRemoved: false,
keySeparator: false,
};

export default config;
7 changes: 7 additions & 0 deletions src/frontend/packages/i18n/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default {
rootDir: './',
testEnvironment: 'node',
transform: {
'^.+\\.(ts)$': 'ts-jest',
},
};
24 changes: 24 additions & 0 deletions src/frontend/packages/i18n/package.json
Original file line number Diff line number Diff line change
@@ -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 changes: 21 additions & 0 deletions src/frontend/packages/i18n/tsconfig.json
Original file line number Diff line number Diff line change
@@ -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"]
}

0 comments on commit dfafd33

Please sign in to comment.