Automatically enable all available languages

Right now, we have to create at least two patches for each new language that
becomes available: one to update the tripleo-ui itself, and one to update the
defaults in the puppet code.  This is painful and takes a long time.

This patch turns things around.  Instead of specifying which languages should be
available, we automatically load all of them, and provide users with a way of
disabling some of them should they need to.  When a new language is imported
from the translation team into our code, there is no work necessary.

This is accomplished via Webpack plugin which reads the i18n/locales directory,
parses the data, and generates a Javascript file with the necessary imports and
other useful objects.  The "languages" setting in the config file is changed to
"excludedLanguages", and it's a simple list of language abbreviations.

Futhermore, this patch adds a simple script that verifies that all available
languages are properly enabled i the constants file.

Closes-Bug: #1682452
Change-Id: Idf5a3314c19be18ca6cabbae1e94bc7cb1d1fe94
This commit is contained in:
Honza Pokorny 2017-04-13 11:28:16 -03:00
parent c71308b90c
commit 5c9a687bb7
15 changed files with 229 additions and 120 deletions

1
.gitignore vendored
View File

@ -13,6 +13,7 @@ messages.pot
i18n/extracted-messages*
npm-debug.log
docs/_build
src/js/components/i18n/messages.js
# Files created by releasenotes build
releasenotes/build

21
bin/run_lint.sh Executable file
View File

@ -0,0 +1,21 @@
#!/usr/bin/env bash
# Copyright 2017 Red Hat Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
set -e
eslint --max-warnings 0 src
prettier --single-quote --list-different 'src/**/*.js'
./bin/verify-languages.js

61
bin/verify-languages.js Executable file
View File

@ -0,0 +1,61 @@
#!/usr/bin/env node
/**
* Copyright 2017 Red Hat Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License. You may obtain
* a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
const fs = require('fs');
const babel = require('babel-core');
const _ = require('lodash');
const constants = './src/js/constants/i18n.js';
function getConstants() {
const transformed = babel.transformFileSync(constants).code;
const languageNames = eval(transformed).LANGUAGE_NAMES;
delete languageNames['en'];
return Object.keys(languageNames);
}
function getLocaleFiles() {
return fs.readdirSync('i18n/locales').map(function(el) {
return el.split('.')[0];
});
}
function showError(constants, locales) {
let diff, name;
if (constants.length > locales.length) {
diff = _.difference(constants, locales);
name = 'Locales';
} else {
diff = _.difference(locales, constants);
name = 'Constants';
}
console.log(name, 'is missing', diff.join(', '));
}
function main() {
const constants = getConstants();
const localeFiles = getLocaleFiles();
if (constants.length !== localeFiles.length) {
showError(constants, localeFiles);
process.exit(1);
}
}
main();

View File

@ -16,17 +16,11 @@ window.tripleOUiConfig = {
// 'zaqar_default_queue': 'tripleo',
// Languages
// If you choose more than one language, a language switcher will appear in the navigation bar.
// Only 'en' (English) is enabled by default.
// 'languages': {
// 'de': 'German',
// 'en': 'English',
// 'es': 'Spanish',
// 'id': 'Indonesian',
// 'ja': 'Japanese',
// 'ko-KR': 'Korean',
// 'zh-CN': 'Simplified Chinese'
// },
//
// By default, all available languages are enabled. Use this setting to
// disable certain languages.
//
// 'excludedLanguages': ['de', 'ja'],
// Logging
// 'loggers': ['console']

View File

@ -41,82 +41,5 @@ one JSON file per language (Japanese in this example):
Adding a new language
---------------------
The languages are defined and activated in 2 places. Additionally, the
puppet-tripleo module also needs to be updated for users installing the
UI via the ``openstack undercloud install`` command.
1. The ``I18nProvider`` component
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
To add a new language, import the relevant locale data from the ``react-intl``
package, as well as the JSON file which contains the translation. The new
language then needs to be added to the ``MESSAGES`` constant and the
constructor. Here's an example for Japanese ("ja"):
.. code-block:: js
// ./src/js/components/i18n/I18nProvider
import ja from 'react-intl/locale-data/ja';
import jaMessages from '../../../../i18n/locales/ja.json';
const MESSAGES = {
'ja': jaMessages['ja']
};
class I18nProvider extends React.Component {
constructor() {
super();
addLocaleData([...ja]);
}
2. The ``tripleo-ui`` configuration file
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Finally, you can choose which languages are offered to the user by adding them
to the ``tripleo_ui_config.js`` file:
.. code-block:: js
// Languages
// If you choose more than one language, a language switcher
// will appear in the navigation bar.
'languages': {
'en': 'English',
'ja': 'Japanese'
},
The last step is useful if a language has not -- or only partially -- been
translated yet. In this case an incomplete language can be defined in the app as
part of a regular release, but will not show up in the selector by default. Once
the language translation has been completed it can more easily be backported
mid-release by updating only the corresponding JSON file.
3. The puppet UI manifest
~~~~~~~~~~~~~~~~~~~~~~~~~
When deploying the UI as part of a normal undercloud install, the
configuration file is created and managed by the `puppet-tripleo`_
module. The `manifest for the UI`_ must be modified in two places:
.. code-block:: puppet
# ./manifests/ui.pp
# [*enabled_languages*]
# Which languages to show in the UI.
# An array.
# Defaults to ['en', 'ja']
[...]
$enabled_languages = {
'en' => 'English',
'ja' => 'Japanese'
}
.. _puppet-tripleo: http://git.openstack.org/cgit/openstack/puppet-tripleo
.. _manifest for the UI: http://git.openstack.org/cgit/openstack/puppet-tripleo/tree/manifests/ui.pp
When a new language is added, we need to update the ``src/constants/i18n.js``
file with the name and abbreviation of the language.

View File

@ -70,7 +70,7 @@
"webpack-merge": "^4.1.0"
},
"scripts": {
"lint": "eslint --max-warnings 0 src && prettier --single-quote --list-different 'src/**/*.js'",
"lint": "./bin/run_lint.sh",
"prettier": "prettier --write --single-quote 'src/**/*.js'",
"build": "webpack --env=prod --bail --progress",
"build:dev": "webpack --env=dev --bail --progress",

View File

@ -0,0 +1,5 @@
---
fixes:
- |
Fixes `bug 1682452 <https://launchpad.net/bugs/1682452>`__
Automatically enable all available languages

View File

@ -14,11 +14,11 @@
* under the License.
*/
import { getAppConfig } from '../services/utils';
import { getEnabledLanguages } from '../services/utils';
export default {
detectLanguage(messages) {
const configLanguages = getAppConfig().languages;
const configLanguages = getEnabledLanguages();
let language;
// If the configuration contains only one language and there
// are messages for it, return it;

View File

@ -20,7 +20,7 @@ import React from 'react';
import { Link } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { getAppConfig } from '../services/utils';
import { getEnabledLanguages } from '../services/utils';
import NavTab from './ui/NavTab';
import I18nDropdown from './i18n/I18nDropdown';
@ -52,7 +52,7 @@ export default class NavBar extends React.Component {
}
_renderLanguageDropdown() {
const languages = getAppConfig().languages || {};
const languages = getEnabledLanguages();
// Only include the I18nDropdown if there's more than one
// language to choose from.

View File

@ -22,9 +22,9 @@ import React from 'react';
import Dropdown from '../ui/dropdown/Dropdown';
import DropdownToggle from '../ui/dropdown/DropdownToggle';
import DropdownItem from '../ui/dropdown/DropdownItem';
import { getAppConfig } from '../../services/utils';
import { getEnabledLanguages } from '../../services/utils';
import I18nActions from '../../actions/I18nActions';
import { MESSAGES } from './I18nProvider';
import { MESSAGES } from './messages';
const messages = defineMessages({
language: {
@ -35,7 +35,7 @@ const messages = defineMessages({
class I18nDropdown extends React.Component {
_renderDropdownItems() {
const configLanguages = getAppConfig().languages || {};
const configLanguages = getEnabledLanguages();
const langList = Object.keys(configLanguages).sort(
(a, b) => configLanguages[a] > configLanguages[b]
);

View File

@ -18,36 +18,17 @@ import { addLocaleData, IntlProvider } from 'react-intl';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import React from 'react';
import de from 'react-intl/locale-data/de';
import es from 'react-intl/locale-data/es';
import id from 'react-intl/locale-data/id';
import ja from 'react-intl/locale-data/ja';
import ko from 'react-intl/locale-data/ko';
import zh from 'react-intl/locale-data/zh';
import I18nActions from '../../actions/I18nActions';
import deMessages from '../../../../i18n/locales/de.json';
import esMessages from '../../../../i18n/locales/es.json';
import idMessages from '../../../../i18n/locales/id.json';
import jaMessages from '../../../../i18n/locales/ja.json';
import kokrMessages from '../../../../i18n/locales/ko-KR.json';
import zhcnMessages from '../../../../i18n/locales/zh-CN.json';
// NOTE(hpokorny): src/components/i18n/messages.js is generated by webpack on the fly
import { MESSAGES, LOCALE_DATA } from './messages';
import { getLanguage, getMessages } from '../../selectors/i18n';
export const MESSAGES = {
de: deMessages['de'],
es: esMessages['es'],
id: idMessages['id'],
ja: jaMessages['ja'],
'ko-KR': kokrMessages['ko-KR'],
'zh-CN': zhcnMessages['zh-CN']
};
class I18nProvider extends React.Component {
constructor() {
super();
addLocaleData([...de, ...es, ...id, ...ja, ...ko, ...zh]);
addLocaleData(LOCALE_DATA);
}
componentDidMount() {

11
src/js/constants/i18n.js Normal file
View File

@ -0,0 +1,11 @@
export const LANGUAGE_NAMES = {
en: 'English',
de: 'German',
'en-GB': 'British English',
es: 'Spanish',
id: 'Indonesian',
ja: 'Japanese',
'ko-KR': 'Korean',
'tr-TR': 'Turkish',
'zh-CN': 'Simplified Chinese'
};

97
src/js/plugins/i18n.js Normal file
View File

@ -0,0 +1,97 @@
/* I18nPlugin for Webpack
* ======================
*
* This plugin is used to automatically enable all available languages. It
* looks in the "localePath" configuration variable (which in our case is
* "i18n/locales"), and reads the files in that directory. It then generates a
* Javascript file with all the necessary imports and other useful objects.
* This file is placed in "src/js/components/i18n/messages.js". You can see a
* sample of this file below.
*
* import deMessages from '../../../../i18n/locales/de.json';
* import esMessages from '../../../../i18n/locales/es.json';
*
* import de from 'react-intl/locale-data/de';
* import es from 'react-intl/locale-data/es';
*
* export const MESSAGES = {
* 'de': deMessages['de'],
* 'es': esMessages['es']
* };
*
*
* export const LOCALE_DATA = [...de, ...es];
*/
var fs = require('fs');
var I18nPlugin = function(options) {
this.options = options;
};
I18nPlugin.prototype.apply = function(compiler) {
var files = fs.readdirSync(this.options.localePath);
var locales = files.map(function(file) {
return file.replace('.json', '');
});
var localesLower = locales.map(function(locale) {
return locale.toLowerCase().replace('-', '');
});
var localesSimple = locales.map(function(locale) {
return locale.split('-')[0];
});
var messagesImport = localesSimple
.map(function(locale, index) {
var lower = localesLower[index];
var full = locales[index];
return (
'import ' +
lower +
"Messages from '../../../../i18n/locales/" +
full +
".json';"
);
})
.join('\n');
var reactIntlImport = localesSimple
.map(function(locale, index) {
return (
'import ' + locale + " from 'react-intl/locale-data/" + locale + "';"
);
})
.join('\n');
var messages = locales
.map(function(locale, index) {
var lower = localesLower[index];
return "'" + locale + "': " + lower + "Messages['" + locale + "']";
})
.join(',\n ');
var messagesObj = 'export const MESSAGES = {\n ' + messages + '\n}';
var localeData = localesSimple
.map(function(locale) {
return '...' + locale;
})
.join(', ');
var localeDataObj = 'export const LOCALE_DATA = [' + localeData + '];';
var file =
messagesImport +
'\n\n' +
reactIntlImport +
'\n\n' +
messagesObj +
'\n\n' +
localeDataObj;
fs.writeFileSync('src/js/components/i18n/messages.js', file);
};
module.exports = I18nPlugin;

View File

@ -16,6 +16,7 @@
import { Map, List } from 'immutable';
import store from '../store';
import { LANGUAGE_NAMES } from '../constants/i18n';
/**
* Returns the public url of an openstack API,
@ -61,3 +62,13 @@ export function getProjectId() {
export function getAppConfig() {
return window.tripleOUiConfig || {};
}
export function getEnabledLanguages() {
const excludedLanguages = getAppConfig().excludedLanguages || [];
let configLanguages = Object.assign({}, LANGUAGE_NAMES);
excludedLanguages.map(language => {
delete configLanguages[language];
});
return configLanguages;
}

View File

@ -1,5 +1,6 @@
require('es6-promise').polyfill(); // https://github.com/webpack/css-loader/issues/144
const HtmlWebpackPlugin = require('html-webpack-plugin');
const I18nPlugin = require('./src/js/plugins/i18n');
module.exports = {
entry: __dirname + '/src/js/index.js',
@ -12,6 +13,9 @@ module.exports = {
plugins: [
new HtmlWebpackPlugin({
template: 'src/index.html'
}),
new I18nPlugin({
localePath: 'i18n/locales'
})
],
module: {