Guidelines for Translating Your Project using i18next

Joe Mainwaring - Mar 17 '22 - - Dev Community

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. 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
  2. Guidelines
    1. Organization
    2. Key Names
    3. String Values
    4. Front-end Localisation
    5. Backend Localisation
    6. What NOT to Localise
    7. Testing with Locale Strings
  3. Translation Process
  4. 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")
Enter fullscreen mode Exit fullscreen mode

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 flatten dictionaries when uploaded.

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

// Avoid
t("content:goalsWidget.tabs.company")
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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`
Enter fullscreen mode Exit fullscreen mode

Casing

Key names should be in camelCasing format. Avoid PascalCase completely. Snake_Case is a reserved by i18n for Context and Plurals

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

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

// Avoid: Snake_Case
t("content:actions.new_EPS_survey")
Enter fullscreen mode Exit fullscreen mode

String Values

Passing values

Locale strings aren't always static; sometimes we need to pass data-driven values to the string. i18next provides 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"
Enter fullscreen mode Exit fullscreen mode

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 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"
Enter fullscreen mode Exit fullscreen mode

Context

In addition to plurals, words can be modified through other contexts (example: gender). i18next also provides support for 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"
Enter fullscreen mode Exit fullscreen mode

Casing

Generally, string values added to a locales dictionary should be in Sentence case format. See Grammarly's 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"
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Markup

See React-i18next's documentation on the 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;

Enter fullscreen mode Exit fullscreen mode

Frontend Localisation

Markup Content

Content that is wrapped by tags should be translated.

<p>
  The content between these two p tags should be converted into a locale string
</p>
Enter fullscreen mode Exit fullscreen mode

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} />
Enter fullscreen mode Exit fullscreen mode

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"
};
Enter fullscreen mode Exit fullscreen mode

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"
};
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

The Code Base

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

// Avoid
"updateGoal": "Update {{ goal }}",
Enter fullscreen mode Exit fullscreen mode

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 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 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 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

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .