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:
parent
c71308b90c
commit
5c9a687bb7
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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();
|
|
@ -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']
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
fixes:
|
||||
- |
|
||||
Fixes `bug 1682452 <https://launchpad.net/bugs/1682452>`__
|
||||
Automatically enable all available languages
|
|
@ -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;
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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]
|
||||
);
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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'
|
||||
};
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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: {
|
||||
|
|
Loading…
Reference in New Issue