Compiling dustjs in the browser, with i18n, using PayPal's Dust/Makara helpers.
This example uses requirejs/amd for browser JS dependency management.
It also uses Grunt and some very specific tasks as described below.
This app was generated with the following parameters to generator-kraken:
LM-SJN-00872356:krakex medelman$ yo kraken try18n
,'""`.
hh / _ _ \
|(@)(@)| Release the Kraken!
) __ (
/,'))((`.\
(( (( )) ))
`\ `)(' /'
Tell me a bit about your application:
? Description: i18n sample for dust, makara2, requirejs
? Author: Matt Edelman
? Template library? Dust (via Makara 2)
? Include i18n support? Yes
? Front end package manager ? No
? CSS preprocessor library? LESS
? JavaScript library? RequireJSIn order to perform the i18n-enabled compilation in the browser, we need the browser versions of the appropriate dust helpers:
- dust-makara-helpers
- dust-message-helpers
- dust-usecontent-helper
Note that each of the above modules have a browserPackage section in their package.json file, something like this:
"browserPackage": {
"main": "browser.js"
},That is an indicator to copy-browser-modules (see below under Grunt tasks) to copy files out of those modules for
use in the browser.
dustjs-linkedin, which is also required in the browser, does not have this browserPackage property in its
package.json. But never fear, we can add an override in our main package.json as follows:
"browserPackage": {
"overrides": {
"dustjs-linkedin": {
"main": "dist/dust-full"
}
}
}npm i --save-dev grunt-copy-browser-modules grunt-put-packages-in-requirejs-config
npm i --save dustjs-linkedin@~v2.7.0 dust-message-helper requirejs
(make sure you have installed a 2.7 version of dust as breaking changes are possible on minor versions)
The following grunt tasks (and their core functionality, which can be wrapped into gulp or other tasks):
- https://github.com/aredridel/grunt-copy-browser-modules
- https://github.com/aredridel/grunt-put-packages-in-requirejs-config
We will add a new grunt task to the postinstall script in our package.json:
"postinstall": "grunt postinstall"Then, in our Gruntfile, we define the postinstall task:
grunt.registerTask('postinstall', ['copy-browser-modules', 'put-packages-in-requirejs-config']);Task configurations for these tasks are as follows (note: I used grunt-config-dir, so each task is configured by
a separate file):
tasks/copy-browser-modules.js
'use strict';
module.exports = function dustjs(grunt) {
grunt.loadNpmTasks('grunt-copy-browser-modules');
return {
build: {
root: process.cwd(),
dest: 'public/js/components',
basePath: 'public/js'
}
};
};tasks/put-packages-in-requirejs-config
'use strict';
module.exports = function dustjs(grunt) {
grunt.loadNpmTasks('grunt-put-packages-in-requirejs-config');
return {
packages: {
options: {
src: 'public/js/config.js',
dest: 'public/js/_config.js',
packages: 'public/js/components'
}
}
};
};You'll want to be sure you don't check in any generated files to github. So add the following to your .gitignore file:
public/components
public/js/_config.js
To take advantage of the put-packages-in-requirejs-config task, we need to create a base public/js/config.js file:
'use strict';
requirejs.config({
packages: []
});
define.amd.dust = true;put-packages-in-requirejs-config will add the appropriate values in the packages section, and write out as
_config.js, which we will reference in app.js:
'use strict';
require(['_config'], function (config) {
//application code here
});After all of our changes, we can run npm install and note the postinstall tasks output something like the following:
> [email protected] postinstall /Users/medelman/src/krakex/try18n
> grunt postinstall
Running "copy-browser-modules:build" (copy-browser-modules) task
Running "put-packages-in-requirejs-config:packages" (put-packages-in-requirejs-config) task
Done, without errors.Note the new directory public/js/components.
Also note the new file public/js/_config.js, with the populated packages array:
packages: [
{
'name': 'dust-makara-helpers',
'version': '4.1.2',
'location': 'components/dust-makara-helpers',
'main': 'browser.js'
},
{
'name': 'dust-message-helper',
'version': '4.2.1',
'location': 'components/dust-message-helper',
'main': 'index.js'
},
{
'name': 'dust-usecontent-helper',
'version': '4.0.1',
'location': 'components/dust-usecontent-helper',
'main': 'index.js'
},
{
'name': 'dustjs-linkedin',
'version': '2.7.2',
'location': 'components/dustjs-linkedin',
'main': 'dist/dust-full'
}
]We need to alter how we manage our dust templates just a bit. Because dust will try and resolve
partials like "foo/bar/header" as "foo/bar/header.dust". When mixed with requirejs, this means we
need to name our templates with as *.dust.js. To that end, we will use the grunt task called
grunt-dustjs-configurable.
npm install --save-dev grunt-dustjs-configurableReplace the dustjs sub-task in the grunt build task:
grunt.registerTask('build', ['jshint', 'dustjs-configurable', 'makara-amdify', 'less', 'requirejs', 'copyto']);Add the tasks/dustjs-configurable.js file:
'use strict';
var path = require('path');
module.exports = function dustjs(grunt) {
// Load task
grunt.loadNpmTasks('grunt-dustjs-configurable');
// Options
return {
amd: {
files: [
{
expand: true,
cwd: 'public/templates',
src: '**/*.dust',
dest: '.build/js/templates',
ext: '.dust.js'
}
],
options: {
amd: true,
fullname: function (filepath) {
return path.relative('public', filepath);
}
}
}
};
};Now, when we run grunt build we see that our dust templates have been compiled (but not localized) to .build/js/templates.
Create public/templates/example.dust:
{@useContent bundle="example.properties"}
<h3>{@message key="greeting"/}</h3>
{/useContent}Create locales/US/en/example.properties:
greeting=example.dust rendered on the {where}!
Add a "where" property to the server side data model for the default route (models/index.js):
'use strict';
module.exports = function IndexModel() {
return {
name: 'index',
where: 'server'
};
};Add a reference to "example.dust" in "index.dust", and also an empty div which we will use later for browser rendering:
{>"layouts/master" /}
{<body}
<h1>{@pre type="content" key="greeting"/}</h1>
{>"example" /}
<div id="exampletarget"></div>
{/body}You should be able to refresh the default route and see the new partial. Note it is rendering on the server.
First, we need to communicate the current locale, and base path for languagepack files. Replace the existing html tag
with the following:
<html lang="{locale.language}-{locale.country}" data-langpack="{context.links.resourceBaseUrl|s}/{makara.languagePackPath|s}">To support the above changes, we need to write/register a locale middleware and install/register makara-amdify middleware.
First, locale middleware. Create lib/locale.js:
'use strict';
module.exports = function () {
return function (req, res, next) {
res.locals.locale = {
country: 'US',
language: 'en'
};
next();
};
};Second, let's install makara-amdify and register its middleware:
npm install --save makara-amdifyIn config.json let's register both of these middlewares:
"locale": {
"priority": 118,
"enabled": true,
"module": "path:./lib/locale"
},
"makaraAMDify": {
"priority": 119,
"enabled": true,
"module": {
"name": "makara-amdify",
"method": "middleware"
}
},As you may have astutely guessed, makara-amdify requires that a locale be set so it can calculate the appropriate
languagepack path.
Now add the following to "public/js/config.js" (remember not to edit _config.js, as that file is generated by our grunt task above):
paths: { '_languagepack': document.documentElement.getAttribute('data-langpack') }Now, we will replace the current app.js with the following:
'use strict';
require(['_config'], function (config) {
require(['dustjs-linkedin', 'dust-makara-helpers/browser.amd', 'require' /*, Your modules */ ], function(dust, dmh, require) {
// We make our own dust-to-AMD bridge because the built-in one in
// dust 2.7.2 is funky and communicates via the cache.
dust.onLoad = function(name, cb) {
require([name], function (tmpl) {
cb(null, tmpl);
});
};
dmh.registerWith(dust, {
loader: function (context, bundle, cb) {
require(['_languagepack'], function (lp) {
cb(null, lp[getLang()][bundle]);
});
}
});
dust.render('templates/example.dust', {where: 'browser'}, function (err, data) {
if (err) {
console.warn(err);
} else {
document.querySelector('#exampletarget').innerHTML = data;
window.readyToGo = true;
}
});
// Code here
// set a flag to indicate your application is fully ready for interaction
window.readyToGo = false;
//simulate some pre-loading of assets, building of views, etc.
});
function getLang() {
return document.documentElement.getAttribute('lang');
}
});Now build/restart your app and try the default route. You should now see both the server rendered and client rendered version of "example.dust".
You can specify country=FR&language=fr on the query string for the default route to see French content.
We don't want to rebuild our app every time we want to see a change to the client-rendered template or properties file.
So let's install a couple construx modules and configure them for development mode.
npm install --save-dev construx-makara-amdify construx-dustjs-makara-amd-precompile
Now let's configure construx-makara-amdify and re-configure construx-dustjs in development.json:
"makara-amdify": {
"module": "construx-makara-amdify",
"files": "**/_languagepack.js",
"i18n": "config:i18n",
"ext": "js"
},
"dust": {
"module": "construx-dustjs",
"files": "/js/templates/**/*.js",
"base": "templates",
"ext": "dust",
"config": {
"prepend": "",
"append": "",
"amd": true
},
"precompile": "require:construx-dustjs-makara-amd-precompile"
},NOW, you can delete your .build directory, restart your app, and hit the default route. How cool! It Just Works!!