A map laid out with packaging items, such as a roll of twine, a utility knife, a stamp, and an unwrapped circular gift box.

How to concatenate localized strings mindfully

Techniques you can use to help translators deliver high-quality translations

Francine Navarro
Shopify UX
Published in
9 min readSep 11, 2019

--

Shopify is used by entrepreneurs around the world. And yet, it wasn’t until 2018 that it became available in languages other than English. There are now 17 localized versions of the Shopify admin, and we’re aiming to reach 21 by the end of 2019.

This growing list of supported locales presents some interesting UX challenges for content, design, and development: we’re creating localized address forms, rendering text in various writing modes and characters, and displaying locale-specific images or icons (try to spot the difference between https://www.shopify.de and https://www.shopify.com).

Translating content is really just the beginning. That doesn’t mean we should take it for granted, though! If we’re not careful setting our content up for translation, we could introduce localization bugs that make the product less understandable, usable, and robust in our target locales.

Localization via string externalization

Developers go through a process called string externalization to ensure content can be localized. Instead of being hard-coded into our markup, any string that should be translated is stored outside of our components.

At Shopify, the translated strings for our React components live in JSON files — one for each locale our product supports (an en.json file for English translations, fr.json file for French translations, and so on). Most components have their own set of locale files.

├── MyComponent
│ ├── MyComponent.jsx
│ └── translations
│ └── en.json
│ └── fr.json
│ └── ja.json

├── AnotherComponent
│ ├── AnotherComponent.jsx
│ └── translations
│ └── en.json
│ └── fr.json
│ └── ja.json

Usually, developers will make updates only to the en.json file when creating or editing a component. We can request translations after making a pull request. Our new, untranslated strings will then be sent to the translation team, who will make the corresponding changes to the other locale files.

// en.json (English){
“logIn”: “Log in”
}
// fr.json (French){
“logIn”: “Connexion”
}
// ja.json (Japanese){
“logIn”: “ログイン”
}

To render translated strings in our markup, we use the translate method provided by the react-i18n library (other internationalization libraries will have similar features). If an externalized string consists entirely of static content, we simply pass its key name as a string argument to the function. It will look for the desired translation in the locale file that matches the user’s locale setting, and return that string to us.

// MyComponent.jsxreturn <Button>{i18n.translate(‘logIn’)}</Button>;

Concatenating values in externalized strings

What if a string contains values that update in real-time or come from an API, such as counts, user inputs, or account information? In this case, we would use string interpolation to concatenate those values to an externalized string.

// en.json{
greeting: “Good morning, {userName}!”,
verificationEmail: “A verification email was sent to {email}.”
}
// MyComponent.jsx<h2>{translate(‘greeting’, {userName: user.name})}</h2>
<p>{translate(‘verificationEmail’, {email: user.email})}</p>

String interpolation is also used to combine multiple translated strings into one. This often happens when we have a set of strings following the same format, such as “Reset your password” and “Change your password”. Since these strings differ only by a single word, we might be tempted to create a reusable string for “your password” and interpolate the translations for “Reset” and “Change”.

// en.json{
“passwordAction”: “{action} your password”,
“reset”: “Reset”,
“change”: “Change”
}
// PasswordInput.jsxconst status = ‘change’;return <Link>{translate(‘passwordAction’, {
action: translate(status)
}
)}</Link>;

In addition to avoiding redundancies, this setup enables us to concatenate HTML elements or custom components to an externalized string:

<p>{translate(‘passwordAction’, {
action: <Link>translate(‘reset’)</Link>
}
)}</p>;

These optimizations can sound pretty persuasive. In practice, however, concatenation doesn’t always hold up to edge cases affecting users in other locales.

Concatenating externalized strings: What can go wrong (and how to make it right)

Previous experience has taught us to look for at least three types of translation errors when concatenating externalized strings: incorrect word order, hard-coded punctuation marks, and improper word choice.

Word order

Let’s revisit the example where we’re using concatenation to generate “Reset your password”.

// en.json{
“passwordAction”: “{action} your password”,
“reset”: “Reset”
}

In our English translation, the verb is expected to come before the noun being acted upon. The French and Spanish versions of this string follow the same word order.

  • Réinitialiser votre mot de passe
  • Restablecer tu contraseña

However, the verb comes after the noun in our Japanese and Dutch translations.

  • パスワードをリセット
  • Wachtwoord opnieuw instellen

This requires translators to place the interpolated value at the end of the string when they add localized content to our ja.json and nl.json files:

// ja.json{
“passwordAction”: “パスワードを{action}
“reset”: “リセット”
}
// nl.json{
“passwordAction”: “Wachtwoord {action}”,
“reset”: “opnieuw instellen”
}

Concatenation works well here if the noun appears either at the end or beginning of a sentence. What if it should be somewhere in the middle?

In our German translation, for instance, the noun goes between a verb and an adverb:

  • Setzen Sie Ihr Passwort zurück

Concatenation doesn’t prevent a translator from using an adverb in this example; when they refer to our en.json file, they might infer that “Reset” will be concatenated to the “your password” string above it. This could prompt them to include the appropriate adverb in their translation.

// de.json{
“passwordAction”: “{action} Ihr Passwort zurück”,
“reset”: “Setzen Sie”
}

Depending on the software translators use to update locale files, it may not be obvious that the two strings will be used together. This could lead them to provide a different translation for “Reset” that makes sense when the word is used on its own, but not in a sentence with a possessive pronoun.

A better alternative would be to replace the “passwordAction” string with multiple un-concatenated strings — one for each call-to-action.

// en.json
{
“resetPassword”: “Reset your password”,
“savePassword”: “Save your password”
}

There may be situations where a design requires us to use concatenation. It’s very common to find a string where part of it is rendered as a link, as italicized text, or in a different colour or font weight.

<Link>Reset</Link> your password

In these situations, we should try to provide as much context to translators as possible.

Some internationalization tools can parse HTML elements inside of externalized strings (they may not be able to parse React components, though). We might also have the option to include clarifying comments in our locale files.

// en.yml{
# This may be rendered as “Reset your password”
# or “Save your password”
“passwordAction”: “<a>{action}</a> your password”,
“reset”: “Reset”,
“save”: “Save”
}

Depending on the tools our project uses, those capabilities may not be available to us. If this is the case, we might consider updating the given design to avoid errors that jeopardize our localization efforts.

Punctuation

How might we externalize a string where a numeric value from an API follows a colon?

Unique clicks: 100

One option is to hard-code the colon into the markup and externalize the string that comes before it:

// en.json{
“totalUniqueClicks”: “Unique clicks”
}
// MyComponent.jsxrender <p>{translate(‘totalUniqueClicks’)}: {count}</p>;

While this approach works well in English, it doesn’t allow translators to adapt the colon for other locales. We can avoid this issue by including the colon in the externalized string, and use interpolation to append the dynamic count:

// en.json{
“totalUniqueClicks”: “Unique clicks: {count}”,
}

This setup affords translators more control over how content should be localized. As a result, for the Japanese translation of this string, the translator was able to replace the colon with the 回 (kai) character to denote the number of times something occurs.

{total}のユニークなクリック

As alluded to earlier, how spaces are used in a sentence can vary across locales, too. French expects spaces before and after certain punctuation marks (such as colons) and between words. Conversely, written Japanese does not typically use spaces between words.

Given all these possible variations, how do we decide which values to include in an externalized string? Ultimately, we should aim to provide translators with enough flexibility to determine the best way to localize the entire string — not just a piece of it — for a specific audience.

Word choice

There are many ways to say “many” in English: multiple, various, a lot, and more than one are just a few examples. Which word we decide to use (also known as diction) depends on the words that come before or after it, as well as the overall intent of the sentence.

Imagine we wanted to externalize this set of strings, where the noun is expected to be rendered in bold text:

  • This user is not available
  • This store is not available
  • This theme is not available

We might consider concatenating “user”, “store”, or “theme” to a reusable string via interpolation:

// en.json{
“notAvailable”: “This {item} is not available”,
“user”: “user”,
“store”: “store”,
“theme”: “theme”
}
// MyComponent.jsxconst itemType = ‘user’;return (
<Banner>
{translate(‘notAvailable’, {
item: translate(itemType)
}
)}
</Banner>
);

Although this setup is sufficient for English content, it obscures details that could influence how a translator localizes the string in other languages.

In French, for example, there’s more than one way to translate the word “this”; which word we use depends on whether the subject is masculine or feminine, or starts with a vowel or consonant.

  • Cet utilisateur n’est pas disponible
  • Cette boutique n’est pas disponible
  • Ce thème n’est pas disponible

Our current implementation doesn’t support multiple options for “this”, as it expects one translation to work for three use cases. To get around this issue, we might update the interpolated values so the translator reads them as “This user”, “This store”, and “This theme”.

// en.json{
“notAvailable”: “{item} is not available”,
“user”: “This user”,
“store”: “This store”,
“theme”: “This theme”
}
// fr.json{
“notAvailable”: “{item} n’est pas disponible”,
“user”: “Cet utilisateur”,
“store”: “Cette boutique”,
“theme”: “Ce thème”
}

With this approach, there’s a chance a translator could interpret “This user”, “This store”, or “This theme” as strings that will be used on their own instead of in a broader sentence (especially if the translator is a machine).

A more robust option would be to replace the concatenated strings with ones that make the complete sentence visible to translators:

// en.json{
“notAvailable”: {
“user”: “This user is not available”,
“store”: “This store is not available”,
“theme”: “This theme is not available”
}
}
// fr.json{
“notAvailable”: {
“user”: “Cet utilisateur n’est pas disponible”,
“store”: “Cette boutique n’est pas disponible”,
“theme”: “Ce thème n’est pas disponible”
}
}

If we nest the variations of the string under another key, we can use scoping to fetch the desired translation (assuming it’s supported by our internationalization tool):

// MyComponent.jsxconst itemType = ‘user’;return <Banner>{translate(itemType, {scope: ‘notAvailable’})}</Banner>;

This implementation also works well for languages where verb conjugation depends on the tense of the subject (e.g., whether it is singular or plural, masculine or feminine, etc.).

In our Spanish translation for “Share feedback”, the verb enviar (to send) is conjugated as envíanos (send us).

// en.json{
“newFeature”: {
“shareFeedback”: “Share feedback“
}
}
// es.json{
“newFeature”: {
“shareFeedback”: “Envíanos tus comentarios“
}
}

Had we used concatenation here, we might have removed context that helped our translator decide which verb conjugation was most appropriate for the Spanish version of this string.

Concatenation is one of many techniques we can use when externalizing strings. But just because we can use it doesn’t mean we should. When it’s not done thoughtfully, string concatenation can lead to unnecessary guesswork for translators, broken experiences for end users, and future bugs to fix for developers.

So how can we decide if concatenation is the right tool for the job?

Understand the capabilities of your internationalization infrastructure

Some translation tools provide a means of adding context to externalized strings. Take time to learn about the features supported by the infrastructure, and make decisions based on what’s available. Depending on our technical constraints, there may be times when we have to forego pixel perfection or developer convenience. What we should strive for instead is clarity for translators, which leads to better localized content for our end users.

Optimize for flexibility

Developers are often trained to avoid repeating constants. When it comes to string externalization, we can help translators provide high-quality translations by assuming any character in a string may change during the localization process. Avoiding errors in word order, punctuation, or word choice can make the experience feel more seamless for users in multiple locales.

Use concatenation when it’s sufficient and necessary

Concatenation is best suited for adding dynamic values to a static externalized string. In other cases, we should ask ourselves:

  1. Do we need to concatenate multiple externalized strings together?
  2. Would it compromise the experience for translators and end users?

Not sure how to answer the second question? Coincidentally, we can turn to the same principles that inform web accessibility guidelines for advice: the best solution is one that makes our content perceivable, operable, understandable, and robust.

Many thanks to everyone who made this blog post better: Aleks Ignjatovic and Giulia Greco for sharing their experience as translators with me; Tim Layton, Jen Taylor, and Christian Jaekl for their development insights and thorough feedback; and Gene Shannon for helping me add the finishing touches.

--

--

Front end developer at Shopify. Studied political science, media studies, and social justice before breaking into web development. Currently learning Japanese.