rendrjs / rendr

Render your Backbone.js apps on the client and the server, using Node.js.
MIT License
4.09k stars 312 forks source link

[Question urgent] Dynamic language change with polyglot + rendr #473

Closed cristodcgomez closed 9 years ago

cristodcgomez commented 9 years ago

Hi! I'm having some troubles to load polyglot in client side of my service.

My goal is having 2 buttons that when I click on one of them, the service call extend() method of polyglot and load the language desired. But, I only can load a new instance of polyglot in here:

app/app.js

  initialize: function() {
    this.templateAdapter.registerHelpers(handlebarsHelpers);
    this.polyglot = new Polyglot({locale:'es', phrases: es});
  },

I also overwrote handlebarshelper. to get it working:

module.exports = function(Handlebars) {
  return {
    copyright: function(year) {
      return new Handlebars.SafeString("©" + year);
    },
    t: function (key) {
      console.log(this);
      var res = this.app;
      return res.polyglot.t(key);
    },
    tc: function (key) {
      console.log(this);
      var res = this._app;
      return res.polyglot.t(key);
    }
  };
};

Notice that I have t() and tc(), because my polyglot needs to translate in layout and in the templates of each view, so i use t() in layout and tc in other case.

to get it working, I'm using JQuery to get when someone clicks on the correct button, and I'm setting this in app/view/base.js

module.exports = RendrView.extend({
  postRender: function(){
    console.log(App);
    var that = this;
    $('.es').click(function(){
      App.polyglot.extend(this.es);
      App.polyglot.locale('es');
      localStorage['mk-language'] = that.app.polyglot.locale();
    });

    $('.fr').click(function(){
      App.polyglot.extend(this.fr);
      App.polyglot.locale('fr');
      localStorage['mk-language'] = that.app.polyglot.locale();
    });
  }

I think that it's ugly code, but I'm in a hurry so I need help :confounded: . Thank you in advance

saponifi3d commented 9 years ago

This is the way that I've got it all setup, it works pretty well for us :)

// app/app.js
module.exports = BaseApp.extend({
  initialize: function() {
    this.polyglot = new Polyglot();
    ...
  },

  // This is called directly on the server so we can use Polyglot
  // during server rendering.
  setPhrases: function(phrases) {
    this.polyglot.extend(phrases);
  },

  // The `start` method only gets called in the client on pageload.
  start: function() {
    // This sets the phrases in the client-side so we can use Polyglot
    // during client rendering.  The phrases are accessible with
    // `this.get('phrases')` because in the middleware (below) we set
    // them as an app attribute. An app's attributes are serialized as 
    // JSON and bootstrapped to the client.
    this.setPhrases(this.get('phrases'));

    // If you have lots of phrases, you may as well free up some memory
    // because they will no longer be accessed directly from `this.get()`.
    this.unset('phrases');
    ...
  },
  ...
});

// server/middleware/initI18n.js
module.exports = function() {
  return function(req, res, next) {
    // Fetch phrases from API or cache based on req.
    // Example fake method:
    fetchPhrasesBasedOnReq(req, function(err, phrases) {
      if (err) next(err);

      // Set the phrases so they're usable on the server during
      // view rendering, etc.      
      req.rendrApp.setPhrases(phrases);

      // Now set them as app attributes so they get serialized and
      // bootstrapped onto the page as JSON so we can use them in
      // the client.
      req.rendrApp.set({phrases: phrases});

      next(err);
    });
  };
};

after that I created a handlebars helper to do the translation:

t: function (phrase, options) {
  return this.app.polyglot(phrase, options.hash);
}

that way in your template you can simply inline the button like this: <button>{{ t 'button_text' }}</button>

Dynamically changing the language is a bit harder cause you'll have to deliver all of the translations on page load. The way I've gotten around that is to simply refresh the page to display the new locale. That way you only need to deliver what is being used by the client at that time.

If it's a requirement to do the translations on the client-side only, you might just want to look at the structure of the phrases and maybe have it be a JSON structure that is locale + phrase => translation

cristodcgomez commented 9 years ago

Thank you. I'll try it... I didn't developed that middleware. Maybe it's the solution :)

saponifi3d commented 9 years ago

@cristomc was that able to resolve your issue?

pjanuario commented 9 years ago

@saponifi3d your solution is pretty nice, may be would be nice to put it in a example/post/tutorial, it would be usefull for a lot of people I am pretty sure... :) what do you think?

saponifi3d commented 9 years ago

@pjanuario sounds like a great idea! starts writing a blog post

pjanuario commented 9 years ago

@saponifi3d I didn't get it, if you started writing a blog post or you are asking me to do it! :) Either way I available to help you with that, I don't have experience with polyglot but I can help building a sample application using it.

saponifi3d commented 9 years ago

sorry! I'm writing one about it. The italics were meant to show an action.. I guess not everybody does that! My b. Blog post is coming.

pjanuario commented 9 years ago

@saponifi3d awesome... I hope to read it soon! :)

cristodcgomez commented 9 years ago

I couldn't try it until now... starting to test middleware solution. BTW that blog post will be wellcome to understand better this :)

EDIT: Not working for me :disappointed: I can't start middle-ware code. Maybe wrong place for calling the initI18n?

index.js
/**
 * Initialize Express middleware stack.
 */
server.configure(function (expressApp) {
 ...
  expressApp.use(mw.initI18n);
});
saponifi3d commented 9 years ago

Hmmm, that's def where you want to attach middleware for Rendr, and everything you have looks like it should work. Are you mounting Rendr in another express app? I've seen some issues around that (more of problems w/ the middleware, Rendr mounts fine in multiple express apps).

Also, if you're accessing the this scope in initI18n you'll be in the wrong scope for mw, maybe that's it?

cristodcgomez commented 9 years ago

I started using 1 of the rendr examples (middle-ware example), and I’m not accessing to this scope. At this moment my page don't load (keep loading eternally).

I'll try to upload to my git the code later... maybe I made a mistake :sa: EDIT: I commented the mw line. I'm having this error:

500 TypeError: Cannot call method 'polyglot' of undefined

       at Object.t (/media/Datos/web/market/mkplace/app/lib/handlebarsHelpers.js:11:23)
       at Object.templates.home/index.Handlebars.template.main (/media/Datos/web/market/mkplace/app/templates/compiledTemplates.js:175:78)
       at ret (/media/Datos/web/market/mkplace/node_modules/rendr-handlebars/node_modules/handlebars/dist/cjs/handlebars/runtime.js:137:30)
       at module.exports.Backbone.View.extend.getInnerHtml (/media/Datos/web/market/mkplace/node_modules/rendr/shared/base/view.js:191:12)
       at module.exports.Backbone.View.extend.getHtml (/media/Datos/web/market/mkplace/node_modules/rendr/shared/base/view.js:198:21)
       at ViewEngine.getViewHtml (/media/Datos/web/market/mkplace/node_modules/rendr/server/viewEngine.js:75:15)
       at ViewEngine.render (/media/Datos/web/market/mkplace/node_modules/rendr/server/viewEngine.js:22:16)
       at View.render (/media/Datos/web/market/mkplace/node_modules/express/lib/view.js:93:8)
       at EventEmitter.app.render (/media/Datos/web/market/mkplace/node_modules/express/lib/application.js:566:10)
       at ServerResponse.res.render (/media/Datos/web/market/mkplace/node_modules/express/lib/response.js:938:7)

Maybe it's a mistake about when is using the helper as server?

I created this repo, it's easier see there the code if you need it: https://github.com/cristomc/mkplace

Thank you for helping too! you are awesome!

saponifi3d commented 9 years ago

Oh, I wonder if you're having a problem w/ the private view scope variables in the handle bar helpers, try this._app.polyglot instead of this.app.polyglot. rendr-handlebars moves all of the "private" variables to a variable with an _ prefix so it's not confused with the variables being passed in. Here's some docs on the private variables: https://github.com/rendrjs/rendr-handlebars#helpers

cristodcgomez commented 9 years ago

Thank you! that scope problem is gone :)

I'll try later to fix links to translate, but I think it's almost done

cristodcgomez commented 9 years ago

Finally I managed this to get it working. I made a variation to control languaje switch using cookies (I don't know if its the best choise but I need end this project as fast as I can so... I used it :P)

//server side, I put this in index.js but can be made as a separate file:

  expressApp.use(function (req, res, next) {
    if(req.cookies.lang !== 'es'){ //I check which languaje is set in the cookies, if there is no cookie, fr by default
      res.cookie('lang', 'fr', { maxAge: 900000 });
      req.rendrApp.setPhrases('fr');
    }
    else{
      res.cookie('lang', 'es', { maxAge: 900000 });
      req.rendrApp.setPhrases('es');
    }
    next();
  });
app.js: same as proposed. It has just a little difference. I keep my phrases loaded in this place, so i only need  check phrases text to load it.
...
  , es =require('./i18n/es_ES/locale.json')
  , fr =require('./i18n/fr_FR/locale.json');
...
  initialize: function() {
    this.templateAdapter.registerHelpers(handlebarsHelpers);
    this.polyglot = new Polyglot();
  },
  setPhrases: function(phrases) {
    if(phrases === 'es')
      this.polyglot.extend(es);
    else
      this.polyglot.extend(fr);
  },
...
//app/lib/handlebarsHelpers.js: just modified to control scope of polyglot, not fancy, it can be improved, but it works.
module.exports = function(Handlebars) {
  return {
    copyright: function(year) {
      return new Handlebars.SafeString("&copy;" + year);
    },
     t: function (phrase, options) {
        try{
          return this.app.polyglot.t(phrase, options.hash);
        }
        catch (err){
          return this._app.polyglot.t(phrase, options.hash);
        }
    }
  };
};

Thank you a lot!! now this part of the project is done, so I can finish other parts of the webservice. I'll put a tutorial too in my blog after finish this, It can be usefull for noobs with rendr & express/node like me :D

Greetings!

saponifi3d commented 9 years ago

@cristomc happy to help! feel free to post questions here whenever :) Stoked that you're picking up Rendr. Let me know if you have any problems with the documentation site too! That's a pretty new thing and always looking for feedback on it.

The blog post sounds epic! I'm working on a few myself to help people ease into Rendr. I'll probably start doing more complex workflows for people who already have Rendr apps too.

pjanuario commented 9 years ago

@cristomc Drop the URL here when you the post online! ;)

cristodcgomez commented 9 years ago

of course. ATM I'm bussy due to master's projects and work, but I'll post info about rendr as fast as I can. :)