Skip to content

Commit

Permalink
Add localization support #45
Browse files Browse the repository at this point in the history
  • Loading branch information
gakimball committed Apr 21, 2017
1 parent da203f4 commit 8b2d617
Show file tree
Hide file tree
Showing 10 changed files with 109 additions and 16 deletions.
31 changes: 24 additions & 7 deletions lib/engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ const pify = require('pify');
const load = require('load-whatever');
const Handlebars = require('handlebars');
const extractStack = require('extract-stack');
const folderToObject = require('folder-to-object');
const assign = require('lodash.assign');
const glob = pify(require('glob'));

const readFile = pify(fs.readFile);
Expand Down Expand Up @@ -48,6 +50,10 @@ class PaniniEngine {
return glob(globPath).then(paths => Promise.all(paths.map(p => cb(p))));
}

get i18n() {
return this.locales.length > 0;
}

/**
* Set up common settings for all rendering engines. Because `PaniniEngine` is considered an
* abstract class, this constructor will never be called directly.
Expand All @@ -58,8 +64,13 @@ class PaniniEngine {
throw new TypeError('Do not call the PaniniEngine class directly. Create a sub-class instead.');
}

this.options = options || {};
this.options = assign({
data: 'data',
locales: 'locales'
}, options || {});
this.data = {};
this.locales = [];
this.localeData = {};

if (this.supports('layouts')) {
this.layouts = {};
Expand All @@ -74,12 +85,18 @@ class PaniniEngine {
const extensions = '**/*.{js,json,yml,yaml,cson}';
this.data = {};

return this.constructor.mapPaths(this.options.input, this.options.data, extensions, filePath => {
return load(filePath).then(contents => {
const name = path.basename(filePath, path.extname(filePath));
this.data[name] = contents;
});
});
return Promise.all([
this.constructor.mapPaths(this.options.input, this.options.data, extensions, filePath => {
return load(filePath).then(contents => {
const name = path.basename(filePath, path.extname(filePath));
this.data[name] = contents;
});
}),
folderToObject(path.join(this.options.input, this.options.locales)).then(res => {
this.locales = Object.keys(res);
this.localeData = res;
})
]);
}

/**
Expand Down
20 changes: 15 additions & 5 deletions lib/panini.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const templateHelpers = require('template-helpers');
const handlebarsHelpers = require('handlebars-helpers');
const pathPrefix = require('path-prefix');
const tryRequire = require('try-require');
const translateHelper = require('./translate');
const render = require('./render');
const repeat = require('./repeat');

Expand All @@ -29,6 +30,7 @@ module.exports = class Panini extends EventEmitter {
helpers: 'helpers',
data: 'data',
filters: 'filters',
locales: 'locales',
pageLayouts: {},
engine: 'handlebars',
builtins: true,
Expand Down Expand Up @@ -174,26 +176,34 @@ module.exports = class Panini extends EventEmitter {
// Layout used by this page
layout,
// Path prefix to root directory
root: pathPrefix(file.path, path.join(this.options.input, this.options.pages))
root: pathPrefix(file.path, path.join(this.options.input, this.options.pages)),
// Locale
locale: file.data && file.data.paniniLocale
},
// Helpers
this.getHelpers()
this.getHelpers(file)
);
}

getHelpers() {
getHelpers(file) {
if (!this.options.builtins) {
return {};
}

const coreHelpers = {};

if (this.engine.i18n && file.data && file.data.paniniLocale) {
coreHelpers.translate = translateHelper(this.engine.localeData, file.data.paniniLocale);
}

if (this.options.engine === 'handlebars') {
return {
helpers: assign({repeat}, handlebarsHelpers())
helpers: assign({repeat}, coreHelpers, handlebarsHelpers())
};
}

return {
helpers: templateHelpers()
helpers: assign({}, coreHelpers, templateHelpers())
};
}

Expand Down
38 changes: 36 additions & 2 deletions lib/render.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
'use strict';

const path = require('path');
const fm = require('front-matter');
const through = require('through2');
const assign = require('lodash.assign');
const replaceExt = require('replace-ext');
const pathInsert = require('path-insert');

/**
* Create a transform stream to render a set of pages to HTML.
Expand All @@ -17,10 +19,18 @@ module.exports = function () {
const pages = [];

return through.obj((file, enc, cb) => {
// Wait until the `setup()` function of the rendering engine is done
inst.onReady().then(() => {
inst.emit('parsing');
const page = parse.call(inst, file);
pages.push(page);

if (this.engine.i18n) {
// For sites using i18n, the file is duplicated, once for each locale
const pageList = makeLocaleFiles.call(inst, file).map(f => parse.call(inst, f));
pages.push.apply(pages, pageList);
} else {
const page = parse.call(inst, file);
pages.push(page);
}
cb();
});
}, function (cb) {
Expand All @@ -29,6 +39,30 @@ module.exports = function () {
});
};

/**
* Duplicate the file for a single page so there's one for each locale. The base path of the file is modified to insert the locale as a folder. So, `index.html` becomes `en/index.html`, `es/index.html`, `jp/index.html`, and so on.
* @param {Object} file - Vinyl file.
* @returns {Object[]} Locale-specific versions of file.
*/
function makeLocaleFiles(file) {
const base = path.join(process.cwd(), this.options.input, this.options.pages);

return this.engine.locales.map(locale => {
const newFile = file.clone();

// Insert the language code at the base of the path
newFile.path = pathInsert.start(file.path, base, locale);

// The `paniniLocale` key is used to correctly render translation strings
if (newFile.data) {
newFile.data.paniniLocale = locale;
} else {
newFile.data = {paniniLocale: locale};
}
return newFile;
});
}

/**
* Get the body and data of a page and store it for later rendering to HTML. This is a stream transform function.
* @param {object} file - Vinyl file being parsed.
Expand Down
13 changes: 13 additions & 0 deletions lib/translate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const dotProp = require('dot-prop');

module.exports = (translations, locale) => {
return key => {
const string = dotProp.get(translations[locale], key);

if (typeof string === 'undefined') {
return key;
}

return string;
};
};
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
"chalk": "^1.1.3",
"chokidar": "^1.6.1",
"deepmerge": "^1.3.2",
"dot-prop": "^4.1.1",
"extract-stack": "^1.0.0",
"flexiconfig": "^2.0.0",
"folder-to-object": "^1.0.0",
"front-matter": "^2.0.5",
"glob": "^7.0.0",
"glob-watcher": "^3.1.0",
Expand All @@ -29,6 +31,7 @@
"marked": "^0.3.6",
"meow": "^3.7.0",
"ora": "^1.1.0",
"path-insert": "^1.0.0",
"path-prefix": "^1.0.0",
"pify": "^2.3.0",
"replace-ext": "^1.0.0",
Expand Down
14 changes: 12 additions & 2 deletions test/engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ describe('PaniniEngine', () => {
it('stores options', () => {
const opts = {input: 'src'};
const e = new Engine(opts);
expect(e.options).to.eql(opts);
expect(e.options).to.have.property('input', opts.input);
});

it('creates an object for layouts if the engine supports it', () => {
Expand All @@ -37,9 +37,19 @@ describe('PaniniEngine', () => {
class Engine extends PaniniEngine {}

it('loads data', () => {
const e = new Engine({input: 'test/fixtures/data', data: 'data'});
const e = new Engine({input: 'test/fixtures/data'});
return e.setup().then(() => expect(e.data).to.have.keys(['breakfast']));
});

it('stores list of locales', () => {
const e = new Engine({input: 'test/fixtures/locales'});
return e.setup().then(() => expect(e.locales).to.have.eql(['en', 'jp']));
});

it('loads locale data', () => {
const e = new Engine({input: 'test/fixtures/locales'});
return e.setup().then(() => expect(e.localeData).to.have.keys(['en', 'jp']));
});
});

describe('supports()', () => {
Expand Down
Empty file.
3 changes: 3 additions & 0 deletions test/fixtures/locales/locales/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"panini": "Panini"
}
3 changes: 3 additions & 0 deletions test/fixtures/locales/locales/jp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"panini": "パニニ"
}
Empty file.

0 comments on commit 8b2d617

Please sign in to comment.