Commit e20d79da authored by Matija Obreza's avatar Matija Obreza
Browse files

Merge branch '4-i18n-module' into 'master'

Resolve "i18n Module"

Closes #4

See merge request grin-global/grin-global-ui!4
parents f76a050e 33371662
......@@ -103,7 +103,7 @@
"import/no-internal-modules": "off",
"import/order": "off",
"indent": [2, 2, {"SwitchCase": 1}],
"no-restricted-imports": ["error", {"patterns": ["./*", "../*"]}],
"no-restricted-imports": ["error", {"patterns": ["../*"]}],
"max-classes-per-file": [
"error",
10
......
......@@ -7,3 +7,5 @@ node_modules/
*.iml
react-loadable.json
*.snap
packages/client/lib
packages/i18n/lib
......@@ -8,6 +8,13 @@ technicians, curators and managers to interact with the
## Development environment setup
`node >=10.4.1` and `yarn >=1.21.1` are required. See below for installation
instructions.
Run `yarn install` in the project root directory.
### node and yarn
Install `node 10.4.1` or higher:
```bash
......@@ -23,5 +30,4 @@ sudo apt update
sudo apt install yarn
```
Run `yarn run installAndLink` in the project root directory.
......@@ -9,15 +9,16 @@
"url": "https://gitlab.croptrust.org/grin-global/grin-global-ui"
},
"scripts": {
"installAndLink": "yarn install && lerna bootstrap",
"clean": "lerna run clean"
"clean": "lerna run clean",
"postinstall": "lerna bootstrap && lerna run setup && lerna link",
"build": "lerna run build"
},
"workspaces": [
"packages/i18n",
"packages/client"
],
"devDependencies": {
"lerna": "^3.20.2"
"lerna": "^3.0.0"
},
"engines": {
"node": ">=10.4.1",
......
{
"p": {
"welcome": {
"title": "Grin global application"
}
}
}
{
"label": {
"loadingData": "Loading data..."
"loadingData": null
}
}
......@@ -3,10 +3,15 @@
"version": "0.0.1",
"license": "Apache-2.0",
"scripts": {
"clean": "rimraf target",
"clean": "rimraf lib",
"build": "yarn run i18nGenerate && tsc",
"i18nGenerate": "gg-i18n --moduleName=client",
"i18nFindDuplicate": "gg-i18n-dd",
"i18nScan": "lerna run i18n:i18nscan -- '../**/src/**/*.tsx' --defaultNs=client --ns=\"[client, common]\"",
"test": "jest"
},
"dependencies": {
"@gringlobal/i18n": "*",
"axios": "^0.19.2",
"connected-react-router": "^6.6.1",
"cross-env": "^7.0.0",
......@@ -20,6 +25,7 @@
"i18next-xhr-backend": "^3.2.2",
"immutability-helper": "^3.0.1",
"js-md5": "^0.7.3",
"jsonwebtoken": "^8.5.1",
"path": "^0.12.7",
"prop-types": "^15.7.2",
"react": "^16.12.0",
......@@ -36,14 +42,15 @@
"url-template": "^2.0.8"
},
"devDependencies": {
"@gringlobal/i18n": "*",
"@babel/cli": "^7.8.4",
"@babel/core": "^7.8.4",
"@types/enzyme": "^3.10.5",
"@types/jest": "^25.1.2",
"@types/node": "13.5.3",
"@types/react": "16.9.19",
"@types/react-router": "5.1.4",
"@types/react-router-dom": "5.1.3",
"@types/react": "^16.9.0",
"@types/react-router": "^5.1.0",
"@types/react-router-dom": "^5.1.0",
"@types/redux-form": "8.2.0",
"@types/webpack-env": "^1.15.1",
"@typescript-eslint/eslint-plugin": "^2.17.0",
......
{
"p": {
"welcome":{
"title": "Grin global application"
}
}
}
import * as React from 'react';
import { Link } from 'react-router-dom';
import { withTranslation, WithTranslation } from 'react-i18next';
class WelcomePage extends React.Component<any> {
class WelcomePage extends React.Component<WithTranslation> {
public render() {
const { t } = this.props;
return (
<>
<h1>Welcome</h1>
<h1>{ t('p.welcome.title') }</h1>
<ul>
<li><Link to="/user">To user list</Link></li>
<li><Link to="/login">To login page</Link></li>
......@@ -18,4 +20,4 @@ class WelcomePage extends React.Component<any> {
}
export default WelcomePage;
export default withTranslation()(WelcomePage);
......@@ -14,15 +14,16 @@
"node_modules"
]
},
"outDir": "target/web",
"outDir": "./lib",
"module": "esnext",
"target": "esnext",
"jsx": "react",
"moduleResolution": "node"
},
"exclude": [
"test",
"node_modules",
"build",
"lib",
"typings/main",
"typings/main.d.ts"
],
......
const fs = require('fs');
const chalk = require('chalk');
const minimist = require('minimist');
const args = minimist(process.argv.slice(2));
module.exports = {
options: {
......@@ -20,14 +23,13 @@ module.exports = {
// },
lngs: ['en'],
ns: [
'translations',
'common',
...args.ns.replace(/[\[\]]/gi, '').split(', ')
],
defaultNs: 'translations',
defaultNs: args.defaultNs || 'translations',
defaultValue: null, // '__STRING_NOT_TRANSLATED__',
resource: {
loadPath: 'locales/{{lng}}/{{ns}}.json',
savePath: 'locales/{{lng}}/{{ns}}.json'
loadPath: `${process.env.INIT_CWD}/locales/{{lng}}/{{ns}}.json`,
savePath: `${process.env.INIT_CWD}/locales/{{lng}}/{{ns}}.json`
},
nsSeparator: ':', // namespace separator
keySeparator: '.', // key separator
......
......@@ -3,10 +3,14 @@
"version": "0.0.1",
"license": "Apache-2.0",
"scripts": {
"precompile": "yarn run i18n:generateI18n",
"i18n:generateI18n": "node tools/generateI18n.ts",
"i18n:scanI18nDuplicates": "ts-node tools/duplicateDetector.ts",
"i18n:i18nscan": "i18next-scanner --config i18next-scanner.config.js '../**/src/**/*.tsx'"
"setup": "yarn run build",
"clean": "rimraf lib",
"build": "tsc && lerna link",
"i18n:i18nscan": "i18next-scanner --config i18next-scanner.config.js"
},
"bin": {
"gg-i18n": "lib/tools/generateI18n.js",
"gg-i18n-dd": "lib/tools/duplicateDetector.js"
},
"dependencies": {
"axios": "^0.19.2",
......@@ -21,9 +25,9 @@
"@babel/core": "^7.8.4",
"@types/enzyme": "^3.10.5",
"@types/jest": "^25.1.2",
"@types/react": "16.9.19",
"@types/react-router": "5.1.4",
"@types/react-router-dom": "5.1.3",
"@types/react": "^16.9.0",
"@types/react-router": "^5.1.0",
"@types/react-router-dom": "^5.1.0",
"@types/redux-form": "8.2.0",
"@types/webpack-env": "^1.15.1",
"@typescript-eslint/eslint-plugin": "^2.17.0",
......
import languages from '@gringlobal/i18n/data/Languages';
import languages from './data/Languages';
export default function detectLocaleFromPath(virtualPath: string, path: string, index: number) {
if (virtualPath && path.startsWith(virtualPath)) {
......
......@@ -2,9 +2,9 @@
import i18n from 'i18next';
import axios from 'axios';
// eslint-disable-next-line no-restricted-imports
import optionsBase from '@gringlobal/i18n/options-base';
import optionsBase from './options-base';
// eslint-disable-next-line no-restricted-imports
import langDetector from '@gringlobal/i18n/langDetector';
import langDetector from './langDetector';
import XHR from 'i18next-xhr-backend';
import LanguageDetector from 'i18next-browser-languagedetector';
......@@ -22,30 +22,35 @@ const backend = {
const localeMapping = JSON.parse(window.localesMapping);
axios.get(localeMapping[url])
.then((resp) => resp as any)
.then((resp) => callback(JSON.stringify(resp.data), resp));
.then((resp) => callback(JSON.stringify(resp.data), resp))
.catch((err) => { console.log(`Failed to load mapped locale ${url}`, err)});
},
// addPath: '/locales/{{lng}}/{{ns}}.missing.json',
};
const backendKey = 'backend';
optionsBase[backendKey] = backend;
i18n
.use(XHR)
.use(lngDetector)
.use(initReactI18next)
.init({
detection: {
order: ['catalogLangDetector'],
lookupFromPathIndex: 0,
},
react: {
useSuspense: false,
},
...optionsBase,
});
const i18nClient = (modules = []) => {
const backendKey = 'backend';
const optBase = optionsBase(modules);
optBase[backendKey] = backend;
i18n
.use(XHR)
.use(lngDetector)
.use(initReactI18next)
.init({
detection: {
order: [ langDetector.name ],
lookupFromPathIndex: 0,
},
react: {
useSuspense: false,
},
...optBase,
});
return i18n;
};
export { i18n as i18nClient };
export { i18nClient };
// @ts-ignore
import i18n from 'i18next';
import * as path from 'path';
import optionsBase from '@gringlobal/i18n/options-base';
import optionsBase from './options-base';
import Backend from 'i18next-sync-fs-backend';
import { initReactI18next } from 'react-i18next';
......@@ -13,23 +13,30 @@ const backend = {
// addPath: path.resolve('../assets/locales/{{lng}}/{{ns}}.missing.json'),
};
const backendKey = 'backend';
optionsBase[backendKey] = backend;
i18n
.use(Backend)
.use(i18nextMiddleware.LanguageDetector)
.use(initReactI18next)
.init({
detection: {
order: ['path'],
lookupFromPathIndex: 0,
},
react: {
useSuspense: false,
},
...optionsBase,
});
export {i18n as i18nServer} ;
const i18nServer = (modules = []) => {
const backendKey = 'backend';
const optBase = optionsBase(modules);
optBase[backendKey] = backend;
i18n
.use(Backend)
.use(i18nextMiddleware.LanguageDetector)
.use(initReactI18next)
.init({
detection: {
order: ['path'],
lookupFromPathIndex: 0,
},
react: {
useSuspense: false,
},
...optBase,
});
return i18n;
};
export { i18nServer } ;
import detectLocaleFromPath from '@gringlobal/i18n/detectLocaleFromPath';
import detectLocaleFromPath from './detectLocaleFromPath';
export default {
name: 'pathStartLangDetector',
......
const optionsBase = {
const optionsBase = (modules = []) => ({
fallbackLng: 'en',
preload: ['en'],
load: 'languageOnly' as 'languageOnly', // we only provide en, de -> no region specific locals like en-US, de-DE
// have a common namespace used around the full app
ns: ['translations', 'common'],
defaultNS: 'translations',
ns: [...modules],
defaultNS: modules[0] || 'translations',
nsSeparator: ':', // namespace separator
keySeparator: '.', // key separator
saveMissing: false,
......@@ -33,6 +33,6 @@ const optionsBase = {
return value;
},
},
};
});
export default optionsBase;
#!/usr/bin/env node
const fastGlob = require('fast-glob');
const fileStream = require('fs');
const path = require('path');
const readlineSync = require('readline-sync');
const rootDirectory = process.cwd();
class II18n {
public filePath: string;
public key: string;
......@@ -25,8 +29,8 @@ const duplicates = new Map<string, II18n[]>();
async function main() {
console.log('Running...');
const dupes = fastGlob(['./locales/en/common.json', '../../**/src/**/translations.json', '../../**/src/translations.json'])
.then((paths) => paths.map((path) => testFile(path)));
const dupes = fastGlob([`${rootDirectory}/locales/en/common.json`, `${rootDirectory}/src/**/translations.json`, `${rootDirectory}/src/translations.json`])
.then((paths) => paths.filter((path) => !path.includes('node_modules')).map((path) => testFile(path)));
console.log('Awawiting results...', dupes);
await dupes;
......@@ -61,7 +65,9 @@ const testDuplicate = (json, filePath, currentPath = undefined) => {
duplicates.set(str, [ { filePath, key: (currentPath ? `${currentPath}.` : '') + key, value: str } ]);
}
} else {
testDuplicate(value, filePath, (currentPath ? `${currentPath}.` : '') + key);
if (value) {
testDuplicate(value, filePath, (currentPath ? `${currentPath}.` : '') + key);
}
}
});
};
......
#!/usr/bin/env node
const fg = require('fast-glob');
const _ = require('lodash');
const fs = require('fs');
const md5 = require('js-md5');
const mkdirp = require('mkdirp');
const rimraf = require('rimraf');
const Path = require('path');
const minimist = require('minimist');
// console.log('Env', process.env);
// console.log('Args' , process.argv);
// console.log('CWD', process.cwd());
// console.log('env.INIT_CWD', rootDir);
const rootDir = process.cwd();
console.log(`Scanning ${rootDir}`);
const getPrefix = (path) => {
console.log(`Converting ${path}`);
return path.substring(path.indexOf('./src/') + './src/'.length, path.indexOf('/translations.json'));
return path.substring(path.indexOf('/src/') + '/src/'.length, path.indexOf('/translations.json'));
};
const args = minimist(process.argv.slice(2));
const moduleName = args.moduleName;
const includedLocales = args.includedLocales;
// console.log(args);
// TODO fix generated paths
fg(['../../**/src/**/translations.json', '../../**/src/translations.json'])
fg([`${rootDir}/src/**/translations.json`, `${rootDir}/rc/translations.json`])
.then((entries) => {
console.log(`Inspecting ${entries}`);
return entries.filter((e) => ! /\/node_modules\//.test(e));
......@@ -24,33 +42,52 @@ fg(['../../**/src/**/translations.json', '../../**/src/translations.json'])
});
return result;
})
.then((content) => fs.writeFileSync('locales/en/translations.json', JSON.stringify(content, null, 2)))
.then((content) => {
if (!fs.existsSync(`${rootDir}/locales/en`)) {
mkdirp.sync(`${rootDir}/locales/en`); // create dir if no exist
}
fs.writeFileSync(`${rootDir}/locales/en/${moduleName || 'translations'}.json`, JSON.stringify(content, null, 2))
})
.then(() => {
const content = fs.readFileSync('locales/en/translations.json');
const content = fs.readFileSync(`${rootDir}/locales/en/${moduleName || 'translations'}.json`);
JSON.parse(content);
}).catch((error) => {
console.log('Error reading resulting locales/en/translations.json', error);
console.log(`Error reading resulting locales/en/${moduleName || 'translations'}.json`, error);
process.exit(-1);
})
.then(() => generateHashedLocales());
const generateHashedLocales = () => {
rimraf('./generated/locales', (err) => { // delete old generated locales
fg(['./locales/**/translations.json', './locales/**/common.json']) // scan for all translations files
const included = includedLocales
? includedLocales.replace(/[\[\]]/gi, '').split(',')
: [];
rimraf(`${rootDir}/generated/locales`, (err) => { // delete old generated locales
fg([`${rootDir}/locales/**/${moduleName || 'translations'}.json`, ...included]) // scan for all translations files
.then((entries) => {
const localeHashMapping = {}; // map commonPath -> pathWithHash
mkdirp('./generated/locales', () => {
mkdirp(`${rootDir}/generated/locales`, () => {
entries.forEach((path) => {
const fullPath = Path.normalize(Path.isAbsolute(path) ? path : Path.join(process.cwd(), path));
const fileContent = fs.readFileSync(path); // reading content for generating hash
const contentHash = md5(fileContent);
const newFilePath = path.replace('locales', 'generated/locales').replace('.json', `-${ contentHash }.json`); // adding hash before .json
const mappingPath = fullPath.substring(fullPath.indexOf('/locales'));
// console.log(`${path} --> ${fullPath} --> ${mappingPath}`);
const newFilePath = path.substring(path.indexOf('/locales')).replace('locales', 'generated/locales');
const generatedLocaleDirPath = newFilePath.substring(0, newFilePath.lastIndexOf('/')); // getting all but the filename as folder of locale lang
mkdirp.sync(generatedLocaleDirPath); // create dir if no exist
fs.writeFileSync(newFilePath, fileContent, { flag: 'wx' });
localeHashMapping[path.substring(1)] = newFilePath.replace('./generated', ''); // deleting '/generated' from new path for further loading
console.log(`Mapped ${path} to ${newFilePath}`);
mkdirp.sync(`${rootDir}/${generatedLocaleDirPath}`); // create dir if no exist
fs.writeFileSync(`${rootDir}/${newFilePath.replace('.json', `-${ contentHash }.json`)}`, fileContent, { flag: 'wx' }); // adding hash before .json
fs.writeFileSync(`${rootDir}/${newFilePath}`, fileContent, { flag: 'wx' }); // copying old file for backend
localeHashMapping[mappingPath] = newFilePath.replace('/generated', ''); // deleting '/generated' from new path for further loading
// console.log(`Generated ${ newFilePath } for ${ path }`);
if (mappingPath.endsWith(`/${moduleName}.json`)) {
console.log(`Registering 'translations.json' mapping for ${mappingPath}`);
localeHashMapping[mappingPath.replace(`/${moduleName}.json`, '/translations.json')] = localeHashMapping[mappingPath];
}
});
fs.writeFileSync('./generated/locales/localesMapping.json', JSON.stringify(localeHashMapping), { encoding: 'utf8' }); // after all locales moved creating file with mappings
console.log('Locale mappings', localeHashMapping);
fs.writeFileSync(`${rootDir}/generated/locales/localesMapping.json`, JSON.stringify(localeHashMapping), { encoding: 'utf8' }); // after all locales moved creating file with mappings
});
});
});
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment