Skip to content

Internationalization

MorganeLecurieux edited this page Jan 26, 2023 · 7 revisions

Dates

We use the Luxon library to format dates. However, we have different timezones in the app (organization, customer, lago UTC) implying to have timezone format. By default, the timezone of the app is set to the selected organization timezone so you don't have to deal with it and can just use the Luxon formatter.

import { DateTime } from 'luxon'

DateTime.fromISO("2022-04-13T14:40:26Z").toFormat('LLL. dd, yyyy')}
// --> 2022/04/13

Note : The default format in the app is LLL. dd, yyyy

If you need to use another timezone, you can use the following :

import { formatDateToTZ, getTimezoneConfig } from "~/core/timezone";

const { name, offset /* Can be usefull for displaying informations*/ } =
  getTimezoneConfig(TZ_AMERICA_ARGENTINA_BUENOS_AIRES);

formatDateToTZ("2022-04-13T14:40:26Z", name);

There is also a component that allow to display the date with a tooltip with the same date displayed in different timezone :

import { TimezoneDate } from "~/components/TimezoneDate";
import { TimezoneEnum } from "~/generated/graphql";

const UseTimezone = (date: string, customerTimezone: TimezoneEnum) => {
  <TimezoneDate
    date={date}
    customerTimezone={customerTimezone}
    mainTimezone="organization"
  />;
};

Numbers

You can use the intlFormatNumber function from the ~/core/intlFormatNumber file.

import { intlFormatNumber } from "~/core/intlFormatNumber";

intlFormatNumber(432222.34, {
  currencyDisplay: "code",
  currency: "USD",
  maximumFractionDigits: 2,
});
// --> USD 432,222.34

intlFormatNumber(432222, {
  currencyDisplay: "code",
  currency: "USD",
  minimumFractionDigits: 3,
});
// --> USD 4,322.220 as default format is in cent

intlFormatNumber(0.1, {
  minimumFractionDigits: 2,
  style: "percent",
});
// --> 0.10%

Amount and Currency

Some currency have specificity (ex: allowing 3 decimals, or allowing 0 decimals).

Also, to avoid issues with float storages, amounts are stored on the API in the smalles unit possible (ex: cents for EUR, USD..., regular unit for the currency not accepting decimals).

To deal with this issue in app, several functions have been defined for you to use:

Serializers

SerializeAmount - Use it to convert the amount value according to currency to be sent to the API

import { serializeAmount } from "~/core/serializers/serializeAmount";

serializeAmount(100, CurrencyEnum.Jpy); // --> 100 as JPY does not accepts decimals
serializeAmount(9.95, CurrencyEnum.Jpy); // --> 10 as JPY does not accepts decimals
serializeAmount(9.95, CurrencyEnum.Usd); // --> 995 as USD accepts 2 decimals
serializeAmount(100, CurrencyEnum.Bhd); // --> 100000 as BHD accepts 3 decimals

DeserializeAmount - Use it to convert an amount value received from the API to a readable unit according to the currency

import { deserializeAmount } from "~/core/serializers/serializeAmount";

deserializeAmount(100, CurrencyEnum.Jpy); // --> 100 as JPY does not accepts decimals and is stored in the JPY unit
deserializeAmount(9.95, CurrencyEnum.Jpy); // --> 10 as JPY does not accepts decimals
deserializeAmount(100, CurrencyEnum.Usd); // --> 1 as USD accepts 2 decimals and is stored in cents
deserializeAmount(0.1, CurrencyEnum.Bhd); // --> 100000 as BHD accepts 3 decimals

Components

Amount Input To be sure that accepted values match the currency constraints, we have a component AmountInput that will add the mandatory constraints to the input

import { AmountInput, ComboBox } from "~/components/form";

const Form = () => {
  return (
    <AmountInput
      name="name"
      currency="USD"
      label={translate("text_624453d52e945301380e49b6")}
      value={valuePointer?.amount}
      onChange={handleUpdate}
      InputProps={{
        endAdornment: (
          <InputAdornment position="end">
            {getCurrencySymbol(currency)}
          </InputAdornment>
        ),
      }}
    />
  );
};

Translations

Use in the app

All the translations can be found in the base.json file. To use them you can use the translate function (code here) Note: The translations keys are generated through the ditto app. If you'd like to own and generate your own translations keys, you'll have to overide the base.json file locally.

type translate = (
  key: string,
  data?: Record<string, string | number | undefined | null>,
  plural?: number
) => string;

Translation with dynamic variable

Second argument of the translate function can be an array of data to use. For each key, include {{key}} in your translation string - which will be parsed to replace with the given value.

// in the translation file
{
  "translation_key": "{{name}} has {{count}} cookies in her pocket."
}
// in your component
<Typography>
  {translate("translation_key", {
    name: Morgane,
    count: 7,
  })}
</Typography>

// output --> "Morgane has 7 cookies in her pocket."

Display links in a translation

{
  // In app link
  "translation_in_app_key_id": "Already have an account? <a data-text=\"Log in\" href=\"{{link}}\">-</a>",
  // Link to another website
  "translation_outter_key_id": "By signing up, you agree to our <a rel=\"external\" target=\"_blank\" href=\"https://www.getlago.com/terms-of-service\">Terms of Service</a> and <a rel=\"external\" target=\"_blank\" href=\"https://www.getlago.com/privacy-policy\">Privacy Policy</a>.\nAlready have an account? <a data-text=\"Log in\" href=\"{{link}}\">-</a>"
}

You will then be able to use this key as follow in your React component :

import { useInternationalization } from '~/hooks/useInternationalization'

const MyComponent = () => {
    const { translate } = useInternationalization()

    return (
        <Typography html={translate('translation_key_id', { link: '/login' })} />
        <Typography html={translate('translation_outter_key_id')} />
    )
}

The output will then look like this :

Plurals

Third argument of the translate function can be a number :

  • 0 means 'none'
  • 1 means 'singular'
  • 2 or more means plural

Then use | to split your translations string for the same key.

// in the translation file
{
  "translation_key": "I have no cake | I have {{ count }} cake | I have {{count}} cakes"
}
// Let's say with have this component:
const MyComponent = ({ count }) => {
  return (
    <Typography>{translate("translation_key", { count }, count)}</Typography>
  );
};

// output for count === 0 --> "I have no cake"
// output for count === 1 --> "I have 1 cake"
// output for count === 4 --> "I have 4 cakes"

We are using Ditto based on our Figma files to generate the translations.

Upload the translation keys (Lago team)

  1. Go to Ditto projects and add a new one by importing an existing figma file

  2. Once done, choose which frame you need (try to import the fewer frame to get a maximum of translations)

  3. On each frame, select the translations you need and hide the other ones (you can select several and hide it on the right panel)

  4. Don't forget to turn on the developer mode

  5. Run the command yarn ditto:addNew and select the new project

  6. Run the command yarn ditto to update all the translations

  7. Use the id of translation you want to use in your code and you're good to go !

Note: If you want to update a translation, update it in ditto and then re-run the yarn ditto command

⚠️ Once you're new project is used in production, to avoid resync issues with figma, make sure to unlink the project:

  1. On the left pannel of a project click on manage
  2. Click on each frame "unlink group"
  1. Save, and place you project in the Production Folder