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 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)';
    }
    return 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);
    return 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.

Custom kb.LocalizedObservable:

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 ''
})
// Generated by CoffeeScript 1.3.3
var LocalizedStringLocalizer;

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) {
    if (string_id) {
      return kb.locale_manager.get(string_id);
    } else {
      return '';
    }
  }
});

Views:

<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>

ViewModel and Bindings:

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

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

ko.applyBindings(view_model, $('#kblo_read_only')[0])
// Generated by CoffeeScript 1.3.3
var texts, view_model;

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

view_model = {
  main_text: kb.observable(texts, {
    key: 'main_text_id',
    localizer: LocalizedStringLocalizer
  }),
  toggleLocale: function() {
    if (kb.locale_manager.getLocale() === 'en-GB') {
      return kb.locale_manager.setLocale('fr-FR');
    } else {
      return kb.locale_manager.setLocale('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.

Custom kb.LocalizedObservable:

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) ->
    new_value = Globalize.parseDate(localized_string, 'dd MMMM yyyy', kb.locale_manager.getLocale())

    # reset if invalid
    if not (new_value and _.isDate(new_value))
      return kb.utils.wrappedObservable(this).resetToCurrent()

    value.setTime(new_value.valueOf())
})
// Generated by CoffeeScript 1.3.3
var LongDateLocalizer;

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) {
    var new_value;
    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());
  }
});

Views:

<div id='kblo_read_write'>
  <input data-bind="value: date"/>

  <p>
    <span>Current Locale: <span>
    <span data-bind="text: kb.locale_manager.getLocale()"><span>
  </p>
  <button data-bind="click: toggleLocale">Toggle Locale</button>
</div>

ViewModel and Bindings:

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

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

ko.applyBindings(view_model, $('#kblo_read_write')[0])
// Generated by CoffeeScript 1.3.3
var model, view_model;

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

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

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

Live Result

Current Locale: