Translate admin panel

This commit is contained in:
Pierre Burgy 2016-10-13 20:53:33 +02:00
parent 560e965747
commit a570d3f174
16 changed files with 216 additions and 125 deletions

View File

@ -5,11 +5,12 @@
*/
import React from 'react';
import { FormattedMessage } from 'react-intl';
import messages from './messages';
import styles from './styles.scss';
import LocaleToggle from 'containers/LocaleToggle';
import messages from './messages.json';
import { define } from '../../i18n';
define(messages);
class LeftMenuFooter extends React.Component { // eslint-disable-line react/prefer-stateless-function
render() {

View File

@ -1,13 +0,0 @@
/*
* LeftMenuFooter Messages
*
* This contains all the text for the LeftMenuFooter component.
*/
import { defineMessages } from 'react-intl';
export default defineMessages({
poweredBy: {
id: 'app.components.LeftMenuFooter.poweredBy',
defaultMessage: 'Proudly powered by ',
},
});

View File

@ -0,0 +1,6 @@
{
"poweredBy": {
"id": "app.components.LeftMenuFooter.poweredBy",
"defaultMessage": "Proudly powered by "
}
}

View File

@ -9,13 +9,16 @@ import { injectIntl, intlShape } from 'react-intl';
const ToggleOption = ({ value, message, intl }) => (
<option value={value}>
{intl.formatMessage(message).toUpperCase()}
{typeof message === 'string' ? message : intl.formatMessage(message).toUpperCase()}
</option>
);
ToggleOption.propTypes = {
value: React.PropTypes.string.isRequired,
message: React.PropTypes.object.isRequired,
message: React.PropTypes.oneOfType([
React.PropTypes.object,
React.PropTypes.string,
]),
intl: intlShape.isRequired,
};

View File

@ -1,13 +0,0 @@
/*
* HomePage Messages
*
* This contains all the text for the HomePage component.
*/
import { defineMessages } from 'react-intl';
export default defineMessages({
header: {
id: 'app.components.HomePage.header',
defaultMessage: 'This is HomePage components !',
},
});

View File

@ -11,11 +11,13 @@ import { changeLocale } from '../LanguageProvider/actions';
import { appLocales } from '../../i18n';
import { createSelector } from 'reselect';
import styles from './styles.scss';
import messages from './messages';
import Toggle from 'components/Toggle';
export class LocaleToggle extends React.Component { // eslint-disable-line
render() {
const messages = {};
appLocales.forEach(locale => { messages[locale] = locale.toUpperCase(); });
return (
<div className={styles.localeToggle}>
<Toggle values={appLocales} messages={messages} onToggle={this.props.onLocaleToggle} />

View File

@ -1,21 +0,0 @@
/*
* LocaleToggle Messages
*
* This contains all the text for the LanguageToggle component.
*/
import { defineMessages } from 'react-intl';
import { appLocales } from '../../i18n';
export function getLocaleMessages(locales) {
return locales.reduce((messages, locale) =>
Object.assign(messages, {
[locale]: {
id: `app.components.LocaleToggle.${locale}`,
defaultMessage: `${locale}`,
},
}), {});
}
export default defineMessages(
getLocaleMessages(appLocales)
);

View File

@ -1,21 +0,0 @@
import assert from 'assert';
import { getLocaleMessages } from '../messages';
describe('getLocaleMessages', () => {
it('should create i18n messages for all locales', () => {
const expected = {
en: {
id: 'app.components.LocaleToggle.en',
defaultMessage: 'en',
},
fr: {
id: 'app.components.LocaleToggle.fr',
defaultMessage: 'fr',
},
};
const actual = getLocaleMessages(['en', 'fr']);
assert.deepEqual(expected, actual);
});
});

View File

@ -11,9 +11,11 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import messages from './messages';
import styles from './styles.scss';
import { Link } from 'react-router';
import messages from './messages.json';
import { define } from '../../i18n';
define(messages);
export default class NotFound extends React.Component { // eslint-disable-line react/prefer-stateless-function
@ -21,7 +23,7 @@ export default class NotFound extends React.Component { // eslint-disable-line r
return (
<div className={styles.notFound}>
<h1 className={styles.notFoundTitle}>
<FormattedMessage {...messages.header} />
404
</h1>
<h2 className={styles.notFoundDescription}>
<FormattedMessage {...messages.description} />

View File

@ -1,17 +0,0 @@
/*
* NotFoundPage Messages
*
* This contains all the text for the NotFoundPage component.
*/
import { defineMessages } from 'react-intl';
export default defineMessages({
header: {
id: 'app.components.NotFoundPage.header',
defaultMessage: '404',
},
description: {
id: 'app.components.NotFoundPage.description',
defaultMessage: 'Page not found.',
},
});

View File

@ -0,0 +1,6 @@
{
"description": {
"id": "app.components.NotFoundPage.description",
"defaultMessage": "Page not found."
}
}

View File

@ -1,39 +1,38 @@
/**
* i18n.js
*
* This will setup the i18n language files and locale data for your app.
* This will setup the i18n language files and locale data for your plugin.
*
*/
import { addLocaleData } from 'react-intl';
import enLocaleData from 'react-intl/locale-data/en';
import frLocaleData from 'react-intl/locale-data/fr';
export const appLocales = [
'en',
'fr',
];
import { addLocaleData, defineMessages } from 'react-intl';
import enTranslationMessages from './translations/en.json';
import frTranslationMessages from './translations/fr.json';
import enLocaleData from 'react-intl/locale-data/en';
import frLocaleData from 'react-intl/locale-data/fr';
addLocaleData(enLocaleData);
addLocaleData(frLocaleData);
const formatTranslationMessages = (messages) => {
const formattedMessages = {};
for (const message of messages) {
formattedMessages[message.id] = message.message || message.defaultMessage;
}
return formattedMessages;
};
const appLocales = [
'en',
'fr',
];
const translationMessages = {
en: formatTranslationMessages(enTranslationMessages),
fr: formatTranslationMessages(frTranslationMessages),
en: enTranslationMessages,
fr: frTranslationMessages,
};
const define = messages => {
defineMessages(messages);
};
export {
appLocales,
define,
translationMessages,
};

View File

@ -1,6 +1,4 @@
[
{
"id": "app.components.LeftMenuFooter.poweredBy",
"defaultMessage": "Powered by "
}
]
{
"app.components.LeftMenuFooter.poweredBy": "",
"app.components.NotFoundPage.description": ""
}

View File

@ -1,6 +1,4 @@
[
{
"id": "app.components.LeftMenuFooter.poweredBy",
"defaultMessage": "Propulsé par"
}
]
{
"app.components.LeftMenuFooter.poweredBy": "Propulsé par",
"app.components.NotFoundPage.description": "Page introuvable"
}

View File

@ -9,7 +9,7 @@ https://github.com/yahoo/react-intl/wiki
## Usage
Below we see a `messages.js` file for the `Footer` component example. A `messages.js` file should be included in any simple or container component that wants to use internationalization. You can add this support when you scaffold your component using this boilerplates scaffolding `plop` system.
Below we see a `messages.json` file for the `Footer` component example. A `messages.json` file should be included in any simple or container component that wants to use internationalization. You can add this support when you scaffold your component using this boilerplates scaffolding `plop` system.
All default English text for the component is contained here (e.g. `This project is licensed under the MIT license.`), and is tagged with an ID (e.g. `boilerplate.components.Footer.license.message`) in addition to it's object definition id (e.g. `licenseMessage`).
@ -39,7 +39,7 @@ export default defineMessages({
});
```
Below is the example `Footer` component. Here we see the component including the `messages.js` file, which contains all the default component text, organized with ids (and optionally descriptions). We are also importing the `FormattedMessage` component, which will display a given message from the `messages.js` file in the selected language.
Below is the example `Footer` component. Here we see the component including the `messages.json` file, which contains all the default component text, organized with ids (and optionally descriptions). We are also importing the `FormattedMessage` component, which will display a given message from the `messages.json` file in the selected language.
You will also notice a more complex use of `FormattedMessage` for the author message where alternate or variable values (i.e. `author: <A href="https://twitter.com/mxstbr">Max Stoiber</A>,`) are being injected, in this case it's a react component.

View File

@ -11,6 +11,7 @@ const cheerio = require('cheerio');
const pkg = require(path.resolve(process.cwd(), 'package.json'));
const dllPlugin = pkg.dllPlugin;
const argv = require('minimist')(process.argv.slice(2));
const _ = require('lodash');
// PostCSS plugins
const cssnext = require('postcss-cssnext');
@ -66,6 +67,9 @@ module.exports = require('./webpack.base.babel')({
// Emit a source map for easier debugging
devtool: 'cheap-module-eval-source-map',
// Generate translations files
generateTranslationFiles: generateTranslationFiles(),
});
/**
@ -157,3 +161,160 @@ function templateContent() {
return doc.toString();
}
/**
* Generate translation files according
* to `messages.json` included in the app.
*/
function generateTranslationFiles() {
// App directory
const appDirectory = path.resolve(process.cwd(), 'app');
// Find `message.json` files in the app
findMessagesFiles(appDirectory, (err, messageFiles) => {
if (err) {
return;
}
// Format messages found
const messagesFormatted = formatMessages(messageFiles);
// Get the list of languages supported by this plugin
const pluginLanguages = getPluginLanguages();
// Get current translations values
const currentTranslationsValues = getCurrentTranslationsValues(pluginLanguages);
// Update translations values
const updatedTranslationValues = getUpdatedTranslationValues(currentTranslationsValues, messagesFormatted);
// Write files according to updated translations values
writeTranslationFiles(pluginLanguages, updatedTranslationValues);
});
}
/**
* Find `message.json` files in the app.
*
* @param cb {Function} Callback
*/
function findMessagesFiles(appDir, cb) {
// Name of the messages files
const messagesFileName = 'messages.json';
// App directory
// const dir = path.resolve(process.cwd(), 'app');
// Results
let results = {};
// Parallel search
fs.readdir(appDir, (err, list) => {
if (err) {
return cb(err);
}
let pending = list.length;
if (!pending) {
return cb(null, results);
}
return list.forEach(fileName => {
const filePath = path.resolve(appDir, fileName);
fs.stat(filePath, (errStat, stat) => {
if (stat && stat.isDirectory()) {
findMessagesFiles(filePath, (errFind, res) => {
// Merge with the previous results
results = _.merge(results, res);
if (!--pending) {
cb(null, results);
}
});
} else {
if (fileName === messagesFileName) {
results[filePath] = JSON.parse(fs.readFileSync(filePath, 'utf8'));
}
if (!--pending) {
cb(null, results);
}
}
});
});
});
}
/**
* Returns the list of languages supported by the plugin.
*
* @returns {Array} List of languages support by the plugin
*/
function getPluginLanguages() {
return fs.readdirSync(path.resolve(process.cwd(), 'app', 'translations')).map(fileName => (fileName.replace('.json', '')));
}
/**
* Returns the list of current translations values.
*
* @param languages {Array} List of languages support by the plugin
* @returns {Object} Current translation values
*/
function getCurrentTranslationsValues(languages) {
const currentTranslationsValues = {};
_.forEach(languages, language => {
currentTranslationsValues[language] = JSON.parse(fs.readFileSync((path.resolve(process.cwd(), 'app', 'translations', `${language}.json`)), 'utf8'));
});
return currentTranslationsValues;
}
/**
* Format messages.
*
* @param messageFiles {Object}
* @returns {Object} Messages formatted
*/
function formatMessages(messageFiles) {
const messagesFormatted = {};
_.forEach(messageFiles, messageFile => {
_.forEach(messageFile, message => {
messagesFormatted[message.id] = message.defaultMessage;
});
});
return messagesFormatted;
}
/**
* Merge current translations with new ones.
*
* @param currentTranslationsValues {Object} Current translations value
* @param messagesFormatted {Object} Messages formatted from `messages.json` files
* @returns {Object} Translations values updated
*/
function getUpdatedTranslationValues(currentTranslationsValues, messagesFormatted) {
const updatedTranslationValues = {};
_.forEach(currentTranslationsValues, (value, language) => {
updatedTranslationValues[language] = {};
// Sort the messages and assigns the values
Object.keys(messagesFormatted).sort().forEach(id => {
updatedTranslationValues[language][id] = currentTranslationsValues[language][id] || '';
});
});
return updatedTranslationValues;
}
/**
* Overwrite translations files according to updated values
*
* @param languages {Array} The list of languages supported by the plugin
* @param updatedTranslationValues {Object} Translations values updated
*/
function writeTranslationFiles(languages, updatedTranslationValues) {
_.forEach(languages, (language) => {
fs.writeFileSync(path.resolve(path.resolve(process.cwd(), 'app', 'translations', `${language}.json`)), JSON.stringify(updatedTranslationValues[language], null, 2));
});
}