diff --git a/src/i18n/view.i18n.js b/src/i18n/view.i18n.js index dd065c50..2775e96b 100644 --- a/src/i18n/view.i18n.js +++ b/src/i18n/view.i18n.js @@ -1,36 +1,54 @@ /*jshint multistr:true */ -// TODO probably some kind of mixin would be better, like: my.View - Backbone.View.extend({}); _.extend(my.View, Backbone.I18nView); +"use strict"; +var I18nMessages = function(uniqueID, translations, languageResolverOrLocale, appHardcodedLocale) { + var defaultResolver = function() { + }; -Backbone.I18nView = Backbone.View.extend({ - defaultLocale: 'en', - locale: 'en', - cache: {}, - initializeI18n: function(locale, appHardcodedLocale) { - this.defaultLocale = appHardcodedLocale || 'en'; - this.locale = locale || this.defaultLocale; + // which locale should we use? + languageResolverOrLocale = (typeof languageResolverOrLocale !== 'undefined') ? languageResolverOrLocale : defaultResolver; + appHardcodedLocale = appHardcodedLocale || 'en'; - this.cache[this.locale] = {}; - }, + if (typeof(languageResolverOrLocale) === 'function') { + languageResolverOrLocale = languageResolverOrLocale(); + } + if (languageResolverOrLocale == undefined) { + languageResolverOrLocale = appHardcodedLocale; + } + + if (I18nMessages.prototype._formatters[uniqueID, languageResolverOrLocale]) { + return I18nMessages.prototype._formatters[uniqueID, languageResolverOrLocale]; + } + I18nMessages.prototype._formatters[uniqueID, languageResolverOrLocale] = this; + + // ========== VARIABLES & FUNCTIONS ========== + var self = this; + + this.locale = languageResolverOrLocale; + this.cache= {}; + + this.getLocale = function() { + return this.locale; + }; + + // ============= FormatJS.io backend ========= + + this.t = function(key, values, defaultMessage) { + values = (typeof values !== 'undefined') ? values : {}; - // TODO how to use it from outside? an singleton instance of I18n? use case: pass translated strings into view initializer - t: function(key) { - this.t(key, {}, null); - }, - t: function(key, values, defaultMessage) { // get the message from current locale - var msg = recline.View.translations[this.locale][key]; + var msg = this.translations[this.locale][key]; // fallback to key or default message if no translation is defined if (msg == null) { - if (this.locale != this.defaultLocale) { + if (this.locale != this.appHardcodedLocale) { console.warn("Missing locale for " + this.locale + "." + key); } msg = defaultMessage; } if (msg == null) { msg = key; - if (this.locale === this.defaultLocale) { + if (this.locale === this.appHardcodedLocale) { // no need to define lang entry for short sentences, just use underscores as spaces msg = msg.replace(/_/g, ' '); } @@ -38,9 +56,9 @@ Backbone.I18nView = Backbone.View.extend({ } try { - var formatter = this.cache[this.locale][msg]; + var formatter = this.cache[msg]; if (formatter === undefined) { - this.cache[this.locale][msg] = formatter = new IntlMessageFormat(msg, this.locale); + this.cache[msg] = formatter = new IntlMessageFormat(msg, this.locale); } var formatted = formatter.format(values); @@ -56,16 +74,18 @@ Backbone.I18nView = Backbone.View.extend({ } }, - MustacheFormatter: function() { - var formatter = new Proxy(this, { - get: function(view, name) { + // ============ Mustache integration ======== + + this.mustacheI18Tags = function() { + var tagsProxy = new Proxy(this, { + get: function(messages, name) { return function() { var f = function (text, render) { - var trans = view.t(name, this, text); + var trans = messages.t(name, this, text); return render(trans); } f.toString = function() { - return view.t(name); + return messages.t(name); } return f; }; @@ -76,7 +96,11 @@ Backbone.I18nView = Backbone.View.extend({ }); return { - 't': formatter, + 't': tagsProxy, }; }, -}); \ No newline at end of file + + this.injectMustache = function(tmplData) { + return _.extend(tmplData, self.mustacheI18Tags()); + } +}; \ No newline at end of file diff --git a/test/base.js b/test/base.js index 93ebd69a..dcf016ed 100644 --- a/test/base.js +++ b/test/base.js @@ -22,6 +22,14 @@ var Fixture = { ]; var dataset = new recline.Model.Dataset({records: documents, fields: fields}); return dataset; + }, + + getTranslations: function() { + return { + pl: { + Grid: 'Tabela' + } + }; } }; diff --git a/test/view.i18n.test.js b/test/view.i18n.test.js index 52adfce5..40919b95 100644 --- a/test/view.i18n.test.js +++ b/test/view.i18n.test.js @@ -1,101 +1,80 @@ (function ($) { -module("View - i18n support"); +module("I18nMessages"); test('translate simple key custom locale', function () { - var dataset = Fixture.getDataset(); - var view = new recline.View.MultiView({ - model: dataset, - locale: 'pl' // todo or should it go in the state parameter? - }); + var fmt = I18nMessages('somelib', Fixture.getTranslations(), 'pl'); - equal(view.t('Grid'), 'Tabela'); + equal(fmt.t('Grid'), 'Tabela'); }); test('translate simple key default locale', function () { - var dataset = Fixture.getDataset(); - var view = new recline.View.MultiView({ - model: dataset - }); + var fmt = I18nMessages('somelib', Fixture.getTranslations()); - equal(view.t('Add_row'), 'Add row'); + equal(fmt.t('Add_row'), 'Add row'); }); test('override custom locale', function () { - var dataset = Fixture.getDataset(); - var view = new recline.View.MultiView({ - model: dataset, - locale: 'pl' - }); + var fmt = I18nMessages('recline', recline.View.translations, 'pl'); var oldTranslation = recline.View.translations['pl']['Grid']; - // set custom strings in external app after recline script + // set custom strings in external app after including recline script recline.View.translations['pl']['Grid'] = 'Dane'; - equal(view.t('Grid'), 'Dane'); + equal(fmt.t('Grid'), 'Dane'); + recline.View.translations['pl']['Grid'] = oldTranslation; }); test('override default locale', function () { - var dataset = Fixture.getDataset(); - var view = new recline.View.MultiView({ - model: dataset - }); + var fmt = I18nMessages('recline', recline.View.translations, 'en'); var oldTranslation = recline.View.translations['en']['Grid']; - // set custom strings in external app after recline script + // set custom strings in external app after including recline script recline.View.translations['en']['Grid'] = 'Data'; - equal(view.t('Grid'), 'Data'); + equal(fmt.t('Grid'), 'Data'); + recline.View.translations['en']['Grid'] = oldTranslation; }); test('fallback to key if translation not present', function () { - var dataset = Fixture.getDataset(); - var view = new recline.View.MultiView({ - model: dataset - }); + var fmt = I18nMessages('somelib', Fixture.getTranslations()); - equal(view.t('thiskeydoesnotexist'), 'thiskeydoesnotexist'); + equal(fmt.t('thiskeydoesnotexist'), 'thiskeydoesnotexist'); }); test('fallback to default message', function () { - var dataset = Fixture.getDataset(); - var view = new recline.View.MultiView({ - model: dataset - }); + var fmt = I18nMessages('somelib', Fixture.getTranslations()); - equal(view.t('thiskeydoesnotexist', {}, 'Fallback to default message'), 'Fallback to default message'); + equal(fmt.t('thiskeydoesnotexist', {}, 'Fallback to default message'), 'Fallback to default message'); }); test('mustache formatter - simple key', function () { - var dataset = Fixture.getDataset(); - var view = new recline.View.MultiView({ - model: dataset, - locale: 'pl' - }); + var fmt = I18nMessages('somelib', Fixture.getTranslations(), 'pl'); + // test without template rendering + var mustacheIntegration = fmt.injectMustache({}); + equal(mustacheIntegration.t.Grid, 'Tabela') + + // test within template rendering var template = '{{t.Grid}}'; var tmplData = {}; - // adding i18n support [do it in view before passing data to render functions] - tmplData = _.extend(tmplData, view.MustacheFormatter()); + tmplData = fmt.injectMustache(tmplData); var out = Mustache.render(template, tmplData); equal(out, 'Tabela'); }); test('mustache formatter - complex key', function () { - var dataset = Fixture.getDataset(); - var view = new recline.View.MultiView({ - model: dataset, - }); + var fmt = I18nMessages('somelib', Fixture.getTranslations(), 'pl'); var template = '{{#t.num_records}}{recordCount} records{{/t.num_records}}'; var tmplData = {recordCount: 5}; - // adding i18n support [do it in view before passing data to render functions] - tmplData = _.extend(tmplData, view.MustacheFormatter()); + // injecting i18n support [do it in view before passing data to render functions] + tmplData = fmt.injectMustache(tmplData); var out = Mustache.render(template, tmplData); equal(out, '5 records'); @@ -103,46 +82,80 @@ test('mustache formatter - complex key', function () { test('translate complex key default locale', function () { - var dataset = Fixture.getDataset(); - var view = new recline.View.MultiView({ - model: dataset - }); + var fmt = I18nMessages('somelib', Fixture.getTranslations(), 'pl'); equal(view.t('codeforall', {records: 3}, '{records} records'), '3 records'); }); -test('translate complex key custom locale', function () { - var dataset = Fixture.getDataset(); - var view = new recline.View.MultiView({ - model: dataset, - locale: 'pl' - }); +test('mustache formatter - translate complex key custom locale', function () { + var translations = { + pl: { + codeforall: '{records} rekordy' + } + }; + var fmt = I18nMessages('somelib', translations, 'pl'); - recline.View.translations['pl']['codeforall'] = '{records} rekordy'; - equal(view.t('codeforall', {records: 3}, '{records} records'), '3 rekordy'); + equal(fmt.t('codeforall', {records: 3}, '{records} records'), '3 rekordy'); }); -test('translate complex key custom locale custom count', function () { - var dataset = Fixture.getDataset(); - var view = new recline.View.MultiView({ - model: dataset, - locale: 'pl' - }); - - recline.View.translations['pl']['codeforall'] = '{records, plural, ' + +test('mustache formatter - translate complex key custom locale custom count', function () { + var translations = { + pl: { + codeforall: {records, plural, ' + '=0 {brak zdjęć}' + '=1 {{records} zdjęcie}' + 'few {{records} zdjęcia}' + - 'other {{records} zdjęć}}'; + 'other {{records} zdjęć}}' + } + }; + var fmt = I18nMessages('somelib', translations, 'pl'); - equal(view.t('codeforall', {records: 0}), 'brak zdjęć'); - equal(view.t('codeforall', {records: 1}), '1 zdjęcie'); - equal(view.t('codeforall', {records: 3}), '3 zdjęcia'); - equal(view.t('codeforall', {records: 5}), '5 zdjęć'); + equal(fmt.t('codeforall', {records: 0}), 'brak zdjęć'); + equal(fmt.t('codeforall', {records: 1}), '1 zdjęcie'); + equal(fmt.t('codeforall', {records: 3}), '3 zdjęcia'); + equal(fmt.t('codeforall', {records: 5}), '5 zdjęć'); }); +test('I18nMessages specified locale', function () { + var fmt = I18nMessages('somelib', {}, 'pl'); -// todo test dynamic language changes + equal(fmt.getLocale(), 'pl'); +}); + +test('I18nMessages default locale', function () { + var fmt = I18nMessages('somelib', {}); + + // no language set in HTML tag + equal($('html').attr('lang'), undefined); + + equal(fmt.getLocale(), 'en'); +}); + +test('I18nMessages default html:lang default locale resolver', function () { + var fmt = I18nMessages('somelib', {}); + + + $('html').attr('lang', 'de'); + equal(fmt.getLocale(), 'de'); + $('html').attr('lang', null); +}); + +test('I18nMessages default locale custom resolver', function () { + var localeResolver = function() { return 'fr'; }; + var fmt = I18nMessages('somelib', {}, localeResolver); + + equal(fmt.getLocale(), 'fr'); +}); + +test('I18nMessages singletons, function () { + var lib1_pl = I18nMessages('lib1', {}, 'pl'); + var lib2_pl = I18nMessages('lib2', {}, 'pl'); + var lib1_en = I18nMessages('lib1', {}, 'en'); + + strictEqual(I18nMessages('lib1', {}, 'pl'), lib1_pl); + notStrictEqual(lib1_pl, lib1_en); + notStrictEqual(lib1_pl, lib2_pl); +}); })(this.jQuery);