提交 21d6dc3f 编写于 作者: A Atanas Mirchev

Improved localization through google getMsg()

上级 d2c14c79
......@@ -19,6 +19,9 @@
},
"roboto-fontface": {
"main": "css/roboto-fontface.css"
},
"google-closure-library": {
"main": ["closure/goog/base.js", "closure/goog/deps.js"]
}
},
"devDependencies": {
......
......@@ -22,14 +22,13 @@ import gulpHtmlmin from 'gulp-htmlmin';
import gulpUglify from 'gulp-uglify';
import gulpIf from 'gulp-if';
import gulpUseref from 'gulp-useref';
import gulpRev from 'gulp-rev';
import gulpRevReplace from 'gulp-rev-replace';
import uglifySaveLicense from 'uglify-save-license';
import path from 'path';
import RevAll from 'gulp-rev-all';
import runTasks from 'run-sequence';
import uglifySaveLicense from 'uglify-save-license';
import conf from './conf';
import {multiDest} from './multidest';
/**
* Builds production package for current architecture and places it in the dist directory.
*/
......@@ -40,20 +39,61 @@ gulp.task('build', ['backend:prod', 'build-frontend']);
*/
gulp.task('build:cross', ['backend:prod:cross', 'build-frontend:cross']);
gulp.task(
'build-frontend', ['localize', 'locales-for-backend'], function() { return doRevision(); });
gulp.task('build-frontend:cross', ['localize:cross', 'locales-for-backend:cross'], function() {
return doRevision();
});
/**
* Create a subdirectory for each locale in the default arch directory.
*/
gulp.task('localize', ['distribute-files'], function(doneFn) {
localize([path.join(conf.paths.distPre, conf.arch.default, 'public')], doneFn);
});
/**
* Create a subdirectory for each locale in each of the arch directories.
*/
gulp.task('localize:cross', ['distribute-files:cross'], function(doneFn) {
localize(conf.arch.list.map((arch) => path.join(conf.paths.distPre, arch, 'public')), doneFn);
});
/**
* Copies the locales configuration to the default arch directory.
* This configuration file is then used by the backend to localize dashboard.
*/
gulp.task('locales-for-backend', ['clean-dist'], function() {
return localesForBackend([conf.paths.distPublic]);
});
/**
* Copies the locales configuration to each arch directory.
* This configuration file is then used by the backend to localize dashboard.
*/
gulp.task('locales-for-backend:cross', ['clean-dist'], function() {
return localesForBackend(conf.paths.distPublicCross);
});
/**
* Builds production version of the frontend application for the current architecture.
* Builds production version of the frontend application for the default architecture
* and places it under .tmp, preparing it for localization and revision.
*/
gulp.task('build-frontend', ['fonts', 'icons', 'assets', 'index:prod', 'clean-dist'], function() {
return buildFrontend(conf.paths.distPublic);
gulp.task('distribute-files', ['fonts', 'icons', 'assets', 'index:prod', 'clean-dist'], function() {
return distributeFiles([path.join(conf.paths.distPre, conf.arch.default, 'public')]);
});
/**
* Builds production version of the frontend application for all architecures.
* Builds production versions of the frontend application for all architecures
* and places them under .tmp, preparing them for localization and revision.
*/
gulp.task(
'build-frontend:cross',
['fonts:cross', 'icons:cross', 'assets:cross', 'index:prod', 'clean-dist'],
function() { return buildFrontend(conf.paths.distPublicCross); });
'distribute-files:cross',
['fonts:cross', 'icons:cross', 'assets:cross', 'index:prod', 'clean-dist'], function() {
return distributeFiles(
conf.arch.list.map((arch) => path.join(conf.paths.distPre, arch, 'public')));
});
/**
* Copies assets to the dist directory for current architecture.
......@@ -69,24 +109,22 @@ gulp.task(
/**
* Copies icons to the dist directory for current architecture.
*/
gulp.task('icons', ['clean-dist'], function() { return icons(conf.paths.iconsDistPublic); });
gulp.task('icons', ['clean-dist'], function() { return icons([conf.paths.distPublic]); });
/**
* Copies icons to the dist directory for all architectures.
*/
gulp.task(
'icons:cross', ['clean-dist'], function() { return icons(conf.paths.iconsDistPublicCross); });
gulp.task('icons:cross', ['clean-dist'], function() { return icons(conf.paths.distPublicCross); });
/**
* Copies fonts to the dist directory for current architecture.
*/
gulp.task('fonts', ['clean-dist'], function() { return fonts(conf.paths.fontsDistPublic); });
gulp.task('fonts', ['clean-dist'], function() { return fonts([conf.paths.distPublic]); });
/**
* Copies fonts to the dist directory for all architectures.
*/
gulp.task(
'fonts:cross', ['clean-dist'], function() { return fonts(conf.paths.fontsDistPublicCross); });
gulp.task('fonts:cross', ['clean-dist'], function() { return fonts(conf.paths.distPublicCross); });
/**
* Cleans all build artifacts.
......@@ -98,7 +136,7 @@ gulp.task('clean', ['clean-dist'], function() {
/**
* Cleans all build artifacts in the dist/ folder.
*/
gulp.task('clean-dist', function() { return del([conf.paths.distRoot]); });
gulp.task('clean-dist', function() { return del([conf.paths.distRoot, conf.paths.distPre]); });
/**
* Builds production version of the frontend application.
......@@ -106,12 +144,17 @@ gulp.task('clean-dist', function() { return del([conf.paths.distRoot]); });
* Following steps are done here:
* 1. Vendor CSS and JS files are concatenated and minified.
* 2. index.html is minified.
* 3. CSS and JS assets are suffixed with version hash.
* 4. Everything is saved in the dist directory.
* @param {string|!Array<string>} outputDirs
* 4. Everything is saved in the .tmp/dist directory, ready to be localized and revisioned.
* @param {!Array<string>} outputDirs
* @return {stream}
*/
function buildFrontend(outputDirs) {
function distributeFiles(outputDirs) {
// create an output for each locale
let localizedOutputDirs = outputDirs.reduce((localizedDirs, outputDir) => {
return localizedDirs.concat(
conf.translations.map((translation) => { return path.join(outputDir, translation.key); }));
}, []);
let searchPath = [
// To resolve local paths.
path.relative(conf.paths.base, conf.paths.prodTmp),
......@@ -123,44 +166,106 @@ function buildFrontend(outputDirs) {
.pipe(gulpUseref({searchPath: searchPath}))
.pipe(gulpIf('**/vendor.css', gulpMinifyCss()))
.pipe(gulpIf('**/vendor.js', gulpUglify({preserveComments: uglifySaveLicense})))
.pipe(gulpIf(['**/*.js', '**/*.css'], gulpRev()))
.pipe(gulpUseref({searchPath: searchPath}))
.pipe(gulpRevReplace())
.pipe(gulpIf('*.html', gulpHtmlmin({
removeComments: true,
collapseWhitespace: true,
conservativeCollapse: true,
})))
.pipe(multiDest(outputDirs));
.pipe(multiDest(localizedOutputDirs));
}
/**
* Creates revisions of all .js anc .css files at once (for production).
* Replaces the occurances of those files in index.html with their new names.
* index.html does not get renamed in the process.
* The processed files are then moved to the dist directory.
* @return {stream}
*/
function doRevision() {
// Do not update references other than in index.html. Do not rev index.html itself.
let revAll =
new RevAll({dontRenameFile: ['index.html'], dontSearchFile: [/^(?!.*index\.html$).*$/]});
return gulp.src([path.join(conf.paths.distPre, '**'), '!**/assets/**/*'])
.pipe(revAll.revision())
.pipe(gulp.dest(conf.paths.distRoot));
}
/**
* Replaces the main app.js proprietary logic with a localized version
* for each supported language in each of the arch directories.
* @param {!Array<string>} outputDirs - list of all arch directories
* @param {function(?Error=)} doneFn - callback
*/
function localize(outputDirs, doneFn) {
let tasks = conf.translations.map((translation) => {
let localizedOutputDirs =
outputDirs.map((outputDir) => { return path.join(outputDir, translation.key, 'static'); });
gulp.task(`localize:${translation.key}`, function(doneFn) {
gulp.src(path.join(conf.paths.i18nProd, translation.key, '*.js'))
.pipe(multiDest(localizedOutputDirs, doneFn));
});
return `localize:${translation.key}`;
});
runTasks(tasks, doneFn);
}
/**
* @param {string|!Array<string>} outputDirs
* Copies the locales configuration file at the base of each arch directory, next to
* all of the localized subdirs. This file is meant to be used by the backend binary
* to compare against and determine the right locale to serve at runtime.
* @param {!Array<string>} outputDirs - list of all arch directories
* @return {stream}
*/
function localesForBackend(outputDirs) {
return gulp.src(path.join(conf.paths.base, 'i18n', '*.json')).pipe(multiDest(outputDirs));
}
/**
* Copies the assets files to all dist directories per arch and locale.
* @param {!Array<string>} outputDirs
* @return {stream}
*/
function assets(outputDirs) {
// build for each language and cross-arch
let localizedOutputDirs = outputDirs.reduce((localizedDirs, outputDir) => {
return localizedDirs.concat(
conf.translations.map((translation) => { return path.join(outputDir, translation.key); }));
}, []);
return gulp.src(path.join(conf.paths.assets, '/**/*'), {base: conf.paths.app})
.pipe(multiDest(outputDirs));
.pipe(multiDest(localizedOutputDirs));
}
/**
* @param {string|!Array<string>} outputDirs
* Copies the icons files to all dist directories per arch and locale.
* @param {!Array<string>} outputDirs
* @return {stream}
*/
function icons(outputDirs) {
let localizedOutputDirs = outputDirs.reduce((localizedDirs, outputDir) => {
return localizedDirs.concat(conf.translations.map(
(translation) => { return path.join(outputDir, translation.key, 'static'); }));
}, []);
return gulp
.src(
path.join(conf.paths.materialIcons, '/**/*.+(woff2|woff|eot|ttf)'),
{base: conf.paths.materialIcons})
.pipe(multiDest(outputDirs));
.pipe(multiDest(localizedOutputDirs));
}
/**
* @param {string|!Array<string>} outputDirs
* Copies the font files to all dist directories per arch and locale.
* @param {!Array<string>} outputDirs
* @return {stream}
*/
function fonts(outputDirs) {
let localizedOutputDirs = outputDirs.reduce((localizedDirs, outputDir) => {
return localizedDirs.concat(conf.translations.map(
(translation) => { return path.join(outputDir, translation.key, 'fonts'); }));
}, []);
return gulp
.src(path.join(conf.paths.robotoFonts, '/**/*.+(woff2)'), {base: conf.paths.robotoFonts})
.pipe(multiDest(outputDirs));
.pipe(multiDest(localizedOutputDirs));
}
......@@ -17,6 +17,11 @@
*/
import path from 'path';
/**
* Load the i18n and l10n configuration. Used when dashboard is built in production.
*/
let localization = require('../i18n/locale_conf.json');
/**
* Base path for all other paths.
*/
......@@ -154,6 +159,13 @@ export default {
!!process.env.TRAVIS && process.env.TRAVIS_PULL_REQUEST === 'false',
},
/**
* Configuration for i18n & l10n.
*/
translations: localization.translations.map((translation) => {
return {path: path.join(basePath, 'i18n', translation.file), key: translation.key};
}),
/**
* Absolute paths to known directories, e.g., to source directory.
*/
......@@ -172,21 +184,17 @@ export default {
deploySrc: path.join(basePath, 'src/deploy'),
dist: path.join(basePath, 'dist', arch.default),
distCross: arch.list.map((arch) => path.join(basePath, 'dist', arch)),
distPre: path.join(basePath, '.tmp/dist'),
distPublic: path.join(basePath, 'dist', arch.default, 'public'),
distPublicCross: arch.list.map((arch) => path.join(basePath, 'dist', arch, 'public')),
distRoot: path.join(basePath, 'dist'),
externs: path.join(basePath, 'src/app/externs'),
fontsDistPublic: path.join(basePath, 'dist', arch.default, 'public/fonts'),
fontsDistPublicCross:
arch.list.map((arch) => path.join(basePath, 'dist', arch, 'public/fonts')),
frontendSrc: path.join(basePath, 'src/app/frontend'),
frontendTest: path.join(basePath, 'src/test/frontend'),
goTools: path.join(basePath, '.tools/go'),
goWorkspace: path.join(basePath, '.go_workspace'),
hyperkube: path.join(basePath, 'build/hyperkube.sh'),
iconsDistPublic: path.join(basePath, 'dist', arch.default, 'public/static'),
iconsDistPublicCross:
arch.list.map((arch) => path.join(basePath, 'dist', arch, 'public/static')),
i18nProd: path.join(basePath, '.tmp/i18n'),
integrationTest: path.join(basePath, 'src/test/integration'),
karmaConf: path.join(basePath, 'build/karma.conf.js'),
materialIcons: path.join(basePath, 'bower_components/material-design-icons/iconfont'),
......@@ -198,5 +206,6 @@ export default {
serve: path.join(basePath, '.tmp/serve'),
src: path.join(basePath, 'src'),
tmp: path.join(basePath, '.tmp'),
xtbgenerator: path.join(basePath, '.tools/xtbgenerator/bin/XtbGenerator.jar'),
},
};
// Copyright 2015 Google Inc. All Rights Reserved.
//
// 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.
/**
* @fileoverview Gulp tasks for the extraction of translatable messages.
*/
import childProcess from 'child_process';
import fileExists from 'file-exists';
import gulp from 'gulp';
import gulpUtil from 'gulp-util';
import path from 'path';
import q from 'q';
import conf from './conf';
/**
* Extracts the translatable text messages for the given language key from the pre-compiled
* files under conf.paths.serve.
* @param {string} langKey - the locale key
* @return {!Promise} A promise object.
*/
function extractForLanguage(langKey) {
let deferred = q.defer();
let translationBundle = path.join(conf.paths.base, `i18n/messages-${langKey}.xtb`);
let codeSource = path.join(conf.paths.serve, '*.js');
let command = `java -jar ${conf.paths.xtbgenerator} --lang cs` +
` --xtb_output_file ${translationBundle}` + ` --js ${codeSource}`;
if (fileExists(translationBundle)) {
command = `${command} --translations_file ${translationBundle}`;
}
childProcess.exec(command, function(err, stdout, stderr) {
if (err) {
gulpUtil.log(stdout);
gulpUtil.log(stderr);
deferred.reject();
deferred.reject(new Error(err));
}
return deferred.resolve();
});
return deferred.promise;
}
/**
* Extracts all translation messages into XTB bundles.
*/
gulp.task('extract-translations', ['scripts'], function() {
let promises = conf.translations.map((translation) => extractForLanguage(translation.key));
return q.all(promises);
});
......@@ -27,9 +27,10 @@ import conf from './conf';
* Creates index file in the given directory with dependencies injected from that directory.
*
* @param {string} indexPath
* @param {boolean} dev - development or production build
* @return {!stream.Stream}
*/
function createIndexFile(indexPath) {
function createIndexFile(indexPath, dev) {
let injectStyles = gulp.src(path.join(indexPath, '**/*.css'), {read: false});
let injectScripts = gulp.src(path.join(indexPath, '**/*.js'), {read: false});
......@@ -46,22 +47,27 @@ function createIndexFile(indexPath) {
ignorePath: path.relative(conf.paths.frontendSrc, conf.paths.base),
};
if (dev) {
wiredepOptions.devDependencies = true;
}
return gulp.src(path.join(conf.paths.frontendSrc, 'index.html'))
.pipe(gulpInject(injectStyles, injectOptions))
.pipe(gulpInject(injectScripts, injectOptions))
.pipe(wiredep.stream(wiredepOptions))
.pipe(gulp.dest(indexPath))
.pipe(browserSync.stream());
.pipe(gulp.dest(indexPath));
}
/**
* Creates frontend application index file with development dependencies injected.
*/
gulp.task('index', ['scripts', 'styles'], function() { return createIndexFile(conf.paths.serve); });
gulp.task('index', ['scripts', 'styles'], function() {
return createIndexFile(conf.paths.serve, true).pipe(browserSync.stream());
});
/**
* Creates frontend application index file with production dependencies injected.
*/
gulp.task('index:prod', ['scripts:prod', 'styles:prod'], function() {
return createIndexFile(conf.paths.prodTmp);
return createIndexFile(conf.paths.prodTmp, false);
});
......@@ -40,6 +40,12 @@ function getFileList() {
path.join(conf.paths.frontendTest, '**/*.js'),
path.join(conf.paths.frontendSrc, '**/*.js'),
path.join(conf.paths.frontendSrc, '**/*.html'),
path.join(conf.paths.bowerComponents, 'google-closure-library/closure/goog/base.js'),
{
pattern: path.join(conf.paths.bowerComponents, 'google-closure-library/closure/goog/deps.js'),
included: false,
served: false,
},
]);
}
......@@ -60,7 +66,7 @@ module.exports = function(config) {
// This allows to get elements by selector(angular.element('body')), use find function to
// search elements by class(element.find(class)) and the most important it allows to
// directly test DOM changes on elements, f.e. changes of element width/height.
frameworks: ['jasmine-jquery', 'jasmine', 'browserify'],
frameworks: ['jasmine-jquery', 'jasmine', 'browserify', 'closure'],
browserNoActivityTimeout: 5 * 60 * 1000, // 5 minutes.
......@@ -78,6 +84,7 @@ module.exports = function(config) {
plugins: [
'karma-chrome-launcher',
'karma-closure',
'karma-jasmine',
'karma-jasmine-jquery',
'karma-coverage',
......@@ -148,8 +155,13 @@ module.exports = function(config) {
}
// Convert all JS code written ES6 with modules to ES5 bundles that browsers can digest.
configuration.preprocessors[path.join(conf.paths.frontendTest, '**/*.js')] = ['browserify'];
configuration.preprocessors[path.join(conf.paths.frontendSrc, '**/*.js')] = ['browserify'];
configuration.preprocessors[path.join(conf.paths.frontendTest, '**/*.js')] =
['browserify', 'closure', 'closure-iit'];
configuration.preprocessors[path.join(conf.paths.frontendSrc, '**/*.js')] =
['browserify', 'closure'];
configuration.preprocessors[path.join(
conf.paths.bowerComponents, 'google-closure-library/closure/goog/deps.js')] =
['closure-deps'];
// Convert HTML templates into JS files that serve code through $templateCache.
configuration.preprocessors[path.join(conf.paths.frontendSrc, '**/*.html')] = ['ng-html2js'];
......
......@@ -18,9 +18,10 @@ import through from 'through2';
/**
* Utility function for specifying multiple gulp.dest destinations.
* @param {string|!Array<string>} outputDirs destinations for the gulp dest function calls
* @param {function(?Error=)} doneFn - Callback.
* @return {stream}
*/
export function multiDest(outputDirs) {
export function multiDest(outputDirs, doneFn) {
if (!Array.isArray(outputDirs)) {
outputDirs = [outputDirs];
}
......@@ -30,5 +31,16 @@ export function multiDest(outputDirs) {
outputStream.on('data', (data) => outputs.forEach((dest) => { dest.write(data); }));
outputStream.on('end', () => outputs.forEach((dest) => { dest.end(); }));
// build a closure to track all streams
let stillRunning = outputs.length;
if (doneFn) {
outputs.forEach((output) => output.on('finish', () => {
stillRunning--;
if (stillRunning === 0) {
doneFn();
}
}));
}
return outputStream;
}
......@@ -18,5 +18,10 @@
./node_modules/.bin/bower install --allow-root
# Godep is required by the project. Install it in the tools directory.
# Godep is required by the project. Install it in the .tools directory.
GOPATH=`pwd`/.tools/go go get github.com/tools/godep
# XtbGeneator is required by the project. Clone it into .tools.
if ! [ -a "./.tools/xtbgenerator/bin/XtbGenerator.jar" ]
then
(cd ./.tools/; git clone https://github.com/kuzmisin/xtbgenerator)
fi
......@@ -20,6 +20,7 @@ import gulpAngularTemplatecache from 'gulp-angular-templatecache';
import gulpClosureCompiler from 'gulp-closure-compiler';
import gulpHtmlmin from 'gulp-htmlmin';
import path from 'path';
import runTasks from 'run-sequence';
import webpackStream from 'webpack-stream';
import conf from './conf';
......@@ -53,10 +54,12 @@ gulp.task('scripts', function() {
});
/**
* Compiles frontend JavaScript files into production bundle located in {conf.paths.prodTmp}
* directory.
* Creates a google-closure compilation task in which the .js sources are localized
* for a specific translation / locale.
* @param {!Array<stream>} taskList - existing task list we append to
* @param {Object} translation - translation spec
*/
gulp.task('scripts:prod', ['angular-templates'], function() {
function addCompileTask(taskList, translation) {
let closureCompilerConfig = {
fileName: 'app.js',
// "foo_flag: null" means that a flag is enabled.
......@@ -105,17 +108,46 @@ gulp.task('scripts:prod', ['angular-templates'], function() {
tieredCompilation: true,
};
return gulp
.src([
// Application source files.
path.join(conf.paths.frontendSrc, '**/*.js'),
// Partials generated by other tasks, e.g., Angular templates.
path.join(conf.paths.partials, '**/*.js'),
// Include base.js to enable some compiler functions, e.g., @export annotation handling.
path.join(conf.paths.bowerComponents, 'google-closure-library/closure/goog/base.js'),
])
.pipe(gulpClosureCompiler(closureCompilerConfig))
.pipe(gulp.dest(conf.paths.prodTmp));
if (translation && translation.path) {
closureCompilerConfig.compilerFlags.translations_file = translation.path;
}
let outputDir =
translation ? path.join(conf.paths.i18nProd, `/${translation.key}`) : conf.paths.prodTmp;
// TODO (taimir) : do not run the tasks sequentially once
// gulp-closure-compiler can be run in parallel
gulp.task(
`scripts:${translation ? translation.key : 'default'}`,
taskList.length === 0 ? [] : [taskList[taskList.length - 1]], () => {
return gulp
.src([
// Application source files.
path.join(conf.paths.frontendSrc, '**/*.js'),
// Partials generated by other tasks, e.g., Angular templates.
path.join(conf.paths.partials, '**/*.js'),
// Include base.js to enable some compiler functions, e.g., @export annotation
// handling and getMsg() translations.
path.join(conf.paths.bowerComponents, 'google-closure-library/closure/goog/base.js'),
])
.pipe(gulpClosureCompiler(closureCompilerConfig))
.pipe(gulp.dest(outputDir));
});
taskList.push(`scripts:${translation ? translation.key : 'default'}`);
}
/**
* Compiles frontend JavaScript files into production bundle located in {conf.paths.prodTmp}
* directory. A separated bundle is created for each i18n locale.
*/
gulp.task('scripts:prod', ['angular-templates', 'extract-translations'], function(doneFn) {
let taskList = [];
// add a default compilation
addCompileTask(taskList);
// add a compilation step to stream for each translation file
conf.translations.forEach((translation) => { addCompileTask(taskList, translation); });
runTasks(taskList, doneFn);
});
/**
......
# Using goog.getMsg() for translation
### Localizing dashboard
* The translatable text messages are specified as MSG_<something> attributes of all the controller classes, using goog.getMsg().
* Each variable has a @desc annotation which describes the logical meaning of the text to be translated.
* The supported languages are listed in a separate json file, which is imported into `./build/conf.js` during the build process.
* When doing production builds (e.g. `gulp build-frontend` task), all of the messages are extracted (via XtbGenerator) into the mentioned XTB translation bundles (under the directory `./i18n`, one per language) automatically (`extract-translations` task). Already existing translations are of course not overwritten. Newly found messages are added to the translation bundles. NOTE: AFAICS, old obsolete messages are not automatically removed from the existing bundles => the translators will need to clean them manually, it's the behavior of XtbGenerator. That's the only flaw I've seen so far.
* When building the sources we have the following steps in the build chain:
* Google Closure compiler builds one app.js file for each language, in a separate directory in .tmp, using the respective .xtb translation bundle in the process and injecting the messages.
* Through an intermediate step, the "production-ready" coed is prepared in .tmp, ready to be localized and revisioned.
* Then the localization is done by replacing the app.js files for each locale.
* Finally, throught the `build-frontend` gulp task, the final "compiled" code is moved into `./dist`, keeping the per-language folder structure and is revisioned in the process. The dist folder looks like this:
```
dist/
+-- amd64/
| +-- en/
| | +-- public/
| | +-- index.html <-- uses the assets from static
| | +-- static/
| | +-- vendor.js <--- the packed library deps
| | +-- app.js <--- the localized code (*en* in this case)
| | +-- vendor.css <-- packed vendor .css
| | +-- app.css <-- packed application .css
| +-- ja
| | +-- public/
| | +-- ... same as above...
| |
+-- <other archs> ...
| +-- <locales per arch> ...
|
+-- locale_conf.json
```
* the `locale_conf.json` file is for the dashboard backend. It is used by the backend to determine the right localized version of the `index.html` that should be served.
* The human translators can now easily translate messages in the XTB bundles, and those changes will be picked up by the closure compiler (via our build pipeline) during each production build.
### Serving the localized index.html
* In production, we use our GOLANG backend as a webserver (serves the pages). In development we do not (we serve via browsersync). We do not have internationalization when serving in development mode.
* In production, the GO backend's server component intercepts all requests and checks their "Accept-Language" header, to determine the locale of the user. Based on the locale, the server chooses from which of the `./dist/<locale>` directories to serve. To avoid caching issues in browsers, the "index.html" pages are never cached (header `Cahce-Controle: no-cache` in the requests).
......@@ -29,6 +29,6 @@ import './build/script';
import './build/serve';
import './build/style';
import './build/test';
import './build/i18n';
// No business logic in this file.
{
"translations": [
{"file": "messages-en.xtb", "key": "en"},
{"file": "messages-ja.xtb", "key": "ja"}
]
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE translationbundle>
<translationbundle lang="cs">
<translation id="2869930599919902352" key="MSG_HELLO_WORLD" source="/home/mirchev/go_workspace/src/github.com/kubernetes/dashboard/.tmp/serve/app-dev.js" desc="app name help text in deploy page">An 'app' label with this value will be added to the Replication Controller and Service that get deployed.</translation>
</translationbundle>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE translationbundle>
<translationbundle lang="cs">
<translation id="2869930599919902352" key="MSG_HELLO_WORLD" source="/home/mirchev/go_workspace/src/github.com/kubernetes/dashboard/.tmp/serve/app-dev.js" desc="app name help text in deploy page">Translation in japanese of the app-name text.</translation>
</translationbundle>
\ No newline at end of file
......@@ -63,7 +63,7 @@ func main() {
// Run a HTTP server that serves static public files from './public' and handles API calls.
// TODO(bryk): Disable directory listing.
http.Handle("/", http.FileServer(http.Dir("./public")))
http.HandleFunc("/", LocaleHandler)
http.Handle("/api/", CreateHttpApiHandler(apiserverClient, heapsterRESTClient, config))
// TODO(maciaszczykm): Move to /appConfig.json as it was discussed in #640.
http.Handle("/api/appConfig.json", AppHandler(ConfigHandler))
......
// Copyright 2015 Google Inc. All Rights Reserved.
//
// 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.
package handler
import (
"encoding/json"
"io/ioutil"
"net/http"
"os"
"strings"
"github.com/golang/glog"
)
const defaultDir = "./public/en"
/**
* Localization is a spec for the localization configuration of dashboard.
*/
type Localization struct {
Translations []Translation `json:"translations"`
}
/**
* Translation is a single translation definition spec.
*/
type Translation struct {
File string `json:"file"`
Key string `json:"key"`
}
// LocaleHandler serves different html versions based on the Accept-Language header.
func LocaleHandler(w http.ResponseWriter, r *http.Request) {
if r.URL.EscapedPath() == "/" || r.URL.EscapedPath() == "/index.html" {
// do not store the html page in the cache
w.Header().Add("Cache-Control", "no-store")
}
acceptLanguage := r.Header.Get("Accept-Language")
dirName := determineLocalizedDir(acceptLanguage)
http.FileServer(http.Dir(dirName)).ServeHTTP(w, r)
}
func determineLocalizedDir(locale string) string {
locales, err := getSupportedLocales()
if err != nil {
return defaultDir
}
tokens := strings.Split(locale, "-")
if len(tokens) == 0 {
return defaultDir
}
matchedLocale := ""
for _, l := range locales {
if l == tokens[0] {
matchedLocale = l
}
}
localeDir := "./public/" + matchedLocale
if matchedLocale != "" && dirExists(localeDir) {
return localeDir
}
return defaultDir
}
func dirExists(name string) bool {
if _, err := os.Stat(name); err != nil {
if os.IsNotExist(err) {
return false
}
}
return true
}
func getSupportedLocales() ([]string, error) {
// read config file
localesFile, err := ioutil.ReadFile("./public/locale_conf.json")
if err != nil {
return []string{}, err
}
// unmarshall
localization := Localization{}
err = json.Unmarshal(localesFile, &localization)
if err != nil {
glog.Warning(err)
}
// filter locale keys
result := []string{}
for _, translation := range localization.Translations {
result = append(result, translation.Key)
}
return result, nil
}
......@@ -43,8 +43,7 @@ limitations under the License.
</md-input-container>
<kd-user-help>
An 'app' label with this value will be added to the Replication Controller and Service that get
deployed.
{{ctrl.MSG_HELLO_WORLD}}
<a href="http://kubernetes.io/docs/user-guide/labels/" target="_blank" tabindex="-1">
Learn more <i class="material-icons">open_in_new</i>
</a>
......
......@@ -173,6 +173,14 @@ export default class DeployFromSettingsController {
/** @private {!md.$dialog} */
this.mdDialog_ = $mdDialog;
/**
* @export
* @type string
* @desc app name help text in deploy page
*/
this.MSG_HELLO_WORLD = goog.getMsg(
`An 'app' label with this value will be added to the Replication Controller and Service that get deployed.`);
}
/**
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册