{"id":8459,"date":"2026-10-12T10:47:08","date_gmt":"2026-10-12T10:47:08","guid":{"rendered":"https:\/\/poeditor.com\/blog\/?p=8459"},"modified":"2026-10-12T11:20:45","modified_gmt":"2026-10-12T11:20:45","slug":"node-js-localization","status":"publish","type":"post","link":"https:\/\/poeditor.com\/blog\/node-js-localization\/","title":{"rendered":"Node.js localization: How to translate an express app with i18n-node"},"content":{"rendered":"\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"669\" src=\"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/Node.js-localization-1024x669.png\" alt=\"\" class=\"wp-image-8479\" srcset=\"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/Node.js-localization-1024x669.png 1024w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/Node.js-localization-300x196.png 300w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/Node.js-localization-767x501.png 767w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/Node.js-localization-1536x1003.png 1536w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/Node.js-localization.png 1764w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Once an app has users in more than one country, hardcoded English strings are no longer enough: people expect labels, messages and dates in their own language. Internationalization (i18n) prepares the code so its text can change per language, and localization (l10n) supplies the translations and regional formats for each one.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In this guide, we\u2019ll localize an Express TO-DO app with i18n-node, translate its catalog into French in POEditor, and format dates and numbers with Node\u2019s built-in Intl API.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Overview<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"#Installing-and-configuring-i18n-node\">Installing and configuring i18n-node<\/a><\/li>\n\n\n\n<li><a href=\"#How-to-choose-a-locale-for-each-request\">How to choose a locale for each request<\/a><\/li>\n\n\n\n<li><a href=\"#Using-__()-in-EJS-templates-and-route-handlers\">Using __() in EJS templates and route handlers<\/a><\/li>\n\n\n\n<li><a href=\"#Handling-plurals-with-__n()\">Handling plurals with __n()<\/a><\/li>\n\n\n\n<li><a href=\"#Writing-the-English-JSON-catalog\">Writing the English JSON catalog<\/a><\/li>\n\n\n\n<li><a href=\"#Translating-the-JSON-catalogs-in-POEditor\">Translating the JSON catalogs in POEditor<\/a><\/li>\n\n\n\n<li><a href=\"#Formatting-dates-and-numbers-with-Intl\">Formatting dates and numbers with Intl<\/a><\/li>\n<\/ul>\n\n\n\n<h2 id=\"Installing-and-configuring-i18n-node\" class=\"wp-block-heading\">Installing and configuring i18n-node<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The TO-DO app is an Express 5 application with EJS views and a SQLite database. You register with a username and password, create named lists, add tasks to them, tick them off and filter each list by open or done tasks. The text on those pages is initially hardcoded in English in the views, route handlers and validation module.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"993\" height=\"1024\" src=\"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/01_list_with_tasks_en_v4-993x1024.png\" alt=\"list with tasks\" class=\"wp-image-8460\" srcset=\"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/01_list_with_tasks_en_v4-993x1024.png 993w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/01_list_with_tasks_en_v4-291x300.png 291w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/01_list_with_tasks_en_v4-768x792.png 768w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/01_list_with_tasks_en_v4-1490x1536.png 1490w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/01_list_with_tasks_en_v4-1987x2048.png 1987w\" sizes=\"auto, (max-width: 993px) 100vw, 993px\" \/><figcaption class=\"wp-element-caption\"><em>The list page before translation<\/em><\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">To follow along you need Node.js 22.1 or newer. Run <code>npm install<\/code>, then <code>npm start<\/code>, and open the address it prints; <code>npm run seed <\/code>creates a demo account with the password demo1234 if you\u2019d rather skip registration, and running it again resets that account and deletes its lists.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The library is published on <code>npm<\/code> as <code>i18n<\/code>. The TO-DO app also installs Mustache, which <code>i18n-node<\/code> uses for placeholders, so we can change one of its settings. Add both with <code>npm install i18n mustache<\/code>, then create a module that configures one instance for the whole app.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ i18n.js\n\nconst path = require('node:path');\n\nconst { I18n } = require('i18n');\n\nconst Mustache = require('mustache');\n\nconst i18n = new I18n({\n\n&nbsp;&nbsp;locales: &#091;'en', 'fr'],\n\n&nbsp;&nbsp;defaultLocale: 'en',\n\n&nbsp;&nbsp;directory: path.join(__dirname, 'locales'),\n\n&nbsp;&nbsp;\/\/ en.json is written by hand, so never add keys to the catalogs at runtime.\n\n&nbsp;&nbsp;updateFiles: false,\n\n&nbsp;&nbsp;\/\/ A key with no French translation yet shows its English text.\n\n&nbsp;&nbsp;retryInDefaultLocale: true,\n\n&nbsp;&nbsp;\/\/ No prototype, so an Accept-Language header can't match a built-in\n\n&nbsp;&nbsp;\/\/ property and make i18n.init throw.\n\n&nbsp;&nbsp;fallbacks: Object.create(null),\n\n});\n\n\/\/ EJS already escapes everything it prints, so stop Mustache from\n\n\/\/ HTML-escaping {{placeholder}} values a second time.\n\nMustache.escape = (text) =&gt; text;\n\nmodule.exports = i18n;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The catalogs are one JSON file per locale in locales\/, and each entry pairs a short key such as <code>list_created<\/code> with the text for that locale. The section on the catalogs explains the two settings that go with that choice.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Register the middleware in <code>app.js<\/code>, and put it at the top of the stack together with <code>cookie-parser<\/code>, which the next section needs in order to read the lang cookie.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ app.js\n\nconst cookieParser = require('cookie-parser');\n\nconst i18n = require('.\/i18n');\n\n\/\/ First, so that every later middleware and error page can translate.\n\napp.use(cookieParser());\n\napp.use(i18n.init);\n\n\/\/ ...the locale middleware from the next section goes here...\n\napp.use(express.static(path.join(__dirname, 'public')));\n\napp.use(express.urlencoded({ extended: false }));\n\napp.use(\n\n&nbsp;&nbsp;session({\n\n&nbsp;&nbsp;&nbsp;&nbsp;secret: process.env.SESSION_SECRET || 'change-me-in-production',\n\n&nbsp;&nbsp;&nbsp;&nbsp;resave: false,\n\n&nbsp;&nbsp;&nbsp;&nbsp;saveUninitialized: false,\n\n&nbsp;&nbsp;&nbsp;&nbsp;cookie: { httpOnly: true, sameSite: 'lax' },\n\n&nbsp;&nbsp;})\n\n);<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>i18n.init<\/code> gives each request its own copy of the <code>i18n-node<\/code> API, including <code>req.__()<\/code>, <code>req.__n()<\/code>, <code>req.getLocale()<\/code> and <code>req.setLocale()<\/code>. It also puts the same API in res.locals, so an EJS view can <code>call __() <\/code>without importing anything. It sits above body parsing and the session because an error in either of those goes straight to the error handler, and if i18n.init hasn\u2019t run yet, the error page dies with <code>req.__<\/code> is not a function.<\/p>\n\n\n\n<h2 id=\"How-to-choose-a-locale-for-each-request\" class=\"wp-block-heading\">How to choose a locale for each request<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">i18n.init guesses a locale from the <code>Accept-Language<\/code> header, and it can also read the visitor\u2019s choice from a cookie or from <code>?lang=<\/code> if you set its cookie or <code>queryParameter<\/code> option. In i18n-node 0.15.4, though, it looks those values up in a plain object, so an unknown value such as de falls back to English but some names inherited from <code>Object.prototype<\/code> get through as locale names, and one request with such a name can affect the whole process. Its header guess can also prefer a <code>lower-ranked<\/code> exact match to a <code>higher-ranked<\/code> regional one, which gives French to a browser that sends <code>en-US,fr;q=0.9<\/code> and English to one that sends <code>fr-FR,en<\/code>. The TO-DO app therefore chooses the locale in its own middleware, which checks each source against the configured locales and uses the first match:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>The lang query parameter, as in <code>\/?lang=fr.<\/code><\/li>\n\n\n\n<li>The lang cookie set by the language switcher.<\/li>\n\n\n\n<li>The browser\u2019s Accept-Language header. <code>req.acceptsLanguages()<\/code> lists its languages in the browser\u2019s order of preference, and the first one with a catalog wins, with a regional tag such as <code>fr-FR<\/code> counting as <code>fr<\/code>.<\/li>\n\n\n\n<li>English.<\/li>\n<\/ol>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ app.js\n\n\/\/ Pick the locale: ?lang= first, then the `lang` cookie, then the browser's\n\n\/\/ Accept-Language header, then English. Only locales we have a catalog for\n\n\/\/ are accepted.\n\napp.use((req, res, next) =&gt; {\n\n&nbsp;&nbsp;const locales = i18n.getLocales();\n\n&nbsp;&nbsp;if (locales.includes(req.query.lang)) {\n\n&nbsp;&nbsp;&nbsp;&nbsp;req.setLocale(req.query.lang);\n\n&nbsp;&nbsp;} else if (locales.includes(req.cookies.lang)) {\n\n&nbsp;&nbsp;&nbsp;&nbsp;req.setLocale(req.cookies.lang);\n\n&nbsp;&nbsp;} else {\n\n&nbsp;&nbsp;&nbsp;&nbsp;\/\/ In the header's own order, so that `fr-FR,en` means French.\n\n&nbsp;&nbsp;&nbsp;&nbsp;const lang = req\n\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;.acceptsLanguages()\n\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;.map((tag) =&gt; tag.toLowerCase().split('-')&#091;0])\n\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;.find((code) =&gt; locales.includes(code));\n\n&nbsp;&nbsp;&nbsp;&nbsp;req.setLocale(lang || 'en');\n\n&nbsp;&nbsp;}\n\n&nbsp;&nbsp;next();\n\n});<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>req.setLocale()<\/code> updates req, res and res.locals together, so every translation for the rest of the request uses the chosen locale. Avoid the global <code>i18n.setLocale()<\/code> on a server, since it holds one locale for the whole process. A request that awaits a database call could end up using the locale set by another request.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>i18n.init<\/code> still makes its own guess from the header before this middleware runs, and in 0.15.4 some header values make it throw, because its fallbacks option is a plain object too. Setting fallbacks to an object with no prototype in <code>i18n.js<\/code> stops the throw, and the middleware then replaces whatever locale the guess picked.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The language switcher in the navbar sends the selected language to a route that stores it in a cookie and redirects back to the current page, removing any ?<code>lang= parameter<\/code> that would override the cookie.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ routes\/locale.js\n\nconst express = require('express');\n\nconst i18n = require('..\/i18n');\n\nconst router = express.Router();\n\n\/\/ Back to the page the switcher was on, but only on this site, and\n\n\/\/ without a ?lang= that would override the new cookie. A path starting\n\n\/\/ with \/\/ would be read by the browser as another site, so it is refused.\n\nfunction backTo(req) {\n\n&nbsp;&nbsp;const origin = `${req.protocol}:\/\/${req.get('host')}`;\n\n&nbsp;&nbsp;const back = typeof req.body?.back === 'string' ? req.body.back : '\/';\n\n&nbsp;&nbsp;const url = URL.parse(back, origin);\n\n&nbsp;&nbsp;if (!url || url.origin !== origin || url.pathname.startsWith('\/\/')) return '\/';\n\n&nbsp;&nbsp;url.searchParams.delete('lang');\n\n&nbsp;&nbsp;return url.pathname + url.search + url.hash;\n\n}\n\nrouter.post('\/locale\/:code', (req, res, next) =&gt; {\n\n&nbsp;&nbsp;const { code } = req.params;\n\n&nbsp;&nbsp;if (!i18n.getLocales().includes(code)) return next();\n\n&nbsp;&nbsp;res.cookie('lang', code, { maxAge: 365 * 24 * 60 * 60 * 1000, httpOnly: true, sameSite: 'lax' });\n\n&nbsp;&nbsp;res.redirect(backTo(req));\n\n});\n\nmodule.exports = router;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">In the header partial, the switcher is a single form with one button per language, each posting to its own formaction, and a hidden back field with the current page\u2019s address. The TO-DO app sets <code>res.locals.currentPath<\/code> to <code>req.originalUrl<\/code> for normal page views and to <code>\/<\/code> for a page shown in answer to a form, such as an error after a failed submit, because reloading that address would ask for a page that only accepts form posts.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;%# views\/partials\/header.ejs %&gt;\n\n&lt;html lang=\"&lt;%= getLocale() %&gt;\" data-bs-theme=\"light\"&gt;\n\n&lt;%# ... %&gt;\n\n&lt;form method=\"post\" class=\"m-0 btn-group btn-group-sm\" id=\"language-switcher\" role=\"group\"\n\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;aria-label=\"&lt;%= __('nav_language') %&gt;\"&gt;\n\n&nbsp;&nbsp;&nbsp;&nbsp;&lt;input type=\"hidden\" name=\"back\" value=\"&lt;%= currentPath %&gt;\"&gt;\n\n&nbsp;&nbsp;&nbsp;&nbsp;&lt;button type=\"submit\" formaction=\"\/locale\/en\" lang=\"en\"\n\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;class=\"btn &lt;%= getLocale() === 'en' ? 'btn-secondary' : 'btn-outline-secondary' %&gt;\"&gt;English&lt;\/button&gt;\n\n&nbsp;&nbsp;&nbsp;&nbsp;&lt;button type=\"submit\" formaction=\"\/locale\/fr\" lang=\"fr\"\n\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;class=\"btn &lt;%= getLocale() === 'fr' ? 'btn-secondary' : 'btn-outline-secondary' %&gt;\"&gt;Fran\u00e7ais&lt;\/button&gt;\n\n&lt;\/form&gt;<\/code><\/pre>\n\n\n\n<h2 id=\"Using-__()-in-EJS-templates-and-route-handlers\" class=\"wp-block-heading\">Using __() in EJS templates and route handlers<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">In a view, replace each piece of text with <code>__()<\/code> and its key, and print the result with <code>&lt;%=<\/code>, which <code>HTML-escapes<\/code> it so a stray <code>&lt;<\/code> or <code>&amp;<\/code> in a translation can\u2019t break the markup. It\u2019s easy to miss strings that aren\u2019t body text, such as <code>title<\/code> and <code>aria-label<\/code> attributes, or a confirmation message that ends up in inline JavaScript.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;%# views\/lists\/show.ejs %&gt;\n\n&lt;span class=\"badge rounded-pill &lt;%= task.done ? 'text-bg-success' : 'text-bg-secondary' %&gt;\"&gt;\n\n&nbsp;&nbsp;&nbsp;&nbsp;&lt;%= task.done ? __('task_status_done') : __('task_status_open') %&gt;\n\n&lt;\/span&gt;\n\n&lt;form method=\"post\" action=\"\/tasks\/&lt;%= task.id %&gt;\/toggle\" class=\"m-0\"&gt;\n\n&nbsp;&nbsp;&nbsp;&nbsp;&lt;button type=\"submit\" class=\"btn btn-sm btn-outline-primary toggle-task\"\n\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;title=\"&lt;%= __('task_toggle_title') %&gt;\"&gt;&lt;%= task.done ? __('task_undo') : __('task_mark_done') %&gt;&lt;\/button&gt;\n\n&lt;\/form&gt;\n\n&lt;input type=\"text\" name=\"text\" value=\"&lt;%= task.text %&gt;\" required maxlength=\"255\"\n\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;class=\"form-control form-control-sm task-text\" aria-label=\"&lt;%= __('task_text_label') %&gt;\"&gt;\n\n&lt;form method=\"post\" action=\"\/lists\/&lt;%= list.id %&gt;\/delete\"\n\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;onsubmit=\"return confirm(&lt;%= JSON.stringify(__('delete_list_confirm')) %&gt;);\"&gt;\n\n&nbsp;&nbsp;&nbsp;&nbsp;&lt;button type=\"submit\" class=\"btn btn-outline-danger\"&gt;&lt;%= __('delete_list_submit') %&gt;&lt;\/button&gt;\n\n&lt;\/form&gt;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>JSON.stringify()<\/code> turns the translation into a quoted JavaScript string, and <code>&lt;%=<\/code> escapes its quotes for the HTML attribute, so a French apostrophe or quotation mark can\u2019t end the <code>confirm()<\/code> argument early.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Keys also separate words that English happens to spell the same way. The unlocalized app used Done for both the filter tab and a task\u2019s status badge, but French needs the plural Termin\u00e9es for the tab and the singular Termin\u00e9e for one task, so each gets a key of its own, <code>filter_done<\/code> and <code>task_status<\/code>_done. Naming keys after where the text appears also makes them easier to find later.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Route handlers tend to hold the messages that were built by gluing strings together, such as the unlocalized app\u2019s &#8216;List &#8220;&#8216; + name + &#8216;&#8221; created.&#8217;, and the fix is to give the whole sentence one key and put the name in a named placeholder.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ routes\/lists.js\n\nrouter.post('\/lists', (req, res) =&gt; {\n\n&nbsp;&nbsp;const name = textField(req.body?.name);\n\n&nbsp;&nbsp;const error = validateListName(name, req.__);\n\n&nbsp;&nbsp;if (error) {\n\n&nbsp;&nbsp;&nbsp;&nbsp;req.flash('danger', error);\n\n&nbsp;&nbsp;&nbsp;&nbsp;return res.redirect('\/');\n\n&nbsp;&nbsp;}\n\n&nbsp;&nbsp;const result = db\n\n&nbsp;&nbsp;&nbsp;&nbsp;.prepare('INSERT INTO lists (user_id, name, created_at) VALUES (?, ?, ?)')\n\n&nbsp;&nbsp;&nbsp;&nbsp;.run(req.user.id, name, new Date().toISOString());\n\n&nbsp;&nbsp;req.flash('success', req.__('list_created', { name }));\n\n&nbsp;&nbsp;res.redirect('\/lists\/' + result.lastInsertRowid);\n\n});<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The English text for that key, List <code>\"{{name}}\"<\/code> created., is stored in <code>en.json<\/code>. <code>i18n-node<\/code> accepts <code>printf-style %s<\/code> placeholders as well, but the named form is kinder to translators: in POEditor they see <code>{{name}}<\/code>, know what goes in the gap, and can move it to wherever French word order puts it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The validation module has no request of its own, so calling the shared i18n instance there would use the default locale. Instead, the route passes in the request\u2019s translate function as <code>validateListName(name, req.__)<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ validation.js\n\nfunction validateRegistration({ username, password }, __) {\n\n&nbsp;&nbsp;const errors = {};\n\n&nbsp;&nbsp;if (!username) {\n\n&nbsp;&nbsp;&nbsp;&nbsp;errors.username = __('username_required');\n\n&nbsp;&nbsp;} else if (username.length &lt; 3 || username.length &gt; 80) {\n\n&nbsp;&nbsp;&nbsp;&nbsp;errors.username = __('username_length');\n\n&nbsp;&nbsp;}\n\n&nbsp;&nbsp;\/\/ ...\n\n&nbsp;&nbsp;return errors;\n\n}\n\nfunction validateListName(name, __) {\n\n&nbsp;&nbsp;if (!name) return __('list_name_required');\n\n&nbsp;&nbsp;if (name.length &gt; 100) return __('list_name_too_long');\n\n&nbsp;&nbsp;return null;\n\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Quick tip: Mustache HTML-escapes every value it puts into a <code>{{name}}<\/code> placeholder, and EJS escapes the finished string again, so a list called Rent &amp; bills would show up as <code>Rent &amp;amp; bills<\/code>. The <code>Mustache.escape<\/code> line in <code>i18n.js<\/code> turns the first pass off, which is safe as long as every translation reaches the page through <code>&lt;%=<\/code>. It only takes effect when <code>npm<\/code> installs a single copy of Mustache, so keep your version within the ^4.2.0 range that <code>i18n-node<\/code> 0.15.4 depends on.<\/p>\n\n\n\n<h2 id=\"Handling-plurals-with-__n()\" class=\"wp-block-heading\">Handling plurals with __n()<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The unlocalized TO-DO app chose between task left and tasks left with an if, which assumes every language splits on \u201cone or not one\u201d and bakes English word order into the code. Replace each branch with <code>req.__n()<\/code>, passing the key, the number that decides the form, and an object with the values for the placeholders.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ routes\/lists.js\n\nconst locale = req.getLocale();\n\nconst openLabel = req.__n('tasks_left', openCount, {\n\n&nbsp;&nbsp;count: formatNumber(openCount, locale),\n\n});\n\nconst percentDone = totalCount &gt; 0 ? doneCount \/ totalCount : 0;\n\nconst progressLabel = req.__n('tasks_progress', totalCount, {\n\n&nbsp;&nbsp;percent: formatPercent(percentDone, locale),\n\n&nbsp;&nbsp;count: formatNumber(totalCount, locale),\n\n});<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The forms themselves are defined in the catalog, where a plural key has one entry for each CLDR plural category. <code>i18n-node<\/code> uses the current locale\u2019s plural rules to choose the right one. The <code>{{count}}<\/code> shown to the reader is a separate value already formatted with Intl, because with a <code>%s<\/code> placeholder <code>i18n-node<\/code> prints the raw count, and a French reader then sees <code>1234 t\u00e2ches<\/code> where they expect <code>1 234 t\u00e2ches<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">French also disagrees with English about where the singular stops. Its one category covers both 0 and 1, so an empty list reads <code>Il reste 0 t\u00e2che<\/code>, and it has a many category for exact multiples of a million, such as 1,000,000, which take de in French: 1 000 000 de t\u00e2ches. <code>i18n-node<\/code> picks the right key as long as the French catalog contains it, and filling those keys is the translator\u2019s job in POEditor.<\/p>\n\n\n\n<h2 id=\"Writing-the-English-JSON-catalog\" class=\"wp-block-heading\">Writing the English JSON catalog<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">With keys, <code>en.json<\/code> is the source of the English text, so you write it by hand and add an entry whenever you add a new key to a view or route. A plural key holds an object with the English one and other forms.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ locales\/en.json (excerpt)\n\n{\n\n&nbsp;&nbsp;&nbsp;&nbsp;\"list_name_required\": \"Give the list a name.\",\n\n&nbsp;&nbsp;&nbsp;&nbsp;\"list_name_too_long\": \"List names are at most 100 characters long.\",\n\n&nbsp;&nbsp;&nbsp;&nbsp;\"list_created\": \"List \\\"{{name}}\\\" created.\",\n\n&nbsp;&nbsp;&nbsp;&nbsp;\"filter_all\": \"All\",\n\n&nbsp;&nbsp;&nbsp;&nbsp;\"filter_open\": \"Open\",\n\n&nbsp;&nbsp;&nbsp;&nbsp;\"filter_done\": \"Done\",\n\n&nbsp;&nbsp;&nbsp;&nbsp;\"task_status_done\": \"Completed\",\n\n&nbsp;&nbsp;&nbsp;&nbsp;\"tasks_left\": {\n\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;\"one\": \"{{count}} task left\",\n\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;\"other\": \"{{count}} tasks left\"\n\n&nbsp;&nbsp;&nbsp;&nbsp;},\n\n&nbsp;&nbsp;&nbsp;&nbsp;\"tasks_progress\": {\n\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;\"one\": \"{{percent}} of {{count}} task done\",\n\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;\"other\": \"{{percent}} of {{count}} tasks done\"\n\n&nbsp;&nbsp;&nbsp;&nbsp;}\n\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">That\u2019s why <code>i18n.js<\/code> turns <code>updateFiles<\/code> off. The option is on by default, and it adds every key it can\u2019t find to the current locale\u2019s catalog with the key itself as the text, so a typo in a view would put an entry such as <code>\"list_crated\": \"list_crated\"<\/code> into your hand-written <code>en.json<\/code>. With it off, a key missing from en.json shows up on the page as the bare key, which is easy to spot while you click through the app.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>retryInDefaultLocale: true<\/code> covers the opposite gap. When fr.json has no entry for a key yet, <code>i18n-node<\/code> looks the key up in English instead, so a string added after the last translation round shows its English text rather than <code>list_created<\/code>.<\/p>\n\n\n\n<h2 id=\"Translating-the-JSON-catalogs-in-POEditor\" class=\"wp-block-heading\">Translating the JSON catalogs in POEditor<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">On the POEditor projects page, click the orange <strong>+<\/strong> button (its tooltip reads Add project), name the project TO-DO app (Node.js) in the Add Project dialog, and click Add.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"930\" src=\"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/02_projects_list_new_project_v4-1024x930.png\" alt=\"\" class=\"wp-image-8462\" srcset=\"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/02_projects_list_new_project_v4-1024x930.png 1024w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/02_projects_list_new_project_v4-300x273.png 300w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/02_projects_list_new_project_v4-767x697.png 767w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/02_projects_list_new_project_v4-1536x1396.png 1536w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/02_projects_list_new_project_v4-2048x1861.png 2048w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><figcaption class=\"wp-element-caption\"><em>The new, empty project<\/em><\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Open the project, click the <strong>+<\/strong> button to open Add Languages, and add English and French. Both start at 0%, and POEditor reminds you that the project has no terms yet.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"639\" src=\"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/03_languages_added_v4-1024x639.png\" alt=\"\" class=\"wp-image-8463\" srcset=\"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/03_languages_added_v4-1024x639.png 1024w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/03_languages_added_v4-300x187.png 300w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/03_languages_added_v4-767x479.png 767w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/03_languages_added_v4-1536x959.png 1536w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/03_languages_added_v4-2048x1279.png 2048w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><figcaption class=\"wp-element-caption\"><em>English and French, before any import<\/em><\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Go to Import. On the Import Terms page, choose <code>en.json<\/code>. Several file formats use the <code>.json<\/code> extension, so POEditor lets you either leave the format to auto-detection or choose the type of JSON yourself. Click set it now to show the CHANGE FILE FORMAT menu, which starts on Auto-detect format, and pick Key-Value JSON (.json), the format POEditor lists for <code>i18n-node<\/code>. Leave <em>Also import translations<\/em> to a language switched on, select English, and click <em>Import to project<\/em>.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"887\" height=\"1024\" src=\"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/04_import_en_json_v4-887x1024.png\" alt=\"\" class=\"wp-image-8464\" srcset=\"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/04_import_en_json_v4-887x1024.png 887w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/04_import_en_json_v4-260x300.png 260w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/04_import_en_json_v4-768x886.png 768w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/04_import_en_json_v4-1331x1536.png 1331w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/04_import_en_json_v4-1775x2048.png 1775w\" sizes=\"auto, (max-width: 887px) 100vw, 887px\" \/><figcaption class=\"wp-element-caption\"><em>Importing en.json as Key-Value JSON, with its values going into English<\/em><\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The import reports 83 terms found: 83 terms added; 83 translations found: 83 added, 0 updated in English., and English reaches 100% straight away. Each key becomes a term and each English value its English translation, so there is no need for a separate Copy Terms to Translations step. The four plural keys become plural terms, each with a Plural: line under the term and ONE and OTHER tabs on the English side.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"657\" src=\"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/05_english_plural_term_v4-1024x657.png\" alt=\"\" class=\"wp-image-8465\" srcset=\"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/05_english_plural_term_v4-1024x657.png 1024w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/05_english_plural_term_v4-767x492.png 767w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/05_english_plural_term_v4-300x192.png 300w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/05_english_plural_term_v4-1536x985.png 1536w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/05_english_plural_term_v4-2048x1314.png 2048w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><figcaption class=\"wp-element-caption\"><em>A plural term, with ONE and OTHER tabs in English<\/em><\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">A key such as <code>list_created<\/code> tells a translator where a string belongs but not what it says, so make English the project\u2019s default reference language if POEditor hasn\u2019t already: click the gear icon for the project\u2019s Settings, click Edit Details, set DEFAULT REFERENCE LANGUAGE to English, and click Save project details. The French editor then shows each term\u2019s English text, with its key in small grey type underneath.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Now open French and translate. Keep each placeholder such as <code>{{name}}<\/code> or <code>{{count}}<\/code> exactly as written, though you can move it to wherever French word order wants it. If a translation drops or changes one, QA Checks flag it when you save.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If you would rather review a first draft than translate from scratch, you can pre-fill the French terms with Automatic Translation (Google Translate, DeepL or Azure AI Translator), which is built in, or with AI translation from providers such as OpenAI or Anthropic\u2019s Claude once you add your own API key.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"618\" src=\"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/06_french_translations_v4-1024x618.png\" alt=\"\" class=\"wp-image-8466\" srcset=\"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/06_french_translations_v4-1024x618.png 1024w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/06_french_translations_v4-300x181.png 300w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/06_french_translations_v4-767x463.png 767w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/06_french_translations_v4-1536x928.png 1536w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/06_french_translations_v4-2048x1237.png 2048w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><figcaption class=\"wp-element-caption\"><em>French translations, with the English text and the key for each term<\/em><\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The plural terms show ONE, MANY and OTHER tabs in French, one more than in English. Fill in all three, because when many is missing, <code>i18n-node<\/code> falls back to other and prints 1 000 000 t\u00e2ches without the de that French grammar needs.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"714\" src=\"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/07_plural_term_french_v4-1024x714.png\" alt=\"\" class=\"wp-image-8467\" srcset=\"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/07_plural_term_french_v4-1024x714.png 1024w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/07_plural_term_french_v4-300x209.png 300w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/07_plural_term_french_v4-767x535.png 767w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/07_plural_term_french_v4-1536x1071.png 1536w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/07_plural_term_french_v4-2048x1428.png 2048w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><figcaption class=\"wp-element-caption\"><em>A French plural term, with a tab for each of its three forms<\/em><\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">When French is done, go to Export. On the Export Terms and Translations page, set FILE FORMAT to Key-Value JSON (.json) and click Export File. If some terms are still untranslated, switch on Advanced export options first and set EXPORT to Translated. By default the file includes an empty string for every untranslated term, which the app would show as blank text, while a term missing from the file falls back to English.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"767\" src=\"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/08_export_french_json_v4-1024x767.png\" alt=\"\" class=\"wp-image-8468\" srcset=\"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/08_export_french_json_v4-1024x767.png 1024w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/08_export_french_json_v4-300x225.png 300w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/08_export_french_json_v4-768x575.png 768w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/08_export_french_json_v4-1536x1150.png 1536w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/08_export_french_json_v4-2048x1533.png 2048w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><figcaption class=\"wp-element-caption\"><em>Exporting the French catalog as Key-Value JSON<\/em><\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Save the download as <code>locales\/fr.json<\/code>. It uses the same keys as <code>en.json<\/code>, and the plural entries come back in the object <code>shape __n()<\/code> reads, now carrying the many key.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ locales\/fr.json (excerpt)\n\n{\n\n&nbsp;&nbsp;&nbsp;&nbsp;\"list_created\": \"Liste \u00ab {{name}} \u00bb cr\u00e9\u00e9e.\",\n\n&nbsp;&nbsp;&nbsp;&nbsp;\"tasks_left\": {\n\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;\"one\": \"Il reste {{count}} t\u00e2che\",\n\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;\"many\": \"Il reste {{count}} de t\u00e2ches\",\n\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;\"other\": \"Il reste {{count}} t\u00e2ches\"\n\n&nbsp;&nbsp;&nbsp;&nbsp;},\n\n&nbsp;&nbsp;&nbsp;&nbsp;\"tasks_progress\": {\n\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;\"one\": \"{{percent}} de {{count}} t\u00e2che termin\u00e9e\",\n\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;\"many\": \"{{percent}} de {{count}} de t\u00e2ches termin\u00e9es\",\n\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;\"other\": \"{{percent}} des {{count}} t\u00e2ches termin\u00e9es\"\n\n&nbsp;&nbsp;&nbsp;&nbsp;}\n\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>i18n-node<\/code> reads its catalogs when the app starts, so run <code>npm start again<\/code> (or set autoReload: true in development to have it watch the files). Pick Fran\u00e7ais in the navbar and the list page switches over, with the count and the progress line built from the plural terms.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"995\" height=\"1024\" src=\"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/09_french_list_page_v4-995x1024.png\" alt=\"\" class=\"wp-image-8469\" srcset=\"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/09_french_list_page_v4-995x1024.png 995w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/09_french_list_page_v4-291x300.png 291w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/09_french_list_page_v4-767x790.png 767w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/09_french_list_page_v4-1492x1536.png 1492w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/09_french_list_page_v4-1990x2048.png 1990w\" sizes=\"auto, (max-width: 995px) 100vw, 995px\" \/><figcaption class=\"wp-element-caption\"><em>The list page in French, after using the switcher<\/em><\/figcaption><\/figure>\n\n\n\n<h2 id=\"Formatting-dates-and-numbers-with-Intl\" class=\"wp-block-heading\">Formatting dates and numbers with Intl<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><code>i18n-node<\/code>\u2019s <code>__()<\/code> and <code>__n()<\/code> translate text and nothing else, so the unlocalized <code>format.js<\/code>, which built 9\/24\/2026 1:54 PM by hand and added thousands separators with a regular expression, would keep printing American formats inside French sentences. Replace it with the <code>Intl<\/code> formatters and pass the locale in each time.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ format.js\n\n\/\/ 24 sept. 2026\n\nfunction formatDate(value, locale) {\n\n&nbsp;&nbsp;return new Intl.DateTimeFormat(locale, { dateStyle: 'medium' }).format(new Date(value));\n\n}\n\n\/\/ 24 sept. 2026, 15:07\n\nfunction formatDateTime(value, locale) {\n\n&nbsp;&nbsp;return new Intl.DateTimeFormat(locale, { dateStyle: 'medium', timeStyle: 'short' })\n\n&nbsp;&nbsp;&nbsp;&nbsp;.format(new Date(value));\n\n}\n\n\/\/ 1 234\n\nfunction formatNumber(n, locale) {\n\n&nbsp;&nbsp;return new Intl.NumberFormat(locale).format(n);\n\n}\n\n\/\/ 0.286 -&gt; 28,6 %\n\nfunction formatPercent(fraction, locale) {\n\n&nbsp;&nbsp;return new Intl.NumberFormat(locale, {\n\n&nbsp;&nbsp;&nbsp;&nbsp;style: 'percent',\n\n&nbsp;&nbsp;&nbsp;&nbsp;minimumFractionDigits: 1,\n\n&nbsp;&nbsp;&nbsp;&nbsp;maximumFractionDigits: 1,\n\n&nbsp;&nbsp;}).format(fraction);\n\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Official Node.js builds include full ICU data, so French month names, the comma decimal separator and the space before the percent sign work without installing anything. Route handlers pass <code>req.getLocale()<\/code> themselves, and for the views, add a middleware after the locale middleware that binds the helpers to the request\u2019s locale.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ app.js\n\n\/\/ Formatting helpers for the views, in the request's locale.\n\napp.use((req, res, next) =&gt; {\n\n&nbsp;&nbsp;const locale = req.getLocale();\n\n&nbsp;&nbsp;res.locals.formatDate = (value) =&gt; formatDate(value, locale);\n\n&nbsp;&nbsp;res.locals.formatDateTime = (value) =&gt; formatDateTime(value, locale);\n\n&nbsp;&nbsp;res.locals.formatNumber = (n) =&gt; formatNumber(n, locale);\n\n&nbsp;&nbsp;res.locals.formatPercent = (fraction) =&gt; formatPercent(fraction, locale);\n\n&nbsp;&nbsp;next();\n\n});<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The formatted value then goes into a message as a placeholder, as in <code>__('list_created_date'<\/code>, <code>{ date: formatDate(list.created_at) })<\/code>, and the dashboard shows both plural forms next to French dates.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"473\" src=\"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/10_french_dashboard_v4-1024x473.png\" alt=\"\" class=\"wp-image-8470\" srcset=\"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/10_french_dashboard_v4-1024x473.png 1024w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/10_french_dashboard_v4-300x138.png 300w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/10_french_dashboard_v4-767x354.png 767w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/10_french_dashboard_v4-1536x709.png 1536w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/10_french_dashboard_v4-2048x945.png 2048w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><figcaption class=\"wp-element-caption\"><em>The dashboard in French, with singular and plural task counts<\/em><\/figcaption><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Wrapping up<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The catalogs keep changing along with the app. When you add a string, add its key to en.json and import the file again the same way; the new terms arrive untranslated in French while existing ones keep their translations. After the import, POEditor also lets you delete obsolete terms, the ones whose keys you have removed from <code>en.json<\/code>. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Rewording the English text of a string leaves its key alone, so the term keeps its French translation. To bring the new English wording into POEditor, switch on Overwrite existing translations when you import, and also Mark strings as fuzzy in other languages so the French text is flagged for review. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For edits made directly in POEditor rather than through an import, the project\u2019s settings have an option of the same name that does this for you. Once this happens every release, the <a href=\"\/docs\/api\">POEditor API<\/a> or the GitHub integration can push en.json and pull the translated files as part of your build.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"\/register\/\">Create a free POEditor account<\/a> and import your own en.json to see your catalog with a translation interface around it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For other stacks in this series, see these guides:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"\/blog\/cakephp-localization\/\">CakePHP Localization: How to Translate a CakePHP App with i18n<\/a><\/li>\n\n\n\n<li><a href=\"\/blog\/django-i18n\/\">Django i18n: How to Translate and Localize Your App<\/a><\/li>\n\n\n\n<li><a href=\"\/blog\/python-gettext-localization\/\">Python gettext Localization: How to Translate a Python App<\/a><\/li>\n\n\n\n<li><a href=\"\/blog\/i18next\/\">i18next: What It Is and Why You Should Use It<\/a><\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">FAQ: Node.js localization with i18n-node<\/h2>\n\n\n\n<details class=\"wp-block-details is-layout-flow wp-block-details-is-layout-flow\"><summary><strong>Is i18n-node still maintained, and how does it compare with i18next?<\/strong><\/summary>\n<p class=\"wp-block-paragraph\">As of September 2026, yes. The package is published on npm as i18n, and its latest release, 0.15.4, tightened how locale names become catalog file paths and runs its test suite on Node 22 and 24. <code>i18n-node<\/code> has the narrower scope of the two: it targets server-side Node and Express, keeps JSON catalogs (flat by default) keyed by either the source text or short keys, and can write missing strings to them at runtime. <code>i18next<\/code> is a larger framework built around keys, namespaces and pluggable backends, and it runs in the browser as well as on the server. For a <code>server-rendered<\/code> Express app, <code>i18n-node<\/code> is usually enough.<\/p>\n<\/details>\n\n\n\n<details class=\"wp-block-details is-layout-flow wp-block-details-is-layout-flow\"><summary><strong>Should i18n-node catalogs use keys or the English text?<\/strong><\/summary>\n<p class=\"wp-block-paragraph\"><code>i18n-node<\/code> works with either. With the English text as the key, you can write <code>__('Task added.')<\/code>, let <code>updateFiles<\/code> build the catalog while you click through the app, and skip writing <code>en.json<\/code> yourself, but every rewording creates a new term, and two uses of the same English word share one translation. Keys such as <code>task_added<\/code> take a hand-written <code>en.json<\/code> and <code>updateFiles<\/code> switched off, and in return they survive changes to the English text and give each use of a word its own entry. Most JSON-based projects use keys, and POEditor imports both kinds the same way as Key-Value JSON.<\/p>\n<\/details>\n\n\n\n<details class=\"wp-block-details is-layout-flow wp-block-details-is-layout-flow\"><summary><strong>How do I switch language with a URL instead of a cookie?<\/strong><\/summary>\n<p class=\"wp-block-paragraph\">Read <code>req.query.lang<\/code> in your own middleware after <code>i18n.init<\/code>, check it against <code>i18n.getLocales()<\/code>, and call <code>req.setLocale()<\/code> with it, so any URL with <code>lang=fr<\/code> in its query string renders in French. <code>i18n-node<\/code> also has a queryParameter option, but in version 0.15.4 it checks the value with a plain-object lookup. An unknown value such as de falls back to the default locale, while some names inherited from <code>Object.prototype<\/code> get through as locale names. The parameter only applies to that request, so you need to include it in your links as well. If you prefer path prefixes such as <code>\/fr\/lists<\/code>, <code>i18n-node<\/code> won\u2019t parse them for you, but a small middleware that reads the prefix and calls <code>req.setLocale()<\/code> does the job.<\/p>\n<\/details>\n\n\n\n<details class=\"wp-block-details is-layout-flow wp-block-details-is-layout-flow\"><summary><strong>Does i18n-node format dates and currencies?<\/strong><\/summary>\n<p class=\"wp-block-paragraph\">Not with <code>__()<\/code> and <code>__n()<\/code>, which translate text and choose plural forms but leave every date and number as you pass it in. i18n-node\u2019s <code>__mf()<\/code> can format them inside MessageFormat messages; otherwise, use the Intl API built into Node: <code>Intl.DateTimeFormat<\/code> for dates and times, and <code>Intl.NumberFormat<\/code> for numbers, percentages and currencies, for example new <code>Intl.NumberFormat('fr', { style: 'currency', currency: 'EUR' })<\/code>. Format the value first, then pass the resulting string into the message as a named placeholder, since i18n-node\u2019s <code>%s<\/code> substitution doesn\u2019t localize numbers.<\/p>\n<\/details>\n\n\n\n<details class=\"wp-block-details is-layout-flow wp-block-details-is-layout-flow\"><summary><strong>Can I use i18n-node without Express?<\/strong><\/summary>\n<p class=\"wp-block-paragraph\">Yes. <code>i18n.init<\/code> is a plain (req, res, next) middleware, so it works in any framework that runs Connect-style middleware. In a script, a worker or a CLI tool with no request at all, create an I18n instance and pass the locale on each call, as in <code>i18n.__({ phrase: 'task_added', locale: 'fr' })<\/code>, and only pass locales you have configured, since with <code>updateFiles<\/code> on an unknown one makes <code>i18n-node<\/code> create a new catalog file for it. That avoids the global <code>setLocale()<\/code>, which is shared by everything running in the same process.<\/p>\n<\/details>\n\n\n<div class=\"call-action my-4 d-flex justify-content-between align-items-md-center gap-4 flex-column flex-lg-row\"><div><h3 class=\"fs-4\">Ready to power up localization?<\/h3><span class=\"fs-6\">Subscribe to the POEditor platform today!<\/span><\/div><a class=\"btn btn-b-primary d-flex align-items-center justify-content-center px-4 py-3 flex-shrink-0\" \n\t\t\t\t\thref=\"https:\/\/poeditor.com\/pricing\/?utm_source=blog&#038;utm_medium=btn&#038;utm_campaign=cta_pricing\">See pricing<\/a><\/div>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Once an app has users in more than one country, hardcoded English strings are no longer enough: people expect labels, messages and dates in their own language. Internationalization (i18n) prepares the code so its text can change per language, and localization (l10n) supplies the translations and regional formats for each one. In this guide, we\u2019ll [&hellip;]<\/p>\n","protected":false},"author":6,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[17],"tags":[],"class_list":["post-8459","post","type-post","status-publish","format-standard","hentry","category-localization"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.6 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Node.js localization: How to translate an express app with i18n-node - POEditor Blog<\/title>\n<meta name=\"description\" content=\"Check out our Node.js localization guide, where we localize an Express TO-DO app with i18n-node and translate its catalog in POEditor.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/poeditor.com\/blog\/node-js-localization\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Node.js localization: How to translate an express app with i18n-node - POEditor Blog\" \/>\n<meta property=\"og:description\" content=\"Check out our Node.js localization guide, where we localize an Express TO-DO app with i18n-node and translate its catalog in POEditor.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/poeditor.com\/blog\/node-js-localization\/\" \/>\n<meta property=\"og:site_name\" content=\"POEditor Blog\" \/>\n<meta property=\"article:publisher\" content=\"https:\/\/www.facebook.com\/POEditor\" \/>\n<meta property=\"article:published_time\" content=\"2026-10-12T10:47:08+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-10-12T11:20:45+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/Node.js-localization.png\" \/>\n\t<meta property=\"og:image:width\" content=\"1764\" \/>\n\t<meta property=\"og:image:height\" content=\"1152\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"author\" content=\"POEditor\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:creator\" content=\"@poeditor\" \/>\n<meta name=\"twitter:site\" content=\"@poeditor\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"POEditor\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"16 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/node-js-localization\\\/#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/node-js-localization\\\/\"},\"author\":{\"name\":\"POEditor\",\"@id\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/#\\\/schema\\\/person\\\/db77eb2d09539eaac83dcd8f2af06b17\"},\"headline\":\"Node.js localization: How to translate an express app with i18n-node\",\"datePublished\":\"2026-10-12T10:47:08+00:00\",\"dateModified\":\"2026-10-12T11:20:45+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/node-js-localization\\\/\"},\"wordCount\":3044,\"publisher\":{\"@id\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/#organization\"},\"image\":{\"@id\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/node-js-localization\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/10\\\/Node.js-localization-1024x669.png\",\"articleSection\":[\"Localization\"],\"inLanguage\":\"en-US\"},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/node-js-localization\\\/\",\"url\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/node-js-localization\\\/\",\"name\":\"Node.js localization: How to translate an express app with i18n-node - POEditor Blog\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/node-js-localization\\\/#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/node-js-localization\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/10\\\/Node.js-localization-1024x669.png\",\"datePublished\":\"2026-10-12T10:47:08+00:00\",\"dateModified\":\"2026-10-12T11:20:45+00:00\",\"description\":\"Check out our Node.js localization guide, where we localize an Express TO-DO app with i18n-node and translate its catalog in POEditor.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/node-js-localization\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/poeditor.com\\\/blog\\\/node-js-localization\\\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/node-js-localization\\\/#primaryimage\",\"url\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/10\\\/Node.js-localization.png\",\"contentUrl\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/10\\\/Node.js-localization.png\",\"width\":1764,\"height\":1152},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/node-js-localization\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Node.js localization: How to translate an express app with i18n-node\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/#website\",\"url\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/\",\"name\":\"POEditor Blog\",\"description\":\"All about translation and localization management\",\"publisher\":{\"@id\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/#organization\",\"name\":\"POEditor\",\"url\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/#\\\/schema\\\/logo\\\/image\\\/\",\"url\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/wp-content\\\/uploads\\\/2019\\\/11\\\/logo_head_512_transparent.png\",\"contentUrl\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/wp-content\\\/uploads\\\/2019\\\/11\\\/logo_head_512_transparent.png\",\"width\":512,\"height\":512,\"caption\":\"POEditor\"},\"image\":{\"@id\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/#\\\/schema\\\/logo\\\/image\\\/\"},\"sameAs\":[\"https:\\\/\\\/www.facebook.com\\\/POEditor\",\"https:\\\/\\\/x.com\\\/poeditor\",\"https:\\\/\\\/www.linkedin.com\\\/company\\\/poeditor\\\/\",\"https:\\\/\\\/www.youtube.com\\\/channel\\\/UCXAk1u8N49VRMAqNneENCFA\"]},{\"@type\":\"Person\",\"@id\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/#\\\/schema\\\/person\\\/db77eb2d09539eaac83dcd8f2af06b17\",\"name\":\"POEditor\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/95bca2c465fe6134f210cb2f2fbed4f71bf37833fb285624320e808c865695c1?s=96&d=mm&r=g\",\"url\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/95bca2c465fe6134f210cb2f2fbed4f71bf37833fb285624320e808c865695c1?s=96&d=mm&r=g\",\"contentUrl\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/95bca2c465fe6134f210cb2f2fbed4f71bf37833fb285624320e808c865695c1?s=96&d=mm&r=g\",\"caption\":\"POEditor\"},\"url\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/author\\\/poeditor\\\/\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Node.js localization: How to translate an express app with i18n-node - POEditor Blog","description":"Check out our Node.js localization guide, where we localize an Express TO-DO app with i18n-node and translate its catalog in POEditor.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/poeditor.com\/blog\/node-js-localization\/","og_locale":"en_US","og_type":"article","og_title":"Node.js localization: How to translate an express app with i18n-node - POEditor Blog","og_description":"Check out our Node.js localization guide, where we localize an Express TO-DO app with i18n-node and translate its catalog in POEditor.","og_url":"https:\/\/poeditor.com\/blog\/node-js-localization\/","og_site_name":"POEditor Blog","article_publisher":"https:\/\/www.facebook.com\/POEditor","article_published_time":"2026-10-12T10:47:08+00:00","article_modified_time":"2026-10-12T11:20:45+00:00","og_image":[{"width":1764,"height":1152,"url":"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/Node.js-localization.png","type":"image\/png"}],"author":"POEditor","twitter_card":"summary_large_image","twitter_creator":"@poeditor","twitter_site":"@poeditor","twitter_misc":{"Written by":"POEditor","Est. reading time":"16 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/poeditor.com\/blog\/node-js-localization\/#article","isPartOf":{"@id":"https:\/\/poeditor.com\/blog\/node-js-localization\/"},"author":{"name":"POEditor","@id":"https:\/\/poeditor.com\/blog\/#\/schema\/person\/db77eb2d09539eaac83dcd8f2af06b17"},"headline":"Node.js localization: How to translate an express app with i18n-node","datePublished":"2026-10-12T10:47:08+00:00","dateModified":"2026-10-12T11:20:45+00:00","mainEntityOfPage":{"@id":"https:\/\/poeditor.com\/blog\/node-js-localization\/"},"wordCount":3044,"publisher":{"@id":"https:\/\/poeditor.com\/blog\/#organization"},"image":{"@id":"https:\/\/poeditor.com\/blog\/node-js-localization\/#primaryimage"},"thumbnailUrl":"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/Node.js-localization-1024x669.png","articleSection":["Localization"],"inLanguage":"en-US"},{"@type":"WebPage","@id":"https:\/\/poeditor.com\/blog\/node-js-localization\/","url":"https:\/\/poeditor.com\/blog\/node-js-localization\/","name":"Node.js localization: How to translate an express app with i18n-node - POEditor Blog","isPartOf":{"@id":"https:\/\/poeditor.com\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/poeditor.com\/blog\/node-js-localization\/#primaryimage"},"image":{"@id":"https:\/\/poeditor.com\/blog\/node-js-localization\/#primaryimage"},"thumbnailUrl":"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/Node.js-localization-1024x669.png","datePublished":"2026-10-12T10:47:08+00:00","dateModified":"2026-10-12T11:20:45+00:00","description":"Check out our Node.js localization guide, where we localize an Express TO-DO app with i18n-node and translate its catalog in POEditor.","breadcrumb":{"@id":"https:\/\/poeditor.com\/blog\/node-js-localization\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/poeditor.com\/blog\/node-js-localization\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/poeditor.com\/blog\/node-js-localization\/#primaryimage","url":"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/Node.js-localization.png","contentUrl":"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/Node.js-localization.png","width":1764,"height":1152},{"@type":"BreadcrumbList","@id":"https:\/\/poeditor.com\/blog\/node-js-localization\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/poeditor.com\/blog\/"},{"@type":"ListItem","position":2,"name":"Node.js localization: How to translate an express app with i18n-node"}]},{"@type":"WebSite","@id":"https:\/\/poeditor.com\/blog\/#website","url":"https:\/\/poeditor.com\/blog\/","name":"POEditor Blog","description":"All about translation and localization management","publisher":{"@id":"https:\/\/poeditor.com\/blog\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/poeditor.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/poeditor.com\/blog\/#organization","name":"POEditor","url":"https:\/\/poeditor.com\/blog\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/poeditor.com\/blog\/#\/schema\/logo\/image\/","url":"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2019\/11\/logo_head_512_transparent.png","contentUrl":"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2019\/11\/logo_head_512_transparent.png","width":512,"height":512,"caption":"POEditor"},"image":{"@id":"https:\/\/poeditor.com\/blog\/#\/schema\/logo\/image\/"},"sameAs":["https:\/\/www.facebook.com\/POEditor","https:\/\/x.com\/poeditor","https:\/\/www.linkedin.com\/company\/poeditor\/","https:\/\/www.youtube.com\/channel\/UCXAk1u8N49VRMAqNneENCFA"]},{"@type":"Person","@id":"https:\/\/poeditor.com\/blog\/#\/schema\/person\/db77eb2d09539eaac83dcd8f2af06b17","name":"POEditor","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/secure.gravatar.com\/avatar\/95bca2c465fe6134f210cb2f2fbed4f71bf37833fb285624320e808c865695c1?s=96&d=mm&r=g","url":"https:\/\/secure.gravatar.com\/avatar\/95bca2c465fe6134f210cb2f2fbed4f71bf37833fb285624320e808c865695c1?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/95bca2c465fe6134f210cb2f2fbed4f71bf37833fb285624320e808c865695c1?s=96&d=mm&r=g","caption":"POEditor"},"url":"https:\/\/poeditor.com\/blog\/author\/poeditor\/"}]}},"_links":{"self":[{"href":"https:\/\/poeditor.com\/blog\/wp-json\/wp\/v2\/posts\/8459","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/poeditor.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/poeditor.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/poeditor.com\/blog\/wp-json\/wp\/v2\/users\/6"}],"replies":[{"embeddable":true,"href":"https:\/\/poeditor.com\/blog\/wp-json\/wp\/v2\/comments?post=8459"}],"version-history":[{"count":13,"href":"https:\/\/poeditor.com\/blog\/wp-json\/wp\/v2\/posts\/8459\/revisions"}],"predecessor-version":[{"id":8484,"href":"https:\/\/poeditor.com\/blog\/wp-json\/wp\/v2\/posts\/8459\/revisions\/8484"}],"wp:attachment":[{"href":"https:\/\/poeditor.com\/blog\/wp-json\/wp\/v2\/media?parent=8459"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/poeditor.com\/blog\/wp-json\/wp\/v2\/categories?post=8459"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/poeditor.com\/blog\/wp-json\/wp\/v2\/tags?post=8459"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}