Reloading the backbone view and i18n translation file in Backbone Application? - javascript

I am using i18n.js in BackboneJS application for Text localization. but stuck at a point where i need reload the text translation on language change in the application. i am calling setting view render() function on language change but doesn't work for me, but on reloading index.html it works. So How can i reload the translation file and view to reflect the changes. below is what i tried--
application-bootstrap.js
var locale = {};
locale.language = "en-us";//default
locale.setLanguage = function (language) {
localStorage.setItem('localLanguage', '' + language);
};
locale.getLanguage = function () {
return localStorage.getItem('localLanguage');
};
require.config({
config: {
i18n: {
locale: locale.getLanguage()
}
}
});
settingView.js
define(['compiled/settingTmpl','i18n!nls/setting'
], function (SettingTmpl,setting) {
'use strict';
var SettingView = Backbone.View.extend({
events: {
"change #languageSelect": "changeLocale"
},
initialize: function () {
WellNation.helper.log("Setting view initialize");
this.render();
},
changeLocale: function (e) {
var locale = e.currentTarget.value;
WellNation.locale.setLanguage(locale);
this.render();
},
render: function () {
this.$el.html(SettingTmpl({speak:setting}));
return this;
}
});
return SettingView;
});
settingTmpl.handlebars
<div class="row">
<label>{{speak.language}}</label>
<select id="languageSelect">
<option value="en-us">English (United States)</option>
<option value="fr-fr">Francais (France)</option>
</select>
</div>
nls/fr-fr/setting.js
define({
"language" : "langue"
});
nls/setting.js
define({
"root" : {
"language" : "Language"
},
"fr-fr" : true // The system will accept French
});

According to this SO question and this github issue it's not possible to change locale at runtime with i18n.js.
From official docs it's not clear could we use it at runtime or not: "RequireJS will use the browser's navigator.language or navigator.userLanguage property to determine what locale values to use for my/nls/colors, so your app does not have to change. If you prefer to set the locale, you can use the module config to pass the locale to the plugin"
So after some researches and walk throw the i18n.js source code I found that the best solution for you will be to keep the same structure and use location.reload().

Related

How to lazy load a js file in React (for a multilingual app)

I would like to create a multilingual app with React.
The way I see it would be to have a js file for each language, for example :
en.js:
module.exports = {
langEnglish: 'English',
langFrench: 'French',
navHome: 'Home',
navUsers: 'Users',
...
};
fr.js:
module.exports = {
langEnglish: 'Anglais',
langFrench: 'Français',
navHome: 'Accueil',
navUsers: 'Utilisateurs',
...
};
As each language file will be quite big and there could be dozens of different languages supported, I would prefer to download only the correct file to use depending on the language chosen in order to minimize loading time (and bandwidth usage).
For example I could have a variable in the app state
var App = React.createClass({
getInitialState: function () {
return {
lang: 'en'
};
},
...
and some user control to switch this variable between fr and en.
Is it possible to load only the en.js file on the initial load, and if the user switches the language to French then load and use the fr.js file instead and so on for each language?
Make use of some advanced webpack features, such as code splitting. You can use webpacks require.ensure for async loading your files.
Create a file:
i18n.js
var currentTranslation = {};
module.exports = {
getTranslation: function() {
return currentTranslation;
},
loadI18n: function(region, cb) {
switch (region) {
case 'en':
require.ensure([], function(require) {
cb(currentTranslation = require('./en'));
}, 'i18n-en'); // will create a chunk named 'i18n-en'
break;
case 'fr':
require.ensure([], function(require) {
cb(currentTranslation = require('./fr'));
}, 'i18n-fr'); // will create a chunk named 'i18n-fr'
break;
default:
require.ensure([], function(require) {
cb(currentTranslation = require('./en'));
}, 'i18n-en');
}
}
}
App.js
var i18n = require('./i18n');
and when you need the translation strings to be loaded async
you can call:
i18n.loadI18n('en', function(texts) {
console.log(texts);
});
once webpack loads that chunk, you will be able to get the translation texts using the function
var texts = i18n.getTranslation(); // call this from anywhere and it will return english texts
if you want to switch language, just call
i18n.loadI18n('fr', function(texts) {
console.log(texts);
});
var texts = i18n.getTranslation(); // will return french texts

using variables in i18n resource files

Hello everyone,
I'm using the i18n (for require.js) library to load my translated strings from resource files based on the user's language.
I have used this approach, since I'm using both backbone and require.js in my project. But I'd like also to put an argument/variable to the string stored in the i18n resource file.
Let's say this is the i18n resource file
define({
'root': {
'options': {
'test': 'Yellow {variable.x}'
}
},
"en-us": true
});
now I'm not really sure whether it's possible possible to pass an argument to evaluate the variable inside the resource file.
define(['underscore', 'backbone', 'models/model', 'templates/template' , 'i18n!nls/resource'], function ( _, Backbone, tModel, template, resource) {
var TooltipView = Backbone.View.extend({
el : $('#test'),
initialize: function(options){
this.model = new tModel();
},
render: function(){
var $el = this.$el;
if(template.length != 0){
var compiledTemplate = template['test']( resource, { variable: "14"} ) /// loads pre-compiled template ///
$el.html(compiledTemplate);
}else{
console.log(" [e] No template found. ");
}
});
}
});
return TooltipView;
});
I'd like to achieve this output:
<h1> Yellow 14 </h1>
As #Evgeniy suggested , I should have used the sprintf library for this case.
Solution:
template:
<span> <%= sprintf( data.text, data.id ) %> </span>
resource file:
define({
'root': {
'options': {
'test': 'Yellow %i'
}
},
"en-us": true
});
then in view I needed to change this:
define(['underscore', 'backbone', 'models/model', 'templates/template' , 'i18n!nls/resource', 'sprintf'], function ( _, Backbone, tModel, template, resource, s_utils) {
...
..
var data = _.extend( resource, { id: 11 } , s_utils ); // this enabled me to use the sprintf function in my template file.
var compiledTemplate = template['test']( data ) /// loads pre-compiled template ///
$el.html(compiledTemplate);
..
return TooltipView;
});

how to switch views with the backbone.js router?

This is more of a conceptual question, in terms of using the backbone router and rendering views in backbone.
for the sake of an example (what I'm building to learn this with) I've got a basic CRUD app for contacts, with create form, a listing of all contacts, a contact single view and an edit form.
for simplicities sake I'm going to say that I would only want to see one of these things at a time. Obviously showing and hiding them with jQuery would be trivial, but thats not what I'm after.
I have two ideas,
1) trigger custom events from my router that removes all views and sends events that could be listened for in all views (triggering a close method ) and a main App view that then instantiates a specific view - ie :
App.Router = Backbone.Router.extend({
routes: {
'' : 'index',
'addnew' : 'addNew',
'contacts/:id' : 'singleContact',
'contacts/:id/edit' : 'editContact'
},
index: function(){
vent.trigger('contactR:closeAll');
vent.trigger('contactR:index');
},
addNew: function() {
vent.trigger('contactR:closeAll');
vent.trigger('contactR:addNew');
},
singleContact: function(id) {
vent.trigger('contactR:closeAll');
vent.trigger('contactR:singleContact', id);
},
editContact: function(id) {
vent.trigger('contactR:closeAll');
vent.trigger('contactR:editContact', id);
},
});
(nb : vent is extending the backbone events obj so I can pub / sub )
2) or would / could / should I send a close all event and create an instance of the view in the router ?
Note I'm looking to achieve this without delving into additional libraries or frameworks like marionette etc.
You can use an utility object like this :
var ViewManager = {
currentView : null,
showView : function(view) {
if (this.currentView !== null && this.currentView.cid != view.cid) {
this.currentView.remove();
}
this.currentView = view;
return view.render();
}
}
and whenever you want to show a view use ViewManager.showView(yourView)
App.Router = Backbone.Router.extend({
routes: {
'' : 'index',
'addnew' : 'addNew',
'contacts/:id' : 'singleContact',
'contacts/:id/edit' : 'editContact'
},
index: function(){
var indexView ...
ViewManager.showView(indexView);
},
addNew: function() {
var addNewView ...
ViewManager.showView(addNewView);
},
singleContact: function(id) {
var singleContactView ...
ViewManager.showView(singleContactView);
},
editContact: function(id) {
var editContactView ...
ViewManager.showView(editContactView);
},
});
So it's the ViewManager that's responsible of rendering your views

Marionette Layout switching strategy

I have the following situation:
app.js: Singleton Marionette.Application() where I define a nav, a footer, and a main region. In the initializer I construct Marionette.Contoller's and attach them to the app's this.controller object for later control. I might not construct all the Controller's here, just the ones I want to Eagerly Load. Some are Lazy Loaded later. I also instantiate a Backbone.Router here, and pass in a reference to my app object:
var theApp = new TP.Application();
theApp.addRegions(
{
navRegion: "#navigation",
mainRegion: "#main",
footerRegoin: "#footer"
});
theApp.addInitializer(function()
{
// Set up controllers container and eagerly load all the required Controllers.
this.controllers = {};
this.controllers.navigationController = new NavigationController({ app: this });
this.controllers.loginController = new LoginController({ app: this });
this.controllers.calendarController = new CalendarController({ app: this });
this.router = new Router({ app: this });
});
**Controller.js: this is a general use controller that handles view & model intsantiation and eventing. Each Controller owns its own Marionette.Layout, to be filled into the App.mainRegion. Each Controller binds to the layout's "show" event to fill in the layout's regions with custom views. Each Controller offers a getLayout() interface that returns the controller's associated layout.
Marionette.Controller.extend(
{
getLayout: function() { return this.layout; },
initialize: function()
{
this.views.myView = new MyView();
...
this.layout.on("show", this.show, this);
...
},
show: function()
{
this.layout.myViewRegion.show(myView);
}
});
router.js: the router uses the app singleton to load a Controller's layout into the App's main region:
...
routes:
{
"home": "home",
"login": "login",
"calendar": "calendar",
"": "calendar"
},
home: function ()
{
var lazyloadedController = new LazyLoadController();
this.theApp.mainRegion.show(lazyLoadController.getLayout());
},
login: function (origin)
{
this.theApp.mainRegion.show(this.theApp.controllers.loginController.layout);
}
As it is, everything works fine except for reloading the same layout / controller twice. What happens is that the DOM events defined in the LoginView do not re-bind on second show. Which is easily solved by moving the LoginView initialization code into the "show" event handler for that Controller:
LoginController = Marionette.Controller.extend(
{
...
show: function()
{
if (this.views.loginView)
delete this.views.loginView.close();
this.views.loginView = new LoginView({ model: this.theApp.session });
this.views.loginView.on("login:success", function()
{
});
this.layout.mainRegion.show(this.views.loginView);
}
Now everything works fine, but it undoes part of the reason I created Controller's to begin with: I want them to own a View and its Models, create them once, and not have to destroy & recreate them every time I switch layouts.
Am I missing something? Is this not how I should be using Layouts? Isn't the whole point of Layouts and Regions that I can switch in & out Views at will?
Obviously I wouldn't jump back to LoginController/Layout often, but what about between a HomeController/Layout, CalendarController/Layout, SummaryController/Layout, etc... in a single page application I might switch between those 'top-level' layouts rather often and I would want the view to stay cached in the background.
I think your problem is that you don't maintain a single instance of the controller. The recommended way to handle routing/controllers (based on Brian Mann's videos) is like this
App.module('Routes', function (Routes, App, Backbone, Marionette, $, _) {
// straight out of the book...
var Router = Marionette.AppRouter.extend({
appRoutes: {
"home": "home",
"login": "login",
"calendar": "calendar"
}
});
var API = {
home: function () {
App.Controller.go_home();
},
login: function () {
App.Controller.go_login();
},
calendar: function () {
App.Controller.go_calendar();
}
};
App.addInitializer(function (options) {
var router = new Router({controller: API});
});
});
... and the controller:
App.module("Controller", function (Controller, App, Backbone, Marionette, $, _) {
App.Controller = {
go_home: function () {
var layout = new App.Views.Main();
layout.on('show', function () {
// attach views to subregions here...
var news = new App.Views.News();
layout.newsRegion.show(news);
});
App.mainRegion.show(layout);
},
go_login: function () {
....
},
go_calendar: function () {
....
}
};
});
I suspect your problem is the lazy-loaded controller...

Ensure requireJS conditional module loaded

I'm trying to use requireJS in an existing project. I have an App module that has information about current locale, url ... and want load another module if current locale is something special:
// App.js
define([], function() {
return {
setEnv: function(obj) { //obj: {locale:'en'}
this.env = obj;
that = this;
if( this.env.locale == 'fa' )
require(['locale/fa'], function(fa) {
that.number = fa.number
};
else
this.number = function( n ) { return n+'' }
}
}
});
Locale files are like:
// locale/fa.js
define([], function() {
var enToFaDigit(n) { // a function converts a number to persian representation };
// more stuff
return {
number: enToFaDigit
};
});
Now the problem is i don't know when App module loaded number method. If i was sure locale/fa module should load at some point, i could wrap it using require(['locale/fa'] or use shim. Currently I'm using old blocking way to load appropriate locale/*.js and PHP to choose right file, without problem. But i want know how requireJS programmers write similar code. Probably there is some way better than this code:
require(['app'], function(App) {
if(App.env.locale == 'fa') {
require(['locale/fa'], function(fa) {
// 8-S
}
}
});
This sounds like a case for the i18n plugin! I believe you are allowed to define functions as well as strings. I would define a bundle like so:
//nls/formatters.js
define({
root: {
currency: function(num) {
// implementation for default currency format (en-US often)
}
},
"fa": true
})
And here's your Persian override:
//nls/fa/formatters.js
define({
currency: function(num) {
// implementation for farsi currency format
}
})
In your app.js file:
require(['app','i18n!nls/formatters'], function(App, Formatters) {
Formatters.currency(5.00); // works!
});

Categories