Software localization
Implementing Website I18n with Deno
In the past, we had several tutorials on building i18n apps with Node.js, and we also explored ways of using additional libraries to create multilingual apps.
In this article, we're going to explore Deno, a server-side runtime for JavaScript and TypeScript applications. In other words, we can use either JavaScript or Typescript to write server-side code. What is the difference between Deno and Node.js then you may ask? Well, there are a few architectural differences in terms of security, package management, and so on.
For example, by default, Deno doesn't permit network or file permissions unless it is enabled via a flag. Additionally, it doesn't depend on npm for package management. Deno can import modules from any location on the web, like GitHub, a personal web server, thus making it easy to bootstrap and experiment.
Without further ado, let's create a simple multilingual app using Deno. You can find all the code examples used in this tutorial on GitHub.
First things first, we need to install Deno. It ships as a binary distributable, but we can also build it from the source. Follow the installation instructions here.
Once installed, we can dry test it locally, and we are good to go:
$ deno Deno 1.4.6 exit using ctrl+d or close() > console.log("Hello World") Hello World
In order to use any package, you just import the relevant module using the import statement:
import { serve } from "https://deno.land/std@0.74.0/http/server.ts"; const s = serve({ port: 8000 });
By default, Deno offers a small but stable list of standard library modules located here. Although these modules cover only the basics, they are a good starting point. If we want to build real-world applications, we may need more functionality, and the best place to search for it is through the third-party module list here. Currently, it contains more than 1,100 modules, such as lodash, date-fns and others.
We addressed i18next in our ultimate guide to React localization with i18next and a similar one with Angular. We can leverage this module along with few other ones to create a small functional application with multilingual support.
For this tutorial, we’ll need the following libraries:
- Servest – a middleware framework for Deno to help us scaffold the server, and
- i18next – for the localization of our application
Let's create a simple server that renders an HTML string passing some context data.
First, create a new folder and the index.tsx file as a starting point:
$ mkdir deno-i18n $ cd mkdir deno-i18n $ touch index.tsx
Then add the following code:
import { createApp } from 'https://servestjs.org/@v1.1.6/mod.ts'; // @deno-types="https://servestjs.org/@v1.1.6/types/react/index.d.ts"; import React from "https://dev.jspm.io/react/index.js"; // @deno-types="https://servestjs.org/@v1.1.6/types/react-dom/server/index.d.ts"; import ReactDOMServer from "https://dev.jspm.io/react-dom/server.js"; const app = createApp(); //#1 app.get(new RegExp("^/(.+)"), async (req: any) => { // #2 const [_, name] = req.match; // #3 await req.respond({ status: 200, headers: new Headers({ "content-type": "text/html; charset=UTF-8", }), body: ReactDOMServer.renderToString( // #4 <html> <head> <meta charSet="utf-8" /> <title>Hello</title> </head> <body>Hello {name}</body> </html>, ), }) }) await app.listen({ port: 8000 }) // #5
We've got several comments listed in this code. Let's explain them one by one:
- In #1, we create the application instance,
- In #2, we register a route for the home page,
- In #3, we match a name parameter,
- In #4, We use ReactDOMServer renderToString to render a JSX template,
- In #5, we start the server running at localhost port 8000.
You may have noticed that we used React as a templating engine as it's well documented and flexible. There are alternative template engines we can use, such as Eta or Handlebars. Each engine has its own template methods and ways to extend it, and you may need some configuration to make it work with Servest.
To start the server, a simple Deno run won't work, since we need both network and file permissions. We need a network for opening ports and serving content but also file permissions to read any files or templates we've stored on the disk.
Start the server with the following command:
$ deno run --allow-net --allow-read index.tsx
Then visit the page at localhost:8000/John.
Adding Multilingual Support with
Next, we'll add multilingual support using the well-known i18next library. Let's start off by creating a foundation for our i18n work. We'll create some config data first:
config/i18nConfig.ts
export interface IAppConfig { supportedLocales: { [key: string]: string } } export const appConfig: IAppConfig = { supportedLocales: { 'en': 'English', 'gr': 'Greek', } }
Then, we'll use the supported locales config in a service:
services/languageService.ts
import { keys } from 'http://deno.land/x/lodash@4.17.11-es/lodash.js'; import { appConfig } from "../config/i18nConfig.ts"; class LanguageService { allSupported(): string[] { return keys(appConfig.supportedLocales); } isSupported(code: string): boolean { return !!appConfig.supportedLocales[code]; } value(code: string): string { return appConfig.supportedLocales[code]; } } export default new LanguageService();
Note that we need to use full paths + file extensions when we import other modules, otherwise, Deno will complain when it tries to import them.
Next, we need to initialize the i18next library with that supported information.
Create a new file named i18next.ts under services with the following code:
config/i18next.ts
import i18next from 'https://deno.land/x/i18next/index.js'; import i18nextMiddleware from 'https://deno.land/x/i18next_http_middleware/index.js'; import languageService from './languageService.ts'; i18next .init({ debug: process.env, fallbackLng: 'en', preload: languageService.allSupported(), resources: { en: { translation: { welcome: 'hello world' } }, el: { translation: { welcome: 'Γειά σου Κόσμε' } } } }) const handle = i18nextMiddleware.handle(i18next); export { handle }
Here, we export a handle function and preload some translation messages for each locale.
We first need to push the middleware handler into the app's own list of handlers:
index.tsx
import { handle } from './services/i18next.ts'; const app = createApp(); app.use(handle);
This will allow us to change the locale on demand as the middleware binds the i18n object into the request object. Additionally the middleware will expose i18next's t() function into the request object as well and we can call it as req.t('message_key')
.
For example, let's change the handler to render the welcome message:
index.tsx
app.get(new RegExp("^/(.+)"), async (req: any) => { req.i18n.changeLanguage('el'); await req.respond({ status: 200, headers: new Headers({ "content-type": "text/html; charset=UTF-8", }), body: ReactDOMServer.renderToString( <html> <head> <meta charSet="utf-8" /> <title>Hello</title> </head> <body>Hello {req.t('welcome')}</body> </html>, ), }) })
Here, we change the current language to Greek in the first line using the changeLanguage
method. Then, we use req.t('welcome')
to render the translated message. If we run the server again, we can see the correct string:
Note that you can make the JSX markup a bit more modular, for example, by extracting the core App component inside the body
. Create a new file called App.tsx under the components folder:
components/App.tsx
import React from "https://dev.jspm.io/react/index.js"; export interface IAppProps { message: string; }; const App: React.FC<IAppProps> = (props: IAppProps) => { return ( <div>Hello {props.message}</div> ); }; export default App;
Then, import this component inside the renderToString
:
index.tsx
import App from './components/App.tsx'; app.get(new RegExp("^/(.+)"), async (req: any) => { req.i18n.changeLanguage('el'); const [_, name] = req.match; await req.respond({ status: 200, headers: new Headers({ "content-type": "text/html; charset=UTF-8", }), body: ReactDOMServer.renderToString( <html> <head> <meta charSet="utf-8" /> <title>Hello</title> </head> <body><App message={req.t('welcome')}/></body> </html>, ), }) })
This will render the same result as before.
Directionality
We can get the text directionality of the current language by using the dir() method of the i18n object:
const dir = req.i18n.dir();
Using that information, we can use React Context API to pass the direction on each children component and to the HTML tag as an attribute as well.
index.tsx
const DirectionContext = React.createContext('ltr'); ... const dir = req.i18n.dir(); // Grab the directionality from the request body: ReactDOMServer.renderToString( <html dir={dir}> <head> <meta charSet="utf-8" /> <title>Hello</title> </head> <body> <DirectionContext.Provider value={dir}> <App message={req.t('welcome')}/> </DirectionContext.Provider> </body> </html>, ),
We can consume the context value by using React hooks:
import React from 'https://dev.jspm.io/react/index.js'; import {DirectionContext} from '../contexts/DirectionContext.ts'; export interface IAppProps { message: string; }; const App: React.FC<IAppProps> = (props: IAppProps) => { const dir = React.useContext(DirectionContext); return ( <div>Current Direction: {dir}</div> ); }; export default App;
This will display ltr
for most locales, but in case we use a right-to-left language, it will display rtl
;
Automatically Detecting the User’s Language
We can also provide the option to detect the language the user prefers using the browser preferences or based on previous requests. As we've already imported the i18next_http_middleware
plugin, we only need to add the LanguageDetector handler when we initialize i18next:
services/i18next.ts
i18next .use(i18nextMiddleware.LanguageDetector)
If we look at the source code of this module here, the detector will try a few things to detect the user's preferred language:
- Attempt to find a
?lng=en
query string parameter in the request URL, - Attempt to find a domain cookie called
"i18next"
, - Attempt to find the header with
accept-language
value, - Attempt to find an entry in session storage called
"lng"
.
Once a language is detected, it calls i18n.changeLanguage(language)
with the detected language tag. We can easily test this by providing a lng
parameter to the request.
First, make sure you remove the following line from the handler:
req.i18n.changeLanguage('el');
Then navigate to localhost:8000/hello?lng=el.
Wrapping It Up
We truly hope you were able to get a better understanding of localization with Deno and i18next. For professional i18n and l10n projects, make sure to try Phrase! It's a complete platform for localization designed to cater to all stakeholders, from developers to translators. It will do the heavy lifting from start to finish and make your life as a developer easier with many integrations, such as Git, CI/CDs, and CLIs. Check out all of Phrase's products, and sign up for a free 14-day trial.