<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Arsenii Kozlov</title>
    <description>The latest articles on Forem by Arsenii Kozlov (@arsenii).</description>
    <link>https://forem.com/arsenii</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1202838%2F4396038c-439e-4aa5-ba8a-5317a56b9d5f.png</url>
      <title>Forem: Arsenii Kozlov</title>
      <link>https://forem.com/arsenii</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/arsenii"/>
    <language>en</language>
    <item>
      <title>We added Spanish language support and here's what we had to change in our architecture</title>
      <dc:creator>Arsenii Kozlov</dc:creator>
      <pubDate>Wed, 17 Jan 2024 12:49:09 +0000</pubDate>
      <link>https://forem.com/arsenii/we-added-spanish-language-support-and-heres-what-we-had-to-change-in-our-architecture-581e</link>
      <guid>https://forem.com/arsenii/we-added-spanish-language-support-and-heres-what-we-had-to-change-in-our-architecture-581e</guid>
      <description>&lt;p&gt;In &lt;a href="https://spotsmap.com/map"&gt;spotsmap.com&lt;/a&gt;, from the beginning, we supported three languages, and the architecture of how we implemented multilingual support seemed quite good. However, we kept postponing support for more languages simply because even thinking about it caused us pain.&lt;/p&gt;

&lt;p&gt;A few days ago, we took matters into our own hands and reorganized our architecture. This affected all parts of our system: the database, backend, frontend, and email communication. Here, I will describe what we have changed to make adding new languages as easy as possible. As a result, it helped us to add support for the Spanish language without any difficulties.&lt;/p&gt;

&lt;p&gt;Also, I will share with you some practices on how to improve localization and the mistakes that you should avoid.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;We moved dictionaries from the database to the code repository&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Initially, we used the following database scheme for all dictionaries that require localization - in our case, these were countries, sports, student levels, and so on.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdl36n2s51jtvppio7z43.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdl36n2s51jtvppio7z43.png" alt="Locale (PK id, code, name) &amp;lt;- SportLocale (FK localeId, FK schoolId, name) -&amp;gt; Sport (PK id, masterName)" width="727" height="206"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here, the table &lt;code&gt;Locale&lt;/code&gt; contains supported languages. The table &lt;code&gt;Sport&lt;/code&gt; contains all types of sports that are presented on our website, such as kitesurfing, windsurfing, etc. In this table, the field &lt;code&gt;masterName&lt;/code&gt; is the default name for each type of sport in English. We use this name when we don't have a translation for a specific language. Lastly, the table &lt;code&gt;SportLocale&lt;/code&gt; contains localized names of types of sports for all supported languages.&lt;/p&gt;

&lt;p&gt;How the update process looked like. To add a new value or change something in the current values, a developer has to write a migration for the database, which has to be applied to all databases, depending on the development stage. Also, such a migration requires careful review to ensure it does not break the production environment.&lt;/p&gt;

&lt;p&gt;The migration can look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;Upgrade&lt;/span&gt; &lt;span class="n"&gt;migration&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="nv"&gt;"Sport"&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;"masterName"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;"Sailing"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="nv"&gt;"SportLocale"&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;"localeId"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;"sportId"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;"Sailing"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;"Парусный спорт"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;"Segeln"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;"Navegación"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;Downgrade&lt;/span&gt; &lt;span class="n"&gt;migration&lt;/span&gt;
&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="nv"&gt;"SportLocale"&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nv"&gt;"sportId"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="nv"&gt;"Sport"&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nv"&gt;"id"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The more languages, the more lines of code, and unfortunately, the more mistakes. Moreover, developers have to prepare translations themselves or wait for them from their teammates, which significantly increases development time.&lt;/p&gt;

&lt;p&gt;What we did: We removed all localization tables and dictionary tables from the database, such as &lt;code&gt;Sport&lt;/code&gt; and &lt;code&gt;SportLocale&lt;/code&gt;. Instead, we added a dictionary JSON file &lt;code&gt;dictionaries/en/sport.json&lt;/code&gt; to our backend repository. In the data tables, we replaced the ID of the sport type with its corresponding key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"kitesurfing"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Kitesurfing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"windsurfing"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Windsurfing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sup"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SUP"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, to add a new sport, a developer only has to add one line in the JSON file, and only for English. Just a one line, really. A bit later, I will explain how we translate dictionaries to all supported languages and how it helps us to easily support new languages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;We added the generation of a description based on structured data instead of using pre-rendered texts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We display a description of a sports school on its page. These pages are generated statically to make them SEO-friendly. Initially, we generated texts when we added a new school to the database. There was a separate description for every supported language, stored in the &lt;code&gt;SchoolLocale&lt;/code&gt; table.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhnctfgckt28nk1waa9us.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhnctfgckt28nk1waa9us.png" alt="Locale (PK id, code, name) &amp;lt;- SchoolLocale (FK localeId, FK schoolId, desription) -&amp;gt; School (PK id)" width="727" height="206"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It was okay when we added a school. We just generated three descriptions at the same time and stored them together with the school. However, when we want to add a new language, it requires generating more than 1000 texts for every school. Moreover, we used different outside services, like Google Places API, to collect more details about the school, and these details can change from time to time. So, adding a new language requires regenerating all descriptions - more than 4000 just for 4 languages. What about adding 40 supported languages, as we aim?&lt;/p&gt;

&lt;p&gt;This solution does not look maintainable and easy to scale.&lt;/p&gt;

&lt;p&gt;How we changed the approach.&lt;/p&gt;

&lt;p&gt;We removed the &lt;code&gt;SchoolLocale&lt;/code&gt; table and saved structured JSON data in the &lt;code&gt;School&lt;/code&gt; table, which contains all the necessary information for generating a school description, such as nearby hotels, airports, and more. Now, we generate all the pages on the frontend side during the build time (or incremental static generation time).&lt;/p&gt;

&lt;p&gt;To achieve this, we added a new localization file &lt;code&gt;public/locales/en/description.json&lt;/code&gt;, which looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sections"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"hotels"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{SCHOOL_HOTELS} {POINT_TYPE} {HOTEL_STARS} is {HOTEL_DISTANCE} from the school."&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, to add a new language, we just have to add a localized description file, e.g. &lt;code&gt;public/locales/es/description.json&lt;/code&gt;. And it doesn't take any time from our developers. Absolutely, now developers don't have to do anything here to support more languages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;We rearranged our multilingual email templates&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before we used conditional statements in our email templates. It looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;...
&lt;span class="nt"&gt;&amp;lt;h2&amp;gt;&lt;/span&gt;
{{#en}}Arrival date:{{/en}}
{{#de}}Ankunftsdatum:{{/de}}
{{#ru}}Дата прибытия:{{/ru}}
&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;{{ArrivalDate}}&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, we had over 40 localized strings in the template. Even with just three supported languages, this contains more than 120 lines of code solely for template localization. Now, consider the scenario with 10 or 40 languages.&lt;/p&gt;

&lt;p&gt;For 40 supported languages, it would involve a staggering 1600 lines of code in the email template. This is hardly maintainable. However, our objective is to support 40 or more languages, aiming to make &lt;a href="http://spotsmap.com/"&gt;spotsmap.com&lt;/a&gt; accessible to people worldwide.&lt;/p&gt;

&lt;p&gt;Moreover, it's hard work for a developer to add support for a new language. As a result, we decided to explore an alternative solution. To tackle these challenges, we moved localization strings from the template to our codebase and adopted a standardized translation process used for website UI localization.&lt;/p&gt;

&lt;p&gt;The revised template structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;...
&lt;span class="nt"&gt;&amp;lt;h2&amp;gt;&lt;/span&gt;{{ArrivalDateTitle}}&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;{{ArrivalDate}}&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We created a JSON file &lt;code&gt;templates/en/bookingConfirmation.json&lt;/code&gt;, which houses localization strings for English:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ArrivalDateTitle"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Arrival date"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And again, there is no job for developers to add support for a new language. All we need is a translated JSON file for the corresponding language.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;We moved to Gitloc for translating JSON files&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;From the beginning, it was the responsibility of a product manager to prepare all translations before the task goes to development. Did it work? Actually, no. It would take too much time just to start development, which we couldn't afford. Developers had to manually prepare a lot of translations. Nowadays, you can use ChatGPT to translate the full JSON file. However, what about maintenance when you have to add a few keys in different localization files? Every day. It's a monotonous task that every developer hates. Moreover, it's a low-qualification job, but you pay for it as if it were a highly qualified developer job.&lt;/p&gt;

&lt;p&gt;Now we use the only one "developer first" localization platform &lt;a href="https://gitloc.org/"&gt;Gitloc&lt;/a&gt;. We connected our repository to Gitloc and added a configuration file &lt;code&gt;gitloc.yaml&lt;/code&gt;. For example, there is the &lt;code&gt;gitloc.yaml&lt;/code&gt; that is used in our backend repository. We use almost the same file in our frontend repository.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;defaultLocale&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;en&lt;/span&gt;
  &lt;span class="na"&gt;locales&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;en&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;de&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ru&lt;/span&gt;
  &lt;span class="na"&gt;directories&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;dictionaries&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, to add a new language, developers just have to add one line in the &lt;code&gt;gitloc.yaml&lt;/code&gt; file and push it to the remote Git repository. Gitloc detects that a new language has been added and translates all localization files to the corresponding language. It then commits the translations to the repository. Nothing more.&lt;/p&gt;

&lt;p&gt;The same process applies for application maintenance. Developers add new keys in localization files, and Gitloc detects these changes and translates them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;Just a single line to support a new language. It's unbelievable.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;More localization practices that we learned&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Use patterns for localization strings&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For example, use &lt;code&gt;School works from {FROM} until {UNTIL}&lt;/code&gt; instead of concatenation. It is easier to use and provides more context to translators.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Store localized dictionaries with localization&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Instead of storing dictionaries that require translation, such as months, in a configuration file, we have moved them all to the &lt;code&gt;public/locales&lt;/code&gt; folder. This allows us to use the same localization methods for UI localization.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Store only localization strings in localization files&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Do not store business logic that depends on language, such as date and currency formats, in localization files. It is better to use configuration files or internationalization libraries that can format dates and currencies according to the locale.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Avoid using arrays in localization files&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Instead of using arrays, it is better to use key-value pairs. Many i18n libraries recommend this approach. Additionally, using arrays can result in more translations if something changes. For example, adding a new item in the middle of an array would require translating half of the array. However, adding a new key to an existing object only requires translating one new value.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Avoid using leading and trailing spaces in localization strings&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Instead of using a localization string like &lt;code&gt;"and": " and "&lt;/code&gt;, it is better to use &lt;code&gt;"and": "and"&lt;/code&gt; and add necessary spaces in your code. You can even use &lt;code&gt;"description": "{SOMETHING1} and {SOMETHING2}"&lt;/code&gt; if possible. This helps avoid translation mistakes, whether you use automated or manual translations.&lt;/p&gt;




&lt;p&gt;What was the reason for adding support for Spanish?&lt;/p&gt;

&lt;p&gt;We realized that most of our users are located in countries that speak the languages we support. Previously, we supported three languages: German, English, and Russian. The majority of our users were from Germany, Switzerland, Austria, and Russia. However, we also had a small percentage of users from Spain. We are curious to see how adding Spanish will affect the geographic distribution of our users.&lt;/p&gt;

&lt;p&gt;I believe that in a couple of months, we will collect enough statistics to compare whether there is any improvement or not. I will definitely share these results with you. Just subscribe to me so you don't miss the outcome of this experiment.&lt;/p&gt;

&lt;p&gt;I hope you found something helpful!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>localization</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Sending multilingual emails with Postmark and Gitloc</title>
      <dc:creator>Arsenii Kozlov</dc:creator>
      <pubDate>Fri, 08 Dec 2023 15:29:35 +0000</pubDate>
      <link>https://forem.com/arsenii/optimizing-multilingual-email-communication-strategies-for-efficient-localization-at-scale-301g</link>
      <guid>https://forem.com/arsenii/optimizing-multilingual-email-communication-strategies-for-efficient-localization-at-scale-301g</guid>
      <description>&lt;p&gt;In today's globalized world, effective communication with customers in their preferred language is key for businesses. Whether it's via emails, messengers, or websites, using a client's native language fosters a stronger connection and increases the likelihood of successful transactions. This holds true even for transactional or notification emails, as information can be easily overlooked when presented in a non-native language.&lt;/p&gt;

&lt;p&gt;In this post, I'll share our experience in implementing multilingual transactional emails for &lt;a href="https://spotsmap.com/map"&gt;spotsmap.com&lt;/a&gt; and the evolution of our approach.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; While I'll use Node.js for illustration, the concepts discussed are applicable to various development languages.&lt;/p&gt;




&lt;p&gt;Working on &lt;a href="https://spotsmap.com/map"&gt;spotsmap.com&lt;/a&gt;, we faced the challenge of creating a localized booking confirmation email. Packed with details such as sport types, dates, costs, and course information, the HTML template alone exceeded 500 lines of code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First Solution: Language-Specific Templates&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Initially, we opted for different templates for each supported language. &lt;/p&gt;

&lt;p&gt;Our source code included a configuration object allowing us to select a template based on the user's language. The template for English looked like this, and almost identical templates existed for every language:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;...
&lt;span class="nt"&gt;&amp;lt;h2&amp;gt;&lt;/span&gt;Arrival date:&lt;span class="nt"&gt;&amp;lt;h2&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;{{ArrivalDate}}&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And we used it like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// config.js&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;postmark&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;POSTMARK_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;templates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;bookingConfirmation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;en&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;123456&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;de&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;123457&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;es&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;123458&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// services/postmark.js&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sendMail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendEmailWithTemplate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;From&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;To&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;MessageStream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;outbound&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;TemplateId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;postmarkConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;templates&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;TemplateModel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nx"&gt;request&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Email sent to &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Email was not sent, with error code &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It looks easy; however, we had to change the approach.&lt;/p&gt;

&lt;p&gt;Why? We initially had three large and very similar templates. Adding a new language was relatively simple – just copy the template and adjust it for the new language. However, over time, we found the need to restructure the template. We introduced the capability to book multiple courses in a single order, and replicating this change across all templates became laborious, even with just three languages.&lt;/p&gt;

&lt;p&gt;So, we decided to move to another solution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second Solution: Conditional Templates&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Our second attempt involved consolidating templates with conditionals.&lt;/p&gt;

&lt;p&gt;So we rearranged the template and made only one template for all supported languages. It looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;...
&lt;span class="nt"&gt;&amp;lt;h2&amp;gt;&lt;/span&gt;
{{#en}}Arrival date:{{/en}}
{{#de}}Ankunftsdatum:{{/de}}
{{#es}}Fecha de llegada:{{/es}}
&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;{{ArrivalDate}}&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And we made a few changes in our code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// config.js&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;postmark&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;POSTMARK_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;templates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;bookingConfirmation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;123456&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// services/postmark.js&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sendMail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendEmailWithTemplate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;From&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;To&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;MessageStream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;outbound&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;TemplateId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;postmarkConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;templates&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;TemplateModel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nx"&gt;request&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Email sent to &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Email was not sent, with error code &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Okay, it looks even better.&lt;/p&gt;

&lt;p&gt;However, we had over 40 localized strings in the template. Even with just three supported languages, this translates to writing 120 lines of code solely for template localization. Now, consider the scenario with 10 or 40 languages.&lt;/p&gt;

&lt;p&gt;For 40 supported languages, it would entail a staggering 1600 lines of code in the email template. This is hardly maintainable. However, our objective is to support 40 or more languages, aiming to make &lt;a href="https://spotsmap.com/map"&gt;spotsmap.com&lt;/a&gt; accessible to people worldwide.&lt;/p&gt;

&lt;p&gt;As a result, we decided to explore another solution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Third (and Current) Solution: Code-Based Localization&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To address these challenges, we transitioned to a more efficient approach. We moved localization strings from the template to our codebase and adopted a standardized translation process used for website UI localization.&lt;/p&gt;

&lt;p&gt;The revised template structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;...
&lt;span class="nt"&gt;&amp;lt;h2&amp;gt;&lt;/span&gt;{{ArrivalDateTitle}}&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;{{ArrivalDate}}&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We created a JSON file, e.g., &lt;code&gt;templates/en/bookingConfirmation.json&lt;/code&gt;, housing localization strings for English:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ArrivalDateTitle"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Arrival date"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We modified the &lt;code&gt;sendMail&lt;/code&gt; function accordingly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// services/postmark.js&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sendMail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;strings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`../templates/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.json`&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendEmailWithTemplate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;From&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;To&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;MessageStream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;outbound&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;TemplateId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;postmarkConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;templates&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;TemplateModel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;strings&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nx"&gt;request&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Email sent to &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Email was not sent, with error code &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Additionally, we created a &lt;code&gt;gitloc.yaml&lt;/code&gt; file to automate the translation process:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;defaultLocale&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;en&lt;/span&gt;
  &lt;span class="na"&gt;locales&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
   &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;en&lt;/span&gt;
   &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;de&lt;/span&gt;
   &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;es&lt;/span&gt;
  &lt;span class="na"&gt;directories&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
   &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;templates&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach allowed us to easily generate translations for all supported languages by pushing changes to the remote repository and pulling translations locally. Notably, &lt;a href="https://gitloc.org/"&gt;Gitloc&lt;/a&gt; facilitated seamless support for new languages by updating the &lt;code&gt;gitloc.yaml&lt;/code&gt; file.&lt;/p&gt;




&lt;p&gt;Now, we can effortlessly support numerous languages while maintaining a consistent and manageable codebase.&lt;/p&gt;

&lt;p&gt;Explore further:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Postmark documentation: &lt;a href="https://postmarkapp.com/developer"&gt;https://postmarkapp.com/developer&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Gitloc documentation: &lt;a href="https://docs.gitloc.org/"&gt;https://docs.gitloc.org/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I hope you find these insights valuable!&lt;/p&gt;

</description>
      <category>mail</category>
      <category>postmark</category>
      <category>gitloc</category>
      <category>node</category>
    </item>
    <item>
      <title>How to choose an i18n library for a Next.js 14 application - Part 1</title>
      <dc:creator>Arsenii Kozlov</dc:creator>
      <pubDate>Wed, 29 Nov 2023 09:58:15 +0000</pubDate>
      <link>https://forem.com/arsenii/how-to-choose-an-i18n-library-for-a-nextjs-14-application-part-1-5hjp</link>
      <guid>https://forem.com/arsenii/how-to-choose-an-i18n-library-for-a-nextjs-14-application-part-1-5hjp</guid>
      <description>&lt;p&gt;What is the best choice, and how does one go about selecting an i18n library for production? Let's address this decision-making process.&lt;/p&gt;

&lt;p&gt;In my &lt;a href="https://dev.to/arsenii/forget-about-localization-for-your-web-applications-1993"&gt;previous post&lt;/a&gt;, I selected the &lt;a href="https://www.npmjs.com/package/rosetta"&gt;Rosetta&lt;/a&gt; internationalization library to demonstrate how a multilingual Next.js application can be easily implemented. However, as correctly pointed out in the comments, it's not the optimal choice. I concur; Rosetta was last updated three years ago, making it a red flag for me not to use it in a production environment.&lt;/p&gt;

&lt;p&gt;The article will consist of three parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In the first part, I will discuss the criteria for selecting a library, form a list of libraries, compare them based on the criteria, and choose the three most suitable libraries for further analysis.&lt;/li&gt;
&lt;li&gt;In the second part, I will compare the functional capabilities of the libraries, identify commonalities and differences, and select two of them for testing.&lt;/li&gt;
&lt;li&gt;In the third part, I will test the chosen libraries and draw a conclusion about which one I would use for a productive environment.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Firstly, it is necessary to determine the criteria for a library suitable for production. Based on my own experience, I will highlight the following criteria:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Popularity and Rating&lt;/strong&gt; - this is more about ensuring that many developers are familiar with the library, and there is a sufficient community to address emerging difficulties and problems.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Relevance&lt;/strong&gt; - it is a red flag for me if the library has not been updated for years. This usually means that the library is not evolving and may not support the latest changes in Next.js. Even if everything currently works correctly, there is no guarantee that it won't break with the next Next.js update.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Presence of Quality Documentation and Examples&lt;/strong&gt; - this is an important criterion as it allows for faster implementation of the library and significantly simplifies maintenance in the future.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security&lt;/strong&gt; - the current version of the library should not have known security issues.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Readable Source Code of the Library&lt;/strong&gt; - the assessment of code cleanliness and readability will always be subjective. In most cases, this is not required, but it can be useful, especially when choosing a less popular and unsupported library at the moment. In this article, we will not consider such libraries, so we will not assess them based on this criterion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance&lt;/strong&gt; - it consists of two parts: the library's size and loading time, and the actual code execution speed in the library. We will try to evaluate this during the testing phase.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Functional Capabilities&lt;/strong&gt; - the library should meet the functional requirements of the system to avoid searching for workarounds or having to change the library in the middle of development.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adherence to Standards&lt;/strong&gt; - I will strive to choose libraries for a productive environment that adhere to accepted standards. If necessary, I want to be able to replace one library with another with minimal effort.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In the first part of the article, we will select libraries and evaluate them based on the first four criteria. Using these criteria, we will choose three libraries for further analysis. In the second part, we will assess the chosen libraries based on criteria 7 and 8.&lt;/p&gt;

&lt;p&gt;To begin, let's compile a list of available libraries and look on their popularity:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Library&lt;/th&gt;
&lt;th&gt;Weekly downloads&lt;/th&gt;
&lt;th&gt;Last update&lt;/th&gt;
&lt;th&gt;Rating&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.npmjs.com/package/react-i18next"&gt;react-i18next&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;2.677.938&lt;/td&gt;
&lt;td&gt;11 days ago&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.npmjs.com/package/react-intl"&gt;react-intl&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;1.311.586&lt;/td&gt;
&lt;td&gt;13 days ago&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.npmjs.com/package/next-i18next"&gt;next-i18next&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;350.234&lt;/td&gt;
&lt;td&gt;a month ago&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.npmjs.com/package/next-intl"&gt;next-intl&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;118.061&lt;/td&gt;
&lt;td&gt;3 days ago&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.npmjs.com/package/@lingui/react"&gt;@lingui/react&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;97.660&lt;/td&gt;
&lt;td&gt;2 months ago&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.npmjs.com/package/next-translate"&gt;next-translate&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;73.427&lt;/td&gt;
&lt;td&gt;23 days ago&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.npmjs.com/package/rosetta"&gt;rosetta&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;21.258&lt;/td&gt;
&lt;td&gt;3 years ago&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.npmjs.com/package/next-localization"&gt;next-localization&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;14.404&lt;/td&gt;
&lt;td&gt;2 years ago&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This numbers are current as of November 27, 2023. Web development evolves rapidly, and within a few months, the landscape may have changed.&lt;/p&gt;

&lt;p&gt;Next, we will not consider &lt;code&gt;rosetta&lt;/code&gt; and &lt;code&gt;next-localization&lt;/code&gt; as they have not been updated for more than two years, and as a result, they have the lowest number of downloads, which is constantly decreasing.&lt;/p&gt;

&lt;p&gt;We will check the packages for known vulnerabilities using the Snyk CLI.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~&lt;span class="nv"&gt;$ &lt;/span&gt;snyk &lt;span class="nb"&gt;test &lt;/span&gt;next-intl@latest

Testing next-intl@latest...

Organization:      &lt;span class="k"&gt;***&lt;/span&gt;
Package manager:   npm
Open &lt;span class="nb"&gt;source&lt;/span&gt;:       &lt;span class="nb"&gt;yes
&lt;/span&gt;Project path:      next-intl@latest

✔ Tested next-intl@latest &lt;span class="k"&gt;for &lt;/span&gt;known vulnerabilities, no vulnerable paths found.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After checking all packages, we find that all specified packages do not have any known vulnerabilities - excellent!&lt;/p&gt;

&lt;p&gt;Now, let's take a look at the documentation.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Library&lt;/th&gt;
&lt;th&gt;Comment&lt;/th&gt;
&lt;th&gt;Rating&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.npmjs.com/package/next-intl"&gt;next-intl&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Well-structured and detailed documentation with examples using App Router and Pages Router&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.npmjs.com/package/@lingui/react"&gt;@lingui/react&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Well-structured and detailed documentation, but examples are only described for React applications, requiring adaptation for Next.js.&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.npmjs.com/package/react-i18next"&gt;react-i18next&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Detailed documentation, but in my opinion, poorly structured and overloaded with irrelevant information. Integration example for Next.js leads to a page outside the library documentation.&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.npmjs.com/package/react-intl"&gt;react-intl&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Detailed documentation on the library and its API, lacking examples of using the library in an application. The documentation is oriented towards React, and adapting the library for use in a Next.js application is required.&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.npmjs.com/package/next-translate"&gt;next-translate&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Detailed single-page documentation with a table of contents, improving navigation. Documentation refers to Next.js version 10, which seems outdated. All examples use Pages Router.&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.npmjs.com/package/next-i18next"&gt;next-i18next&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Single-page documentation, usage described only with Pages Router, recommends using react-i18next directly for Next.js 13/14&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Since we will be focusing on developing an application with Next.js 14, I did not evaluate &lt;code&gt;next-i18next&lt;/code&gt;, and we will not consider this library further, as they recommend using &lt;code&gt;react-i18next&lt;/code&gt; instead. We will also not consider the library &lt;code&gt;next-translate&lt;/code&gt; as, in my opinion, its documentation does not appear to be up-to-date and convenient enough for easy and quick integration into our future system.&lt;/p&gt;

&lt;p&gt;My personal choice unequivocally falls on &lt;code&gt;next-intl&lt;/code&gt; as the most qualitatively documented and &lt;code&gt;react-i18next&lt;/code&gt; as the most popular library.&lt;/p&gt;

&lt;p&gt;Choosing a third option for further analysis seems more challenging, with two libraries competing: &lt;code&gt;@lingui/react&lt;/code&gt; and &lt;code&gt;react-intl&lt;/code&gt;. Both will require additional exploration to integrate into a Next.js 14 application. One is very popular, with over a million downloads per week, while the other has a more structured documentation.&lt;/p&gt;

&lt;p&gt;Both of these libraries use approaches that are different from &lt;code&gt;next-intl&lt;/code&gt; and &lt;code&gt;react-i18next&lt;/code&gt;. &lt;code&gt;@lingui/react&lt;/code&gt; uses an additional CLI for extracting translatable strings from page code, which, in my opinion, complicates the development process and makes it harder to abstract translations from the code. &lt;code&gt;react-intl&lt;/code&gt; uses special javascript objects describing translatable messages.&lt;/p&gt;

&lt;p&gt;For further exploration, let's consider all four libraries: &lt;code&gt;next-intl&lt;/code&gt;, &lt;code&gt;react-i18next&lt;/code&gt;, &lt;code&gt;@lingui/react&lt;/code&gt;, and &lt;code&gt;react-intl&lt;/code&gt;. We will try to compare their different approaches and the functional capabilities they provide.&lt;/p&gt;

&lt;p&gt;If you use other libraries and are confident that they can compete with the ones selected, please let me know in the comments, and I will be sure to investigate them.&lt;/p&gt;

&lt;p&gt;Subscribe for notifications not to miss the next parts.&lt;/p&gt;




&lt;p&gt;While you're waiting for the next round.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;We need your feedback&lt;/strong&gt; about our recently launched &lt;a href="https://gitloc.org/"&gt;Gitloc&lt;/a&gt;, a localization platform for modern web applications — an ideal choice for indie developers and small teams.&lt;/p&gt;

&lt;p&gt;We invite you to test our system, which will only take 10-15 minutes. Follow the instructions &lt;a href="https://dev.to/arsenii/forget-about-localization-for-your-web-applications-1993"&gt;in the previous article&lt;/a&gt; or check out &lt;a href="https://docs.gitloc.org/gitloc-org-reference/"&gt;documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It is crucial for us to receive feedback from developers at this stage. You can leave your feedback in the comments to the latest release in our &lt;a href="https://t.me/gitloc_org"&gt;Telegram channel&lt;/a&gt; or write to &lt;a href="//mailto:feedback@gitloc.org"&gt;feedback@gitloc.org&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We will definitely support all our early users. &lt;a href="https://app.gitloc.org/sign-up"&gt;Register through the link for free&lt;/a&gt;, and let's make localization no longer a headache for developers.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>i18n</category>
      <category>webdev</category>
      <category>localization</category>
    </item>
    <item>
      <title>Forget about localization for your web applications</title>
      <dc:creator>Arsenii Kozlov</dc:creator>
      <pubDate>Mon, 06 Nov 2023 08:49:31 +0000</pubDate>
      <link>https://forem.com/arsenii/forget-about-localization-for-your-web-applications-1993</link>
      <guid>https://forem.com/arsenii/forget-about-localization-for-your-web-applications-1993</guid>
      <description>&lt;p&gt;No, no, doing localized web applications that are accessible for people around the world is still relevant. But you should forget about doing localization if you are a developer.&lt;/p&gt;

&lt;p&gt;As a developer, you know that integrating localization into your application is effortful, no matter whether you use CLI-based localization platforms or just store localization in your repository. You should always remember them, if they are current, if their version is suitable for the application version, if they are processed, and so on.&lt;/p&gt;

&lt;p&gt;Let me show you how you can forget about all these issues and focus on development. Here, I will describe a solution based on Next.js. However, it can be easily applied to many other modern frameworks and approaches like React, Vue, Flask, and even backends such as Node.js, Python, and others, and also for using any internationalization libraries.&lt;/p&gt;




&lt;p&gt;We won't reinvent the wheel, so let’s take a localized web application sample prepared by the Next.js team: &lt;a href="https://github.com/vercel/next.js/tree/canary/examples/with-i18n-rosetta"&gt;https://github.com/vercel/next.js/tree/canary/examples/with-i18n-rosetta&lt;/a&gt;. Just run the following command.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npx create-next-app --example with-i18n-rosetta with-i18n-rosetta-app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let’s see what we have as a result.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--jwkjFH8N--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/46mk0974ldszzjocp546.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--jwkjFH8N--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/46mk0974ldszzjocp546.png" alt="Repository structure" width="800" height="390"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It’s a simple Next.js application configured with the &lt;a href="https://github.com/lukeed/rosetta"&gt;https://github.com/lukeed/rosetta&lt;/a&gt; internationalization library. All localization values are stored in JSON files under the &lt;code&gt;/locales&lt;/code&gt; folder, with each language in a separate file. Localization is configured in the &lt;code&gt;lib/i18n.js&lt;/code&gt; file, where you can find English (en) as the default language and German (de) as an additional language. That’s great!&lt;/p&gt;

&lt;p&gt;When we start the application (with the command &lt;code&gt;npm run dev&lt;/code&gt;), we can see the next page.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--rqKUGOIy--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o3ew6u1cn59822nlh5n1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--rqKUGOIy--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o3ew6u1cn59822nlh5n1.png" alt="English index page" width="800" height="242"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Messages for the English version can be found in &lt;code&gt;locales/en.json&lt;/code&gt; file, and for the German version in &lt;code&gt;locales/de.json&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--e_IQtX68--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g70nisdr5dpn6e64oy2z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--e_IQtX68--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g70nisdr5dpn6e64oy2z.png" alt="Localization files" width="800" height="200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That works! Great.&lt;/p&gt;

&lt;p&gt;Let’s add this repository to GitHub; it is necessary for a few next steps. I have created a new public repository on my GitHub: &lt;a href="https://github.com/arseniy-kozlov/with-i18n-rosetta.git"&gt;https://github.com/arseniy-kozlov/with-i18n-rosetta.git&lt;/a&gt;, and ran the following commands.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git init
git remote add origin https://github.com/arseniy-kozlov/with-i18n-rosetta.git
git add .
git commit -m “Initial commit”
git push origin master
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We are close to our goal – to forget about localization – only a few simple steps left. Let’s create an account on &lt;a href="https://app.gitloc.org/sign-up"&gt;Gitloc&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--mVHjLtM0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ybwcfo2fcf09dhlpwuyk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--mVHjLtM0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ybwcfo2fcf09dhlpwuyk.png" alt="Gitloc Sign Up" width="331" height="485"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Create a new team.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--1EVtjj3---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2ohcq5ljm7czhfsu1xkj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--1EVtjj3---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2ohcq5ljm7czhfsu1xkj.png" alt="Create a new team" width="751" height="280"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And connect our repository. Click &lt;strong&gt;Add repository&lt;/strong&gt; and choose GitHub there.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--m-p9YXN3--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8llg6dblef2e1sphvxdu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--m-p9YXN3--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8llg6dblef2e1sphvxdu.png" alt="Add repository" width="800" height="309"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then authorize the gitloc-org application on the GitHub consent page.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--0QwBxQ5I--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/011g3x1wn7ph5lspdf73.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--0QwBxQ5I--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/011g3x1wn7ph5lspdf73.png" alt="Authorize gitloc-org application" width="526" height="841"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And confirm on the next step.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ffg30U7C--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6ebbwq7umqir9e4emy1q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ffg30U7C--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6ebbwq7umqir9e4emy1q.png" alt="Confirm authorization" width="332" height="582"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Choose the &lt;code&gt;with-i18n-rosetta&lt;/code&gt; repository.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--7S9qvtzL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xmz9f0a6ehvwbmrjy22h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--7S9qvtzL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xmz9f0a6ehvwbmrjy22h.png" alt="Choose the repository" width="800" height="399"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And confirm on the next page.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--1hbQKkmi--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/63dajrdul7v85my64wxi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--1hbQKkmi--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/63dajrdul7v85my64wxi.png" alt="Repository confirmation" width="800" height="339"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let’s return to our repository and add a simple &lt;code&gt;gitloc.yaml&lt;/code&gt; file to the root of the repository.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--eVbJa3X3--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/efxzw2wpcfdsaenxem35.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--eVbJa3X3--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/efxzw2wpcfdsaenxem35.png" alt="gitloc.yaml" width="754" height="346"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;defaultLocale&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;en&lt;/span&gt;
  &lt;span class="na"&gt;locales&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;en&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;de&lt;/span&gt;
  &lt;span class="na"&gt;directories&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;locales&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;That’s it!&lt;/strong&gt; Right now, forget about doing localizations and focus on development. Let’s do it together.&lt;/p&gt;

&lt;p&gt;Let’s update our messages in &lt;code&gt;locales/en.json&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--iOwVoeDf--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/63cjjqgmwmg47mi0m7z5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--iOwVoeDf--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/63cjjqgmwmg47mi0m7z5.png" alt="English changes" width="800" height="188"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I have removed the &lt;code&gt;text&lt;/code&gt; key, modified the &lt;code&gt;description&lt;/code&gt; value, and added a new &lt;code&gt;title&lt;/code&gt; key. Then let’s make corresponding changes in the &lt;code&gt;pages/[lng]/index.js&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--AOW99Aj6--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gffvxtv0wen7gzc3ca2e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--AOW99Aj6--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gffvxtv0wen7gzc3ca2e.png" alt="Index page changes" width="800" height="294"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then let’s commit changes and push them to the remote repository.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git add .
git commit -m ‘gitloc.org test’
git push origin master
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that, Gitloc detects our changes, automatically translates them, and commits them to the repository. It takes only a few seconds, and you can pull translations to your local repository.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ailEHFPG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f6uhmdviwr3phlfimtqd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ailEHFPG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f6uhmdviwr3phlfimtqd.png" alt="Pulling changes" width="629" height="252"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let’s run our application and check the changes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--SOtzjj4N--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vy0ebhhvbc24cdwb6atr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--SOtzjj4N--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vy0ebhhvbc24cdwb6atr.png" alt="English version page" width="800" height="333"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--CoHSOpo---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fuoqb79ibh6yqvisukk3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--CoHSOpo---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fuoqb79ibh6yqvisukk3.png" alt="German version page" width="800" height="333"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Good job!&lt;/p&gt;




&lt;p&gt;Learn more about Gitloc - &lt;a href="https://gitloc.org/"&gt;https://gitloc.org/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We will be very grateful for all your feedback. I hope you enjoy!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>architecture</category>
      <category>localization</category>
      <category>nextjs</category>
    </item>
  </channel>
</rss>
