[フレーム]
Last Updated: February 25, 2016
·
7.153K
· filipefmelo

AngularJS and i18n

Hi guys, last week I saw that some of you are having trouble implementing a general solution for i18n in AngularJS. So, here's my 2 cents on the matter.

The problem:
We needed to implement a i18n solution which included YAML support and value replacement so it would be general and easy to customize.

The solution:
We need two things for this to happen properly, a service which can parse YAML and return a JSON object and a LanguageService which will return the user's locale (or force a locale defined by the user, for language change purposes).
So, we wrote a service that loads up a YAML file that is named according to the user's locale (for instance en-us.i18n.yml)

i18n Service

angular.module('MyAppApp.services').service('i18nService', ['yamljs', 'LanguageService', function(yamljs, LanguageService) {
 var errorString = "i18n error: ";
 var self = this;
 self.loaded = false;
 self.isLoading = false;
 self.yaml = {};

 this.replaceValues = function(str, obj, dotnotation) {
 if(obj) {
 var regex;
 for(i in obj) {
 regex = new RegExp("@" + i + "@", "g");
 str = str.replace(regex, obj[i]);
 }
 }
 if(!str) return errorString + dotnotation;
 return str;
 }

 this.recompose = function(obj, string) {

 var parts = string.split('.');
 var newObj = obj[parts[0]];
 if(parts[1]) {
 parts.splice(0, 1);
 var newString = parts.join('.');
 return this.recompose(newObj, newString);
 }
 return newObj;
 }


 this.t = function(dotNotation, objectReplace) {
 if(!self.isLoading) {
 if(self.loaded) {
 return self.replaceValues(self.recompose(self.yaml, dotNotation), objectReplace, dotNotation);
 } else {
 self.isLoading = true;
 yamljs.loadFromFile("yaml/" + LanguageService.getCurrentLanguage() + ".i18n.yml").then(function(o) {
 self.yaml = o;
 self.loaded = true;
 self.isLoading = false;
 return self.replaceValues(self.recompose(self.yaml, dotNotation), objectReplace, dotNotation);

 }, function() {
 console.error("Error loading locale file. Trying default locale file.");
 self.isLoading = true;
 yamljs.loadFromFile("yaml/en-us.i18n.yml").then(function(o) {
 self.yaml = o;
 self.loaded = true;
 self.isLoading = false;
 return self.replaceValues(self.recompose(self.yaml, dotNotation), objectReplace, dotNotation);

 }, function() {
 self.isLoading = false;
 console.error("Error loading default locale YAML file (en-us)");
 });
 });
 }
 }
 };
}]);

YAML JS

angular.module('StoreApp.services').service('yamljs', ['$http', '$q', function($http, $q) {
 var self = this;
// YAML - Core - Copyright TJ Holowaychuk <tj@vision-media.ca> (MIT Licensed)
// Modified by Filipe Melo <filipe.melo@centralway.com> as an AngularJS Service in 17.04.2013

// --- Helpers

 /**
 * Return 'near "context"' where context
 * is replaced by a chunk of _str_.
 *
 * @param {string} str
 * @return {string}
 * @api public
 */

 var context = function(str) {
 if(typeof str !== 'string') return ''
 str = str
 .slice(0, 25)
 .replace(/\n/g, '\\n')
 .replace(/"/g, '\\\"')
 return 'near "' + str + '"'
 }

// --- Lexer

 /**
 * YAML grammar tokens.
 */

 var tokens = [
 ['comment', /^#[^\n]*/],
 ['indent', /^\n( *)/],
 ['space', /^ +/],
 ['true', /^\b(enabled|true|yes|on)\b/],
 ['false', /^\b(disabled|false|no|off)\b/],
 ['null', /^\b(null|Null|NULL|~)\b/],
 ['string', /^"(.*?)"/],
 ['string', /^'(.*?)'/],
 ['timestamp', /^((\d{4})-(\d\d?)-(\d\d?)(?:(?:[ \t]+)(\d\d?):(\d\d)(?::(\d\d))?)?)/],
 ['float', /^(\d+\.\d+)/],
 ['int', /^(\d+)/],
 ['doc', /^---/],
 [',', /^,/],
 ['{', /^\{(?![^\n\}]*\}[^\n]*[^\s\n\}])/],
 ['}', /^\}/],
 ['[', /^\[(?![^\n\]]*\][^\n]*[^\s\n\]])/],
 [']', /^\]/],
 ['-', /^\-/],
 [':', /^[:]/],
 ['string', /^(?![^:\n\s]*:[^\/]{2})(([^:,\]\}\n\s]|(?!\n)\s(?!\s*?\n)|:\/\/|,(?=[^\n]*\s*[^\]\}\s\n]\s*\n)|[\]\}](?=[^\n]*\s*[^\] \}\s\n]\s*\n))*)(?=[,:\]\}\s\n]|$)/],
 ['id', /^([\w][\w -]*)/]
 ];

 /**
 * Tokenize the given _str_.
 *
 * @param {string} str
 * @return {array}
 * @api private
 */

 var tokenize = function(str) {
 var token, captures, ignore, input,
 indents = 0, lastIndents = 0,
 stack = [], indentAmount = -1

 // Windows new line support (CR+LF, \r\n)
 str = str.replace(/\r\n/g, "\n");

 while(str.length) {
 for(var i = 0, len = tokens.length; i < len; ++i)
 if(captures = tokens[i][1].exec(str)) {
 token = [tokens[i][0], captures],
 str = str.replace(tokens[i][1], '')
 switch(token[0]) {
 case 'comment':
 ignore = true
 break
 case 'indent':
 lastIndents = indents
 // determine the indentation amount from the first indent
 if(indentAmount == -1) {
 indentAmount = token[1][1].length
 }

 indents = token[1][1].length / indentAmount
 if(indents === lastIndents)
 ignore = true
 else if(indents > lastIndents + 1)
 throw new SyntaxError('invalid indentation, got ' + indents + ' instead of ' + (lastIndents + 1))
 else if(indents < lastIndents) {
 input = token[1].input
 token = ['dedent']
 token.input = input
 while(--lastIndents > indents)
 stack.push(token)
 }
 }
 break
 }
 if(!ignore)
 if(token)
 stack.push(token),
 token = null
 else
 throw new SyntaxError(context(str))
 ignore = false
 }
 return stack
 }

// --- Parser

 /**
 * Initialize with _tokens_.
 */

 function Parser(tokens) {
 this.tokens = tokens
 }

 /**
 * Look-ahead a single token.
 *
 * @return {array}
 * @api public
 */

 Parser.prototype.peek = function() {
 return this.tokens[0]
 }

 /**
 * Advance by a single token.
 *
 * @return {array}
 * @api public
 */

 Parser.prototype.advance = function() {
 return this.tokens.shift()
 }

 /**
 * Advance and return the token's value.
 *
 * @return {mixed}
 * @api private
 */

 Parser.prototype.advanceValue = function() {
 return this.advance()[1][1]
 }

 /**
 * Accept _type_ and advance or do nothing.
 *
 * @param {string} type
 * @return {bool}
 * @api private
 */

 Parser.prototype.accept = function(type) {
 if(this.peekType(type))
 return this.advance()
 }

 /**
 * Expect _type_ or throw an error _msg_.
 *
 * @param {string} type
 * @param {string} msg
 * @api private
 */

 Parser.prototype.expect = function(type, msg) {
 if(this.accept(type)) return
 throw new Error(msg + ', ' + context(this.peek()[1].input))
 }

 /**
 * Return the next token type.
 *
 * @return {string}
 * @api private
 */

 Parser.prototype.peekType = function(val) {
 return this.tokens[0] &&
 this.tokens[0][0] === val
 }

 /**
 * space*
 */

 Parser.prototype.ignoreSpace = function() {
 while(this.peekType('space'))
 this.advance()
 }

 /**
 * (space | indent | dedent)*
 */

 Parser.prototype.ignoreWhitespace = function() {
 while(this.peekType('space') ||
 this.peekType('indent') ||
 this.peekType('dedent'))
 this.advance()
 }

 /**
 * block
 * | doc
 * | list
 * | inlineList
 * | hash
 * | inlineHash
 * | string
 * | float
 * | int
 * | true
 * | false
 * | null
 */

 Parser.prototype.parse = function() {
 switch(this.peek()[0]) {
 case 'doc':
 return this.parseDoc()
 case '-':
 return this.parseList()
 case '{':
 return this.parseInlineHash()
 case '[':
 return this.parseInlineList()
 case 'id':
 return this.parseHash()
 case 'string':
 return this.advanceValue()
 case 'timestamp':
 return this.parseTimestamp()
 case 'float':
 return parseFloat(this.advanceValue())
 case 'int':
 return parseInt(this.advanceValue())
 case 'true':
 this.advanceValue();
 return true
 case 'false':
 this.advanceValue();
 return false
 case 'null':
 this.advanceValue();
 return null
 }
 }

 /**
 * '---'? indent expr dedent
 */

 Parser.prototype.parseDoc = function() {
 this.accept('doc')
 this.expect('indent', 'expected indent after document')
 var val = this.parse()
 this.expect('dedent', 'document not properly dedented')
 return val
 }

 /**
 * ( id ':' - expr -
 * | id ':' - indent expr dedent
 * )+
 */

 Parser.prototype.parseHash = function() {
 var id, hash = {}
 while(this.peekType('id') && (id = this.advanceValue())) {
 this.expect(':', 'expected semi-colon after id')
 this.ignoreSpace()
 if(this.accept('indent'))
 hash[id] = this.parse(),
 this.expect('dedent', 'hash not properly dedented')
 else
 hash[id] = this.parse()
 this.ignoreSpace()
 }
 return hash
 }

 /**
 * '{' (- ','? ws id ':' - expr ws)* '}'
 */

 Parser.prototype.parseInlineHash = function() {
 var hash = {}, id, i = 0
 this.accept('{')
 while(!this.accept('}')) {
 this.ignoreSpace()
 if(i) this.expect(',', 'expected comma')
 this.ignoreWhitespace()
 if(this.peekType('id') && (id = this.advanceValue())) {
 this.expect(':', 'expected semi-colon after id')
 this.ignoreSpace()
 hash[id] = this.parse()
 this.ignoreWhitespace()
 }
 ++i
 }
 return hash
 }

 /**
 * ( '-' - expr -
 * | '-' - indent expr dedent
 * )+
 */

 Parser.prototype.parseList = function() {
 var list = []
 while(this.accept('-')) {
 this.ignoreSpace()
 if(this.accept('indent'))
 list.push(this.parse()),
 this.expect('dedent', 'list item not properly dedented')
 else
 list.push(this.parse())
 this.ignoreSpace()
 }
 return list
 }

 /**
 * '[' (- ','? - expr -)* ']'
 */

 Parser.prototype.parseInlineList = function() {
 var list = [], i = 0
 this.accept('[')
 while(!this.accept(']')) {
 this.ignoreSpace()
 if(i) this.expect(',', 'expected comma')
 this.ignoreSpace()
 list.push(this.parse())
 this.ignoreSpace()
 ++i
 }
 return list
 }

 /**
 * yyyy-mm-dd hh:mm:ss
 *
 * For full format: http://yaml.org/type/timestamp.html
 */

 Parser.prototype.parseTimestamp = function() {
 var token = this.advance()[1]
 var date = new Date
 var year = token[2]
 , month = token[3]
 , day = token[4]
 , hour = token[5] || 0
 , min = token[6] || 0
 , sec = token[7] || 0

 date.setUTCFullYear(year, month - 1, day)
 date.setUTCHours(hour)
 date.setUTCMinutes(min)
 date.setUTCSeconds(sec)
 date.setUTCMilliseconds(0)
 return date
 }

 /**
 * Evaluate a _str_ of yaml.
 *
 * @param {string} str
 * @return {mixed}
 * @api private
 */

 var parse = function(str) {
 return (new Parser(tokenize(str))).parse();
 }

 /**
 * Load YAML from a file
 *
 * @param {string} filename
 * @param {object} ctx
 * @param {function} fn
 * @api public
 */

 this.loadFromFile = function(filename, ctx, fnSuccess, fnError) {
 var deferred = $q.defer();
 $http({ method: 'GET', url: filename }).success(function(response) {
 deferred.resolve(parse(response));
 }).error(function() {
 deferred.reject({});
 });

 return deferred.promise;
 };

 return this;
}]);

And of course an example YAML file:

YAML file

login:
 title: "Login"
welcome:
 title: "Welcome @user@"

How to use it? Here goes:

//Javascript after injecting the service into your controller, you can use it this way
var translated_login_title = i18nService.t("login.title"); //should return "Login"

var replacement_object = {
 user: "CoderWall"
}
var translated_and_replaced_welcome_title = i18nService.t("welcome.title", replacement_object); //should return "Welcome CoderWall"

This concludes the i18n how to, if you have any question, write them down in the comments section.

1 Response
Add your response

Hey there! Nice article, and yea we're already working on a defacto solution for i18n in angular apps. Maybe you're interested on helping out? Checkout angular-translate right here: http://pascalprecht.github.io/angular-translate

over 1 year ago ·

AltStyle によって変換されたページ (->オリジナル) /