Skip to content

Instantly share code, notes, and snippets.

@timaschew
Last active October 16, 2018 08:10
Show Gist options
  • Save timaschew/a8f4c6bcf5af3f53bd65cd22e9aaa931 to your computer and use it in GitHub Desktop.
Save timaschew/a8f4c6bcf5af3f53bd65cd22e9aaa931 to your computer and use it in GitHub Desktop.
i18n support for panini

i18n datastructure

datastructure need to be created before invoking the panini gulp task

Creating the object from a yml file, for each language

 const i18nArray = ['de_DE', ' en_US'].map(lang => {
    const ymlFilePath = path.join(__dirname, '..', SRC_DIRECTORY, 'data/yml', lang) + '.yml'
    try {
      const i18nData = yaml.safeLoad(fs.readFileSync(ymlFilePath))
      i18nData.__key = lang
      return {
        data: i18nData,
        key: lang,
        delimiter: '-'
      }
    } catch (err) {
      console.error(err)
      throw err
    }
  })

You need to do a loop over the i18nArray and call in each iteration the panini gulp task. Because it's a async loop you need to sync the iterations with this method for instance: https://andreasrohner.at/posts/Web%20Development/Gulp/How-to-synchronize-a-Gulp-task-that-starts-multiple-streams-in-a-loop/

In your hbs partial and helper you can access to a global i18n object which contains the content of the yml file.

var marked = require('marked');
var frontMatter = require('front-matter');
var path = require('path');
var fs = require('fs');
var Handlebars = require('handlebars');
var SRC_DIRECTORY = 'src'
var unescapeHbs = function (content) {
// use a library here which handle all cases
return content.replace(/{{(.*)}}/g, function (a, b) {
var transformed = b
.replace(/>/g, '>')
.replace(/'/g, "'")
.replace(/"/g, '"')
return `{{${transformed} }}`
})
}
/* Markdown wrap as default everything into a paragraph.
* But when you call a hbs helper/partial wrap it with a <div> by default if it's not wrapped already
*/
var wrapHandlebar = function (content) {
return content.replace(/(<.*>)?({{.*}})(<\/.*>)?/g, function (ctx, pre, hbs, post) {
if (pre == null || post === null) {
return '<div>' + hbs + '</div>' // wrap into div
}
return pre + hbs + post // it is wrapped already
})
}
/*
* This is a custom markdown renderer which allows
* to contain HSB syntax in it which are resolved by a sub template
*/
module.exports = function(context) {
var pageName = context.data.root.page;
var directoryOnly = context.data.root.directory;
var i18nKey = context.data.root.i18n.__key
var markdownPath = path.join(SRC_DIRECTORY, 'data/md', directoryOnly, pageName + '-' + i18nKey) + '.md';
var content = fs.readFileSync(markdownPath, {encoding: 'utf8'});
var parsed = frontMatter(content);
var compiled = marked(wrapHandlebar(parsed.body));
// unescape handlebars within markdown
compiled = unescapeHbs(compiled)
// compile handlebars within a markdown again with hbs to render hbs within markdown (partials)
// see http://stackoverflow.com/questions/10537724/handlebars-helper-for-template-composition
// and http://jsfiddle.net/dain/NRjUb/
var subTemplate = Handlebars.compile(compiled);
var subTemplateContext = Object.assign({}, this, context.hash);
return new Handlebars.SafeString(subTemplate(subTemplateContext));
}

origin setup

see: https://github.com/zurb/foundation-zurb-template/tree/master/src

directory hierarchy:

src/
├── assets
│   ├── img
│   ├── js
│   └── scss
├── data
├── layouts
│   └── default.html
├── pages
│   └── index.html
├── partials
└── styleguide
    ├── index.md
    └── template.html

directory hierarchy for i18n:

src/
├── assets
│   ├── img
│   ├── js
│   └── scss
├── data
│   ├── md
│   │   ├── feebdack_de_DE.md
│   │   ├── feebdack_en_US.md
│   │   ├── index_de_DE.md
│   │   └── index_en_US.md
│   └── yml
│       ├── de_DE.yml
│       └── en_US.yml
├── layouts
│   └── default.html
├── pages
│   ├── feedback.html
│   └── index.html
├── partials
│   ├── footer.hbs
│   └── header.hbs
└── styleguide
    ├── index.md
    └── template.html

Files in pages are just entry points, the file content is an empty string. For each of the files in page there will be a lookup for markdown files within data/md with a suffix of a defined list of LANGUAGES, in this case: ['de_DE', 'en_US'].

Within the markdown files you can call handlebars again, for that I added a handlebars helper. If you have a running text you put it into the markdown file itself. For deep structured text you can use a a handlebars partial and a yml file instead.

paninin patch

The exported functino needs to handle the i18n object as a second argument and pass it to the render function. This is the function call in the gulp job.

module.exports = function(options, i18n) {
  var panini = new Panini(options, i18n.data);
  panini.loadBuiltinHelpers();
  panini.Handlebars.registerHelper('renderMarkdown', markdownHbs);
  panini.refresh();
  module.exports.refresh = panini.refresh.bind(panini);
  // Compile pages with the above helpers
  return panini.render(i18n);
}
// this file is copied and modified from github.com/zurb/panini
// https://github.com/zurb/panini/blob/master/lib/render.js
// diff: https://gist.github.com/timaschew/8e8111066bee2e21119e336d658fe4f0
var extend = require('deepmerge');
var fm = require('front-matter');
var path = require('path');
var fs = require('fs');
var gutil = require('gulp-util');
var frontMatter = require('front-matter');
var through = require('through2');
var processRoot = require('panini/lib/processRoot');
var sourceDirectory = 'src';
module.exports = function(i18n) {
if (i18n == null) {
i18n = {}
}
this.i18n = i18n
return through.obj(render.bind(this));
}
/**
* Renders a page with a layout. The page also has access to any loaded partials, helpers, or data.
* @param {object} file - Vinyl file being parsed.
* @param {string} enc - Vinyl file encoding.
* @param {function} cb - Callback that passes the rendered page through the stream.
*/
function render(file, enc, cb) {
try {
// Get the HTML for the current page and layout
var page = fm(file.contents.toString());
var pageData;
// Determine which layout to use
var basePath = path.relative(this.options.root, path.dirname(file.path));
var layout =
page.attributes.layout ||
(this.options.pageLayouts && this.options.pageLayouts[basePath]) ||
'default';
var layoutTemplate = this.layouts[layout];
if (!layoutTemplate) {
if (layout === 'default') {
throw new Error('Panini error: you must have a layout named "default".');
}
else {
throw new Error('Panini error: no layout named "'+layout+'" exists.');
}
}
// Build page data with globals
pageData = extend({}, this.data);
// Add any data from stream plugins
pageData = (file.data) ? extend(pageData, file.data) : pageData;
// Add this page's front matter
pageData = extend(pageData, page.attributes);
var directoryOnly = path.relative(path.join(sourceDirectory, 'pages'), path.relative(process.cwd(), path.dirname(file.path)));
// var directoryOnly = path.relative('templates/src/pages', path.relative(process.cwd(), path.dirname(file.path)));
// Finish by adding constants
pageData = extend(pageData, {
directory: directoryOnly,
page: path.basename(file.path, '.html'),
layout: layout,
root: processRoot(file.path, this.options.root)
});
var originFilePath = file.path // without i18n
var i18nKey = (this.i18n.delimiter || '') + (this.i18n.key || '')
file.path = file.path.substr(0, file.path.length - '.html'.length) + i18nKey + '.html'
var pageName = path.basename(file.path, '.html');
var markdownPath = path.join(process.cwd(), sourceDirectory, '/data/md', directoryOnly, pageName) + '.md';
try {
fs.accessSync(markdownPath, fs.F_OK);
var content = fs.readFileSync(markdownPath, {encoding: 'utf8'});
var parsed = frontMatter(content);
pageData.fm = parsed.attributes;
if (page.body.trim() === '') {
page.body =
'<row>'+
' <columns small="12" large="12">'+
' {{{{renderMarkdown}}}}'+
' {{{{/renderMarkdown}}}}'+
' </columns>'+
'</row>';
}
} catch (err) {
gutil.log(gutil.colors.red('md file not found'), 'for page', gutil.colors.yellow(pageName), 'and language', gutil.colors.yellow(i18nKey));
file.missingMarkdownFile = true;
}
// Now create Handlebars templates out of them
var pageTemplate = this.Handlebars.compile(page.body + '\n');
// Add special ad-hoc partials for #ifpage and #unlesspage
this.Handlebars.registerHelper('ifpage', require('panini/helpers/ifPage')(pageData.page));
this.Handlebars.registerHelper('unlesspage', require('panini/helpers/unlessPage')(pageData.page));
// Finally, add the page as a partial called "body", and render the layout template
this.Handlebars.registerPartial('body', pageTemplate);
file.contents = new Buffer(layoutTemplate(pageData));
}
catch (e) {
if (layoutTemplate) {
// Layout was parsed properly so we can insert the error message into the body of the layout
this.Handlebars.registerPartial('body', 'Panini: template could not be parsed <br> \n <pre>{{error}}</pre>');
file.contents = new Buffer(layoutTemplate({ error: e }));
}
else {
// Not even once - write error directly into the HTML output so the user gets an error
// Maintain basic html structure to allow Livereloading scripts to be injected properly
file.contents = new Buffer('<!DOCTYPE html><html><head><title>Panini error</title></head><body><pre>'+e+'</pre></body></html>');
}
// Log the error into console
console.log('Panini: rendering error ocurred.\n', e);
}
finally {
// This sends the modified file back into the stream
cb(null, file);
}
}
@fredericpfisterer
Copy link

Thanks for sharing. I'll look into it.

@gokhanozdemir
Copy link

Hello Anton,

Looks like a great solution, but I could not make it work. Could you please enlighten more about where to add your fixes? For example I put the code from i18n-data-structure.md into my gulpfile.babel.js before gulp, then I created a new helper in src/helpers from markdown-hbs.js. I also modified render.js according to you files changes but I am not sure where to add the patch you put with panini.md and make it work.

Thanks for sharing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment