Stop Chaos! React & React Native Translations Done Right Once

Have you ever had problem with translating an React or React Native application, and always end up with problems with keys, adding extra pipes, hardcoded translations? Get in this tutorial with me and we will solve this problem for once.

Ta treść nie jest dostępna w języku polskim.

Critical problems with usual react translation libraries, i18n and others

The significance of translations appears to be overlooked, with the second language often treated as an afterthought in my projects; some even begin with non-English strings hardcoded in templates, only to encounter challenges later when attempting to modify them, as the lack of type safety prevents immediate detection of all occurrences, resulting in an inconsistent user experience where English words may unexpectedly appear in a Spanish interface, negatively impacting the brand's reputation.

The i18n module, while initially a convenient tool for handling translations in React and React Native apps, often falls short in several key areas. One prevalent issue is the prevalence of hardcoded strings within templates, making it challenging to maintain and update translations efficiently. Additionally, the lack of IntelliSense support hampers developers' productivity, leading to potential errors and inconsistencies in translations. Moreover, the module's limited capabilities for managing different languages result in cumbersome workflows and difficulties in maintaining language-specific content.

Another significant drawback is the cumbersome process of storing translations in separate JSON files, which can lead to cluttered project structures and version control issues. These challenges collectively contribute to a suboptimal translation experience, urging developers to seek alternative solutions for their localization needs.

Solution is simple; And no library is needed

In fact, doing a bullet proof translation mechanism for React and React Native projects does not need another library at all. In this tutorial we are going to solve this with a single hook:

  • Defining useS hook to use a string object.
  • Defining useLocale hook to keep locale settings, and also detect locale based on the user OS settings or browser settings.

What you need before

You can apply what you learn here to new projects, as well as your existing project

  • Install fireback v1.1.9 or later. You can find the binaries for different systems here: https://github.com/torabian/fireback/releases for installers binary, and you can place it inside your project folder and ignore it.

  • I use the VSCode, and "Run On Save extension" is enabled. You can run the translation from CLI as well, but having this extension makes life easier by far.

Step 1: Verify you have access to fireback

You need to make sure you have access to fireback binary, either installed globally, or you put it in the project directory, and ignored the file.

Fireback also could be installed on npm using `npm i -D fireback-tools'. Make sure you install it as dev dependency, so it's binaries do not bundle with react native apps.

Step 2: Create useLocale hook

export function useLocale() {
  const [locale, setLocale] = useState({
    lang: 'en',
    region: 'us',
  })

  const dir = locale.lang === 'fa' || locale.lang === 'ar' ? 'rtl' : 'ltr'

  // Here you can implement the rest of the necessary functionality
  // for detecting the locale, or for example, extra function to set
  // only the language or so.

  return {lang, region, dir}
}

Step 3: Create the useS hook.

import { useLocale } from "./useLocale";

// This typescript generic is important, to use strongly typed translation
// keys
export function useS<T>(v: T): T {
  /*
   * This could be your own definition of the locale language
   * This variable usually should include the current language in the app
   * such as 'en', or 'fa', etc.
   * you might also extend it to separate the language-locale such as en-us
   * i haven't done yet due to no need
   */
  const { lang } = useLocale();

  /*
   * Here we check if there lang is anything other than en, and in translation
   * files we have such thing with a $ prefix - which auto generated by fireback
   * we will access them instead. useS function will return always T,
   * so all the translations will be typesafe anyway from typescript compiler
   * perspective
   */
  if (!lang || lang === "en") {
    return v;
  } else if (!v["$" + lang]) {
    return v;
  } else {
    return v["$" + lang];
  }
}

Step 4: Create 'strings' folder

How you organize the translations is up to you, in fact. I prefer to create a strings folder per module and place all of their translations into that directory. This way, when I move a module folder to another project or if I want to make it a library, I won't have to worry about the translations; they're already solved and attached to the component or module.

On the other hand, if you want to create a strings folder for the entire app, nothing would stop you.

Step 5: Create strings/strings-en.yml

It's critical to keep in mind that the Fireback Language Editor module assumes English as the primary language of the app. Simply put, English must be present, and other languages will be synced with keys from that one.

Now, let's put the content inside the YAML. All translation keys must go under the content key, otherwise they will not be considered translations.

If you are not familiar with YAML, think of it as JSON without quotes, and remember:

content:
  loading: Loading...
  done: Done :)

Is different from the code below. (Basically intention is super important)

content:
  loading: Loading...
  done: Done :)

So far, this was all the necessary steps we needed to take for translating our app.

Step 6: Use and config fireback language generator

Now we need to use the Fireback language generator to simplify key generation for us.

fireback gen strings --path ./src/strings/strings-en.yml --langs pl,fa

This command will generate two additional files in the same directory: strings-pl.yml and strings-fa.yml. If you open them, you will see identical content to your strings-en.yml.

Also in the same directory, you will find a translations.ts file. It contains content similar to the snippet below, which has generated TypeScript constants for all three languages and exported them as strings.

Step 7: Make this automated with VSCode run-on-save

Using the CLI each time for building the translations is cumbersome. Therefore, we utilize the "Run on save" extension in VSCode, which you can install.

Then, in our settings.json in VSCode, we need to add these rules. (If you've never modified this file, create a folder named .vscode at the root of your project and add settings.json inside it.)

{
  // other configuration before

  "emeraldwalk.runonsave": {
    "commands": [
      {
        "match": "strings-([a-z][a-z]).yml$",
        "cmd": "fireback gen strings --path ${file} --langs en,fa"
      }
    ]
  }

  // other configuration after
}

It would basically detect any strings-xx.yml change in the project, and will run strings compiler for you.

Step 8: Use translations

At this stage, the tarnslation dictionary has been created fully, and we can use them by useS and access the keys on the s object. It's good to keep the names consistent across the entire project to make it clean.

React.js:

import { strings } from "./strings/translations";
export function MyComponent() {
  const s = useS(strings)

  return <p>{s.loading}</p>
}

React Native:

import { Text } from 'react-native';
import { strings } from "./strings/translations";
export function MyComponent() {
  const s = useS(strings)

  return <Text>{s.loading}</Text>
}

Conclusion

To summarize, we have just created a robust system for adding translations to our react or react native app. This method will help prevent many errors, such as missing keys in certain languages and hard-coded strings for translation.

The Fireback Strings CLI offers additional options that you may want to explore on your own. This feature is available starting from Fireback v1.1.9