Table of contents
- The problem
- The smarloc approach
- Express.js: How to setup ?
- Apollo GraphQL: How to setup ?
- Generating translations
- Wrapping up
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()}`;
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.
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 ?
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');
Then, you'll add an Express.js middleware which will translate returned json on-the-fly.
import translator from 'smartloc/express';
app.use(translator());
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 !`,
});
});
If you run this, you'll notice that your api will return things like:
{
"hello": "Hello !",
"time": "It is <put your date here>, mate !"
}
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 ?
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
}
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 {};
})
});
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"
}
}
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`;
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" }
}
}
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é"
}
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'));
🎉 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.
Top comments (2)
I leave here a link to a Spanish version of this article: "Cómo generar traducciones de forma sencilla desde un servidor Express/Apollo GraphQL, by Olivier Guimbal". -Traducción a español aquí.
Hello @oguimbal I'm trying to implement this with not success, I'm using typegraphql as framework for schema. Can I get any help from you ?