Guidelines for Translating Your Project using i18next
Tags: javascript, react, tutorial, nextjs
Published: 2022-03-17 04:03:41

The following guide is a document I authored to provide my organization with guidelines for implementing localization in our application.  These guidelines assume you've followed implementation instructions for i18next and are ready to begin defining your strings in a separate file.  

These guidelines also reference a third-party translation service called [Lokalise](https://lokalise.com/).  We chose Lokalise because it was competitive in terms of pricing, and offered a CI/CD integration so we could automate our translation process.  

I've redacted a few parts that were specific to our project, but you're welcome to use the remainder of these guidelines with your own projects.  

## Table of Contents

1. [Purpose](#purpose)
1. [Guidelines](#guidelines)
   1. [Organization](#organization)
   1. [Key Names](#key-names)
   1. [String Values](#string-values)
   1. [Front-end Localisation](#frontend-localisation)
   1. [Backend Localisation](#backend-localisation)
   1. [What NOT to Localise](#what-not-to-localise)
   1. [Testing with Locale Strings](#testing-with-locale-strings)
1. [Translation Process](#translation-process)
1. [References](#references)

## Purpose

This is a living document intended to assist engineers with localisation & translations for `your-project-here`. Its intent is to communicate processes and best practices in order for the team to implement a consistent & efficient translation strategy. Contributors to this project are encouraged to update this document as needed to keep its content aligned up-to-date with the current direction of `your-project-here`.

## Guidelines

### Organization

#### Namespaces

The root-level of the locales dictionary is reserved for namespaces. Avoid arbitrarily adding strings to the root-level of the dictionary.

```
// Good
t("content:activityFeed.filter")

// Avoid
t("activityFeedFilter")
```

#### Deep-Nesting

When adding strings to the locales dictionary, avoid deep-nesting of keys within the JSON object. `namespace:groupName.keyName` is sufficient for organizational needs & describing context.

> Why? Nesting reduces the available-width on-screen for reviewing the locale source, and translation services like [Lokalise](https://lokalise.com) flatten dictionaries when uploaded.

```
// Good
t("content:goalsWidget.company")

// Avoid
t("content:goalsWidget.tabs.company")
```

### Key Names

#### KISS

_Keep it simple, stupid._ Key names should be short when possible, but descriptive enough to understand the intended context.

```
// Good
t("content:branding.backgroundContrast") => "Site header text and icon color"

// Avoid
t("content:branding.siteHeaderTextAndIconColor")
```

#### Readability

Since keys replace the content in the source, key names should be _human-readable_. Avoid using arbritary acronyms or cutting off words with key names.

> Why? Key names are referenced by both engineers and translators. Key names which are not human-readable will come off as cryptic to those who are not familiar with the code and increases the cognitive load to perform tasks.

```
// Good
t("content:adminNavigation.performanceManagement")

// Avoid: arbitrary acronyms
t("content:adminNavigation.pm")

// Avoid: Cutting off words
t("content:adminNavigation.perfMan")
```

**_Exception_**: Acronyms which are industry-standards (example: `SAML`), or are public-facing (example: `EPS`) are allowed in key names.

```
// Allowed Exception: industry-standard acronyms
t("content:login.onErrorSAMLAssertion")

// Allowed Exception: public-facing acronyms
t("content:actions.newEPSSurvey")
```

#### Consistency with Context

When strings share a similar context (for example, errors), use a similar convention for the key names.

> Why? Using consistent conventions for the key names reduces the cognitive load for both engineers and translators, and makes maintenance easier if translated strings need to be revised to fit within designs.

```
// Good
t("content:branding.errorOnSave") => "There was a problem saving your branding settings. Check your input and try again."

// Avoid
t("content:branding.problemSavingBrandingSettings")
```

#### Context over Implementation

Key names should describe the context of the string and not the implementation of the string.

> Why? Excluding the implementation in the key name provides _some flexibility_ with re-using the string in the context of the parent.

```
// Good
t("content:branding.uploadBackgroundImage")

// Avoid
t("content:branding.buttonUpload")
```

**_Exception_**: When the context of the string is already clearly-described by the parent key. In this case, the key name can describe the implementation (example: `title`)

```
// Allowed Exception: Parent describes context
t("content:actions.title")
```

**_Exception_**: When the string exists in a supportive context to another string. In this case, the key name should be prefixed with the name of the string it supports, followed by the type of implementation (example: `uploadBackgroundImageTooltip`)

```
// Allowed Exception: Supportive context
t("content:branding.uploadBackgroundImageTooltip") // supports `content.branding.uploadBackgroundImage`
```

#### Casing

Key names should be in `camelCasing` format. Avoid `PascalCase` completely. `Snake_Case` is a reserved by i18n for [Context](https://www.i18next.com/translation-function/context) and [Plurals](https://www.i18next.com/translation-function/plurals)

```
// Good
t("content:actions.newEPSSurvey")

// Avoid: PascalCase
t("content:Actions.NewEPSSurvey")

// Avoid: Snake_Case
t("content:actions.new_EPS_survey")
```

### String Values

#### Passing values

Locale strings aren't always static; sometimes we need to pass _data-driven_ values to the string. i18next provides [interpolation](https://www.i18next.com/translation-function/interpolation), which allows us to do this without having to break apart the string into fragments.

```
// Locale String
{
    "content": {
        "recognitionCard": {
            "recognized": "{{sender}} recognized"
        }
    }
}

// Translation Instruction
t("content:recognitionCard.recognized", { sender: 'Noah' }) // renders as "Noah recognized"
```

#### Plurals

Many words which describe a quantity are modified based on the count of the item (example: `Day` describes a single day, while `Days` describes more than one). i18next provides support for [plurals](https://www.i18next.com/translation-function/plurals) by appending `_plural` to the original key name.

```
// Locale Strings
{
    "content": {
        "notificationsPage": {
            "newNotification": "You have a new notification",
            "newNotification_plural": "You have {{count}} new notifications"
        }
    }
}

// Translation Instruction
t("content:notificationsPage.newNotification", { count: 1 }) => "You have a new notification"
t("content:notificationsPage.newNotification", { count: 7 }) => "You have 7 new notifications"
```

#### Context

In addition to plurals, words can be modified through other contexts (example: gender). i18next also provides support for [Context](https://www.i18next.com/translation-function/context) by appending an `_enum` context value to the original key name, where `enum` is the context being passed.

```
// Locale Strings
{
    "content": {
        "celebrationCard": {
            "anniversary": "{{recipient}} is celebrating a work anniversary",
            "anniversary_male": "{{recipient}} is celebrating his work anniversary",
            "anniversary_female": "{{recipient}} is celebrating her work anniversary"
        }
    }
}

// Translation Instruction
t("content:celebrationCard.anniversary", { recipient: "Brian" }) => "Brian is celebrating a work anniversary"
t("content:celebrationCard.anniversary", { recipient: "Brian", context: "male" }) => "Brian is celebrating his work anniversary"
t("content:celebrationCard.anniversary", { recipient: "Katrina", context: "female" }) => "Katrina is celebrating her work anniversary"
```

#### Casing

Generally, string values added to a locales dictionary should be in `Sentence case` format. See [Grammarly's Capitalization Rules](https://www.grammarly.com/blog/capitalization-rules/) for a detailed explanation for when to use capitalization.

> Why? Uppercase letters/words can imply different meanings in different languages, and we can leverage cascading style sheets (CSS) to change the format of a string value (example: UPPERCASE)

```
// Good
{
    "content": {
        "goalsWidget": {
            "company": "Company"
        }
    }
}

// Avoid (use CSS instead)
{
    "content": {
        "goalsWidget": {
            "company": "COMPANY"
        }
    }
}
```

#### Markup

See React-i18next's documentation on the [Trans Component](https://react.i18next.com/latest/trans-component)

#### String Fragments

Avoid fragmenting strings as it reduces the quality of translations. Use Interpolation to pass values into the strings so translators understand the full context.

> Why? The organization of a string can differ between languages. For example, "Send points by Jan 1" has the date at the end of the sentence, but the same string translated into Japanese, "1 月 1 日より前にポイントを送信" shifts the date (1 月 1 日) to the front of the sentence.

```
// This would be a date method in real life
const deadline = "Jan 1";

// Good
t("content:pointBalance.sendBefore", { expirationDate: deadline})

// Avoid:  This will result in a poor translation
t("content:pointsBalance.sendBefore") + " " + deadline;

```

### Frontend Localisation

#### Markup Content

Content that is wrapped by tags should be translated.

```html
<p>
  The content between these two p tags should be converted into a locale string
</p>
```

#### Markup Props

Some properties used by HTML elements and React components pass string values using Props, which are ultimately rendered to the user.

```
<!-- basic html example involving localization -->
<input type="text" id="searchBar" placeholder="{t("content:header.searchPlaceholder")}" />

<!-- React component example -->
<PageHeader header="{t("content:branding.title")}" withBottomBorder={false} />
```

### Backend Localisation

#### Translate before returning Response

Localised content in the backend is to be translated prior to returning a response. Avoid passing the locale keys to the client.

> Why? Passing locale keys back to the client requires the client to have a copy of the localisation dictionary in order to render the text. Since an API will be used by multiple clients, doing so is impractical.

```
// Good
return {
    pong: context.t("content:ping.pong")
};

// Avoid: Passing locale keys to client
return {
    pong: "content:ping.pong"
};
```

#### Group by presentation over layer

Group the strings for localisation by what presents the rendered string over the location of the string in the source.

> Why? Focusing on the presentation layer reduces the number of groupings in the locale dictionary, which will have an added benefit of reducing the amount of redundant strings in the dictionary.

```
// Good
return {
    home: context.t("content:navigation.home")
};

// Avoid: Grouping by source location
return {
    checkIn: "content:navigation.fetchLegayPM.checkIn"
};
```

### What NOT to Localise

#### Logs

Logs are used internally within `Your Company Here` for diagnosis and debugging. Since the customer is not the intended audience for logging events, these strings should never be localized. 

```
// Good
console.error("Error making a PM fetch for the current user context", e);

// Avoid
console.error(t("error.userContext.fetchPM"), e);
```

#### Enums

Enums are references used during runtime to execute coding instructions. **Enums should NEVER** be localised, doing so will break the application.

#### Placeholders

Occasionally, you may encounter placholders in the codebase which require a follow-up story to properly implement for production. Avoid localizing placeholders when encountered as they are short-lived and should be removed before the application is public-facing in an international setting.

```
// Avoid: Loading/Error Placeholders
if (loading) {
  // TODO: Use a standardized loader here
  return <b>Loading</b>;
} else if (error) {
  // TODO: User a standardized error message here.
  return <b>Error</b>;
}
```

#### The Code Base

- `locales/*/content.json`: add new terms here & don't forget the Labels and Placeholders if you are adding a new field to the UI

- Please use nesting to avoid extra unnecessary code https://www.i18next.com/translation-function/nesting

```
// Good
"updateGoal": "Update $t(customTerms:customTerminology.goal)",

// Avoid
"updateGoal": "Update {{ goal }}",
```

### Testing with Locale Strings

As rendered text is converted to locale strings, you may encounter broken tests that have to be updated. Recommendations to address broken tests:

- [i18next's official documentation](https://react.i18next.com/misc/testing#example-test-using-this-configuration) on testing support. Start with this first.
- Some tests are being triggered using `fireEvent.click` and `getByText` from `@testing-library/react`. Preferably, these tests should be updated to trigger off of a property other than the actual rendered text (such as `data-testid`). If necessary, you can key off of the locale key instead of the rendered text (avoid this unless no other options are possible).

## Translation Process

The `your-project-here` project leverages a continuous-delivery process for submitting & receiving translations through the [Lokalise](https://www.lokalise.com) platform. How it works:

- Engineers add locale strings to english dictionaries in `your-project-here` as part of their feature branches
- When the feature branch is merged, a [Github Action](https://github.com/features/actions) is triggered which uploads the changes from the locale dictionaries to Lokalise
- Using the Lokalise platform, Product reviews the new strings and places a translation order with the translation vendor.
- Translation vendor processes the order, returns translations to the Lokalise Platform.
- Product reviews & accepts the work from the translation vendor, and triggers a download of the translated strings.
- The download process generates a pull request within `your-project-here` to reintegrate the translated content into the project.  **IMPORTANT:** Do not include the english dictionary from the translation service in your pull request as it could overwrite any new strings that were added but not yet translated.  
- Engineers review the pull request & can trigger an on-demand branch to test prior to approval.
- Upon merge, the translated content is then deployed.

## References

- [MDN: Localization content best practices](https://developer.mozilla.org/en-US/docs/Mozilla/Localization/Localization_content_best_practices)
- [i18next](https://www.i18next.com/): i18next is the dependency used for rendering translations within Kazoo-web
- [Lokalise](https://lokalise.com): Translation-management service used by Kazoo
- [Grammarly: Capitalization Rules](https://www.grammarly.com/blog/capitalization-rules/)