Tutorial: kb.LocalizedObservable

Knockback.js uses a convention of storing your custom Locale Manager in 'kb.locale_manager' so that it can be globally used to localize aspects of your application (please see the Custom Locale Manager tutorial for usage examples).

The kb.LocalizedObservable watches the 'kb.locale_manager' instance for changes and updates itself accordingly.

warning This is one of the few cases where you are likely to derive from a Knockback class and where you need to know about observable returned. Basically, when you derive your own type of kb.LocalizedObservable, you need to return the wrapped observable rather than the instance itself from the constructor usingkb.utils.wrappedObservable

We'll use the following Locale Manager for these examples:

class LocaleManager
  constructor: (locale_identifier, @translations_by_locale) ->
    @current_locale = ko.observable(locale_identifier)

  get: (string_id) ->
    return '(no translation)' unless @translations_by_locale[@current_locale()]
    return '(no translation)' unless @translations_by_locale[@current_locale()].hasOwnProperty(string_id)
    return @translations_by_locale[@current_locale()][string_id]

  getLocale: -> return @current_locale()
  setLocale: (locale_identifier) ->
    @current_locale(locale_identifier)
    @trigger('change', @)

_.extend(LocaleManager.prototype, Backbone.Events)

kb.locale_manager = new LocaleManager('en-GB', {
  'en-GB':
    body: 'I ought to localize this sentence.'
  'fr-FR':
    body: 'J\'ai besoin de localiser cette phrase.'
})
var ko = kb.ko;

var LocaleManager = function(locale_identifier, translations_by_locale) {
  this.translations_by_locale = translations_by_locale;
  this.current_locale = ko.observable(locale_identifier);

  this.get = function(string_id) {
    if (!this.translations_by_locale[this.current_locale()]) {return '(no translation)';}
    if (!this.translations_by_locale[this.current_locale()].hasOwnProperty(string_id)) {return '(no translation)';}
    this.translations_by_locale[this.current_locale()][string_id];
  };

  this.getLocale = function() { return this.current_locale(); };
  this.setLocale = function(locale_identifier) {
    this.current_locale(locale_identifier);
    this.trigger('change', this);
  };
};

_.extend(LocaleManager.prototype, Backbone.Events);

kb.locale_manager = new LocaleManager('en-GB', {
  'en-GB': {
    body: 'I ought to localize this sentence.'
  },
  'fr-FR': {
    body: 'J\'ai besoin de localiser cette phrase.'
  }
});

Nested String Id

If you want to simply localize some read-only text based on a nested string id, you typically use a ko.Observable to access the attribute with the string id and use a localizer that simply looks up the string in the kb.locale_manager.

LocalizedStringLocalizer = kb.LocalizedObservable.extend({
  constructor: (value, options, view_model) ->
    kb.LocalizedObservable.prototype.constructor.apply(this, arguments)
    return kb.utils.wrappedObservable(@)

  read: (string_id) ->
    return if (string_id) then kb.locale_manager.get(string_id) else ''
})
var LocalizedStringLocalizer = kb.LocalizedObservable.extend({
  constructor: function(value, options, view_model) {
    kb.LocalizedObservable.prototype.constructor.apply(this, arguments);
    return kb.utils.wrappedObservable(this);
  },
  read: function(string_id) {
    return string_id ? kb.locale_manager.get(string_id) : '';
  }
});
<div id='kblo_read_only'>
  <p data-bind="text: main_text"></p>

  <p>
    <span>Current Locale: <span>
    <span data-bind="text: kb.locale_manager.getLocale()"><span>
  </p>
  <button data-bind="click: toggleLocale">Toggle Locale</button>
</div>
texts = new Backbone.Model({main_text_id: 'body'})

view_model =
  main_text: kb.observable(texts, {key: 'main_text_id', localizer: LocalizedStringLocalizer})
  toggleLocale: ->
    kb.locale_manager.setLocale(if kb.locale_manager.getLocale() is 'en-GB' then 'fr-FR' else 'en-GB')

ko.applyBindings(view_model, $('#kblo_read_only')[0])
var ko = kb.ko;

var texts = new Backbone.Model({main_text_id: 'body'});

view_model = {
  main_text: kb.observable(texts, {key: 'main_text_id', localizer: LocalizedStringLocalizer}),
  toggleLocale: function() {
    return kb.locale_manager.setLocale(kb.locale_manager.getLocale() === 'en-GB' ? 'fr-FR' : 'en-GB');
  }
};

ko.applyBindings(view_model, $('#kblo_read_only')[0]);

Live Result

Current Locale:

Localized Input

If you want to simply label an input field, you can use a read-only localized observable that only looks up the string in the kb.locale_manager.

LongDateLocalizer = kb.LocalizedObservable.extend({
  constructor: (value, options, view_model) ->
    kb.LocalizedObservable.prototype.constructor.apply(this, arguments)
    return kb.utils.wrappedObservable(@)

  read: (value) ->
    return Globalize.format(value, 'dd MMMM yyyy', kb.locale_manager.getLocale())

  write: (localized_string, value) ->
    # reset if invalid
    new_value = Globalize.parseDate(localized_string, 'dd MMMM yyyy', kb.locale_manager.getLocale())
    if not (new_value and _.isDate(new_value))
      return kb.utils.wrappedObservable(this).resetToCurrent()

    value.setTime(new_value.valueOf())
})
var LongDateLocalizer = kb.LocalizedObservable.extend({
  constructor: function(value, options, view_model) {
    kb.LocalizedObservable.prototype.constructor.apply(this, arguments);
    return kb.utils.wrappedObservable(this);
  },
  read: function(value) {
    return Globalize.format(value, 'dd MMMM yyyy', kb.locale_manager.getLocale());
  },
  write: function(localized_string, value) {
    // reset if invalid
    var new_value = Globalize.parseDate(localized_string, 'dd MMMM yyyy', kb.locale_manager.getLocale());
    if (!(new_value && _.isDate(new_value))) {
      return kb.utils.wrappedObservable(this).resetToCurrent();
    }
    return value.setTime(new_value.valueOf());
  }
});
<div id='kblo_read_write'>
  <input class='input-small pull-right' data-bind="value: date, valueUpdate: 'keyup'"/>

  <p>
    <span>Current Locale: <span>
    <span data-bind="text: kb.locale_manager.getLocale()"><span>
  </p>
  <button data-bind="click: toggleLocale">Toggle Locale</button>
</div>
model = new Backbone.Model({date: new Date()})

view_model =
  date: kb.observable(model, {key: 'date', localizer: LongDateLocalizer})
  toggleLocale: ->
    kb.locale_manager.setLocale(if kb.locale_manager.getLocale() is 'en-GB' then 'fr-FR' else 'en-GB')

ko.applyBindings(view_model, $('#kblo_read_write')[0])
var ko = kb.ko;

var model = new Backbone.Model({date: new Date()});

view_model = {
  date: kb.observable(model, {key: 'date', localizer: LongDateLocalizer}),
  toggleLocale: function() {
    return kb.locale_manager.setLocale(kb.locale_manager.getLocale() === 'en-GB' ? 'fr-FR' : 'en-GB');
  }
};

ko.applyBindings(view_model, $('#kblo_read_write')[0]);

Live Result

Current Locale: