mirror of
https://github.com/strapi/strapi.git
synced 2025-11-13 16:52:18 +00:00
Translate admin panel
This commit is contained in:
parent
560e965747
commit
a570d3f174
@ -5,11 +5,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import messages from './messages';
|
|
||||||
import styles from './styles.scss';
|
import styles from './styles.scss';
|
||||||
import LocaleToggle from 'containers/LocaleToggle';
|
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
|
class LeftMenuFooter extends React.Component { // eslint-disable-line react/prefer-stateless-function
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
@ -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 ',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
6
public/app/components/LeftMenuFooter/messages.json
Normal file
6
public/app/components/LeftMenuFooter/messages.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"poweredBy": {
|
||||||
|
"id": "app.components.LeftMenuFooter.poweredBy",
|
||||||
|
"defaultMessage": "Proudly powered by "
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -9,13 +9,16 @@ import { injectIntl, intlShape } from 'react-intl';
|
|||||||
|
|
||||||
const ToggleOption = ({ value, message, intl }) => (
|
const ToggleOption = ({ value, message, intl }) => (
|
||||||
<option value={value}>
|
<option value={value}>
|
||||||
{intl.formatMessage(message).toUpperCase()}
|
{typeof message === 'string' ? message : intl.formatMessage(message).toUpperCase()}
|
||||||
</option>
|
</option>
|
||||||
);
|
);
|
||||||
|
|
||||||
ToggleOption.propTypes = {
|
ToggleOption.propTypes = {
|
||||||
value: React.PropTypes.string.isRequired,
|
value: React.PropTypes.string.isRequired,
|
||||||
message: React.PropTypes.object.isRequired,
|
message: React.PropTypes.oneOfType([
|
||||||
|
React.PropTypes.object,
|
||||||
|
React.PropTypes.string,
|
||||||
|
]),
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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 !',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@ -11,11 +11,13 @@ import { changeLocale } from '../LanguageProvider/actions';
|
|||||||
import { appLocales } from '../../i18n';
|
import { appLocales } from '../../i18n';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import styles from './styles.scss';
|
import styles from './styles.scss';
|
||||||
import messages from './messages';
|
|
||||||
import Toggle from 'components/Toggle';
|
import Toggle from 'components/Toggle';
|
||||||
|
|
||||||
export class LocaleToggle extends React.Component { // eslint-disable-line
|
export class LocaleToggle extends React.Component { // eslint-disable-line
|
||||||
render() {
|
render() {
|
||||||
|
const messages = {};
|
||||||
|
appLocales.forEach(locale => { messages[locale] = locale.toUpperCase(); });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.localeToggle}>
|
<div className={styles.localeToggle}>
|
||||||
<Toggle values={appLocales} messages={messages} onToggle={this.props.onLocaleToggle} />
|
<Toggle values={appLocales} messages={messages} onToggle={this.props.onLocaleToggle} />
|
||||||
|
|||||||
@ -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)
|
|
||||||
);
|
|
||||||
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -11,9 +11,11 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import messages from './messages';
|
|
||||||
import styles from './styles.scss';
|
import styles from './styles.scss';
|
||||||
import { Link } from 'react-router';
|
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
|
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 (
|
return (
|
||||||
<div className={styles.notFound}>
|
<div className={styles.notFound}>
|
||||||
<h1 className={styles.notFoundTitle}>
|
<h1 className={styles.notFoundTitle}>
|
||||||
<FormattedMessage {...messages.header} />
|
404
|
||||||
</h1>
|
</h1>
|
||||||
<h2 className={styles.notFoundDescription}>
|
<h2 className={styles.notFoundDescription}>
|
||||||
<FormattedMessage {...messages.description} />
|
<FormattedMessage {...messages.description} />
|
||||||
|
|||||||
@ -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.',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
6
public/app/containers/NotFoundPage/messages.json
Normal file
6
public/app/containers/NotFoundPage/messages.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"description": {
|
||||||
|
"id": "app.components.NotFoundPage.description",
|
||||||
|
"defaultMessage": "Page not found."
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,39 +1,38 @@
|
|||||||
/**
|
/**
|
||||||
* i18n.js
|
* 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 { addLocaleData, defineMessages } from 'react-intl';
|
||||||
import frLocaleData from 'react-intl/locale-data/fr';
|
|
||||||
|
|
||||||
export const appLocales = [
|
|
||||||
'en',
|
|
||||||
'fr',
|
|
||||||
];
|
|
||||||
|
|
||||||
import enTranslationMessages from './translations/en.json';
|
import enTranslationMessages from './translations/en.json';
|
||||||
import frTranslationMessages from './translations/fr.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(enLocaleData);
|
||||||
addLocaleData(frLocaleData);
|
addLocaleData(frLocaleData);
|
||||||
|
|
||||||
const formatTranslationMessages = (messages) => {
|
const appLocales = [
|
||||||
const formattedMessages = {};
|
'en',
|
||||||
for (const message of messages) {
|
'fr',
|
||||||
formattedMessages[message.id] = message.message || message.defaultMessage;
|
];
|
||||||
}
|
|
||||||
|
|
||||||
return formattedMessages;
|
|
||||||
};
|
|
||||||
|
|
||||||
const translationMessages = {
|
const translationMessages = {
|
||||||
en: formatTranslationMessages(enTranslationMessages),
|
en: enTranslationMessages,
|
||||||
fr: formatTranslationMessages(frTranslationMessages),
|
fr: frTranslationMessages,
|
||||||
|
};
|
||||||
|
|
||||||
|
const define = messages => {
|
||||||
|
defineMessages(messages);
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
appLocales,
|
||||||
|
define,
|
||||||
translationMessages,
|
translationMessages,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,4 @@
|
|||||||
[
|
{
|
||||||
{
|
"app.components.LeftMenuFooter.poweredBy": "",
|
||||||
"id": "app.components.LeftMenuFooter.poweredBy",
|
"app.components.NotFoundPage.description": ""
|
||||||
"defaultMessage": "Powered by "
|
}
|
||||||
}
|
|
||||||
]
|
|
||||||
@ -1,6 +1,4 @@
|
|||||||
[
|
{
|
||||||
{
|
"app.components.LeftMenuFooter.poweredBy": "Propulsé par",
|
||||||
"id": "app.components.LeftMenuFooter.poweredBy",
|
"app.components.NotFoundPage.description": "Page introuvable"
|
||||||
"defaultMessage": "Propulsé par"
|
}
|
||||||
}
|
|
||||||
]
|
|
||||||
@ -9,7 +9,7 @@ https://github.com/yahoo/react-intl/wiki
|
|||||||
|
|
||||||
## Usage
|
## 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`).
|
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.
|
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.
|
||||||
|
|
||||||
|
|||||||
@ -11,6 +11,7 @@ const cheerio = require('cheerio');
|
|||||||
const pkg = require(path.resolve(process.cwd(), 'package.json'));
|
const pkg = require(path.resolve(process.cwd(), 'package.json'));
|
||||||
const dllPlugin = pkg.dllPlugin;
|
const dllPlugin = pkg.dllPlugin;
|
||||||
const argv = require('minimist')(process.argv.slice(2));
|
const argv = require('minimist')(process.argv.slice(2));
|
||||||
|
const _ = require('lodash');
|
||||||
|
|
||||||
// PostCSS plugins
|
// PostCSS plugins
|
||||||
const cssnext = require('postcss-cssnext');
|
const cssnext = require('postcss-cssnext');
|
||||||
@ -66,6 +67,9 @@ module.exports = require('./webpack.base.babel')({
|
|||||||
|
|
||||||
// Emit a source map for easier debugging
|
// Emit a source map for easier debugging
|
||||||
devtool: 'cheap-module-eval-source-map',
|
devtool: 'cheap-module-eval-source-map',
|
||||||
|
|
||||||
|
// Generate translations files
|
||||||
|
generateTranslationFiles: generateTranslationFiles(),
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -157,3 +161,160 @@ function templateContent() {
|
|||||||
|
|
||||||
return doc.toString();
|
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));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user