Skip to content

Commit

Permalink
Translation enablement
Browse files Browse the repository at this point in the history
* uses (requires) the Globalization Pipeline bound service
* use `npm run gen-i18n` to pickup any changes to index.html
* use localized number formatting on the client side
* escape HTML from `Hey <b>you!</b>` to `Hey {b}you!{/b}` for translation

Fixes: cfsworkload#1
  • Loading branch information
srl295 committed May 25, 2016
1 parent 252e640 commit d65e4da
Show file tree
Hide file tree
Showing 10 changed files with 460 additions and 146 deletions.
228 changes: 124 additions & 104 deletions README.md

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion app.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ initDBConnection();
// create a new express server
var app = express();

app.set('appEnv', appEnv); // save the appEnv for later use
app.set('port', appEnv.port);
app.set('view engine', 'ejs');

Expand Down Expand Up @@ -70,7 +71,9 @@ app.use(express.static(__dirname + '/public'));
app.get('/', function(req, res) {
res.sendfile(__dirname + '/public/index.html');
});


app.use('/i18n', require('./i18n-router')(appEnv).router);

// Create the MQTT server
var mqttServe = new mosca.Server({});

Expand Down
74 changes: 74 additions & 0 deletions i18n-extract.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright (c) 2016 IBM Corp. All rights reserved.
// Use of this source code is governed by the Apache License,
// Version 2.0, a copy of which can be found in the LICENSE file.

// this file extracts source-language (English) content out of the HTML doc
// in order to produce public/scripts/en.json

const jsdom = require('jsdom');
const jquery = require('jquery');
const fs = require('fs');

var virtualConsole = jsdom.createVirtualConsole();
virtualConsole.on("jsdomError", function (error) {
console.error(error.stack, error.detail);
});

const htmlFile = './public/index.html';

jsdom.env({
// url: 'file://' + process.cwd() + '/public/index.html',
html: fs.readFileSync(htmlFile),
scripts: [/*fs.readFileSync(*/'node_modules/jquery/dist/jquery.min.js'/*)*/,
/*fs.readFileSync(*/'./node_modules/jquery-selectorator/dist/selectorator.min.js'/*)*/],
virtualConsole: virtualConsole,
done: function(err, window) {
if(err) {
console.error(err);
return;
}

console.log('read:', htmlFile);
// var $ = jquery(window);
var $ = window.$;

// Now, determine which objects should be extracted
var m = require('./public/scripts/en-extra.json'); // output map. Start with extra list.

function extract(stuff) {
$(stuff).each(function() {
const t = $(this);
if(t.hasClass('no-t')) return; // skip

if (t.text() && t.text() !== '') {
m[t.getSelector()] = t.html()
.replace(/</g,'{')
.replace(/>/g,'}');
}
if (t.attr('title') && t.attr('title') !== '') {
m[ 'title::' + t.getSelector()] = t.attr('title');
}
if (t.attr('placeholder') && t.attr('placeholder') !== '') {
m[ 'placeholder::' + t.getSelector()] = t.attr('placeholder');
}
});
}

// the options
extract($('option'));
// the window title
// extract($('title'));
// buttons
extract($('.t'));

// log it to screen
//console.dir(m);
console.log('Extracted', Object.keys(m).length, 'items');

const jsonFile = 'public/scripts/en.json';
// write the .json version
fs.writeFileSync(jsonFile,
JSON.stringify(m));
console.log('wrote: ', [jsonFile] );
}
});
153 changes: 153 additions & 0 deletions i18n-router.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
// Copyright (c) 2016 IBM Corp. All rights reserved.
// Use of this source code is governed by the Apache License,
// Version 2.0, a copy of which can be found in the LICENSE file.

const detectLocale = require('locale-detector');
const gp = require('g11n-pipeline');
const express = require('express');
const router = express.Router();
const Q = require('q');
const sourceLanguage = 'en';

function getBundleInfoOrCreate(bundle) {
var deferred = Q.defer();
bundle.getInfo({}, function(err, data) {
if(err && (err.toString().indexOf('ResourceNotFoundException') !== -1)) {
// does not exist, create
console.log('g11n-pipeline creating bundle',bundle.id );
return deferred.resolve(Q.ninvoke(bundle, 'create', {
sourceLanguage: sourceLanguage,
targetLanguages: [] // start empty
})
// .then(Q.fcall(function(){
// return bundle;
// })
.then(function() {
var deferred2 = Q.defer();
// console.log('..getting info');
// return Q.ninvoke(bundle, 'getInfo', {}); < did not work?!
// get info again on our newly created bundle
bundle.getInfo({}, function(err2, data2) {
if(err2) return deferred2.reject(err2);
return deferred2.resolve(data2);
});
return deferred2.promise;
}));
}
if(err) return deferred.reject(err);
console.log('g11n-pipeline using existing bundle',bundle.id );
return deferred.resolve(data);
});
return deferred.promise;
}

// need appEnv to get the client.
module.exports = function(appEnv) {
const gpClient = gp.getClient({appEnv: appEnv});
const bundleName = appEnv.name + (appEnv.isLocal?'-local':'');
const bundle = gpClient.bundle(bundleName);
console.log('g11n-pipeline bundle name',bundleName);

// Promise with full bundle info
var bundleInfoPromise =
getBundleInfoOrCreate(gpClient.bundle(bundleName));

console.dir(require('./public/scripts/en.json'));

// Promise for the bundle: for reading, only after bundle is created and populated.
var bundlePromise =
bundleInfoPromise
// upload our strings
.then(function() {
return Q.ninvoke(gpClient.bundle(bundleName),
'uploadStrings',
{languageId: sourceLanguage, strings: require('./public/scripts/en.json')});
})
.then(function() {
return bundle;
});

// A promise for the array of all languages: [ 'en', 'de', … ]
var targetLanguagesPromise =
bundleInfoPromise
.then(function(bundleInfo) {
// extract just the list of languages
var langs = [bundleInfo.sourceLanguage]
.concat(bundleInfo.targetLanguages||[]);
return langs;
});

// Make sure the target langs are ready.
targetLanguagesPromise.then(function(langs) {
if(!langs) throw new Error('No target languages!');
console.log('Bundle ready!', bundleName, 'langs:', langs);
}).done();

// Make sure we can fetch English
bundlePromise
.then(function() {return 'en'; })
.then(fetchBundle)
.done();

/**
* Returns a promise to fetch the specified language bundle.
*/
function fetchBundle(lang) {
return bundlePromise.then(function(bundle){
bundle = gpClient.bundle(bundleName);
if(!bundle) throw new Error('Hey, bundle is null!');
// need to unpack bundle into a 'get' call
// return Q.ninvoke(bundle, 'getStrings', {languageId: lang});
var deferred = Q.defer();
bundle.getStrings({languageId: lang}, function(err, data) {
if(err) return deferred.reject(err);
return deferred.resolve(data);
});
return deferred.promise;
})
.then(function(result) {
// Restructure data
return {
lang: lang,
data: result.resourceStrings
};
});
};


// Test route
router.get('/loctest', function(req, res) {
targetLanguagesPromise.then(function(langs) {
console.log('Langs='+langs);
res.end(detectLocale(req.headers['accept-language'], langs)
|| sourceLanguage); // fallback.
}, function(e) {
res.writeHead(500, {'Content-Type': 'text/plain'});
res.end('Internal Error');
console.error(e);
});
});

// Return the bundle as javascript
router.get('/auto.js', function(req, res) {
targetLanguagesPromise
.then(function(langs) {
var lang = detectLocale(req.headers['accept-language'], langs)
|| sourceLanguage;
return lang;
})
.then(fetchBundle)
.then(function(json) {
res.end('i18n_bundle=' + JSON.stringify(json)+';\n'); // fallback.
}, function(e) {
res.writeHead(500, {'Content-Type': 'text/plain'});
res.end('Internal Error');
console.error(e);
})
.done();
});

return {
router: router
};
}
17 changes: 13 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
{
"name": "blue-messenger",
"version": "0.0.2",
"version": "0.0.3",
"description": "A simple nodejs app for Bluemix",
"scripts": {
"start": "node app.js"
"start": "node app.js",
"gen-i18n": "node i18n-extract.js"
},
"dependencies": {
"ascoltatori": "0.21.0",
Expand All @@ -12,9 +13,12 @@
"ejs": "*",
"express": "4.12.x",
"fs": "*",
"g11n-pipeline": "^1.1.6",
"locale-detector": "^1.0.1",
"morgan": "*",
"mosca": "0.30.x",
"optional": "^0.1.3"
"optional": "^0.1.3",
"q": "^1.4.1"
},
"repository": {
"type": "git",
Expand All @@ -31,5 +35,10 @@
"email": "srloomis@us.ibm.com"
}
],
"license": "Apache-2.0"
"license": "Apache-2.0",
"devDependencies": {
"jquery": "^2.2.3",
"jsdom": "^9.1.0",
"jquery-selectorator": "^0.1.6"
}
}
38 changes: 20 additions & 18 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
<link type="text/css" rel="stylesheet" href="stylesheets/style.css"/>

<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<script src="/i18n/auto.js"></script>
<script src="/scripts/paho.js"></script>
<script src="/scripts/i18n.js"></script>
<script src="/scripts/script.js"></script>
<meta charset="utf-8"/>
</head>
Expand All @@ -18,7 +20,7 @@
<img class="nb-bluemix-logo" src="images/bluemix-logo.png">
</a>
<a class="nb-button-link top_home" >
<span class="nb-devopsservices-text">IBM <b>Bluemix</b> Architecture Center</span>
<span class="t nb-devopsservices-text">IBM <b>Bluemix</b> Architecture Center</span>
</a>
</div>
</div>
Expand All @@ -29,42 +31,42 @@ <h1>Blue Messenger</h1>
<br/>

<div class="col-xs-2">
<label for="rates">Messaging Rate:</label>
<label class="t" for="rates">Messaging Rate:</label>
<select class="form-control input-md" id="rates">
<option>Low</option>
<option>Medium</option>
<option selected>High</option>
<option value="100">Low</option>
<option value="10">Medium</option>
<option value="1" selected>High</option>
</select>
</div>
<div class="col-xs-2">
<label for="times">Duration in Minutes:</label>
<label class="t" for="times">Duration in Minutes:</label>
<select class="form-control input-md" id="times">
<option>1</option>
<option>3</option>
<option selected>5</option>
<option>10</option>
<option class="no-t t-num" value="1">#</option>
<option class="no-t t-num" value="3">#</option>
<option class="no-t t-num" value="5" selected>#</option>
<option class="no-t t-num" value="10">#</option>
</select>
</div>
<br/><br/><br/><br/>

<label for="message">Message:</label>
<textarea class="form-control" rows="8" id="message" placeholder="Write your message here..."></textarea>
<label class="t" for="message">Message:</label>
<textarea class="t form-control" rows="8" id="message" placeholder="Write your message here..."></textarea>
<br/>

<button id="send" class="btn btn-lg" style="margin-bottom:20px">Send a Message</button>
<button id="send" class="t btn btn-lg" style="margin-bottom:20px">Send a Message</button>
<hr/>
</div>

<div class="container" align="center">

<h3>Generate a <span id = "spamrate">high</span> message load for <span id ="duration">5</span> minutes!</h3>
<h3 class="t" >Generate a <span id = "spamrate">high</span> message load for <span id ="duration">5</span> minutes!</h3>
<br/>
<div class="btn-group">
<button id="start" class="btn btn-lg"><abbr title = "Start sending automated messages!">Start</abbr></button>
<button id="stop" class="btn btn-lg" disabled><abbr title = "Stop sending automated messages!">Stop</abbr></button>
<button id="start" class="btn btn-lg"><abbr class="t" title = "Start sending automated messages!">Start</abbr></button>
<button id="stop" class="btn btn-lg" disabled><abbr class="t" title = "Stop sending automated messages!">Stop</abbr></button>
</div>
<br/><br/>
<h4>You have sent <span id="messageCount">0</span> messages!</h4>
<h4 class="t">You have sent <span id="messageCount">0</span> messages!</h4>
</div>
</body>
</html>
</html>
3 changes: 3 additions & 0 deletions public/scripts/en-extra.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"_disconnected": "Disconnected..."
}
1 change: 1 addition & 0 deletions public/scripts/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"_disconnected":"Disconnected...","#rates > option:eq(0)":"Low","#rates > option:eq(1)":"Medium","#rates > option:eq(2)":"High",".nb-devopsservices-text":"IBM {b}Bluemix{/b} Architecture Center","body > div:eq(1) > div:eq(0) > label:eq(0)":"Messaging Rate:","body > div:eq(1) > div:eq(1) > label:eq(0)":"Duration in Minutes:",".container > label.t":"Message:","placeholder::#message":"Write your message here...","#send":"Send a Message","h3.t":"Generate a {span id=\"spamrate\"}high{/span} message load for {span id=\"duration\"}5{/span} minutes!","#start > .t":"Start","title::#start > .t":"Start sending automated messages!","#stop > .t":"Stop","title::#stop > .t":"Stop sending automated messages!","h4.t":"You have sent {span id=\"messageCount\"}0{/span} messages!"}
Loading

0 comments on commit d65e4da

Please sign in to comment.