What I Learned From Localizing a Popular Web App

Things I picked up to make a person feel that the app was made just for them.

At the end of 2019 I joined the YouVersion software engineering team. At that time the YouVersion mobile app had close to 500 million installations, and bible.com was one of the leading websites for Bible search results with millions of hits per month globally. The biggest site I worked on prior to that had a thousand visits on a good month. I stepped into a world of scale beyond my comprehension.

Fast forward to 2025, the YouVersion ecosystem delivers over 3,400 Bible translations in over 2,200 languages to every country on the planet. During my first five years at YouVersion I learned a lot about delivering a customized experience for people around the world. In this article I’ll share what I learned to hopefully help you build for people outside of your local cultural context.

Localization Is Not Just Translation

This was the first big shift in my understanding. Localization is so much more than just translating words. It’s about making a person feel that the app was made just for them. Translation is certainly a big part of localization, but images, interface layouts, color choices, app icons, are all part of creating a truly localized experience for someone in another culture.

Localized Interfaces

Interface layout is an important aspect of localization. English is written from the left to the right. But Arabic is written from the right to the left. Translation will take care of this writing direction, but what about the menu on a page or an arrow icon for “next” and “previous”? If those are not also changed the interface would looks weird or confusing to someone in Egypt trying to use the app.

Relevant Images

Think about images for example. Imagine a picture of light-skinned cowboys in rural western Oklahoma alongside the text “The Bible Is For You”. Next imagine a Japanese person in downtown Tokyo seeing that the first time they opened up YouVersion. The two cultures are completely different. The cowboys do not represent Japanese culture, therefore the image does not give the impression that the bible is for them. By changing the image to one of people in urban Japan, that image and text become fully localized. The text and image communicate that the Bible is personal and written for the person who opened the app.

Accessibility Is Part Of Localization

Accessibility is not always connected to localization, but I would argue that it is absolutely part of localization. It is building the app for the local context of someone who has a disadvantage of some kind. I always put localization and accessibility in project requirements, as they are inseparable in my mind.

Technical Things

Language Tags

There are several standards for language tags, but the de-facto standard for web is IETF BCP-47 (https://en.wikipedia.org/wiki/IETFlanguagetag). BCP-47 allows for top-level language tags, like en , regional variants such as en-GB , and custom codes for languages that are not yet categorized like un-contacted tribal languages. The choice of depth for language tags will be determined by your use case, but starting right away with BCP-47 will set you up for success in the long term.

Deliver best-guess localized experience

In order to quickly deliver a great localized experience, the app should quickly make a best guess as to which locale it should deliver on first paint. There are several ways to do this, but here’s my recommendation:

  1. User choice - set in a settings database somewhere, unified across your app(s).
  2. User choice from a previous visit, stored in a cookie. This can be loaded by the server before any code is sent to the browser to immediately ship the right locale.
  3. accept-language header, sent by the browser to the server and usually based on browser settings.
  4. IP address - this might involve extra work to lookup the locale for an IP, and also can be incorrect on VPNs.
  5. navigator.language, only available in the browser.

CSS Logical Properties

Back in the day we used to do margin-left and margin-right, pushing the RTL logic to the developer. Nowadays CSS Logical Properties are a thing, so you can set it and forget it. For example, margin-left becomes margin-inline-start. These values will properly snap to the correct side based on the axis origin of the language’s layout.

Tailwind supports some of these natively, and most of them through a plugin tailwind-css-logical. Tailwind also has rtl: and ltr: utilities in cases where logical properties are insufficient.

String Formation

Strings should be formed in translation source files, rather than in the JavaScript code. If sentences or phrases are broken up, they become sentence fragments and are not able to be translated. All variable or string formation should be done in the string JSON file. Forming strings in the JavaScript code does not allow for proper translations. Variable placeholders should be used in the JSON code and replaced at runtime.

Example
In English 5 years ago is a phrase where the number is first, and the words are second. In German, it is translated Vor 5 Jahre where the number is in the middle. It can only be translated correctly if the count is supplied as a variable in the json string which the translators can move to the correct position. Building the string later in javascript would break the translation into German.

Do:

// locale-strings/en.json
"years_ago": "{{count}} years ago.",

// component.js
t('years_ago', { count: 5 })}

Do Not:

// locale-strings/en.json
"years_ago": "years ago"

// component.js
`${count} ${t('years_ago')}.`

Plural Forms

In English we have two plural forms, one and other for zero and more than one. Other languages have several plural forms. Arabic, for example has zero, one, two, few, many, other.

There are two count-based string types that need plural forms, one with a number in the string, and the other without a number in the string.

Do:

"friends": {
  "have_zero": "I have {{count}} friends.",
  "have_one": "I have {{count}} friend.",
  "have_two": "I have {{count}} friends.",
  "have_few": "I have {{count}} friends.",
  "have_many": "I have {{count}} friends.",
  "have_other": "I have {{count}} friends.",
}
t('friends.have', {count: 0}) // I have 0 friends.
t('friends.have', {count: 1}) // I have 1 friend.
t('friends.have', {count: 4}) // I have 4 friends.

Do:

"friends": {
  "here_zero": "None of my friends are here.",
  "here_one": "My friend is here.",
  "here_two": "My friends are here.",
  "here_few": "My friends are here.",
  "here_many": "My friends are here.",
  "here_other": "My friends are here.",
}
t('friends.here', {count: 0}) // None of my friends are here.
t('friends.here', {count: 1}) // My friend is here.
t('friends.here', {count: 4}) // My friends are here.

Do Not:

"have": {
  "singular": "I have {{friend_count}} friend.",
  "plural": "I have {{friend_count}} friends."
}
const friends = 4
t(friends === 1 ? 'have.singular' : 'have.plural',
  {friend_count: friends})

Line Breaks and Formatting

For the same reasons as String Formation, when we line break or otherwise break up a sentence it's impossible for those sentence fragments to be correctly translated while preserving the line breaks. It's better to use CSS or work with the designers to change the strings to not use sentence fragments. Line breaks, other html tags, etc. are ok between sentences. Formatting, like bold or italics, is ok within a sentence.

Do:

// locale-strings/common.json
"a": "This is a sentence."
"b": "This is <strong>a sentence</strong>."
"c": "This is a sentence.\n This is <strong>another</strong> sentence."
"d": "This is a sentence.<br /> This is another sentence."

Do Not:

// locale-strings/common.json
"a": "This is a\n sentence."
"b": "This is a <br /> sentence."

Data Formats

Date, time, currency, number, and unit formats are all localizable. JavaScript has built in support for a lot of these, but there are some limitations. dayjs has better localization options, but some locales are not widely supported and will need custom patching or other solutions to overcome. Some format values are only available in the raw cldr data sets and need to be imported for usage.

Routes & Page URLs

Some search engines support localized characters in URLs, like Cyrillic and Sanskrit. For SEO and a good user experience, urls with things like blog titles can be localized. A few notes:

Icons

If your app has icons that can be localized, for example text on a color background, it is best to deliver those icons. If an icon doesn’t make sense for a specific locale, then making a more relevant choice is a great way to localize the experience.

react-i18next: component substitution

Components and html tags should be written in JSX and passed in using the Trans component from i18next. This simplifies the string composition for the translator, and gives developers greater control over component properties without needing to re-submit a new string for translation.

Tag names in the JSON strings can be anything, but generally they should match the HTML tag to be replaced. If there are more than one of the same tag type, use numeric identifiers.

Do:

// locales/en.json
"support_text": "Go to <a>this link</a> and <a2>this other link</a2>."

// component.js
<Trans
  i18nKey="support_text"
  components={{ 
    a: <Link to="/internal-route" />,
    a2: <a href="https://example.com/2" target="_blank" />
  }}
/>

Do Not:

// locales/en.json
"support_text": "Go to {{link1}} and {{link2}}."

// component.js
t('support_text', {
  a: <Link to="/internal-route">this link</Link>,
  a2: <a href="https://example.com/2" target="_blank">this other link</a>
})

Do you have more knowledge or tips for localization? Let me know, I'd love to update this resource!