Internationalization with Handlebars - express

I'm trying to internationalize my application that uses Express and Handlebars. Is it possible to get Handlebars partials (fragments) to load and render the localization resource file?
Noting that I've already read this question: express3-handlebars and 18next-node - internationalisation based on page?.
Here is my directory structure:
views/
index.html
login.html
fragments/
frag1.html
frag2.html
frag3.html
locales/
index.json
login.json
fragments/
frag1.json
frag2.json
frag3.json
If necessary, I can separate the JSON files in the locales/ directory to be something like this:
locales/
en-CA/
index.json
...other files
fr-CA/
index.json
...other files
Here is the relevant code in my server.js file:
// ...
hbs = exphbs.create({
extname: '.html',
layoutsDir: [
__dirname + '/views'
],
partialsDir: [
__dirname + '/views/fragments'
],
helpers: {
'json': function(context) {
return JSON.stringify(context);
},
't': function(k) {
// ?
}
}
});
app.engine('.html', hbs.engine);
app.set('view engine', 'html');
The t helper is what I need help with. In my templates/template fragments, I have these:
<h1>{{ t 'pageTitle' }}</h1>
<p>{{ t 'foo' }}</p>
<p>{{ t 'moreThings' }}</p>
And my JSON file could look like this:
{
"pageTitle": "Hello world",
"foo": "Paragraph contents here",
"moreThings": "There are %d things"
}
Also how do I deal with the printf parameters?

Doing internationalization in your application means doing two things:
1) Determine which locale should be used
Depending on how you determine the used locale it can be difficult to do this inside a helper. Helpers do not have access to the request object for instance. To be honest i cannot think of a good way to do this inside a helper.
Personally i use the i18n-abide middle-ware to do internationalization. They have several options to determine the locale for a given request. Once locale is determined it is added as a property to the request object. So you only need to determine the locale once for each request. An other advantage is that you have also access to the locale outside the handlebars helper.
2) Access the resource files
To access the resource files from within a helper means that you should read and parse the resource files outside the helper. Parsing resource files every time you need to translate a string really hurts performance.
Here you also should use middle-ware. You can do something like the pseudo code below.
function setup() {
// Load resource files from disk and parse them.
var resources = { /* parsed resources*/ }
return function(req, res, next) {
var locale = determineLocalFunction(req);
req.getText = function(label) {
return resources[local][label];
}
}
}
Now you can use the req.getText function every where in your code. Personally i never use language labels inside a partial. Instead i pass all the language strings needed in a partial using a data object. The reason behind this is that i think partials should be as re-usable as possible. Using hardcoded language labels inside them makes them less re-useable.
When you do want to use the getText function in your partials you can pass to getText function to your partial.
Something like this:
var objectPassedToPartial = {
getText: req.getText
}
Use it like:
{{getText 'label'}}
Read more about Mozilla's i18n-abide solution, i really love it.

Related

dynamic vue-router base on webpack applications

Good day.
Problem definition:
I would like to deploy my Vue app, that utilizes vue-router, on dynamic path, which should be controlled by the WebServer, utilize vue-router in history mode, and avoid re-packaging the application for each Deployment.
F.e.
Run same app at
http://localhost/subpath/index.html
and
http://localhost/another-subpath/index.html
As vue-router configuration is done at the packaging stage (f.e. by webpack), and thus is not designed to be controlled after the packaging stage, making this simple but so-common setup not viable.
Also, vue-router have quite a complicated lifecycle, which does not allow to easily override that base setting at the application-level.
In addition - webpack ends up hardcoding actual resources into the html body, which ensures their proper consumption by the Client at the load-time, but as chunks are inter-dependent - they are almost impossible to be injected/edited dynamically. Application integrity fails in case of any modification to those on-post-DomReady event.
Research:
My search so far have not yielded any viable options to set such configuration up.
I came up with a couple of viable solutions.
One of the problems is that changing the Router Base is not the only thing one needs, in order to dynamically change the App's root, and make it function properly on dynamic URL.
In case of vue+webpack - actual scripts are added to the index.html on-build-stage, and thus - again - end up being hardcoded.
There are few options of fixing that, which I came up with.
The Ugly solution:
Regex-replace resources URLs at the index.html, on the web-server level, and hardcoded setting for your vue-router-base.
This is very questionable approach, but viable.
Unfortunately I do not have examples of that approach left, so - I can't provide any, but this should be pretty straightforward:
On your servlet-side, Pseudocode:
let fileContent = getFileContent(requested_file_name);
if (servingFile == "index.html") {
fileContent = fileContent.regexReplace("http://url_hardcoded_at_packaging_stage", "http://url_application_is_deployed_at");
fileContent = fileContent.regexReplace("setting_token_in_your_index_html", "http://url_application_is_deployed_at");
}
return fileContent;
In addition to that you will need to add this setting_token_in_your_index_html in your index.html:
<head>
<script type="text/javascript">
window.my_app_settings = {routerBase:"setting_token_in_your_index_html"}
</script>
</head>
And consume it at the vue-router level:
export default new Router({
mode: "history",
base: window.my_app_settings.routerBase,
routes: [...]
...
});
This approach is good in a sense that there is no need to modify anything on your vue-app level, and all the changes can be kept only on your servlet-side, whatever it is.
Also it has 0 performance impact on the vue-app itself.
Still ugly, but a lots better, from code-perspective at least:
Make vue-app base-aware.
This solution is not super-simple, but works reliably on all major web-servers and can be summarized as
Dynamically add vue-resources at the app init, based on cookie provided by the Web-Server.
This approach allows to "elegantly" modify all the resources URLs, with minimum impact on the performance-side, and keep things as dynamic as they can be.
This approach consists of few small changes on the Packaging, and the app-levels, and also relies on cookie to inform the app about custom base.
Provide the custom URL cookie from Server:
At your web-server add the cookie header (all major web-servers support such functionality):
(f.e. in Scalatra)
val contextShiftCookie = "subpath_where_ui_deployed";
val cookie = new Cookie("ui_deployment_root", contextShiftCookie );
cookie.setPath("/");
response.addCookie( cookie );
App modifications (index.html):
At the <head> section:
<script type="text/javascript">
// Build script names, for later injection.
let stringsJs = [
<% for (let js in htmlWebpackPlugin.files.js) { %>
"<%= htmlWebpackPlugin.files.js[js] %>",
<% } %>
];
let stringsCss = [
<% for (let css in htmlWebpackPlugin.files.css) { %>
"<%= htmlWebpackPlugin.files.css[css] %>",
<% } %>
];
// Simple vanilla Cookie getter (replace with something else if needed).
function getCookie(cname) {
var name = cname + "=";
var decodedCookie = decodeURIComponent(document.cookie);
var ca = decodedCookie.split(';');
for(var i = 0; i < ca.length; i++) {
var c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return "";
}
// Simple vanilla onDomReady handler (replace with something else if needed).
(function(exports, d) {
function domReady(fn, context) {
function onReady(event) {
d.removeEventListener("DOMContentLoaded", onReady);
fn.call(context || exports, event);
}
function onReadyIe(event) {
if (d.readyState === "complete") {
d.detachEvent("onreadystatechange", onReadyIe);
fn.call(context || exports, event);
}
}
d.addEventListener && d.addEventListener("DOMContentLoaded", onReady) ||
d.attachEvent && d.attachEvent("onreadystatechange", onReadyIe);
}
exports.domReady = domReady;
})(window, document);
// Calculating vue router base.
let routerBase = "/";
if (getCookie("ui_deployment_root")) {
routerBase = getCookie("ui_deployment_root");
// console.log("Found base cookie", routerBase);
} else {
console.log("No base cookie found");
}
let scriptsBase = routerBase == "/"? "" : routerBase;
// Prefilling basic settings.
window.my_vue_app_config = {
routerBase: routerBase
};
function loadOneScript(){
if (window.my_vue_app_scripts.length > 0) {
let currentScript = window.my_vue_app_scripts.shift();
currentScript.onload = loadOneScript;
document.getElementsByTagName("body").item(0).append(currentScript);
}
}
// Injecting scripts using deployment context.
domReady(function(event) {
let head = document.getElementsByTagName("head").item(0);
stringsCss.forEach(css => {
let script = document.createElement("link");
script.setAttribute("rel","stylesheet");
script.setAttribute("href", scriptsBase + css);
head.append(script);
});
window.my_vue_app_scripts = [];
stringsJs.forEach(js => {
let script = document.createElement("script");
script.setAttribute("type","text/javascript");
script.setAttribute("src", scriptsBase + js);
window.my_vue_app_scripts.push(script);
});
loadOneScript();
});
</script>
Step-by-step explanation:
Consume base setting as-soon-as-it-is-available, BEFORE actual chunks are loaded, to ensure that vue-router can consume it nicely.
Gather all the script names from WebPack (see let stringsJs and let stringsCss)
Load all the css as soon as the DomReady (because css is consumed on-the-fly, and there is nothing specific about loading them. They just should be available). See stringsCss.forEach for that.
Load all the js chunks one-by-one, as this is the only way to ensure that webpack-ed app will be initialized properly (see stringsJs.forEach, and loadOneScript routine).
Scripts loading is done in such manner that each script (chunk) requests another chunk as soon as it is loaded and consumed by the Client (browser). This ensures app integrity, and proper initialization, regardless of how it was packed.
Chunks are processed in proper order, provided by the webpack, again - to ensure integrity.
vue-router changes:
Consume new setting at the router-level:
export default new Router({
mode: "history",
base: window.my_vue_app_config.routerBase,
routes: [...]
...
});
Webpack mechanism changes:
To support custom bundling at the webpack:
<% for (let js in htmlWebpackPlugin.files.js) { %>
"<%= htmlWebpackPlugin.files.js[js] %>",
<% } %>
, you will need to modify the /app/build/webpack.ENV.conf.js file:
...
new HtmlWebpackPlugin({
...
inject: false,
...
}),
Where .ENV. should be your desired bundle target, but I suggest changing this for all your environments, as then you will be sure that your index.html changes work properly on all Environments.
Performance considerations:
This was my biggest concern with this "sequential scripts injection", because it sounds very bad, from performance perspective.
But, to my biggest surprise, actual tests have shown only ~0.100-0.150ms raize in the JavaScript processing time, compared to the fully-static serve of the Application, according to tests on all major Web Clients (browsers).
This basically renders performance concern irrelevant in my case, but we wary that this might differ in your case.
Other impact on the application lifecycle is totally neglectable, at least in my case (less than 0.05ms in overall impact on the App Load).
P.S.
As this approach is something I've developed purely myself - I would appreciate constructive criticism, and improvement proposals :)

Translations Service

I'm looking for a solution/idea to dynamically change the translation value of each key in Sparatcus translations files outside the code. I don't want only to create a file and override the I18nModule config, I'm looking for some kind of service/API like Lokalize API to be able to change the translation values outside the code.
Thanks in advance!
The internationalisation features are prepared for this. Although we do not have a service at hand for the localised labels, Spartacus is prepared for this. You can read more about this at https://sap.github.io/spartacus-docs/i18n/#lazy-loading. You can configure loadPath to an API endpoint, including the variable language (lng) and namespace (ns).
imports: [
B2cStorefrontModule.withConfig({
i18n: {
backend: {
loadPath: 'assets/i18n-assets/{{lng}}/{{ns}}.json'
// crossOrigin: true (use this option when i18n assets come from a different domain)
},
chunks: translationChunksConfig
}
})
];

JSReport External javascript with Json data required

We have hosted jsreport node application on EBS. We created template and using css and javascripts from a static website(hosted internally). In the external javascript file we are using variables similar to what jsreport requires i.e. {{variablename}} which does not work. When we add the javascript inline in the template it works.
We know there should be some other way around to specify this but could not find it.
This won't work. jsreport templating engines only compile and process the html output, not the referenced scripts.
However you can try this approach:
Put a placeholder in a template content where you want to put external script. Lets say we want to put inline jquery
<script>
$$$myScript
</script>
<script>
$(() => {
alert('yes I have jquery inlined')
})
</script>
Create jsreport custom server script which downloads your external script, in this case jquery, and replace the placeholder with its content
var getReq = require('request').get
function beforeRender(req, res, done) {
getReq('https://code.jquery.com/jquery-3.1.0.min.js', (err, res, body) => {
req.template.content = req.template.content.replace('$$$myScript', body.toString())
done()
})
}
The script will run before the templating engines are executed therefore you can use templating engines tags inside it now.
playground live demo here

Compiling Dust templates from Different Sources

I am trying to compile and render a Dust template in Express using content from two different sources:
Dust files located under the /views directory
A response as a string from an external CDN
My goal is to receive a string response from the CDN, which will have content referencing the Dust files stored locally in /views. It will look something like this:
"{>layout/}
{<content}
<h1>Here is the dynamic content that will change based on the CDN request</h1>
{<content}"
The layout.dust file is stored locally under /views, which is referenced from the CDN's string response.
I am trying to compile the string response in my route by doing:
var compiled = dust.compile(templateStr, 'catalog_template');
dust.loadSource(compiled);
dust.render('catalog_template', dustParams, function(err, out) {
if(err) {
console.log(err);
}
console.log(out);
res.render(out);
});
But rendering the file causes an error:
[Error: Template Not Found: layout]
So somehow I need to compile the CDN's string with layout.dust (which is located in my /views directory). What is the best way to do this?
You just need to compile layout.dust by itself and when you render the CDN template, dust will pull in the layout partial. You can have dust dynamically compile templates from your filesystem like so:
dust.onLoad = function (name, callback) {
// Get src from filesystem
fs.readFile('path to template', function (err, data) {
if (err) throw err;
callback(err, data.toString());
});
}
Make sure you require the node fs module in your code.

Rails asset pipeline & coffeescript files, how to bind actions in various files to ajax calls?

Using rails 3 asset pipeline, I've structured the javascript (by using coffeescript) to files regarding the model. For example, all comment writing related javascript is stored to /app/assets/javascripts/comments.js.coffee, and user overlay related (fetching a:href's and triggering ajax on them) are stored in /app/assets/javascripts/users.js.coffee.
However, now I'm using more and more AJAX calls, where HTML content is pulled dynamically to the site. The problem is that I need to execute the javascript in various files, but as coffeescript is scoped inside a function, I can not access them.
Let's say that I've got a general.js.coffee file with following code
$(document).ready ->
# Parse all images with action
$("img.clickableaction").click ->
# Fetch some content
$.ajax
url: "something.php"
dataType: "html"
complete: (xhr, status) ->
# We got the content, set it up to a container
$("#somecontainer").html(receiveddata)
# The tricky part:
# run code in comments.js.coffee for #somecontainer (or for the whole dom)
# run code in users.js.coffee for #somecontainer (or for the whole dom)
And comments.js.coffee contains for example the following:
$(document).ready ->
commentDiv = $('div#commentsContainer')
commentsFetch = $('a.commentsFetch')
# Set commentid for comments fetch
commentsFetch.bind 'ajax:beforeSend', (event, xhr, settings) ->
# do stuff
The comments.js.coffee code works for the initial page view, e.g. the HTML code that was received when user loaded the page. But now, I need to parse the comments.js.coffee code for the content returned from the ajax call.
I can not make a function inside comments.js because it is scoped away, and can not be accessed from the general.js. This is what coffeescript produces:
(function() {
$(document).ready(function() {
var commentDiv, commentsFetch;
commentDiv = $('div#commentsContainer');
commentsFetch = $('a.commentsFetch');
}
})
I could make a global function for each separate file, e.g. window.comments && window.users, but then I'd need to call window.comments from the many places where I need to have ajax oncomplete call. On the long term, that will produce duplicate and hard to maintain -code.
How could something like this be made:
// comments.js
window.ajaxExecuteBlocks.push(function() { // comments related stuff });
// user.js
window.ajaxExecuteBlocks.push(function() { // user related stuff });
// general.js ajax on complete:
window.runExecuteBlocks()
Then, runExecuteBlocks would somehow run through all the functions that have been initialized in various controller-specific javascript files.
Anyone implemented similar system?