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));
+ });
+}