Tutorial: Custom Locale Manager

Knockback.js does not provide a Locale Manager implementation since it depends on how you want to store and where you want to manage your localized strings.

If you choose to use kb.LocalizedObservable, it assumes that you assign your implementation to 'kb.locale_manager'.

A Simple Implementation

Because Knockback actually only requires a Backbone.Model-like signature, a custom locale manager can be implemented quite easily. Here is an example:

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

In more detail...we create an observable with the current locale so the locale identifier can be displayed:

@current_locale = ko.observable(locale_identifier)
this.current_locale = ko.observable(locale_identifier);

We create a get() method that is used by ko.observable to listen for changes using Backbone.Events:

get: (string_id) -> ...
get: function(string_id) {...}

We trigger a Backbone.Events event to notify a subscribed ko.observable

setLocale: (locale_identifier) ->
  @current_locale(locale_identifier)
  @trigger('change', @)
setLocale: function(locale_identifier) {
  this.current_locale(locale_identifier);
  this.trigger('change', this);
}

We mixin Backbone.Events into our LocaleManager to provide 'bind', 'trigger', and 'unbind':

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

<div id='lm_simple'>
  <p>
    <span data-bind="text: label_name"></span>
    <span data-bind="text: name"></span>
  </p>

  <p>
    <span>Current Locale: <span>
    <span data-bind="text: simple_locale_manager.getLocale()"><span>
  </p>
  <button data-bind="click: toggleLocale">Toggle Locale</button>
</div>
# create and initialize the locale manager with two languages
simple_locale_manager = new LocaleManager('en', {
  'en':
    name: 'Name: '
  'fr':
    name: 'Nom: '
})

bob = new Backbone.Model({name: 'Bob'})

view_model = kb.viewModel(bob)

# add a localized label for 'name' and a function for toggling the current locale
view_model.label_name = kb.observable(simple_locale_manager, 'name')
view_model.toggleLocale = ->
  simple_locale_manager.setLocale(if (simple_locale_manager.getLocale() is 'en') then 'fr' else 'en')

ko.applyBindings(view_model, $('#lm_simple')[0])
// create and initialize the locale manager with two languages
var simple_locale_manager = new LocaleManager('en', {
  'en': {
    name: 'Name: '
  },
  'fr': {
    name: 'Nom: '
  }
});

var bob = new Backbone.Model({name: 'Bob'});

var view_model = kb.viewModel(bob);

// add a localized label for 'name' and a function for toggling the current locale
view_model.label_name = kb.observable(simple_locale_manager, 'name');
view_model.toggleLocale = function() {
  return simple_locale_manager.setLocale(simple_locale_manager.getLocale() === 'en' ? 'fr' : 'en');
};

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

Live Result

Current Locale:

An Advanced Implementation

To make a more advanced version of the locale manager, you will want to add error checking and might choose to add parameters to the get() function, trigger on each string, and allow to query for all available locales (among other possible enhancements!):

class LocaleManager
  constructor: (locale_identifier, @translations_by_locale) ->
    @current_locale = ko.observable()
    @setLocale(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)

    string = @translations_by_locale[@current_locale()][string_id]
    return string if arguments == 1     # no arguments

    # add arguments
    string = string.replace("{#{index}}", arg) for arg, index in Array.prototype.slice.call(arguments, 1)
    return string

  getLocale: -> return @current_locale()
  setLocale: (locale_identifier) ->
    @current_locale(locale_identifier)
    @trigger('change', @)
    @trigger("change:#{key}", value) for key, value of @translations_by_locale[@current_locale()]

  getLocales: ->
    locales = []
    locales.push(string_id) for string_id, value of @translations_by_locale
    return locales

_.extend(LocaleManager.prototype, Backbone.Events)
var ko = kb.ko;

var LocaleManager = function() {
  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)'; }

    var string = this.translations_by_locale[this.current_locale()][string_id];
    if (arguments === 1) { return string; }     // no arguments

    // add arguments
    arguments = Array.prototype.slice.call(arguments, 1);
    for (var index = _i = 0; _i < arguments.length; index = ++_i) { string = string.replace("{" + index + "}", arguments[index]); }
    return string;
  };

  this.getLocale = function() { return this.current_locale(); };
  this.setLocale = function(locale_identifier) {
    this.current_locale(locale_identifier);
    this.trigger('change', this);
    var map = this.translations_by_locale[this.current_locale()];
    for (key in map) { this.trigger("change:" + key, map[key]); }
  };

  this.getLocales = function() {
    var locales = [];
    for (var string_id in this.translations_by_locale) {
      value = this.translations_by_locale[string_id];
      locales.push(string_id);
    }
    return locales;
  };
};

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

Bulk Localized Form Labels

If you need to create a large number of localized labels, you could use kb.ViewModel like:

<div id='lm_bulk_labels'>
  <!-- ko foreach:['name', 'start_date', 'end_date'] -->
  <p>
    <span data-bind="text: $parent.labels[$data]"></span>
    <span data-bind="text: $parent[$data]"></span>
  </p>
  <!-- /ko -->

  <p>
    <span>Current Locale: <span>
    <span data-bind="text: bulk_locale_manager.getLocale()"><span>
  </p>
  <button data-bind="click: toggleLocale">Toggle Locale</button>
</div>
bulk_locale_manager = new LocaleManager('en', {
  'en':
    name: 'Name: '
    start_date: 'Start Date: '
    end_date: 'End Date: '
  'fr':
    name: 'Nom: '
    start_date: 'Commencement: '
    end_date: 'Clôture: '
})

view_model = kb.viewModel(new Backbone.Model({name: 'Bob', start_date: '1/1/2012', end_date: '1/2/2012'}))

# use ko.observable to create multiple labels quickly
view_model.labels = kb.viewModel(bulk_locale_manager, ['name', 'start_date', 'end_date'])
view_model.toggleLocale = ->
  bulk_locale_manager.setLocale(if (bulk_locale_manager.getLocale() is 'en') then 'fr' else 'en')

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

var bulk_locale_manager = new LocaleManager('en', {
  'en': {
    name: 'Name: ',
    start_date: 'Start Date: ',
    end_date: 'End Date: '
  },
  'fr': {
    name: 'Nom: ',
    start_date: 'Commencement: ',
    end_date: 'Clôture: '
  }
});

var view_model = kb.viewModel(new Backbone.Model({name: 'Bob', start_date: '1/1/2012', end_date: '1/2/2012'}));

// use ko.observable to create multiple labels quickly
view_model.labels = kb.viewModel(bulk_locale_manager, ['name', 'start_date', 'end_date']);
view_model.toggleLocale = function() {
  return bulk_locale_manager.setLocale(bulk_locale_manager.getLocale() === 'en' ? 'fr' : 'en');
};

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

Live Result

Current Locale: