Internationalization (i18n) for Deno with i18next

Deno i18n

You may already know how to properly internationalize a client side application, like described in this React based tutorial, this Angular based tutorial or this Vue based tutorial.

In this blog post we will shed light on Deno.

Why do I need to handle i18n in Deno?

Think of all user faced content not directly rendered in your browser...

Let's check that out...

We will show some examples that uses i18next as i18n framework. If you're curious to know why we suggest i18next, have a look at this page.

Command line interface (CLI)

Let's start with something simple: a verry small CLI app. We are defining a sayhi command with optional language and name parameters that should respond with a salutation in the appropriate language.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { parse } from "https://deno.land/std/flags/mod.ts";

const { args } = Deno;
const parsedArgs = parse(args);

const cmd = parsedArgs._[0];

if (cmd !== "sayhi" && cmd !== "s") {
throw new Error(`unknown command ${cmd}`);
}

const name = parsedArgs.n || parsedArgs.name;
const language = parsedArgs.l || parsedArgs.language;

console.log({ name, language })

Ok, now let's create a new i18n.ts file and setup i18next accordingly:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import i18next from "https://deno.land/x/i18next/index.js";
import enTranslation from "./locales/en/translation.json" assert {
type: "json",
};
import deTranslation from "./locales/de/translation.json" assert {
type: "json",
};

const systemLocale = Intl.DateTimeFormat().resolvedOptions().locale;

i18next
.use(Backend)
.init({
// debug: true,
fallbackLng: "en",
resources: {
en: {
translation: enTranslation,
},
de: {
translation: deTranslation,
},
}
});

export default (lng: string | undefined | null) =>
i18next.getFixedT(lng || systemLocale);

And also our translation resources:

1
2
3
4
5
6
7
8
9
10
11
// locales/en/translations.json
{
"salutation": "Hello World!",
"salutationWithName": "Hello {{name}}!"
}

// locales/de/translations.json
{
"salutation": "Hallo Welt!",
"salutationWithName": "Hallo {{name}}!"
}

Now we can use the i18n.ts export like that:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { parse } from "https://deno.land/std/flags/mod.ts";
import i18n from "./i18n.ts";

const { args } = Deno;
const parsedArgs = parse(args);

const cmd = parsedArgs._[0];

if (cmd !== "sayhi" && cmd !== "s") {
throw new Error(`unknown command ${cmd}`);
}

const name = parsedArgs.n || parsedArgs.name;
const language = parsedArgs.l || parsedArgs.language;

const t = i18n(language);
if (name) {
console.log(t("salutationWithName", { name }));
} else {
console.log(t("salutation"));
}

Ok, what's the result?

1
2
3
4
5
6
7
8
9
10
11
## if we execute the cli command without any parameters...
deno run --allow-read mod.ts sayhi
## result: Hello World!

## if we execute the cli command with a language parameter...
deno run --allow-read mod.ts sayhi --language de
## result: Hallo Welt!

## if we execute the cli command with a language parameter and a name parameter...
deno run --allow-read mod.ts sayhi --language de --name John
## result: Hallo John!

Easy, isn't it?

You can also i.e. use the i18next-fs-backend to dynamically load your translations, for example like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import i18next from "https://deno.land/x/i18next/index.js";
import Backend from "https://deno.land/x/i18next_fs_backend/index.js";

const systemLocale = Intl.DateTimeFormat().resolvedOptions().locale;

i18next
.use(Backend)
.init({
// debug: true,
initImmediate: false, // setting initImediate to false, will load the resources synchronously
fallbackLng: "en",
preload: ['en', 'de'],
backend: {
loadPath: "locales/{{lng}}/{{ns}}.json",
},
});

export default (lng: string | undefined | null) =>
i18next.getFixedT(lng || systemLocale);

🧑‍💻 A code example can be found here.

A possible next step...

A possible next step could be to professionalize the translation management. This means the translations would be "managed" (add new languages, new translations etc...) in a translation management system (TMS), like locize and synchronized with your code. To see how this could look like, check out Step 1 in this tutorial.

Server Side Rendering (SSR)

For this example we will use the http framework abc (created by 木杉, but any other framework will also work.

This time we will use a different i18next module, i18next-http-middleware. It can be used for all Deno web frameworks, like abc or ServestJS, but also for Node.js web frameworks, like express or Fastify.

As already said, here we will use abc.

Let's again start with the i18n.js file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import i18next from 'https://deno.land/x/i18next/index.js'
import Backend from 'https://deno.land/x/i18next_fs_backend/index.js'
import i18nextMiddleware from 'https://deno.land/x/i18next_http_middleware/index.js'

i18next
.use(Backend)
.use(i18nextMiddleware.LanguageDetector)
.init({
// debug: true,
initImmediate: false, // setting initImediate to false, will load the resources synchronously
backend: {
loadPath: 'locales/{{lng}}/{{ns}}.json'
},
fallbackLng: 'en',
preload: ['en', 'de', 'it']
})

export const i18n = i18next
export const middleware = i18nextMiddleware

And our translation resources...

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// locales/en/translations.json
{
"home": {
"title": "Hello World!"
},
"server": {
"started": "Server is listening on port {{port}}."
}
}

// locales/de/translations.json
{
"home": {
"title": "Hallo Welt!"
},
"server": {
"started": "Der server lauscht auf dem Port {{port}}."
}
}

// locales/it/translations.json
{
"home": {
"title": "Ciao Mondo!"
},
"server": {
"started": "Il server sta aspettando sul port {{port}}."
}
}

A simple ejs template:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<html>

<head>
<title>i18next - abc with dejs</title>
</head>

<body>
<h1><%= t('home.title') %></h1>
<div><a href="/?lng=en">english</a>&nbsp; | &nbsp;<a href="/?lng=de">deutsch</a> | &nbsp;<a href="/?lng=it">italiano</a></div>
<hr />
<div><a href=<%= "/raw?lng=" + i18n.resolvedLanguage %>>raw test</a></div>
</body>

</html>

Our "main" file index.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// deno run --allow-net --allow-read index.js
import { Application } from 'https://deno.land/x/abc/mod.ts'
import { config } from "https://deno.land/x/dotenv/mod.ts"
import { i18n, middleware } from './i18n.js'
import { renderFile } from 'https://deno.land/x/dejs/mod.ts'

const port = config.PORT || 8080
const app = new Application()

app.renderer = {
render(name, data) {
return renderFile(`./views/${name}.html`, data)
}
}

const handle = middleware.handle(i18n)

app.use((next) =>
(c) => {
handle(c)
return next(c)
}
)

app.get('/', (c) => c.render('index', { t: c.request.t, i18n: c.request.i18n }))
app.get('/raw', (c) => c.request.t('home.title'))

app.start({ port })

console.log(i18n.t('server.started', { port }))
console.log(i18n.t('server.started', { port, lng: 'de' }))
console.log(i18n.t('server.started', { port, lng: 'it' }))

Now start the app and check what language you're seeing... dejs abc

If you check the console output you'll also see something like this:

1
2
3
4
node app.js
## Server is listening on port 8080.
## Der server lauscht auf dem Port 8080.
## Il server sta aspettando sul port 8080.

Yes, if you like, you can also internationalize your log statements 😁

🧑‍💻 A code example can be found here.

A possible next step...

Do you wish to manage your translations in a translation management system (TMS), like locize?

Just use this cli to synchronize the translations with your code. To see how this could look like check out Step 1 in this tutorial.

Alternatively, use i18next-locize-backend instead of the i18next-fs-backend. If you're running your code in a serverless environment, make sure you read this advice first!

There's also an i18next crash course video.

🎉🥳 Conclusion 🎊🎁

As you see i18n is also important for Deno.

I hope you’ve learned a few new things about Deno server side internationalization and modern localization workflows.

So if you want to take your i18n topic to the next level, it's worth to try i18next and also locize.

👍

Share