Add Custom Translation Text

When running a multi-language site, a key component is offering text translations to shoppers who use different languages. As a platform, NetSuite offers a number of translations built in, but these only provide partial coverage. To complete it you will need to know how to write code that can serve translations, as well as how to add your own text.

In this article we're going to look at:

  • The translate custom method added to Underscore, used to translate text in JavaScript
  • The translate custom helper added to Handlebars, used to translate text in templates
  • Setting up custom language files (dictionaries)
  • Advanced translations, using parameters
  • Translating item data

For the purposes of this demonstration I will need to a custom module to work with (as all standard functionality is pre-translated) and for that I'm going to use the testimonials module I provided a while back. I also need to have a site that uses at least two languages, so I've set up my site to use Canadian French.

Translate with Underscore

Right off the bat, I should say that this is a potentially misleading heading. The ability to translate using the translate method isn't provided by Underscore as such; rather, it is a custom method that we add to Underscore via Utilities > Utils.js.

It's quite simple and clever how it works:

  1. Takes any parameters and adds them to an array
  2. Takes the text and checks for a key in the language file that matches the string
  3. Takes the corresponding value and mixes in the parameters as variables into the string value
  4. Returns the final 'translated' text string

This function is then rolled into Underscore, so that it can be called as a method using code like this:

// Syntax:
_('<string to translate>').translate(<parameter 1>,<parameter 2>);

// Example:
_('You have visited us $(0) times and bought $(1) products!).translate(visits,itemsPurchased);

Where $(0) and $(1) are used to substitute in the parameters.

The string you are translating must match exactly what is in the dictionary file. If it is not, it will be printed as is. We'll talk about this later.

Another thing to note is that, which I hope is obvious, is that you obviously don't need to use parameters in your string if you don't need them. In that case, just put a normal string and pass nothing in the function, like this:

_('Welcome to our store!').translate();

 

Translate with Handlebars

Like the Underscore method above, the one we use in the Handlebars template isn't something that comes as standard. We have a custom helper that does the translation work and returns the localized string.

Here's the cool thing though: we cleverly just use the Underscore method we just defined. Go to HandlebarsExtras > HandlebarsExtras.js to see the code.

One of the extra thing to note about this helper is that we use the SafeString method. Now, Handlebars default approach to expressions (things wrapped in curly brackets) is to escape them, so, for example, if you try to use HTML tags it will escape the < and > signs, thus simply printing the code, rather than rendering it. SafeString is used here because we are telling Handlebars that we are sure that the string we're providing is safe and so we don't need it to escape it.

There are also other ways to do/prevent escaping, if you're interested.

What this means is that with both the Underscore and Handlebars methods, you are able to include HTML in your translations. If you remember, we did this in testimonials_testimonial.tpl with this corker:

<span><div data-view="StarRating"></div>{{translate '<b>$(0)</b>: <i>$(1)</i> <small>($(2))</small>' writerName title createdDate}}</span>

An example of what this translates to is this:

You'll may also remember that during the tutorial, we didn't actually go over adding these translations to the site. Thus, your non-English-speaking shoppers won't receive a translation appropriate to them. Zut alors!

So let's take a look at the dictionary files, and then how we can add our own text.

Understanding Dictionaries

Every language you want to translate to must have its own dictionary file for each application. Thus, if you support three languages and have three applications (shopping, checkout and my account), you will need 9 dictionary files.

Dictionary files take on a specific naming convention of <language>_<LOCALE>. The first part is a two-character language code as per ISO-639-1; the second part is a two-character country code as per ISO-3166-1. For example, this gives us en_US.js, which is the dictionary for the English used in the United States of America, and fr_CA.js, which is the French used in Canada.

You can find our default dictionary files in the Languages folders under CheckoutApplication, MyAccountApplication and ShoppingApplication.

Choosing the Best Approach for Customization

Now, with new dictionary entries, there are three ways you can handle this:

  1. Edit the source dictionary files
  2. Create a new module and then override the source dictionary files
  3. Extend the dictionary files

I've ordered these in ascending personal preference.

Edit Source Files

We rarely, if ever, recommend editing source files and dictionary files come the closest to being allowable. The principle reason for not editing source files is because of when it comes time to migrate to newer code: replacing functions or swathes of code reduces the chances of new code working, and makes it difficult for you to merge in the code. Well, dictionary files are just a giant blob of key-value pairs, and you could just easily add your translations to the bottom of the file to clearly demarcate your changes. This, however, makes me uneasy.

Override Source Files

The second solution is to simply override the files. Create a new module, set up the directory structure, etc. For each language you want to translate, you'd need to create a new file, first as a copy of the original and then with your additions at the end. Then, in ns.package.json, you'd need to override the originals. This is like if you were to override a template or Sass file.

This certainly has a strong sense of cleanness to it, but given the amount of overrides involved it may end up being a lot of work. In other words, if your aim is a clear separation then you might end up building a big wall when all you needed was a fence.

Extend Source Files

The last solution is the one I prefer. While it is not as clean as the previous idea, it is far more intuitive. The SC.Translations object is available everywhere throughout the site and you can extend it in any JavaScript file. From here, you can take two directions: you can create one big, global file that has all of your additional translations in it, or you can create files on a per-module basis.

Creating a global file has that elusive cleanness to it, as all of your translations in one place. The compromise to this, though, is that as you add additional languages and translations, the file starts to become very large. And, with size, comes difficulty in handling it.

An alternative to this is to create translation files on a per module basis. What this means is that each time you create or customize a module, you create a file that contains all of the translations and is included when the module is mounted to the app. This encloses the translations in the module in which they are used, making it a lot easier to 'package' up work. The downside, however, is that you can't resuse the translations without creating a dependency.

It's this method, translations per module, that I'm going to cover now. Think about your site and what's best for you.

Extending SC.Translations

So, to reiterate the plan, we're going to add an additional file to my existing testimonials module, that we created in a previous tutorial. This is going to contain translations for US English and Canadian French. If you're not working on the testimonials module like I am, then you'll need to abstract the instructions. Note that the translations I'm providing here were done by a machine, so may not be entirely accurate.

Create a file for your translations. For my module, I'm creating Testimonials.Translations.js in Testimonials > JavaScript. In it goes the following:

define('Testimonials.Translations'
, [
    'underscore'
  ]
, function (
    _
  )
{
  'use strict';

  return  {
    addTranslations: function addTranslations() {
      var locale = SC.ENVIRONMENT.currentLanguage.locale
      , dictionary = SC.Translations;

      if (locale === 'en_US')
      {
        _.extend(dictionary,
        {
          "What our customers say": "What our customers say"
        , "Leave yours": "Leave yours"
        , "Thank you for your testimonial. We\'ll review and publish it soon.": "Thank you for your testimonial. We\'ll review and publish it soon."
        , "Title": "Title"
        , "Write your testimonial": "Write your testimonial"
        , "New Testimonial": "New Testimonial"
        });
      }
      else if (locale === 'fr_CA')
      {
        _.extend(dictionary,
        {
          "What our customers say": "Ce que nos clients disent"
        , "Leave yours": "Laissez les vôtres"
        , "Thank you for your testimonial. We\'ll review and publish it soon.": "Nous vous remercions de votre témoignage. Nous allons passer en revue et de le publier bientôt."
        , "Title": "Titre"
        , "Write your testimonial": "Écrivez votre témoignage"
        , "New Testimonial": "Nouveau Témoignage"
        });
      }
    }
  }
});

So what we're doing is creating a function that extends the SC.Translations object. Now, keep in mind that the values in this object depend on what language the user requests; in NetSuite terms, this means determining what locale the user is from. Lucky for us, this is already something built into code so all we need to do is pull it from the session information. We check what it is, and then can add in the translations afterwards. How do we do this? Simply by using the extend Underscore method.

If you're not going with this particular approach, the _.extend method is the core bit of code that you will use. You could, for example, simply extend SC.Translations in an existing JS file. It's our best practice recommendation.

To plug this in to the module, add it as a dependency to the entry point file, Testimonials.js. Then, assuming you've named the dependency Translations, put the following code in the mountToApp function:

Translations.addTranslations();

Simple bit of code: you're just telling it to call the function we defined in the other file. And, as this is in the mountToApp function, it'll be called straight away when the module is loaded.

So, after saving the files, starting my local server and visiting the French version of my site, I can now see my translations in place.

If I click the Laissez les vôtres button, then I can see how my form looks.

Pretty cool. Remember, SC is available globally so at any point you can type SC.Translations in to your console to see all of your translations, handy for troubleshooting.

Advanced Translations with Parameters

We've already covered the basics of translation, so you should be good to go. But let's return to something mentioned earlier: the use of parameters in strings.

Parameters can be used to stand in for variables. A very simple example is a string used to state how many items are in the shopper's cart, eg, "$(0) items", where $(0) is a placeholder. You can have as many placeholders as you wish, although is not common to go beyond three or four.

As the same underlying JavaScript is used for translation, you can run placeholder substitution in both templates and JavaScript. To pass values to the templates, you must set them in the getContext function. For example, take a look at the Cart module. In Cart.Summary.View.js we set a number of values, which are then passed to cart_summary.tpl. We have the itemCount property being passed to the template, which then allows us to substitute in the number of items in the cart when the following translation is required:

{{translate 'Subtotal <span class="cart-summary-item-quantity-subtotal">$(0) item</span>' itemCount}}

Additional values for substitution are then tacked onto the end in the order of how the placeholders are enumerated. For templates, you separate these values with spaces, and for JS files you use commas.

Translating Item Data

Some product fields are automatically translated by the application; for the ones that aren't, you can modify and set your own in the backend:

  1. In NetSuite, go to a product and edit it
  2. Click on System Information > Translation
  3. Enter the translations for each of the fields

Summary

When running a site that is multi-language, it is vital that you translate as much text as possible. Via custom functionality, we offer ways to translate text. You may translate text either in the template or JavaScript file using the translate helper or method.

Any text wrapped in this code must then have a corresponding entry in language files — any text without an entry in the user's language will result in the user being shown the string untranslated by default. Therefore, you'll need to keep your separate language files up-to-date for every language your site offers.

There are a number of ways to achieve this. You could modify the source files directly, but we recommend against modifying source files at all. You could also create a custom module and simply override each language file with your own translations, which, while quite 'clean', does mean a number of unwieldy files. The final one, which I advocate, is to extend the object that contains all of the translations on a per module basis.

Further Information