DEV Community

loading...
Cover image for i18n - Express/Apollo GraphQL server translation made simple

i18n - Express/Apollo GraphQL server translation made simple

oguimbal profile image Olivier Guimbal ・5 min read

Table of contents

Say you have a server.
Say you are using Express.js and/or Apollo Graphql server.
Say your users speak different languages.
Say you live in 2020, your server is only an api server, and dont care about templating engines.

πŸŽ‰ Yey ! This article is for you.

It introduces yet another i18n lib I developped for my company needs which (IMO) simplifies the creation of multi-languages servers implementation.

The problem

When writing code in an Express route handler, you have access to the original "request" object.

"good" you would say. I can access "Cookies", "Accept-Languages" header, or whatever that defines my user language, and translate the returned content based on that.

To which I would reply: Okay sure, but do you really want to carry around your request object as an argument of some kind in your whole codebase, just for the sake of knowing your user language ?

Doesnt it feel kinda wrong ?

Arent you tired of calling weird static methods with weird syntaxes to get a translated string from your code ?

Are you really gonna refactor those 500k uni-language code files that you already wrote just to add language support ?

How the heck are you supposed to store translatable strings ?

If like me, you feel this is wrong, read on...

The smarloc approach.

A good example is worth a thousand words:

// before
const myFunction = () => `Hello, it is now ${new Date()}`;
// after
const myFunction = () => loc`Hello, it is now ${new Date()}`;
Enter fullscreen mode Exit fullscreen mode

See the difference ? Yea, there is 'loc' in front of my string.
Your function is not returning a 'string' anymore, rather a 'LocStr' object.

Here lies the hack... you dont have to know your user language when you emit the string that will eventually have to be translated. You can pass around this object in your whole app without telling the code that manipulates it that this not an actual string.

latest

Translation will then occur at the latest instant, when serializing the json response sent to your client. That's when 'Accept-Language' header or whatever will be read, and when instances of 'LocStr' strings in the returned json will really be translated. At the latest moment.

Express.js: How to setup ?

expressjs

First and foremost (this must be done before any code uses smartloc), you'll have to tell which in which language you write you strings in.

import {setDefaultlocale} from 'smartloc';

// lets say our developpers use english in code
setDefaultLocale('en');
Enter fullscreen mode Exit fullscreen mode

Then, you'll add an Express.js middleware which will translate returned json on-the-fly.

import translator from 'smartloc/express';

app.use(translator());
Enter fullscreen mode Exit fullscreen mode

By default, it will look for translations matching the 'Accept-Language' header of incoming requests, and will default to the default language you provided.

You can now use smartloc like this

import {loc} from 'smartloc';

app.get('/', (req, res) => {
    // sends a JSON object containing text to translate,
    // without bothering to translate it.
    res.json({
        // this string will get an automatic ID
        hello: loc`Hello !`,

        // notice that you can provide an ID like that
        // and use regular templating syntax:
        time: loc('myTime')`It is ${new Date()}, mate !`,
    });
});
Enter fullscreen mode Exit fullscreen mode

If you run this, you'll notice that your api will return things like:

{
    "hello": "Hello !",
    "time": "It is <put your date here>, mate !"
}
Enter fullscreen mode Exit fullscreen mode

Okay, that's fine, but how that doesnt tell us how to provide actual translations to strings...

Yes, for that, you'll have to jump to the Generate translations section πŸ™‚

A simple version of this example is here (nb: it provides hard-coded translations, not using translation files)

Apollo GraphQL: How to setup ?

graphql

Setting up for an Apollo GraphQL server is almost the same thing as setting up for an Express.js server.

In short, it is the same as described in the previous section, except you will not have to use the express translator middleware.

Instead, you will have to declare in your schema which strings are translatable by using the 'GLocString' type instead of 'GraphQLString', like this:

import {GLocString} from 'smartloc/graphql';

//...
{
   type: GLocString,
   resolve: () => loc`Hello !` // will be translated
}
Enter fullscreen mode Exit fullscreen mode

Then build your apollo server like that:

import {localizeSchema, localizedContext} from 'smartloc/graphql';

const apollo = new ApolloServer({
    schema: localizeSchema(schema),
    context: localizedContext(async http => {
        // build your own context here as usual
        return {};
    })
});
Enter fullscreen mode Exit fullscreen mode

When doing that, all GLocString properties or JSOn properties of your schema will be automatically translated when their resolvers return things containing LocStr instances.

Then, as with the Express.js explanation, jump to Generate translations section to know how to refresh your translations πŸ™‚

A simple version of this example is here (nb: it provides hard-coded translations, not using translation files)

Generating translations

If you're here, I'll assume that you have read one of the previous two sections.

Lets say you now want to add support for French. First, add something like this in the "scripts" section of your package.json:

{
   "scripts": {
       "collect": "smartloc collect --format=json --locales=fr-FR --defaultLocale=en-US --generateDefault"
    }
}
Enter fullscreen mode Exit fullscreen mode

When you run npm run collect, it will (re)generate two files in the i18n directory:

  • en-us.json : You can forget this file, it is here for reference because you put the --generateDefault option in commandline, and you can provide translations in it (in which case the actual string in code will never reach your client), but you can leave it as it.

  • fr-fr.json : This is where you'll have to put your translations.

In these files, translations are grouped by the left dot of strings IDs.
For instance, if you had:

loc`Automatic ID`;
loc('someId')`Some ID string`;
loc('someGroup.someId')`Some grouped ID string`;
Enter fullscreen mode Exit fullscreen mode

It will generate something like this:

{
   "$default": {
       "<sha of your string>": { "source": "Automatic ID" },
       "someId": { "source": "Some ID string" }
    },
    "someGroup": {
        "someId": { "source": "Some grouped ID string" }
    }
}
Enter fullscreen mode Exit fullscreen mode

Just add a corresponding "target" to each "source", and you'll be good to go. For instance:

{ 
  "source": "Some grouped ID string",
  "target": "Une chaine avec ID groupΓ©"
}
Enter fullscreen mode Exit fullscreen mode

Then, at startup, just tell smartloc where it should look for translations:

import {loadAllLocales} from 'smartloc';
import path from 'path';

// load all locales in the i18n directory
// ... you could also use loadLocale() to only load one locale file.
loadAllLocales(path.resolve(__dirname, 'i18n'));
Enter fullscreen mode Exit fullscreen mode

πŸŽ‰ Here it is ! If your translation files are OK, you'll have a fully functioning multi-language API server !

I will let you guess how to add more than one translation :)

Wrapping up

This introduction scratched the surface of what can be done with this lib.

We've been using it for months @ justice.cool and I must say I am pretty happy with it.

Before anyone comments something like "you know, there are other libs that" ... I know that there already are plenty of other i18n libs, but I felt like developping a simpler one, which felt good to me. If it doesnt to you, well... that's bad luck mate. Keep using those monsters out there.

To know a bit more about advanced usages (transform strings, storing translatable strings, manual translations, ... refer to smartloc repo ), or open an issue, I'll be glad to answer it.

Discussion (0)

Forem Open with the Forem app