diff --git a/README.md b/README.md index 666d517b..e7a211ab 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,22 @@ also need to include the [CLDR.js pluralization library](https://github.com/jamesarosen/CLDR.js) and set `CLDR.defaultLanguage` to the current locale code (e.g. "de"). +#### New: I18N_TRANSLATE_HELPER_SPAN + +In previous versions of Ember-I18n, the `{{t}}` helper emitted a `` tag +by default; the tag name could be changed, but the tag could not be removed. + +Ember-I18n now uses Metamorph tags so it no longer requires a wrapping tag. +Emitting a `` is still the default for backwards-compatibility reasons, +but this will change in the next major release. If you wish to opt to +tagless translations, set + +```js +Ember.FEATURES.I18N_TRANSLATE_HELPER_SPAN = false; +``` + +The examples below assume this feature flag is set to `true` (the default). + ### Examples Given @@ -35,37 +51,55 @@ Em.I18n.translations = { 'button.add_user.disabled': 'Saving...' }; ``` + #### A simple translation: ```html

{{t "user.edit.title"}}

``` yields ```html -

Edit User

+

+ + Edit User + +

``` -#### Remove the `span` by specifying a `tagName`: + +#### Emit directly into the h2: ```html {{t "user.edit.title" tagName="h2"}} ``` yields ```html +

Edit User

+ ``` + #### Set interpolated values directly: ```html

{{t "user.followers.title" count="2"}}

``` yields ```html -

All 2 Followers

+

+ + All 2 Followers + +

``` + #### Bind interpolated values: ```html

{{t "user.followers.title" countBinding="user.followers.count"}}

``` yields ```html -

All 2 Followers

+

+ + All 2 Followers + +

``` if `user.getPath('followers.count')` returns `2`. @@ -96,7 +130,9 @@ Add the mixin `Em.Button.reopen(Em.I18n.TranslateableAttributes)` and use like t yields ```html ``` @@ -109,7 +145,9 @@ yields yields ```html + Add + ``` #### Nested Translation Syntax: diff --git a/lib/i18n.js b/lib/i18n.js index 3d18b973..e6d0c474 100644 --- a/lib/i18n.js +++ b/lib/i18n.js @@ -159,51 +159,80 @@ return ++Ember.uuid; } + var TranslationView = Ember._MetamorphView.extend({ + + translationKey: null, + + wrappingTagName: Ember.computed(function(propertyName, newValue) { + if (arguments.length > 1 && newValue != null) { return newValue; } + + var useSpanByDefault; + + if (Ember.FEATURES.hasOwnProperty('I18N_TRANSLATE_HELPER_SPAN')) { + useSpanByDefault = Ember.FEATURES.I18N_TRANSLATE_HELPER_SPAN; + } else { + Ember.deprecate('The {{t}} helper will no longer use a tag in future versions of Ember.I18n. Set Ember.FEATURES.I18N_TRANSLATE_HELPER_SPAN to false to quiet these warnings and maintain older behavior.'); + useSpanByDefault = true; + } + + return useSpanByDefault ? 'span' : null; + }), + + render: function(buffer) { + var wrappingTagName = this.get('wrappingTagName'); + var text = Ember.I18n.t(this.get('translationKey'), this.get('context')); + + if (wrappingTagName) { buffer.push('<' + wrappingTagName + ' id="' + uniqueElementId() + '">'); } + buffer.push(text); + if (wrappingTagName) { buffer.push(''); } + } + + }); + EmHandlebars.registerHelper('t', function(key, options) { - var attrs, context, data, elementID, result, tagName, view; - context = this; - attrs = options.hash; - data = options.data; - view = data.view; - tagName = attrs.tagName || 'span'; + var context = this; + var data = options.data; + var attrs = options.hash; + var tagName = attrs.tagName; delete attrs.tagName; - elementID = uniqueElementId(); + + var translationView = TranslationView.create({ + context: attrs, + translationKey: key, + wrappingTagName: tagName + }); Ember.keys(attrs).forEach(function(property) { - var bindPath, currentValue, invoker, isBindingMatch, normalized, normalizedPath, observer, propertyName, root, _ref; - isBindingMatch = property.match(isBinding); - - if (isBindingMatch) { - propertyName = isBindingMatch[1]; - bindPath = attrs[property]; - currentValue = get(context, bindPath, options); - attrs[propertyName] = currentValue; - invoker = null; - normalized = EmHandlebars.normalizePath(context, bindPath, data); - _ref = [normalized.root, normalized.path], root = _ref[0], normalizedPath = _ref[1]; - - observer = function() { - var elem, newValue; - if (view.$() == null) { - Ember.removeObserver(root, normalizedPath, invoker); - return; - } - newValue = get(context, bindPath, options); - elem = view.$("#" + elementID); - attrs[propertyName] = newValue; - return elem.html(I18n.t(key, attrs)); - }; - - invoker = function() { - Ember.run.scheduleOnce('afterRender', observer); - }; - - return Ember.addObserver(root, normalizedPath, invoker); - } + var isBindingMatch = property.match(isBinding); + if (!isBindingMatch) { return; } + + var propertyName = isBindingMatch[1]; + var bindPath = attrs[property]; + var currentValue = get(context, bindPath, options); + + attrs[propertyName] = currentValue; + + var invoker = null; + var normalized = EmHandlebars.normalizePath(context, bindPath, data); + var _ref = [normalized.root, normalized.path], root = _ref[0], normalizedPath = _ref[1]; + + var observer = function() { + if (translationView.$() == null) { + Ember.removeObserver(root, normalizedPath, invoker); + return; + } + attrs[propertyName] = get(context, bindPath, options); + translationView.rerender(); + }; + + invoker = function() { + Ember.run.scheduleOnce('afterRender', observer); + }; + + return Ember.addObserver(root, normalizedPath, invoker); }); - result = '<%@ id="%@">%@'.fmt(tagName, elementID, I18n.t(key, attrs), tagName); - return new EmHandlebars.SafeString(result); + data.view.appendChild(translationView); }); var attrHelperFunction = function(options) { diff --git a/spec/spec_support.js b/spec/spec_support.js index 24e1bea4..ed9a40c4 100644 --- a/spec/spec_support.js +++ b/spec/spec_support.js @@ -5,6 +5,8 @@ mocha.globals([ 'jQuery*' ]); } + Ember.FEATURES = Ember.FEATURES || {}; + function renderTemplate(template, options) { if (options == null) options = {}; options.template = Ember.Handlebars.compile(template); diff --git a/spec/suite.hdbs b/spec/suite.hdbs index 7c6b08c9..0471ed32 100644 --- a/spec/suite.hdbs +++ b/spec/suite.hdbs @@ -27,6 +27,7 @@ + diff --git a/spec/taglessTranslateHelperSpec.js b/spec/taglessTranslateHelperSpec.js new file mode 100644 index 00000000..b1865ccd --- /dev/null +++ b/spec/taglessTranslateHelperSpec.js @@ -0,0 +1,62 @@ +describe("{{t}}", function() { + + describe("with Ember.FEATURES.I18N_TRANSLATE_HELPER_SPAN on", function() { + + beforeEach(function() { + Ember.FEATURES.I18N_TRANSLATE_HELPER_SPAN = true; + }); + + it("emits a by default", function() { + var view = this.renderTemplate('{{t "foo.bar"}}'); + expect(view.$('span').text()).to.equal('A Foobar'); + }); + + it("includes an element ID for backwards compatibility", function() { + var view = this.renderTemplate('{{t "foo.bar"}}'); + expect(view.$('span').attr('id')).to.not.equal(undefined); + }); + + it('obeys a custom tag name', function() { + var view = this.renderTemplate('{{t "foo.bar" tagName="h2"}}'); + expect(view.$('h2').html()).to.equal('A Foobar'); + }); + + }); + + describe("with Ember.FEATURES.I18N_TRANSLATE_HELPER_SPAN off", function() { + + beforeEach(function() { + Ember.FEATURES.I18N_TRANSLATE_HELPER_SPAN = false; + }); + + it("doesn't emit a by default", function() { + var view = this.renderTemplate('{{t "foo.bar"}}'); + expect(view.$('span').length).to.equal(0); + expect(view.$().text()).to.equal('A Foobar'); + }); + + it("interpolates values", function() { + var view = this.renderTemplate('{{t "bars.all" count="597"}}'); + expect(view.$().text()).to.equal('All 597 Bars'); + }); + + it("still supports setting a tagName", function() { + var view = this.renderTemplate('{{t "foo.bar" tagName="span"}}'); + expect(view.$('span').text()).to.equal('A Foobar'); + }); + + it("includes an element ID when tagName is specified for backwards compatibility", function() { + var view = this.renderTemplate('{{t "foo.bar" tagName="span"}}'); + expect(view.$('span').attr('id')).to.not.equal(undefined); + }); + + it("updates text", function() { + var view = this.renderTemplate('{{t "bars.all" countBinding="view.count"}}', { count: 992 }); + expect(view.$().text()).not.to.equal('All 993 Bars'); + Ember.run(view, 'set', 'count', 993); + expect(view.$().text()).to.equal('All 993 Bars'); + }); + + }); + +}); diff --git a/spec/translateHelperSpec.js b/spec/translateHelperSpec.js index ad67200a..45525ecd 100644 --- a/spec/translateHelperSpec.js +++ b/spec/translateHelperSpec.js @@ -1,4 +1,10 @@ describe('{{t}}', function() { + + beforeEach(function() { + // compatibility mode: + Ember.FEATURES.I18N_TRANSLATE_HELPER_SPAN = true; + }); + it('outputs simple translated strings', function() { var view = this.renderTemplate('{{t "foo.bar"}}'); @@ -59,22 +65,6 @@ describe('{{t}}', function() { }); }); - it('uses a span by default', function() { - var view = this.renderTemplate('{{t "foo.bar"}}'); - - Ember.run(function() { - expect(view.$('span').html()).to.equal('A Foobar'); - }); - }); - - it('obeys a custom tag name', function() { - var view = this.renderTemplate('{{t "foo.bar" tagName="h2"}}'); - - Ember.run(function() { - expect(view.$('h2').html()).to.equal('A Foobar'); - }); - }); - it('handles interpolations from contextual keywords', function() { var view = this.renderTemplate('{{t "foo.bar.named" nameBinding="view.favouriteBeer" }}', { favouriteBeer: 'IPA'