diff --git a/public/app/components/LeftMenuFooter/index.js b/public/app/components/LeftMenuFooter/index.js index 6a2ef3ead7..b562dbebfb 100644 --- a/public/app/components/LeftMenuFooter/index.js +++ b/public/app/components/LeftMenuFooter/index.js @@ -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() { diff --git a/public/app/components/LeftMenuFooter/messages.js b/public/app/components/LeftMenuFooter/messages.js deleted file mode 100644 index a869eecad4..0000000000 --- a/public/app/components/LeftMenuFooter/messages.js +++ /dev/null @@ -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 ', - }, -}); diff --git a/public/app/components/LeftMenuFooter/messages.json b/public/app/components/LeftMenuFooter/messages.json new file mode 100644 index 0000000000..1259c5c559 --- /dev/null +++ b/public/app/components/LeftMenuFooter/messages.json @@ -0,0 +1,6 @@ +{ + "poweredBy": { + "id": "app.components.LeftMenuFooter.poweredBy", + "defaultMessage": "Proudly powered by " + } +} diff --git a/public/app/components/ToggleOption/index.js b/public/app/components/ToggleOption/index.js index 488a54be7f..dc1ebbe51e 100644 --- a/public/app/components/ToggleOption/index.js +++ b/public/app/components/ToggleOption/index.js @@ -9,13 +9,16 @@ import { injectIntl, intlShape } from 'react-intl'; const ToggleOption = ({ value, message, intl }) => ( ); 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, }; diff --git a/public/app/containers/HomePage/messages.js b/public/app/containers/HomePage/messages.js deleted file mode 100644 index 7c86832845..0000000000 --- a/public/app/containers/HomePage/messages.js +++ /dev/null @@ -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 !', - }, -}); diff --git a/public/app/containers/LocaleToggle/index.js b/public/app/containers/LocaleToggle/index.js index 9f7e4c0542..5883b99625 100644 --- a/public/app/containers/LocaleToggle/index.js +++ b/public/app/containers/LocaleToggle/index.js @@ -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 (
diff --git a/public/app/containers/LocaleToggle/messages.js b/public/app/containers/LocaleToggle/messages.js deleted file mode 100644 index cefec26ca5..0000000000 --- a/public/app/containers/LocaleToggle/messages.js +++ /dev/null @@ -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) -); diff --git a/public/app/containers/LocaleToggle/tests/messages.test.js b/public/app/containers/LocaleToggle/tests/messages.test.js deleted file mode 100644 index 64e7c670b3..0000000000 --- a/public/app/containers/LocaleToggle/tests/messages.test.js +++ /dev/null @@ -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); - }); -}); diff --git a/public/app/containers/NotFoundPage/index.js b/public/app/containers/NotFoundPage/index.js index 5c45e1372d..34c70b707e 100644 --- a/public/app/containers/NotFoundPage/index.js +++ b/public/app/containers/NotFoundPage/index.js @@ -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 (

- + 404

diff --git a/public/app/containers/NotFoundPage/messages.js b/public/app/containers/NotFoundPage/messages.js deleted file mode 100644 index c22f700c00..0000000000 --- a/public/app/containers/NotFoundPage/messages.js +++ /dev/null @@ -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.', - }, -}); diff --git a/public/app/containers/NotFoundPage/messages.json b/public/app/containers/NotFoundPage/messages.json new file mode 100644 index 0000000000..5926a2ac01 --- /dev/null +++ b/public/app/containers/NotFoundPage/messages.json @@ -0,0 +1,6 @@ +{ + "description": { + "id": "app.components.NotFoundPage.description", + "defaultMessage": "Page not found." + } +} \ No newline at end of file diff --git a/public/app/i18n.js b/public/app/i18n.js index cac61e0c7e..563664f434 100644 --- a/public/app/i18n.js +++ b/public/app/i18n.js @@ -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, }; diff --git a/public/app/translations/en.json b/public/app/translations/en.json index 65494e4dcc..b818de931d 100644 --- a/public/app/translations/en.json +++ b/public/app/translations/en.json @@ -1,6 +1,4 @@ -[ - { - "id": "app.components.LeftMenuFooter.poweredBy", - "defaultMessage": "Powered by " - } -] \ No newline at end of file +{ + "app.components.LeftMenuFooter.poweredBy": "", + "app.components.NotFoundPage.description": "" +} \ No newline at end of file diff --git a/public/app/translations/fr.json b/public/app/translations/fr.json index df6089ec64..6799e05ab3 100644 --- a/public/app/translations/fr.json +++ b/public/app/translations/fr.json @@ -1,6 +1,4 @@ -[ - { - "id": "app.components.LeftMenuFooter.poweredBy", - "defaultMessage": "Propulsé par" - } -] \ No newline at end of file +{ + "app.components.LeftMenuFooter.poweredBy": "Propulsé par", + "app.components.NotFoundPage.description": "Page introuvable" +} \ No newline at end of file diff --git a/public/docs/js/i18n.md b/public/docs/js/i18n.md index 7f2841c983..99f220da22 100644 --- a/public/docs/js/i18n.md +++ b/public/docs/js/i18n.md @@ -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: Max Stoiber,`) are being injected, in this case it's a react component. diff --git a/public/internals/webpack/webpack.dev.babel.js b/public/internals/webpack/webpack.dev.babel.js index f2b626de46..c6791abf7b 100644 --- a/public/internals/webpack/webpack.dev.babel.js +++ b/public/internals/webpack/webpack.dev.babel.js @@ -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)); + }); +}