forwardemail / email-templates

Create, preview (browser/iOS Simulator), and send custom email templates for Node.js. Made for @forwardemail, @ladjs, @cabinjs, @spamscanner, and @breejs.
https://forwardemail.net/docs/send-emails-with-node-js-javascript
MIT License
3.67k stars 337 forks source link

handlebars and translate varialbes #283

Closed lludol closed 6 years ago

lludol commented 6 years ago

Hello,

I can't find how to send parameters to the i18n method ("t"). Here an example:

my_translation_key = "It's now woking {{ firstname }} OR {{ user.firstname }} OR {{ this.user.firstname }} OR this.firstname"
<div>
    Not working: {{ t 'my_translation_key' firstname="user.firstname" }}<br>
    Not working: {{ t 'my_translation_key' user="user" }}<br>
    Not working: {{ t 'my_translation_key' }}<br>
</div>

Email template config:

// ...
const email = new Email({
    message: {
        from: "example@example.com",
    },
    send: true,
    transport:  myTransporter,
    views: {
        root: path.resolve('./src/emails/templates'),
        options: {
            extension: 'handlebars',
        },
    },
    i18n: {
        locales: this.languages,
        defaultLocale: 'en',
        directory: path.resolve('./src/locales/mails'), // json translations files
    },
});

email.send({
    template: myTemplate,
    message:  {
        to: "foo@bar.com",
    },
    locals: {
        user: {
            firstname: 'Foo',
        },
    },
}).then(...);

The result:

Not working: It's now woking OR OR OR
Not working: It's now woking OR OR OR
Not working: It's now woking OR OR OR

The only thing that works is %s but if there is too many parameters in the translation key it's not understandable because I have to put every parameter like this:

another_translation_key= "An example with 4 params: %s, %s, %s, %s"

Example: {{ t 'another_translation_key' param1 param2 param3 param4 }}

My question is simple, how can I send multiple parameters to Handlebars translation method?

Thank you.

niftylettuce commented 6 years ago

I don't use handlebars unfortunately, but did you figure this out yet?

niftylettuce commented 6 years ago

Hey did you figure this out? I'm closing because it seems like you may have. Let me know. I wasn't able to find much on Google w/handlebars.

pylebecq commented 3 years ago

Hi.

I was having the exact same issue, so I decided to investigate and here is everything I learned about using the i18n support from email-template with the handlebars template engine.

Why it's not woking

When you add a i18n config key in the options of the new Email(options) call, it will be stored in the Email object. When you ask email-template to render a template you give two arguments:

email-template will basically find the template location, find the template engine to use, and, before rendering the template, if you gave some i18n config, it will basically instantiate a I18N object from the @ladjs/i18n package and forward it the configuration you gave, while also asking it to register all translation functions inside your template locals.

So, when you give a i18n, if you write:

const html = email.render("user-registration.hbs", { user: { name: "Awesome User" } });

You can imagine it's the same as writing the following (I'm using a console.log() syntax here):

const html = email.render("user-registration.hbs", {
  user: { name: "Awesome User" },
  t: [Function: bound i18nTranslate],
  tn: [Function: bound i18nTranslatePlural],
  tl: [Function: bound i18nTranslationList],
  th: [Function: bound i18nTranslationHash],
  tmf: [Function: bound i18nMessageformat],
  // [...] and other functions I won't write here because you get the point
});

These functions come directly from the i18n package. t is i18n.__ function, tn is i18n.__n function, etc.

It looks good. Basically I would like to call t("greetings", { name: "Awesome User" }) in my template and my translation file is this:

{
  // [...]
  "greetings": "Hello {{ name }},"
}

In handlebars, there is no syntax to create an object literal. But when you quickly read through the handlebars documentation, you can find the following syntax in the "Helpers with hash argument":

{{ t "greetings" name=user.name }}

However, this does not do what you would expect at first. It translates to the following (again, using a console.log() syntax here):

t("greetings", {
  name: "t",
  hash: { name: "Awesome User" },
  data: { root: [Object] },
  loc: { start: [Object], end: [Object] }
});

And the result after rendering is Greetings, t. In that case I get a t because I used {{ name }} in my translation file and it matches one of the keys used by handlebars when calling the function so I was even more 🤯 but usually you would have no value when using any other key.

So this syntax cannot be used that way. It is designed to call a handlebars helper function.

Solution I used: a custom i18n helper

This is the solution I chose because it was the easiest for me. I introduced a proper i18n helper for every function added by the i18n support from email-template injecting i18n base functions inside my locals, and I rely on the fact that I always have a recipient in my locals, which has a key locale.

  function helpers() {
    return {
      $t(key: string, options: any) {
        return options.data.root.t(
          { phrase: key, locale: options.data.root.recipient.locale },
          options.hash,
        );
      },
      // and same for $tn, $tl, $th, etc.
    };
  }

and I use it this way:

// The locals that you want to send to your template. Does not matter where it comes from, I use a simple object here for the sake of the example.
const locals = { user: { name: "Awesome User" } }

const localsForTemplate = {
  ...helpers(),
  ...defaultLocals(),
  ...locals
};

const html = email.render("user-registration.hbs", localsForTemplate);

And the template looks like this:

{{ $t "greetings" name=user.name }}

I know there are other ways to register helpers in handlebars but I did not want to have to instantiate handlebars myself to register the helper and use a custom render function so I just went for this and it works fine.

Hopefully this will be useful to someone.

Solution which did not work: a helper to return an object as parameter

Handlebars supports sub-expressions which allows to use the return value from a helper as an argument for another helper. So I though I could introduce a helper which will return only the hash from handlebars options:

  function helpers() {
    return {
      object(options: any) {
        return options.hash;
      },
    };
  }

And using it this way in the template:

{{ t "greetings" (object name=user.name) }}

This translates to:

t("greetings", { name: "Awesome User" }, {
  name: "t",
  hash: {},
  data: { root: [Object] },
  loc: { start: [Object], end: [Object] }
});

I thought this would work but unfortunately, the i18n package way of checking if we invoke the function with named parameters or positional parameters is the following:

  var argsEndWithNamedObject = function (args) {
    return (
      args.length > 1 &&
      args[args.length - 1] !== null &&
      typeof args[args.length - 1] === 'object'
    )
  }

It uses the last argument instead of using the second position as a fixed position for values to substitute in the translation. In our case, the last argument is the options object given by handlebars and it matches all the conditions from the i18n package, so it does not work as expected as you get the same result as before: i18n uses this options object as substitutions instead of using what we gave as a second parameter. Unfortunate!

niftylettuce commented 3 years ago

Incredible write-up @pylebecq! Would you like to make a PR to add this to the README? I'd gladly accept.

🙇

pylebecq commented 3 years ago

Here it is: https://github.com/forwardemail/email-templates/pull/414 👍