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
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 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 and 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, 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 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 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 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
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.
<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 UIPlease 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 on testing support. Start with this first.
- Some tests are being triggered using
fireEvent.click
andgetByText
from@testing-library/react
. Preferably, these tests should be updated to trigger off of a property other than the actual rendered text (such asdata-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
- MDN: Localization content best practices
- i18next: i18next is the dependency used for rendering translations within Kazoo-web
- Lokalise: Translation-management service used by Kazoo
- Grammarly: Capitalization Rules