Add a command line interface. #3

Merged
merged 7 commits into from May 17, 2012
View
@@ -147,7 +147,95 @@ var js_string_represenation = mf.precompile(
// MessageFormat object. See the source of `MessageFormat.compile` for more details.
```
-## Usage
+### The CLI compiler
+
+If you don't want to compile your templates programmatically, you can use the built in CLI compiler.
+
+This tool is in early stage. It was tested on Linux and Windows, but if you find a bug, please create an issue.
+
+#### Usage
+
+ > [sudo] npm install -g messageformat
+
+ > messageformat
+ Usage: messageformat -l [locale] [INPUT_DIR] [OUTPUT_DIR]
+
+ --locale, -l locale to use [mandatory]
+ --inputdir, -i directory containings messageformat files to compile $PWD
+ --output, -o output where messageformat will be compiled $PWD
+ --watch, -w watch `inputdir` for change false
+ --namespace, -ns object in the browser containing the templates window.i18n
+ --include, -I Glob patterns for files to include in `inputdir` **/*.json
+ --stdout, -s Print the result in stdout instead of writing in a file false
+ --verbose, -v Print logs for debug false
+
+If your prefer looking at an example [go there](messageformat.js/tree/master/example).
+
+
+`messageformat` will read every JSON files in `inputdir` and compile them to `output`.
+
+When using the CLI, the following commands will works exactly the same:
+
+ > messageformat --locale en ./example/en
+ > messageformat --locale en ./example/en ./i18n.js
+ > messageformat --locale en --inputdir ./example/en --output ./i18n.js
+
+or even shorter
+
+ > cd example/en
+ > messageformat -l en
+
+You can also do it with a unix pipe
+
+ > messageformat -l en --stdout > i18n.js
+
+Take a look at the example [inputdir](messageformat.js/tree/master/example/en) and [output](messageformat.js/blob/master/example/en/i18n.js)
+
+A watch mode is available with the `--watch` or `-w` option.
+
+
+#### The JSON messageformat files
+
+The original JSON files are simple objects, with a key and a messageformat string as value, like [this one](messageformat.js/blob/master/example/en/sub/folder/plural.json):
+
+ {
+ "test": "Your {NUM, plural, one{message} other{messages}} go here."
+ }
+
+The CLI walks into `inputdir` recursively so you can structure your messageformat with [dirs and subdirs](messageformat.js/tree/master/example/en).
+
+
+#### In the browser
+
+Now that you have compiled your messageformat, you can use it in your [html](messageformat.js/blob/master/example/index.html) by adding a `<script src="index.js"></script>`.
+
+In the browser, the global `window.i18n` is an object containing the messageformat compiled functions.
+
+ > i18n
+ Object
+ colors: Object
+ blue: [ Function ]
+ green: [ Function ]
+ red: [ Function ]
+ "sub/folder/plural": Object
+ test: [ Function ]
+
+You could then use it:
+
+ $('<div>').text( window.i18n[ 'sub/folder/plural' ].test( { NUM: 1 } ) ).appendTo('#content');
+
+The namespace `window.i18n` could be changed with the `--namespace` or `-ns` option.
+
+Subdirectories messageformat are available in the `window.i18n` namespace, prefixed with their relative path :
+
+ > window.i18n['sub/folder/plural']
+ Object
+ * test: [ Function ]
+
+`sub/folder` is the path, `plural` is the name of [the JSON file](messageformat.js/blob/master/example/en/sub/folder/plural.json), `test` is the key used.
+
+
+A working example is available [here](messageformat.js/tree/master/example).
### No Frills
View
@@ -0,0 +1,191 @@
+#!/usr/bin/env node
+
+var nopt = require("nopt")
+ fs = require('fs'),
+ vm = require('vm'),
+ coffee = require('coffee-script'), /* only for watchr */
+ watch = require('watchr').watch,
+ Path = require('path'),
+ join = Path.join,
+ glob = require("glob"),
+ async = require('async'),
+ MessageFormat = require('../'),
+ _ = require('underscore'),
+ knownOpts = {
+ "locale" : String,
+ "inputdir" : Path,
+ "output" : Path,
+ "watch" : Boolean,
+ "namespace" : String,
+ "include" : String,
+ "stdout" : Boolean,
+ "verbose" : Boolean
+ },
+ description = {
+ "locale" : "locale to use [mandatory]",
+ "inputdir" : "directory containings messageformat files to compile",
+ "output" : "output where messageformat will be compiled",
+ "watch" : "watch `inputdir` for change",
+ "namespace" : "object in the browser containing the templates",
+ "include" : "Glob patterns for files to include in `inputdir`",
+ "stdout" : "Print the result in stdout instead of writing in a file",
+ "verbose" : "Print logs for debug"
+ },
+ defaults = {
+ "inputdir" : process.cwd(),
+ "output" : process.cwd(),
+ "watch" : false,
+ "namespace" : 'window.i18n',
+ "include" : '**/*.json',
+ "stdout" : false,
+ "verbose" : false
+ },
+ shortHands = {
+ "l" : "--locale",
+ "i" : "--inputdir",
+ "o" : "--output",
+ "w" : "--watch",
+ "ns" : "--namespace",
+ "I" : "--include",
+ "s" : "--stdout",
+ "v" : "--verbose"
+ },
+ options = nopt(knownOpts, shortHands, process.argv, 2),
+ argvRemain = options.argv.remain,
+ inputdir;
+
+// defaults value
+_(defaults).forEach(function(value, key){
+ options[key] = options[key] || value;
+})
+
+
+if(argvRemain && argvRemain.length >=1 ) options.inputdir = argvRemain[0];
+if(argvRemain && argvRemain.length >=2 ) options.output = argvRemain[1];
+
+if(!options.locale) {
+ console.error('Usage: messageformat -l [locale] [INPUT_DIR] [OUTPUT_DIR]')
+ console.error('')
+ console.error(nopt.usage(knownOpts, shortHands, description, defaults));
+ process.exit(-1);
+}
+
+var inputdir = options.inputdir;
+
+compile();
+if(options.watch){
+ return watch(options.inputdir, _.debounce(compile, 100));
+}
+
+
+function handleError( err, data ){
+ if(err){
+ err = err.message ? err.message : err;
+ return console.error('--->\t'+ err);
+ }
+}
+
+function compile(){
+ build(inputdir, options, function(err, data){
+ if( err ) return handleError( err );
+ write(data, function(err, output){
+ if( err ) return handleError( err );
+ if( options.verbose ) console.log(output + " written.");
+ })
+ });
+}
+
+function write( data, callback ){
+ data = data.join('\n');
+ if(options.stdout) {
+ return console.log(data);
+ }
+ var output = options.output;
+ fs.stat(output, function(err, stat){
+ if(err){
+ // do nothing
+ }else if(stat.isFile()){
+ // do nothing
+ }else if(stat.isDirectory()){
+ // if `output` is a directory, create a new file called `i18n.js` in this directory.
+ output = join(output, 'i18n.js');
+ }else{
+ return engines.handleError(ouput, 'is not a file nor a directory');
+ }
+
+ fs.writeFile( output, data, 'utf8', function( err ){
+ if( typeof callback == "function" ) callback(err, output);
+ });
+ });
+};
+
+
+
+function build(inputdir, options, callback){
+ // arrays of compiled templates
+ var compiledMessageFormat = [];
+
+ // read locale file
+ var localeFile = join(__dirname, '..', 'locale', options.locale + '.js');
+ if(options.verbose) console.log('Load locale file: ' + localeFile);
+ fs.readFile(localeFile, function(err, localeStr){
+ if(err) handleError(new Error('locale ' + options.locale + ' not supported.' ));
+ var script = vm.createScript(localeStr);
+ // needed for runInThisContext
+ global.MessageFormat = MessageFormat;
+ script.runInThisContext();
+
+ if( options.verbose ) { console.log('Read dir: ' + inputdir); }
+ // list each file in inputdir folder and subfolders
+ glob(options.include, {cwd: inputdir}, function(err, files){
+ files = files.map(function(file){
+ // normalize the file name
+ return file.replace(inputdir, '').replace(/^\//, '');
+ })
+
+ async.forEach(files, readFile, function(err){
+ // errors are logged in readFile. No need to print them here.
+ var fileData = [
+ '(function(){ ' + options.namespace + ' || (' + options.namespace + ' = {}) ',
+ 'var MessageFormat = { locale: {} };',
+ localeStr
+ ].concat(compiledMessageFormat)
+ .concat(['})();']);
+ return callback(null, _.flatten(fileData));
+ });
+
+ // Read each file, compile them, and append the result in the `compiledI18n` array
+ function readFile(file, cb){
+ var path = join(inputdir, file);
+ fs.stat(path, function(err, stat){
+ if(err) { handleError(err); return cb(); }
+ if(!stat.isFile()) {
+ if( options.verbose ) { handleError('Skip ' + file); }
+ return cb();
+ }
+
+ fs.readFile(path, 'utf8', function(err, text){
+ if(err) { handleError(err); return cb() }
+
+ var nm = join(file).split('.')[0].replace(/\\/g, '/'); // windows users should have the same key.
+
+ if( options.verbose ) console.log('Building ' + options.namespace + '["' + nm + '"]');
+ compiledMessageFormat.push(compiler( options, nm, JSON.parse(text) ));
+ cb();
+ });
+ });
+ }
+ });
+ });
+}
+
+function compiler(options, nm, obj){
+ var mf = new MessageFormat(options.locale),
+ compiledMessageFormat = [options.namespace + '["' + nm + '"] = {}'];
+
+ _(obj).forEach(function(value, key){
+ var str = mf.precompile( mf.parse(value) );
+ compiledMessageFormat.push(options.namespace + '["' + nm + '"]["' + key + '"] = ' + str);
+ });
+ return compiledMessageFormat;
+}
@@ -0,0 +1,5 @@
+{
+ "red": "red",
+ "blue": "blue",
+ "green": "green"
+}
View
@@ -0,0 +1,57 @@
+(function(){ window.i18n || (window.i18n = {})
+var MessageFormat = { locale: {} };
+MessageFormat.locale.en = function ( n ) {
+ if ( n === 1 ) {
+ return "one";
+ }
+ return "other";
+};
+
+window.i18n["sub/folder/plural"] = {}
+window.i18n["sub/folder/plural"]["test"] = function(d){
+var r = "";
+r += "Your ";
+if(!d){
+throw new Error("MessageFormat: No data passed to function.");
+}
+var lastkey_1 = "NUM";
+var k_1=d[lastkey_1];
+var off_0 = 0;
+var pf_0 = {
+"one" : function(d){
+var r = "";
+r += "message";
+return r;
+},
+"other" : function(d){
+var r = "";
+r += "messages";
+return r;
+}
+};
+if ( pf_0[ k_1 + "" ] ) {
+r += pf_0[ k_1 + "" ]( d );
+}
+else {
+r += (pf_0[ MessageFormat.locale["en"]( k_1 - off_0 ) ] || pf_0[ "other" ] )( d );
+}
+r += " go here.";
+return r;
+}
+window.i18n["colors"] = {}
+window.i18n["colors"]["red"] = function(d){
+var r = "";
+r += "red";
+return r;
+}
+window.i18n["colors"]["blue"] = function(d){
+var r = "";
+r += "blue";
+return r;
+}
+window.i18n["colors"]["green"] = function(d){
+var r = "";
+r += "green";
+return r;
+}
+})();
@@ -0,0 +1,3 @@
+{
+ "test": "Your {NUM, plural, one{message} other{messages}} go here."
+}
@@ -0,0 +1,5 @@
+{
+ "red": "rouge",
+ "blue": "bleu",
+ "green": "vert"
+}
Oops, something went wrong.