index.js
function xgettext (template, options) {
  options = options || {};
  var identifiers = options.identifiers || Catalog.DEFAULT_IDENTIFIERS;
  var defaultDomain = options.defaultDomain || 'messages';
  var commentIdentifiers = options.commentIdentifiers || ['gettext-comment'];
  var filename = options.filename;

  // sanity check identifiers
  Object.keys(identifiers).forEach(function (id) {
    if (identifiers[id].indexOf('msgid') === -1) {
      throw new Error('Every id must have a msgid parameter, but "' + id + '" doesn\'t have one');
    }
  });

  var msgs = {};

  var Visitor = Handlebars.Visitor;

  function GettextExtractor () {}
  GettextExtractor.prototype = new Visitor();

  var extract = function (statement) {
    var path = statement.path;
    var params = statement.params;

    if (Object.keys(identifiers).indexOf(path.original) === -1) {
      return;
    }

    var spec = identifiers[path.original];
    var msgidParam = params[spec.indexOf('msgid')];

    if (!msgidParam) {
      throw new Error('No msgid');
    }

    if (msgidParam.type !== 'StringLiteral') {
      // don't extract variables
      return;
    }

    var msgid = msgidParam.value;
    var contextIndex = spec.indexOf('msgctxt');

    var context = null; // null context is *not* the same as empty context
    if (contextIndex >= 0) {
      var contextParam = params[contextIndex];
      if (!contextParam) {
        // throw an error if there's supposed to be a context but not enough
        // parameters were passed to the handlebars helper
        throw new Error('Expected a context for msgid "' + msgid + '" but none was given');
      }
      if (contextParam.type !== 'StringLiteral') {
        // Don't extract if context isn't a string literal.
        // Hopefully they've statically defined this message somewhere else
        return;
      }

      context = contextParam.value;
    }

    var domain = defaultDomain;
    var domainIndex = spec.indexOf('domain');
    if (domainIndex !== -1) {
      var domainParam = params[domainIndex];
      if (!domainParam) {
        throw new Error('Expected a domain for msgid "' + msgid + '" but none was given');
      }
      if (domainParam.type !== 'StringLiteral') {
        // don't extract if domain isn't a string literal
        return;
      }

      domain = domainParam.value;
    }

    var key = Catalog.messageToKey(msgid, context);

    var pluralIndex = spec.indexOf('msgid_plural');
    if (pluralIndex !== -1) {
      var pluralParam = params[pluralIndex];
      if (!pluralParam) {
        throw new Error('No plural specified for msgid "' + msgid + '"');
      }
      if (pluralParam.type !== 'StringLiteral') {
        return;
      }

      if (msgs[domain] && msgs[domain][key]) {
        // there's an existing message, check it's plural form to make sure it matches
        var plural = pluralParam.value;
        var existingPlural = msgs[domain][key].msgid_plural;

        if (plural && existingPlural && existingPlural !== plural) {
          throw new Error('Incompatible plural definitions for msgid "' + msgid +
            '" ("' + existingPlural + '" and "' + plural + '")');
        }
      }
    }

    // only add message to catalog after we've sanity checked it
    msgs[domain] = msgs[domain] || {};
    msgs[domain][key] = msgs[domain][key] || {
      extractedComments: [],
      references: [],
      fields: {} // extracted fields get placed here so they don't clobber anything
    };
    var message = msgs[domain][key];

    // return AST so consumer has as much information as possible
    message.ast = statement;

    var loc = statement.loc;
    message.references.push({
      filename: filename,
      start: {
        line: loc.start.line,
        column: loc.start.column
      },
      end: {
        line: loc.end.line,
        column: loc.end.column
      }
    });

    spec.forEach(function (prop, i) {
      var param = params[i];
      if (!param) {
        return;
      }

      var knownFields = Catalog.DEFAULT_IDENTIFIERS.dcnpgettext;
      if (knownFields.indexOf(prop) !== -1) {
        // field name doesn't conflict with anything, we can save it at top level
        message[prop] = params[i].value;
      }

      // save all fields under .fields to prevent collisions
      message.fields[prop] = params[i].value;
    });

    // extract comments
    params.forEach(function (param) {
      if (param.type !== 'SubExpression') {
        return;
      }

      var id = param.path.original;
      if (commentIdentifiers.indexOf(id) === -1) {
        return;
      }

      if (!param.params[0]) {
        throw new Error('Helper "' + id + '" has no parameters. Expected a comment string.');
      } else if (param.params[0].type !== 'StringLiteral') {
        // there's no good reason to have a variable comment, since comments are
        // only useful for extraction
        throw new Error("Can't extract non-string comment");
      }

      message.extractedComments.push(param.params[0].value);

      // continue iterating, in case there are more
      // subexpression with comments
    });
  };

  GettextExtractor.prototype.MustacheStatement = function (statement) {
    extract(statement);
    Visitor.prototype.MustacheStatement.call(this, statement);
  };

  GettextExtractor.prototype.SubExpression = function (expression) {
    extract(expression);
    Visitor.prototype.SubExpression.call(this, expression);
  };

  var ast = Handlebars.parse(template);
  new GettextExtractor().accept(ast);

  return msgs;
}
function xgettext (template, options) {
  options = options || {};
  var identifiers = options.identifiers || Catalog.DEFAULT_IDENTIFIERS;
  var defaultDomain = options.defaultDomain || 'messages';
  var commentIdentifiers = options.commentIdentifiers || ['gettext-comment'];
  var filename = options.filename;

  // sanity check identifiers
  Object.keys(identifiers).forEach(function (id) {
    if (identifiers[id].indexOf('msgid') === -1) {
      throw new Error('Every id must have a msgid parameter, but "' + id + '" doesn\'t have one');
    }
  });

  var msgs = {};

  var Visitor = Handlebars.Visitor;

  function GettextExtractor () {}
  GettextExtractor.prototype = new Visitor();

  var extract = function (statement) {
    var path = statement.path;
    var params = statement.params;

    if (Object.keys(identifiers).indexOf(path.original) === -1) {
      return;
    }

    var spec = identifiers[path.original];
    var msgidParam = params[spec.indexOf('msgid')];

    if (!msgidParam) {
      throw new Error('No msgid');
    }

    if (msgidParam.type !== 'StringLiteral') {
      // don't extract variables
      return;
    }

    var msgid = msgidParam.value;
    var contextIndex = spec.indexOf('msgctxt');

    var context = null; // null context is *not* the same as empty context
    if (contextIndex >= 0) {
      var contextParam = params[contextIndex];
      if (!contextParam) {
        // throw an error if there's supposed to be a context but not enough
        // parameters were passed to the handlebars helper
        throw new Error('Expected a context for msgid "' + msgid + '" but none was given');
      }
      if (contextParam.type !== 'StringLiteral') {
        // Don't extract if context isn't a string literal.
        // Hopefully they've statically defined this message somewhere else
        return;
      }

      context = contextParam.value;
    }

    var domain = defaultDomain;
    var domainIndex = spec.indexOf('domain');
    if (domainIndex !== -1) {
      var domainParam = params[domainIndex];
      if (!domainParam) {
        throw new Error('Expected a domain for msgid "' + msgid + '" but none was given');
      }
      if (domainParam.type !== 'StringLiteral') {
        // don't extract if domain isn't a string literal
        return;
      }

      domain = domainParam.value;
    }

    var key = Catalog.messageToKey(msgid, context);

    var pluralIndex = spec.indexOf('msgid_plural');
    if (pluralIndex !== -1) {
      var pluralParam = params[pluralIndex];
      if (!pluralParam) {
        throw new Error('No plural specified for msgid "' + msgid + '"');
      }
      if (pluralParam.type !== 'StringLiteral') {
        return;
      }

      if (msgs[domain] && msgs[domain][key]) {
        // there's an existing message, check it's plural form to make sure it matches
        var plural = pluralParam.value;
        var existingPlural = msgs[domain][key].msgid_plural;

        if (plural && existingPlural && existingPlural !== plural) {
          throw new Error('Incompatible plural definitions for msgid "' + msgid +
            '" ("' + existingPlural + '" and "' + plural + '")');
        }
      }
    }

    // only add message to catalog after we've sanity checked it
    msgs[domain] = msgs[domain] || {};
    msgs[domain][key] = msgs[domain][key] || {
      extractedComments: [],
      references: [],
      fields: {} // extracted fields get placed here so they don't clobber anything
    };
    var message = msgs[domain][key];

    // return AST so consumer has as much information as possible
    message.ast = statement;

    var loc = statement.loc;
    message.references.push({
      filename: filename,
      start: {
        line: loc.start.line,
        column: loc.start.column
      },
      end: {
        line: loc.end.line,
        column: loc.end.column
      }
    });

    spec.forEach(function (prop, i) {
      var param = params[i];
      if (!param) {
        return;
      }

      var knownFields = Catalog.DEFAULT_IDENTIFIERS.dcnpgettext;
      if (knownFields.indexOf(prop) !== -1) {
        // field name doesn't conflict with anything, we can save it at top level
        message[prop] = params[i].value;
      }

      // save all fields under .fields to prevent collisions
      message.fields[prop] = params[i].value;
    });

    // extract comments
    params.forEach(function (param) {
      if (param.type !== 'SubExpression') {
        return;
      }

      var id = param.path.original;
      if (commentIdentifiers.indexOf(id) === -1) {
        return;
      }

      if (!param.params[0]) {
        throw new Error('Helper "' + id + '" has no parameters. Expected a comment string.');
      } else if (param.params[0].type !== 'StringLiteral') {
        // there's no good reason to have a variable comment, since comments are
        // only useful for extraction
        throw new Error("Can't extract non-string comment");
      }

      message.extractedComments.push(param.params[0].value);

      // continue iterating, in case there are more
      // subexpression with comments
    });
  };

  GettextExtractor.prototype.MustacheStatement = function (statement) {
    extract(statement);
    Visitor.prototype.MustacheStatement.call(this, statement);
  };

  GettextExtractor.prototype.SubExpression = function (expression) {
    extract(expression);
    Visitor.prototype.SubExpression.call(this, expression);
  };

  var ast = Handlebars.parse(template);
  new GettextExtractor().accept(ast);

  return msgs;
}
function xgettext ( template , options ) { options = options || { } ; var identifiers = options . identifiers || Catalog . DEFAULT_IDENTIFIERS ; var defaultDomain = options . defaultDomain || 'messages' ; var commentIdentifiers = options . commentIdentifiers || [ 'gettext-comment' ] ; var filename = options . filename ; // sanity check identifiers Object . keys ( identifiers ) . forEach ( function ( id ) { if ( identifiers [ id ] . indexOf ( 'msgid' ) === - 1 ) { throw new Error ( 'Every id must have a msgid parameter, but "' + id + '" doesn\'t have one' ) ; } } ) ; var msgs = { } ; var Visitor = Handlebars . Visitor ; function GettextExtractor ( ) { } GettextExtractor . prototype = new Visitor ( ) ; var extract = function ( statement ) { var path = statement . path ; var params = statement . params ; if ( Object . keys ( identifiers ) . indexOf ( path . original ) === - 1 ) { return ; } var spec = identifiers [ path . original ] ; var msgidParam = params [ spec . indexOf ( 'msgid' ) ] ; if ( ! msgidParam ) { throw new Error ( 'No msgid' ) ; } if ( msgidParam . type !== 'StringLiteral' ) { // don't extract variables return ; } var msgid = msgidParam . value ; var contextIndex = spec . indexOf ( 'msgctxt' ) ; var context = null ; // null context is *not* the same as empty context if ( contextIndex >= 0 ) { var contextParam = params [ contextIndex ] ; if ( ! contextParam ) { // throw an error if there's supposed to be a context but not enough // parameters were passed to the handlebars helper throw new Error ( 'Expected a context for msgid "' + msgid + '" but none was given' ) ; } if ( contextParam . type !== 'StringLiteral' ) { // Don't extract if context isn't a string literal. // Hopefully they've statically defined this message somewhere else return ; } context = contextParam . value ; } var domain = defaultDomain ; var domainIndex = spec . indexOf ( 'domain' ) ; if ( domainIndex !== - 1 ) { var domainParam = params [ domainIndex ] ; if ( ! domainParam ) { throw new Error ( 'Expected a domain for msgid "' + msgid + '" but none was given' ) ; } if ( domainParam . type !== 'StringLiteral' ) { // don't extract if domain isn't a string literal return ; } domain = domainParam . value ; } var key = Catalog . messageToKey ( msgid , context ) ; var pluralIndex = spec . indexOf ( 'msgid_plural' ) ; if ( pluralIndex !== - 1 ) { var pluralParam = params [ pluralIndex ] ; if ( ! pluralParam ) { throw new Error ( 'No plural specified for msgid "' + msgid + '"' ) ; } if ( pluralParam . type !== 'StringLiteral' ) { return ; } if ( msgs [ domain ] && msgs [ domain ] [ key ] ) { // there's an existing message, check it's plural form to make sure it matches var plural = pluralParam . value ; var existingPlural = msgs [ domain ] [ key ] . msgid_plural ; if ( plural && existingPlural && existingPlural !== plural ) { throw new Error ( 'Incompatible plural definitions for msgid "' + msgid + '" ("' + existingPlural + '" and "' + plural + '")' ) ; } } } // only add message to catalog after we've sanity checked it msgs [ domain ] = msgs [ domain ] || { } ; msgs [ domain ] [ key ] = msgs [ domain ] [ key ] || { extractedComments : [ ] , references : [ ] , fields : { } // extracted fields get placed here so they don't clobber anything } ; var message = msgs [ domain ] [ key ] ; // return AST so consumer has as much information as possible message . ast = statement ; var loc = statement . loc ; message . references . push ( { filename : filename , start : { line : loc . start . line , column : loc . start . column } , end : { line : loc . end . line , column : loc . end . column } } ) ; spec . forEach ( function ( prop , i ) { var param = params [ i ] ; if ( ! param ) { return ; } var knownFields = Catalog . DEFAULT_IDENTIFIERS . dcnpgettext ; if ( knownFields . indexOf ( prop ) !== - 1 ) { // field name doesn't conflict with anything, we can save it at top level message [ prop ] = params [ i ] . value ; } // save all fields under .fields to prevent collisions message . fields [ prop ] = params [ i ] . value ; } ) ; // extract comments params . forEach ( function ( param ) { if ( param . type !== 'SubExpression' ) { return ; } var id = param . path . original ; if ( commentIdentifiers . indexOf ( id ) === - 1 ) { return ; } if ( ! param . params [ 0 ] ) { throw new Error ( 'Helper "' + id + '" has no parameters. Expected a comment string.' ) ; } else if ( param . params [ 0 ] . type !== 'StringLiteral' ) { // there's no good reason to have a variable comment, since comments are // only useful for extraction throw new Error ( "Can't extract non-string comment" ) ; } message . extractedComments . push ( param . params [ 0 ] . value ) ; // continue iterating, in case there are more // subexpression with comments } ) ; } ; GettextExtractor . prototype . MustacheStatement = function ( statement ) { extract ( statement ) ; Visitor . prototype . MustacheStatement . call ( this , statement ) ; } ; GettextExtractor . prototype . SubExpression = function ( expression ) { extract ( expression ) ; Visitor . prototype . SubExpression . call ( this , expression ) ; } ; var ast = Handlebars . parse ( template ) ; new GettextExtractor ( ) . accept ( ast ) ; return msgs ; }
Given a Handlebars template string returns the list of i18n strings.

@param template {string} Handlebars template
@param {object} options
@return {object} set of translatable strings
Given a Handlebars template string returns the list of i18n strings .