In this article, we will translate localisation messages with DeepL.
DeepL is a much more accurate translation method than Google Translate and others. With DeepL, you can avoid manual translation or even hiring translation professionals.
This script can work for virtually all applications that use the following convention:
[directory]/
en.json
es.json
fr.json
And if en.json
was the file containing the source language messages:
{
"8HJxXG": "Sign up",
"Kvo3A0": "Copyright",
"QhHk4l": "Home | {name}",
"U8QBHO": "Powered by",
"cXBJ7U": "Privacy",
"g5pX+a": "About",
"AyGauy": "Login",
"xkr+zo": "Terms",
"zFegDD": "Contact"
}
For example, this could be the localisation messages for a simple front-end application.
Now we know what the data looks like, let's get started.
Setting up DeepL Translate
DeepL offers a free translation API. It grants access to all its available features. Go to their API page and click Sign up for free under Deepl API Free. Fill in your details, and you'll receive a free API key.
Using the API
Run the following command to install the DeepL API wrapper into your NodeJS project.
npm i deepl-node
Using CommonJS, you can initialise the wrapper like so:
const authKey = "*******";
const translator = new deepl.Translator(authKey, {
serverUrl: "https://api-free.deepl.com",
});
Reading all available locales
If you extract your messages with a tool like FormatJS, it will only create the base file. The script needs to know to which other languages to translate the messages. Do this by keeping your message files in a separate directory. Then create empty files for the target locales.
Now, we need a function that gets the names of all the message files in that directory.
const getDirectoryLocales = async (directory) => {
let filenames = await fs.readdir(directory);
// Only gets filenames with .json extension
filenames = filenames.filter((filename) =>
filename.endsWith(FILE_EXTENSION)
);
// Removes the extension from the filenames
const locales = filenames.map((filename) =>
filename.slice(0, filename.lastIndexOf(FILE_EXTENSION))
);
return locales;
};
Getting the Source Language Messages
Define a function to read the contents of a JSON file and parse it as an object.
const getLocaleMessages = async (filepath) => {
let messages = {};
try {
messages = JSON.parse(await fs.readFile(filepath));
} catch (e) {
throw new Error("Couldn't parse source file:", filepath);
}
return messages;
};
Translating Messages
Now we need to define a function which translates only the values of an object. It will follow the file convention stated previously.
Once DeepL translates all the values into a target language, we write the object to a new JSON file.
const translateMessages = async (messages, source, locales) => {
// Used to recreate object with translated values
const keys = Object.keys(messages);
const values = Object.values(messages);
let translatedMessages = {};
let translatedValues = [];
for (let locale of locales) {
// Skip overwriting source language
if (locale === source) continue;
translatedMessages = {};
translatedValues = await translator.translateText(
values,
sourceLanguage,
locale
);
translatedValues.forEach((translatedValue, i) => {
translatedMessages[keys[i]] = translatedValue;
});
await fs.writeFile(
path.join(targetDirectory, locale + FILE_EXTENSION),
JSON.stringify(translatedMessages)
);
}
};
Wrapping up
Here is the code for the entire file. We export a function that will follow all the steps we created in order.
const deepl = require("deepl-node");
const fs = require("fs/promises");
const path = require("path");
const FILE_EXTENSION = ".json";
const authKey = "*******";
const translator = new deepl.Translator(authKey, {
serverUrl: "https://api-free.deepl.com",
});
const getDirectoryLocales = async (directory) => {
let filenames = await fs.readdir(directory);
// Only gets filenames with .json extension
filenames = filenames.filter((filename) =>
filename.endsWith(FILE_EXTENSION)
);
// Removes the extension from the filenames
const locales = filenames.map((filename) =>
filename.slice(0, filename.lastIndexOf(FILE_EXTENSION))
);
return locales;
};
const getLocaleMessages = async (filepath) => {
let messages = {};
try {
messages = JSON.parse(await fs.readFile(filepath));
} catch (e) {
throw new Error("Couldn't parse source file:", filepath);
}
return messages;
};
const translateMessages = async (messages, source, locales) => {
// Used to recreate object with translated values
const keys = Object.keys(messages);
const values = Object.values(messages);
let translatedMessages = {};
let translatedValues = [];
for (let locale of locales) {
// Skip overwriting source language
if (locale === source) continue;
translatedMessages = {};
translatedValues = await translator.translateText(
values,
sourceLanguage,
locale
);
translatedValues.forEach((translatedValue, i) => {
translatedMessages[keys[i]] = translatedValue;
});
await fs.writeFile(
path.join(targetDirectory, locale + FILE_EXTENSION),
JSON.stringify(translatedMessages)
);
}
};
exports.localise = async (targetDirectory, sourceLanguage) => {
const locales = await getDirectoryLocales(targetDirectory);
if (!locales.includes(sourceLanguage)) {
throw new Error("Source language file not found: " + sourceLanguage);
}
const messages = await getLocaleMessages(
path.join(targetDirectory, sourceLanguage + FILE_EXTENSION)
);
await translateMessages(messages, source, locales);
};
Great! Now you know how to add a script to localise the messages for your web page.
For a more functional example of this feature, check out SimpleLocalise. They offer free hosting of your translation files and a nice UI to manage translations. They also provide integrations that work with popular i18n libraries like FormatJS. On top of that, there are many different guides for usage with any type of application, via their REST API.
Stay tuned for more if you enjoyed it. Thank you and goodbye.