Skip to content
This repository has been archived by the owner on Jul 30, 2024. It is now read-only.

Commit

Permalink
[FEATURE] Added support for custom object notation delimiter
Browse files Browse the repository at this point in the history
  • Loading branch information
oliversalzburg committed Oct 1, 2014
1 parent e2b779d commit 916446d
Show file tree
Hide file tree
Showing 6 changed files with 205 additions and 174 deletions.
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,26 @@ The solution is available in npm and bower packages for the backend and frontend

If you want to hide something until the translation is loaded, inject the i18n service into your scope and use `i18n.loaded` with [`ngShow`](http://docs.angularjs.org/api/ng/directive/ngShow).


### Object Notation
If you're using i18n-node's object notation functionality, additional configuration is required if you're using a delimiter other than `.`.

#### Backend
Provide the delimiter in your `i18nRoutes.configure` call. For example:

i18nRoutes.configure( app, { directory : path.join( applicationRoot, "locales" ), objectNotation : "→" } );

#### Frontend
Provide the delimiter in your `angular.module` call, by using the `i18nProvider`. For example:

```javascript
var yourApp = angular.module( "yourApp", [ "i18n" ] )
.config( [ "i18nProvider", function( i18nProvider ) {
i18nProvider.setObjectNotation( "" );
} ] );
```


### How it works
To make this approach work, we have to make several changes to the application at hand. The final setup is as follows:

Expand Down
2 changes: 1 addition & 1 deletion bower.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "i18n-node-angular",
"version": "1.1.3",
"version": "1.2.0",
"description": "i18n-node for Angular",
"main": "i18n-node-angular.js",
"homepage": "https://github.com/oliversalzburg/i18n-node-angular",
Expand Down
4 changes: 2 additions & 2 deletions dist/i18n-node-angular.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

339 changes: 174 additions & 165 deletions i18n-node-angular.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,180 @@

var i18nModule = angular.module( "i18n", [] );

i18nModule.provider( "i18n", function() {
var i18nProvider = this;

i18nProvider.objectNotation = ".";
i18nProvider.setObjectNotation = function( delimiter ) {
i18nProvider.objectNotation = delimiter;
};

/**
* The main i18n service which handles retrieval of the translation map sends single translation terms to the backend.
*/
i18nProvider.$get = [ "$rootScope", "$http", "$q", function( $rootScope, $http, $q ) {
var i18nService = function() {

// We use this deferred to keep track of if the last locale loading request has completed.
this._localeLoadedDeferred = $q.defer();
// If a lot of locale loading is requested, we collect all the promises on this stack so we can later resolve them.
this._deferredStack = [];

// A handy boolean that indicates if the currently requested locale was loaded.
this.loaded = false;

// Initialize the service with a given locale.
this.init = function( locale ) {
if( locale != this.userLanguage ) {
if( this._localeLoadedDeferred ) {
this._deferredStack.push( this._localeLoadedDeferred );
}
this._localeLoadedDeferred = $q.defer();
this.loaded = false;
this.userLanguage = locale;

var service = this;

$http( {
method : "get",
url : "/i18n/" + locale,
cache : true
} ).success( function( translations ) {
$rootScope.i18n = translations;
service.loaded = true;
service._localeLoadedDeferred.resolve( $rootScope.i18n );

while( service._deferredStack.length ) {
service._deferredStack.pop().resolve( $rootScope.i18n );
}
} );
}

return this._localeLoadedDeferred.promise;
};

/**
* Syntactic sugar. Returns a promise to return the i18n service, once the translation map is loaded.
* @returns {defer.promise|*|promise}
*/
this.i18n = function() {
var serviceDeferred = $q.defer();

var service = this;
this.ensureLocaleIsLoaded().then( function() {
serviceDeferred.resolve( service );
} );

return serviceDeferred.promise;
};

/**
* Returns a promise to return the translation map, once it is loaded.
* @returns {defer.promise|*|promise}
*/
this.ensureLocaleIsLoaded = function() {
return this._localeLoadedDeferred.promise;
};

/**
* Retrieve a translation object from the translation catalog, using object notation.
* @param {String} literal The path of the object to look up.
* @returns {*}
*/
this.getTranslationObject = function( literal ) {
var result = literal.split( i18nProvider.objectNotation ).reduce( function( object, index ) {
if( !object || !object.hasOwnProperty( index ) ) return null;
return object[ index ];
}, $rootScope.i18n );
return result;
};

/**
* Translate a given term, using the currently loaded translation map.
* @param {String} name The string to translate.
* @returns {String} The translated string or the input, if no translation was available.
*/
this.__ = function( name ) {
if( !$rootScope.i18n ) {
return name;
}

var translation = $rootScope.i18n[ name ] || this.getTranslationObject( name );
if( !translation ) {
translation = name;

// Temporarily store the original string in the translation table
// to avoid future lookups causing additional GET requests to the backend.
$rootScope.i18n[ name ] = translation;

// Invoke the translation endpoint on the backend to cause the term to be added
// to the translation table on the backend.
// Additionally, store the returned, translated term in the translation table.
// The term is very unlikely to be actually translated now, as it was most
// likely previously unknown in the users locale, but, hey.
$http.get( "/i18n/" + this.userLanguage + "/" + encodeURIComponent( name ) ).success( function( translated ) {
$rootScope.i18n[ name ] = translated;
} );
}

// If an implementation of vsprintf is loaded and we have additional parameters,
// try to perform the substitution and return the result.
if( arguments.length > 1 && typeof( vsprintf ) == "function" ) {
translation = vsprintf( translation, Array.prototype.slice.call( arguments, 1 ) );
}

return translation;
};

/**
* Translate a given term and pick the singular or plural version depending on the given count.
* @param {Number} count The number of items, depending on which the correct translation term will be chosen.
* @param {String} singular The term that should be used if the count equals 1.
* @param {String} plural The term that should be used if the count doesn't equal 1.
* @returns {String} The translated phrase depending on the count.
*/
this.__n = function( count, singular, plural ) {
if( !$rootScope.i18n ) {
return singular;
}

var translation = $rootScope.i18n[ singular ] || this.getTranslationObject( name );
if( !translation ) {
if( !plural ) {
plural = singular;
}

translation = { one : singular, other : plural };

// Temporarily store the original string in the translation table
// to avoid future lookups causing additional GET requests to the backend.
$rootScope.i18n[ singular ] = translation;

// Invoke the translation endpoint on the backend to cause the term to be added
// to the translation table on the backend.
// Additionally, store the returned, translated term in the translation table.
// The term is very unlikely to be actually translated now, as it was most
// likely previously unknown in the users locale, but, hey.
$http.get( "/i18n/" + this.userLanguage + "/" + encodeURIComponent( singular ) + "?plural=" + encodeURIComponent( plural ) + "&count=" + encodeURIComponent( count ) ).success( function( translated ) {
$rootScope.i18n[ singular ] = translated;
} );
}

translation = (count == 1) ? translation.one : translation.other;

// If an implementation of vsprintf is loaded, try to perform the substitution and return the result.
if( typeof( vsprintf ) == "function" ) {
translation = vsprintf( translation, [ count ] );
}

return translation;
};
};

return new i18nService();
} ];
} );

/**
* The i18nLocale directive can (and should) be used to tell the i18n service which locale to use.
* You may just want to combine it with the ngApp directive in your DOM. For example:
Expand All @@ -56,171 +230,6 @@
};
} ] );

/**
* The main i18n service which handles retrieval of the translation map sends single translation terms to the backend.
*/
i18nModule.factory( "i18n", [ "$rootScope", "$http", "$q", function( $rootScope, $http, $q ) {
var i18nService = function() {

// We use this deferred to keep track of if the last locale loading request has completed.
this._localeLoadedDeferred = $q.defer();
// If a lot of locale loading is requested, we collect all the promises on this stack so we can later resolve them.
this._deferredStack = [];

// A handy boolean that indicates if the currently requested locale was loaded.
this.loaded = false;

// Initialize the service with a given locale.
this.init = function( locale ) {
if( locale != this.userLanguage ) {
if( this._localeLoadedDeferred ) {
this._deferredStack.push( this._localeLoadedDeferred );
}
this._localeLoadedDeferred = $q.defer();
this.loaded = false;
this.userLanguage = locale;

var service = this;

$http( {
method : "get",
url : "/i18n/" + locale,
cache : true
} ).success( function( translations ) {
$rootScope.i18n = translations;
service.loaded = true;
service._localeLoadedDeferred.resolve( $rootScope.i18n );

while( service._deferredStack.length ) {
service._deferredStack.pop().resolve( $rootScope.i18n );
}
} );
}

return this._localeLoadedDeferred.promise;
};

/**
* Syntactic sugar. Returns a promise to return the i18n service, once the translation map is loaded.
* @returns {defer.promise|*|promise}
*/
this.i18n = function() {
var serviceDeferred = $q.defer();

var service = this;
this.ensureLocaleIsLoaded().then( function() {
serviceDeferred.resolve( service );
} );

return serviceDeferred.promise;
};

/**
* Returns a promise to return the translation map, once it is loaded.
* @returns {defer.promise|*|promise}
*/
this.ensureLocaleIsLoaded = function() {
return this._localeLoadedDeferred.promise;
};

/**
* Retrieve a translation object from the translation catalog, using object notation.
* @param {String} literal The path of the object to look up.
* @returns {*}
*/
this.getTranslationObject = function( literal ) {
var result = literal.split( "." ).reduce( function( object, index ) {
if( !object || !object.hasOwnProperty( index ) ) return null;
return object[ index ];
}, $rootScope.i18n );
return result;
};

/**
* Translate a given term, using the currently loaded translation map.
* @param {String} name The string to translate.
* @returns {String} The translated string or the input, if no translation was available.
*/
this.__ = function( name ) {
if( !$rootScope.i18n ) {
return name;
}

var translation = $rootScope.i18n[ name ] || this.getTranslationObject( name );
if( !translation ) {
translation = name;

// Temporarily store the original string in the translation table
// to avoid future lookups causing additional GET requests to the backend.
$rootScope.i18n[ name ] = translation;

// Invoke the translation endpoint on the backend to cause the term to be added
// to the translation table on the backend.
// Additionally, store the returned, translated term in the translation table.
// The term is very unlikely to be actually translated now, as it was most
// likely previously unknown in the users locale, but, hey.
$http.get( "/i18n/" + this.userLanguage + "/" + encodeURIComponent( name ) ).success( function( translated ) {
$rootScope.i18n[ name ] = translated;
} );
}

// If an implementation of vsprintf is loaded and we have additional parameters,
// try to perform the substitution and return the result.
if( arguments.length > 1 && typeof( vsprintf ) == "function" ) {
translation = vsprintf( translation, Array.prototype.slice.call( arguments, 1 ) );
}

return translation;
};

/**
* Translate a given term and pick the singular or plural version depending on the given count.
* @param {Number} count The number of items, depending on which the correct translation term will be chosen.
* @param {String} singular The term that should be used if the count equals 1.
* @param {String} plural The term that should be used if the count doesn't equal 1.
* @returns {String} The translated phrase depending on the count.
*/
this.__n = function( count, singular, plural ) {
if( !$rootScope.i18n ) {
return singular;
}

var translation = $rootScope.i18n[ singular ] || this.getTranslationObject( name );
if( !translation ) {
if( !plural ) {
plural = singular;
}

translation = { one : singular, other : plural };

// Temporarily store the original string in the translation table
// to avoid future lookups causing additional GET requests to the backend.
$rootScope.i18n[ singular ] = translation;

// Invoke the translation endpoint on the backend to cause the term to be added
// to the translation table on the backend.
// Additionally, store the returned, translated term in the translation table.
// The term is very unlikely to be actually translated now, as it was most
// likely previously unknown in the users locale, but, hey.
$http.get( "/i18n/" + this.userLanguage + "/" + encodeURIComponent( singular ) + "?plural=" + encodeURIComponent( plural ) + "&count=" + encodeURIComponent( count ) ).success( function( translated ) {
$rootScope.i18n[ singular ] = translated;
} );
}

translation = (count == 1) ? translation.one : translation.other;

// If an implementation of vsprintf is loaded, try to perform the substitution and return the result.
if( typeof( vsprintf ) == "function" ) {
translation = vsprintf( translation, [ count ] );
}

return translation;
};
};

return new i18nService();
} ] );

/**
* i18n filter to be used conveniently in templates.
* When looking to translate just a single phrase, pass the phrase into the filter like so:
Expand Down
Loading

0 comments on commit 916446d

Please sign in to comment.