Localisation

Localisation

Localisation support is provided by the next-intl library.

For information on how this is setup see their getting started docs.

Localised routing has been setup inline with the official example provided here.

Translation

There are multiple translation source files, which one a translation lives in is dependant on where it’s used.

If a translation is used across both web and mobile apps it will belong in the shared workspace.

  • If it is common across products then place it in shared/messages/en-GB.json. An example would be ‘Save’ or ‘Cancel’.
  • If it is specific to a single product but used in both web and mobile apps then place it in shared/messages/product/en-GB.json. An example would be ‘A fresh take on dog food’ would live in shared/messages/dogs/en-GB.json.

If a translation is specific to a single product and platform it belongs in the app workspace for that product.

  • A translation specific to the cats workspace would belong in apps/cats/app/messages/en-GB.json

Client components

import { useTranslations } from "next-intl"

const t = useTranslations()

Server components

import { getTranslations } from "next-intl/server"

const t = await getTranslations("your-namespace")

Layouts with translation

When building layouts it is important to remember that the copy being placed into an element will vary in length. In languages such as German for example the length of a word can easily double when translated.

Keep this in mind and be sure to compensate for variable word lengths.

The currently selected locale can be accessed via the useLocale hook provided by next-intl

import { useLocale } from "next-intl"

const locale = useLocale()

Typescript integration with translation files

Our translation files are typed providing autocompletion and type safety. See the next-intl documentation for more information.

Routes

Routes are also localised, localisations are added to the translation file and then added via the navigation handler.

www.butternutbox.com/en-GB/login
www.butternutbox.de/de-DE/anmeldung

Default localisation values

We can define certain values globally to reduce redundancy and improve consistency. These values are accessible through any translation files without needing to be explicitly passed through. For example {brand} can be used to refer to the brand name of the product anywhere in our translation files.

"message": "{brand} is the best petfood on the market!"

In the above example we can adapt our sites easily for markets where we have multiple different brands for the same product ie. Butternut Box and PsiBuffet. See the next-intl documentation for more information.

Time and dates

Formatting dates won’t require passing through the locale. As our pages throughout the app are wrapped with the <NextIntlClientProvider /> HOC, next-intl is aware of the locale at all times.

Basic Usage

import { useFormatter } from "next-intl"
 
const format = useFormatter()
 
return (
  <Typography>
    {format.dateTime(new Date(), {dateStyle: "full"})}
  </Typography>
)
⚠️

Be aware that translation keys over 7 levels deep will silently fail ie “1.2.3.4.5.6.7.8”

Read More

For more information on how to format dates and time see the next-intl docs.

For more usage examples refer to Storybook.

Currency

Similarly to date/time formatting, number formatting won’t require passing through the locale.

Basic Usage

import { useFormatter } from "next-intl"
 
const format = useFormatter()
 
return (
  <Typography>
    {format.number(1000, {
      style: "currency",
      currency: Currencies.GBP
    })}
  </Typography>
)

Read More

For more information on how to format Numbers see the next-intl docs.

For more usage examples refer to Storybook.

Grammar

Several common grammar rules that tend to be repeated a lot across our copy are available for use from our shared translation file.

  "grammar": {
    "adjectives": {
      "possessive": "{gender, select, female {her} male {his} other {their}}"
    },
    "pronouns": {
      "possessive": "{gender, select, female {hers} male {his} other {theirs}}",
      "singular": "{gender, select, female {she} male {he} other {they}}",
      "contracted": "{gender, select, female {she's} male {he's} other {they're}}",
      "objective": "{gender, select, female {her} male {him} other {them}}",
      "personal": "{gender, select, female {her} male {his} other {theirs}}"
    }
  },

This allows engineers to avoid repetition and enforce the correct grammar when translating copy.

  "tip": "Try feeding {objectivePronoun} after playtime or outdoor adventures when {possessiveAdjective} appetite is up. Think: Hunt–Fetch–Kill–Eat (just like {possessiveAdjective} wild ancestors).",
const objectivePronoun = useMemo(
  () =>
    t("grammar.pronouns.objective", {
      gender: animals.length > 1 ? false : animals[0]?.gender
    }),
  [animals, t]
)
 
const possessiveAdjective = useMemo(
  () =>
    t("grammar.adjective.possessive", {
      gender: animals.length > 1 ? false : animals[0]?.gender
    }),
  [animals, t]
)
 
{
  t(`tip`, {
    objectivePronoun,
    possessiveAdjective
  })
}

Ideally these grammar rules should be abstracted to a hook/provider so they only need to be defined once and can be accessed easily - see the cats usePlan hook for an example.