Skip to content

Add support for multiple fallback languages (fallback waterfall) #345

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .jshintrc
Original file line number Diff line number Diff line change
@@ -27,6 +27,7 @@
"element",
"expect",
"inject",
"it"
"it",
"JSON"
]
}
111 changes: 90 additions & 21 deletions dist/angular-gettext.js
Original file line number Diff line number Diff line change
@@ -49,7 +49,7 @@ angular.module('gettext').constant('gettext', function (str) {
*/
return str;
});


/**
* @ngdoc service
* @module gettext
@@ -60,9 +60,9 @@ angular.module('gettext').constant('gettext', function (str) {
* @requires https://docs.angularjs.org/api/ng/service/$cacheFactory $cacheFactory
* @requires https://docs.angularjs.org/api/ng/service/$interpolate $interpolate
* @requires https://docs.angularjs.org/api/ng/service/$rootScope $rootScope
* @description Provides set of method to translate stings
* @description Provides set of method to translate strings
*/
angular.module('gettext').factory('gettextCatalog', ["gettextPlurals", "gettextFallbackLanguage", "$http", "$cacheFactory", "$interpolate", "$rootScope", function (gettextPlurals, gettextFallbackLanguage, $http, $cacheFactory, $interpolate, $rootScope) {
angular.module('gettext').factory('gettextCatalog', ["gettextPlurals", "gettextFallbackLanguage", "gettextUtil", "$http", "$cacheFactory", "$interpolate", "$rootScope", function (gettextPlurals, gettextFallbackLanguage, gettextUtil, $http, $cacheFactory, $interpolate, $rootScope) {
var catalog;
var noContext = '$$noContext';

@@ -172,6 +172,14 @@ angular.module('gettext').factory('gettextCatalog', ["gettextPlurals", "gettextF
* @description Active language.
*/
currentLanguage: 'en',
/**
* @ngdoc property
* @name gettextCatalog#fallbackLanguages
* @public
* @type {Object.<Array>.<String>}
* @description Fallback languages.
*/
fallbackLanguages: {},
/**
* @ngdoc property
* @name gettextCatalog#cache
@@ -204,6 +212,28 @@ angular.module('gettext').factory('gettextCatalog', ["gettextPlurals", "gettextF
return this.currentLanguage;
},

/**
* @ngdoc method
* @name gettextCatalog#setFallbackLanguages
* @public
* @param {Object.<Array>.<String>} fallbacks set of string arrays where the key is the source language, and the strings in the array the fallback langauges to try, in order
* @description Sets the fallback languages.
*/
setFallbackLanguages: function (fallbacks) {
this.fallbackLanguages = gettextUtil.copy(fallbacks || {});
},

/**
* @ngdoc method
* @name gettextCatalog#getFallbackLanguages
* @public
* @returns {Object.<Array>.<String>} fallback languages
* @description Returns the fallback languages.
*/
getFallbackLanguages: function () {
return gettextUtil.copy(this.fallbackLanguages);
},

/**
* @ngdoc method
* @name gettextCatalog#setStrings
@@ -258,7 +288,7 @@ angular.module('gettext').factory('gettextCatalog', ["gettextPlurals", "gettextF
* @protected
* @param {String} language language name
* @param {String} string translation key
* @param {Number=} n number to build sting form for
* @param {Number=} n number to build string form for
* @param {String=} context translation key context, e.g. {@link doc:context Verb, Noun}
* @returns {String|Null} translated or annotated string or null if language is not set
* @description Translate a string with the given language, count and context.
@@ -273,6 +303,32 @@ angular.module('gettext').factory('gettextCatalog', ["gettextPlurals", "gettextF
return plurals[gettextPlurals(language, n)];
},

/**
* @ngdoc method
* @name gettextCatalog#getFallbackStringFormFor
* @protected
* @param {String} language language name
* @param {String} string translation key
* @param {Number=} n number to build string form for
* @param {String=} context translation key context, e.g. {@link doc:context Verb, Noun}
* @param {String=} stringPlural plural translation key
* @returns {String|Null} translated or annotated string or null if language is not set
* @description Translate a string with the given language, count and context.
*
* First it tries a language (e.g. `en-US`) then {@link gettextCatalog#fallbackLanguages language}, if any, then {@link gettextFallbackLanguage fallback} (e.g. `en`).
*/
getFallbackStringFormFor: function (language, string, n, context, stringPlural) {
var fallbackLanguages = (this.fallbackLanguages[language] || []).slice();
var defaultFallbackLanguage = gettextFallbackLanguage(language);
if (defaultFallbackLanguage) { fallbackLanguages.push(defaultFallbackLanguage); }

var output = this.getStringFormFor(language, string, n, context);
for (var i = 0; i < fallbackLanguages.length; i++) {
output = output || this.getStringFormFor(fallbackLanguages[i], string, n, context);
}
return output || prefixDebug(n === 1 ? string : stringPlural);
},

/**
* @ngdoc method
* @name gettextCatalog#getString
@@ -283,8 +339,6 @@ angular.module('gettext').factory('gettextCatalog', ["gettextPlurals", "gettextF
* @returns {String} translated or annotated string
* @description Translate a string with the given scope and context.
*
* First it tries {@link gettextCatalog#currentLanguage gettextCatalog#currentLanguage} (e.g. `en-US`) then {@link gettextFallbackLanguage fallback} (e.g. `en`).
*
* When `scope` is supplied it uses Angular.JS interpolation, so something like this will do what you expect:
* ```js
* var hello = gettextCatalog.getString("Hello {{name}}!", { name: "Ruben" });
@@ -293,10 +347,7 @@ angular.module('gettext').factory('gettextCatalog', ["gettextPlurals", "gettextF
* Avoid using scopes - this skips interpolation and is a lot faster.
*/
getString: function (string, scope, context) {
var fallbackLanguage = gettextFallbackLanguage(this.currentLanguage);
string = this.getStringFormFor(this.currentLanguage, string, 1, context) ||
this.getStringFormFor(fallbackLanguage, string, 1, context) ||
prefixDebug(string);
string = this.getFallbackStringFormFor(this.currentLanguage, string, 1, context);
string = scope ? $interpolate(string)(scope) : string;
return addTranslatedMarkers(string);
},
@@ -305,7 +356,7 @@ angular.module('gettext').factory('gettextCatalog', ["gettextPlurals", "gettextF
* @ngdoc method
* @name gettextCatalog#getPlural
* @public
* @param {Number} n number to build sting form for
* @param {Number} n number to build string form for
* @param {String} string translation key
* @param {String} stringPlural plural translation key
* @param {$rootScope.Scope=} scope scope to do interpolation against
@@ -315,10 +366,7 @@ angular.module('gettext').factory('gettextCatalog', ["gettextPlurals", "gettextF
* @description Translate a plural string with the given context.
*/
getPlural: function (n, string, stringPlural, scope, context) {
var fallbackLanguage = gettextFallbackLanguage(this.currentLanguage);
string = this.getStringFormFor(this.currentLanguage, string, n, context) ||
this.getStringFormFor(fallbackLanguage, string, n, context) ||
prefixDebug(n === 1 ? string : stringPlural);
string = this.getFallbackStringFormFor(this.currentLanguage, string, n, context, stringPlural);
if (scope) {
scope.$count = n;
string = $interpolate(string)(scope);
@@ -353,7 +401,7 @@ angular.module('gettext').factory('gettextCatalog', ["gettextPlurals", "gettextF

return catalog;
}]);


/**
* @ngdoc directive
* @module gettext
@@ -531,7 +579,7 @@ angular.module('gettext').directive('translate', ["gettextCatalog", "$parse", "$
}
};
}]);


/**
* @ngdoc factory
* @module gettext
@@ -564,7 +612,7 @@ angular.module("gettext").factory("gettextFallbackLanguage", function () {

return null;
};
});
});
/**
* @ngdoc filter
* @module gettext
@@ -593,7 +641,7 @@ angular.module('gettext').filter('translate', ["gettextCatalog", function (gette
filter.$stateful = true;
return filter;
}]);


// Do not edit this file, it is autogenerated using genplurals.py!
angular.module("gettext").factory("gettextPlurals", function () {
var languageCodes = {
@@ -725,7 +773,7 @@ angular.module("gettext").factory("gettextPlurals", function () {
return languageCodes[langCode];
}
});


/**
* @ngdoc factory
* @module gettext
@@ -816,10 +864,31 @@ angular.module('gettext').factory('gettextUtil', function gettextUtil() {
return first + target.substr(1);
}

/**
* @ngdoc method
* @name gettextUtil#copy
* @public
* @param {object} o Object to copy.
* @returns {object} A copy of the object.
* @description Makes a deep copy of an object, making sure to not keep any references to the original object.
*/
function copy(o) {
var output;
var v;
var key;
output = Array.isArray(o) ? [] : {};
for (key in o) {
v = o[key];
output[key] = (typeof v === 'object') ? copy(v) : v;
}
return output;
}

return {
trim: trim,
assert: assert,
startsWith: startsWith,
lcFirst: lcFirst
lcFirst: lcFirst,
copy: copy
};
});
2 changes: 1 addition & 1 deletion dist/angular-gettext.min.js
70 changes: 59 additions & 11 deletions src/catalog.js
Original file line number Diff line number Diff line change
@@ -10,7 +10,7 @@
* @requires https://docs.angularjs.org/api/ng/service/$rootScope $rootScope
* @description Provides set of method to translate strings
*/
angular.module('gettext').factory('gettextCatalog', function (gettextPlurals, gettextFallbackLanguage, $http, $cacheFactory, $interpolate, $rootScope) {
angular.module('gettext').factory('gettextCatalog', function (gettextPlurals, gettextFallbackLanguage, gettextUtil, $http, $cacheFactory, $interpolate, $rootScope) {
var catalog;
var noContext = '$$noContext';

@@ -120,6 +120,14 @@ angular.module('gettext').factory('gettextCatalog', function (gettextPlurals, ge
* @description Active language.
*/
currentLanguage: 'en',
/**
* @ngdoc property
* @name gettextCatalog#fallbackLanguages
* @public
* @type {Object.<Array>.<String>}
* @description Fallback languages.
*/
fallbackLanguages: {},
/**
* @ngdoc property
* @name gettextCatalog#cache
@@ -152,6 +160,28 @@ angular.module('gettext').factory('gettextCatalog', function (gettextPlurals, ge
return this.currentLanguage;
},

/**
* @ngdoc method
* @name gettextCatalog#setFallbackLanguages
* @public
* @param {Object.<Array>.<String>} fallbacks set of string arrays where the key is the source language, and the strings in the array the fallback langauges to try, in order
* @description Sets the fallback languages.
*/
setFallbackLanguages: function (fallbacks) {
this.fallbackLanguages = gettextUtil.copy(fallbacks || {});
},

/**
* @ngdoc method
* @name gettextCatalog#getFallbackLanguages
* @public
* @returns {Object.<Array>.<String>} fallback languages
* @description Returns the fallback languages.
*/
getFallbackLanguages: function () {
return gettextUtil.copy(this.fallbackLanguages);
},

/**
* @ngdoc method
* @name gettextCatalog#setStrings
@@ -221,6 +251,32 @@ angular.module('gettext').factory('gettextCatalog', function (gettextPlurals, ge
return plurals[gettextPlurals(language, n)];
},

/**
* @ngdoc method
* @name gettextCatalog#getFallbackStringFormFor
* @protected
* @param {String} language language name
* @param {String} string translation key
* @param {Number=} n number to build string form for
* @param {String=} context translation key context, e.g. {@link doc:context Verb, Noun}
* @param {String=} stringPlural plural translation key
* @returns {String|Null} translated or annotated string or null if language is not set
* @description Translate a string with the given language, count and context.
*
* First it tries a language (e.g. `en-US`) then {@link gettextCatalog#fallbackLanguages language}, if any, then {@link gettextFallbackLanguage fallback} (e.g. `en`).
*/
getFallbackStringFormFor: function (language, string, n, context, stringPlural) {
var fallbackLanguages = (this.fallbackLanguages[language] || []).slice();
var defaultFallbackLanguage = gettextFallbackLanguage(language);
if (defaultFallbackLanguage) { fallbackLanguages.push(defaultFallbackLanguage); }

var output = this.getStringFormFor(language, string, n, context);
for (var i = 0; i < fallbackLanguages.length; i++) {
output = output || this.getStringFormFor(fallbackLanguages[i], string, n, context);
}
return output || prefixDebug(n === 1 ? string : stringPlural);
},

/**
* @ngdoc method
* @name gettextCatalog#getString
@@ -231,8 +287,6 @@ angular.module('gettext').factory('gettextCatalog', function (gettextPlurals, ge
* @returns {String} translated or annotated string
* @description Translate a string with the given scope and context.
*
* First it tries {@link gettextCatalog#currentLanguage gettextCatalog#currentLanguage} (e.g. `en-US`) then {@link gettextFallbackLanguage fallback} (e.g. `en`).
*
* When `scope` is supplied it uses Angular.JS interpolation, so something like this will do what you expect:
* ```js
* var hello = gettextCatalog.getString("Hello {{name}}!", { name: "Ruben" });
@@ -241,10 +295,7 @@ angular.module('gettext').factory('gettextCatalog', function (gettextPlurals, ge
* Avoid using scopes - this skips interpolation and is a lot faster.
*/
getString: function (string, scope, context) {
var fallbackLanguage = gettextFallbackLanguage(this.currentLanguage);
string = this.getStringFormFor(this.currentLanguage, string, 1, context) ||
this.getStringFormFor(fallbackLanguage, string, 1, context) ||
prefixDebug(string);
string = this.getFallbackStringFormFor(this.currentLanguage, string, 1, context);
string = scope ? $interpolate(string)(scope) : string;
return addTranslatedMarkers(string);
},
@@ -263,10 +314,7 @@ angular.module('gettext').factory('gettextCatalog', function (gettextPlurals, ge
* @description Translate a plural string with the given context.
*/
getPlural: function (n, string, stringPlural, scope, context) {
var fallbackLanguage = gettextFallbackLanguage(this.currentLanguage);
string = this.getStringFormFor(this.currentLanguage, string, n, context) ||
this.getStringFormFor(fallbackLanguage, string, n, context) ||
prefixDebug(n === 1 ? string : stringPlural);
string = this.getFallbackStringFormFor(this.currentLanguage, string, n, context, stringPlural);
if (scope) {
scope.$count = n;
string = $interpolate(string)(scope);
23 changes: 22 additions & 1 deletion src/util.js
Original file line number Diff line number Diff line change
@@ -88,10 +88,31 @@ angular.module('gettext').factory('gettextUtil', function gettextUtil() {
return first + target.substr(1);
}

/**
* @ngdoc method
* @name gettextUtil#copy
* @public
* @param {object} o Object to copy.
* @returns {object} A copy of the object.
* @description Makes a deep copy of an object, making sure to not keep any references to the original object.
*/
function copy(o) {
var output;
var v;
var key;
output = Array.isArray(o) ? [] : {};
for (key in o) {
v = o[key];
output[key] = (typeof v === 'object') ? copy(v) : v;
}
return output;
}

return {
trim: trim,
assert: assert,
startsWith: startsWith,
lcFirst: lcFirst
lcFirst: lcFirst,
copy: copy
};
});
37 changes: 36 additions & 1 deletion test/unit/catalog.js
Original file line number Diff line number Diff line change
@@ -162,14 +162,49 @@ describe("Catalog", function () {
});

it("Should return string from fallback language if current language has no translation", function () {
var strings = { Hello: "Hallo" };
catalog.setStrings("nl", strings);
catalog.setCurrentLanguage("de");
catalog.setFallbackLanguages({ de: ["nl"] });
assert.equal(catalog.getString("Bye"), "Bye");
assert.equal(catalog.getString("Hello"), "Hallo");
});

it("Should return string from first available fallback language if current language has no translation", function () {
var stringsNl = { Hello: "Hallo" };
var stringsFr = { Hello: "Bonjour" };
catalog.setStrings("nl", stringsNl);
catalog.setStrings("fr", stringsFr);
catalog.setCurrentLanguage("de");
catalog.setFallbackLanguages({ de: ["en", "fr", "nl"] });
assert.equal(catalog.getString("Bye"), "Bye");
assert.equal(catalog.getString("Hello"), "Bonjour");
});

it("Should not modify original object used to set fallback languages when translating", function () {
var fallbackLanguages = { en_US: ["fr"] };
var originalFallbacks = JSON.stringify(fallbackLanguages);
catalog.setFallbackLanguages(fallbackLanguages);
catalog.getFallbackStringFormFor("en_US", "Hello");
assert.equal(JSON.stringify(fallbackLanguages), originalFallbacks);
});

it("Should not modify internal fallback languages object when translating", function () {
var fallbackLanguages = { en_US: ["fr"] };
catalog.setFallbackLanguages(fallbackLanguages);
catalog.getFallbackStringFormFor("en_US", "Hello");
assert.equal(JSON.stringify(catalog.getFallbackLanguages()), JSON.stringify(fallbackLanguages));
});

it("Should return string from default fallback language if current language has no translation", function () {
var strings = { Hello: "Hallo" };
catalog.setStrings("nl", strings);
catalog.setCurrentLanguage("nl_NL");
assert.equal(catalog.getString("Bye"), "Bye");
assert.equal(catalog.getString("Hello"), "Hallo");
});

it("Should not return string from fallback language if current language has translation", function () {
it("Should not return string from default fallback language if current language has translation", function () {
var stringsEn = { Baggage: "Baggage" };
var stringsEnGB = { Baggage: "Luggage" };
catalog.setStrings("en", stringsEn);