Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(i18n): switch to ICU message format syntax #1158

Merged
merged 1 commit into from
Oct 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions .storybook/preview.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import React, { useEffect } from 'react';
import { useEffect } from 'react';
import type { Preview } from '@storybook/react';
import { withScreenshot } from 'storycap';
import i18n from 'i18next';
import { initReactI18next, I18nextProvider } from 'react-i18next';
import ICU from 'i18next-icu';
import icuEn from 'i18next-icu/locale-data/en';
import icuJa from 'i18next-icu/locale-data/ja';
import icuEs from 'i18next-icu/locale-data/es';
import icuPt from 'i18next-icu/locale-data/pt';
import icuCs from 'i18next-icu/locale-data/cs';

import { DSProvider, createIconLibrary } from '../src/theme';
import en from '../src/locales/en-US';
Expand All @@ -18,6 +24,7 @@ import '@fontsource/inter/600.css';
import '@fontsource/inter/700.css';
import '@fontsource/space-mono/400.css';
import '../src/tokens/tokens.css';
import { SlowBuffer } from 'buffer';

function clearDatatableLS() {
Object.keys(localStorage)
Expand All @@ -28,8 +35,12 @@ clearDatatableLS();
createIconLibrary();
window.Math.random = () => 0.5;

i18n.use(initReactI18next).init({
i18n.use(ICU).use(initReactI18next).init({
debug: true,
i18nFormat: {
localeData: [icuEn, icuJa, icuEs, icuPt, icuCs],
formats: {},
},
resources: {
'en-US': { sscds: en },
'ja-JP': { sscds: ja },
Expand Down
62 changes: 62 additions & 0 deletions docs/localization.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,68 @@ We've chosen to implement localization using `i18next` and `react-i18next`. This

`i18next` provides a robust framework for managing translations, while `react-i18next` offers React-specific bindings that make it simple to use localized content within our components. This approach allows for dynamic language switching and efficient management of translation resources.

## String interpolation

Our localization system utilizes the ICU (International Components for Unicode) Message Format instead of the default i18next syntax for string interpolation and pluralization. This format provides a more powerful and standardized way to handle complex localization scenarios.

### What is ICU Message Format?

ICU Message Format is a standardized localization format used widely in software internationalization. It allows for sophisticated string formatting, including pluralization, gender, and selectional mechanisms. This format is more expressive than simple key-value pairs and can handle a wide variety of linguistic rules across different languages.

### How It Works

ICU Message Format uses placeholders and formatting instructions within curly braces {}. These placeholders can be simple variables, or they can include formatting options for numbers, dates, and pluralization.

### Syntax Examples

#### Basic String Interpolation

To insert a variable into a string, use the variable name within curly braces:

```
"Hello, {name}!"
```

When providing the translation, you would use:

```json
{
"greeting": "Hello, {name}!"
}
```

And in your code:

```js
t('greeting', { name: 'Alice' });
```

This would result in: "Hello, Alice!"

#### Pluralization

For pluralization, use the plural keyword followed by the variable and the different forms:

```
"{count, plural, =0 {No items} one {{count} item} other {{count} items}}"
```

In your translation file:

```json
{
"itemCount": "{count, plural, =0 {No items} one {{count} item} other {{count} items}}"
}
```

In your code:

```js
t('itemCount', { count: 0 }); // "No items"
t('itemCount', { count: 1 }); // "1 item"
t('itemCount', { count: 5 }); // "5 items"
```

## Integration into Existing Apps

Integrating our localized components into your existing application is straightforward. Here are the steps:
Expand Down
40 changes: 27 additions & 13 deletions jest.setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,37 @@ import '@testing-library/jest-dom/extend-expect';
import 'jest-styled-components';
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import ICU from 'i18next-icu';
import icuEn from 'i18next-icu/locale-data/en';
import icuJa from 'i18next-icu/locale-data/ja';
import icuEs from 'i18next-icu/locale-data/es';
import icuPt from 'i18next-icu/locale-data/pt';
import icuCs from 'i18next-icu/locale-data/cs';

import { createIconLibrary, resetIconLibrary } from './src';
import en from './src/locales/en-US';

i18n.use(initReactI18next).init({
resources: {
'en-US': { sscds: en },
},
defaultNS: 'sscds',
keySeparator: false,
nsSeparator: '|',
lng: 'en-US',
fallbackLng: 'en-US',
interpolation: {
escapeValue: false,
},
});
i18n
.use(ICU)
.use(initReactI18next)
.init({
debug: true,
i18nFormat: {
localeData: [icuEn, icuJa, icuEs, icuPt, icuCs],
formats: {},
},
resources: {
'en-US': { sscds: en },
},
defaultNS: 'sscds',
keySeparator: false,
nsSeparator: '|',
lng: 'en-US',
fallbackLng: 'en-US',
interpolation: {
escapeValue: false,
},
});

Object.defineProperty(document, 'fonts', {
value: {
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
"dedent": "^1.5.1",
"fast-deep-equal": "^3.1.3",
"i18next": ">=17.3.1",
"i18next-icu": "^1.4.2",
"numeral": "^2.0.6",
"pullstate": "^1.23.0",
"ramda": "^0.28.0",
Expand Down
50 changes: 20 additions & 30 deletions src/locales/cs-CZ.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,33 +20,23 @@ const locale = {
'filters.number.error': 'Použijte pouze čísla',
'filters.tagsInput.placeholder': 'Zadejte hodnotu',
'datatable.pagination.itemCounter.short':
'{{firstRowIndex}}-{{lastRowIndex}} z {{totalRowCount}}',
'datatable.pagination.itemCounter.full_one':
'{{firstRowIndex}}-{{lastRowIndex}} z {{totalRowCount}} řádku celkem',
'datatable.pagination.itemCounter.full_few':
'{{firstRowIndex}}-{{lastRowIndex}} z {{totalRowCount}} řádků celkem',
'datatable.pagination.itemCounter.full_other':
'{{firstRowIndex}}-{{lastRowIndex}} z {{totalRowCount}} řádků celkem',
'{firstRowIndex}-{lastRowIndex} z {totalRowCount}',
'datatable.pagination.itemCounter.full':
'{firstRowIndex}-{lastRowIndex} z {totalRowCount} {count, plural, one {řádku} few {řádků} other {řádků}} celkem',
'datatable.pagination.rowsPerPage': 'Řádků na stránku',
'datatable.pagination.rowsPerPage.short': 'Řádky',
'datatable.pagination.goToFirstPage': 'Jít na první stránku tabulky',
'datatable.pagination.goToLastPage': 'Jít na poslední stránku tabulky',
'datatable.pagination.goToPreviousPage': 'Jít na předchozí stránku tabulky',
'datatable.pagination.goToNextPage': 'Jít na další stránku tabulky',
'datatable.topToolbar.itemCounter_one': '{{totalRowCount}} řádek',
'datatable.topToolbar.itemCounter_few': '{{totalRowCount}} řádky',
'datatable.topToolbar.itemCounter_other': '{{totalRowCount}} řádků',
'datatable.topToolbar.hiddenColumns_one': '{{count}} skrytý sloupec',
'datatable.topToolbar.hiddenColumns_few': '{{count}} skryté sloupce',
'datatable.topToolbar.hiddenColumns_other': '{{count}} skrytých sloupců',
'datatable.topToolbar.itemCounter':
'{totalRowCount} {count, plural, one {řádek} few {řádky} other {řádků}}',
'datatable.topToolbar.hiddenColumns':
'{count} {count, plural, one {skrytý sloupec} few {skryté sloupce} other {skrytých sloupců}}',
'datatable.topToolbar.columns': 'Sloupce',
'datatable.topToolbar.fullScreen': 'Celá obrazovka',
'datatable.selection.itemCounter_one':
'Vybráno <bold>{{selectedRowCount}}</bold> z {{totalRowCount}} řádku',
'datatable.selection.itemCounter_few':
'Vybráno <bold>{{selectedRowCount}}</bold> z {{totalRowCount}} řádků',
'datatable.selection.itemCounter_other':
'Vybráno <bold>{{selectedRowCount}}</bold> z {{totalRowCount}} řádků',
'datatable.selection.itemCounter':
'Vybráno <bold>{selectedRowCount}</bold> z {totalRowCount} {count, plural, one {řádku} few {řádků} other {řádků}}',
'datatable.selection.clearSelection': 'Zrušit výběr',
'datatable.selection.toggleAll': 'Přepnout výběr všech řádů',
'datatable.selection.toggleRow': 'Přepnout výběr řádku',
Expand All @@ -73,26 +63,26 @@ const locale = {
'datatable.settings.reset': 'Obnovit výchozí',
'datatable.settings.hiding.showAll': 'Zobrazit všechny sloupce',
'datatable.settings.hiding.hideAll': 'Skrýt všechny sloupce',
'datatable.settings.hiding.showColumn': 'Zobrazit sloupec {{columnName}}',
'datatable.settings.hiding.hideColumn': 'Skrýt sloupec {{columnName}}',
'datatable.settings.hiding.showColumn': 'Zobrazit sloupec {columnName}',
'datatable.settings.hiding.hideColumn': 'Skrýt sloupec {columnName}',
'datatable.settings.pinnig.pinAll': 'Připnout všechny sloupce',
'datatable.settings.pinnig.unpinAll': 'Odepnout všechny sloupce',
'datatable.settings.pinnig.pinColumn': 'Připnout sloupec {{columnName}}',
'datatable.settings.pinnig.unpinColumn': 'Odepnout sloupec {{columnName}}',
'datatable.settings.ordering.reorder': 'Změnit pořadí sloupce {{columnName}}',
'datatable.settings.pinnig.pinColumn': 'Připnout sloupec {columnName}',
'datatable.settings.pinnig.unpinColumn': 'Odepnout sloupec {columnName}',
'datatable.settings.ordering.reorder': 'Změnit pořadí sloupce {columnName}',
'datatable.settings.ordering.screenReader.instructions':
'Chcete-li vybrat sloupec tabulky, stiskněte Mezerník nebo Enter. Pomocí šipek nahoru a dolů upravte pozici sloupce v tabulce. Dalším stisknutím Mezerníku nebo Eneru sloupec v nové pozici uvolněte nebo stisknutím Escape operaci zrušte.',
'datatable.settings.ordering.screenReader.pickedUp':
'Byl vybrán sloupec {{header}}.',
'Byl vybrán sloupec {header}.',
'datatable.settings.ordering.screenReader.movedOver':
'Sloupec {{activeHeader}} se přesunul nad sloupec {{overHeader}}.',
'Sloupec {activeHeader} se přesunul nad sloupec {overHeader}.',
'datatable.settings.ordering.screenReader.dropped':
'Sloupec {{activeHeader}} byl uvolněn.',
'Sloupec {activeHeader} byl uvolněn.',
'datatable.settings.ordering.screenReader.droppedOver':
'Sloupec {{activeHeader}} byl uvolněn nad sloupcem {{overHeader}}',
'Sloupec {activeHeader} byl uvolněn nad sloupcem {overHeader}',
'datatable.settings.ordering.screenReader.dragCancel':
'Přetahování bylo zrušeno. Sloupec {{header}} byl uvolněn.',
'Přetahování bylo zrušeno. Sloupec {header} byl uvolněn.',
'datatable.settings.ordering.screenReader.notDroppableArea':
'Sloupec {{activeHeader}} již není nad oblastí, kam lze položku upustit.',
'Sloupec {activeHeader} již není nad oblastí, kam lze položku upustit.',
} as const;
export default locale;
45 changes: 20 additions & 25 deletions src/locales/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,27 +20,23 @@ const locale = {
'filters.number.error': 'Use only numbers',
'filters.tagsInput.placeholder': 'Enter value',
'datatable.pagination.itemCounter.short':
'{{firstRowIndex}}-{{lastRowIndex}} of {{totalRowCount}}',
'datatable.pagination.itemCounter.full_one':
'{{firstRowIndex}}-{{lastRowIndex}} of {{totalRowCount}} total row',
'datatable.pagination.itemCounter.full_other':
'{{firstRowIndex}}-{{lastRowIndex}} of {{totalRowCount}} total rows',
'{firstRowIndex}-{lastRowIndex} of {totalRowCount}',
'datatable.pagination.itemCounter.full':
'{firstRowIndex}-{lastRowIndex} of {totalRowCount} {count, plural, one {total row} other {total rows}}',
'datatable.pagination.rowsPerPage': 'Rows per page',
'datatable.pagination.rowsPerPage.short': 'Rows',
'datatable.pagination.goToFirstPage': 'Go to the first page of table',
'datatable.pagination.goToLastPage': 'Go to the last page of table',
'datatable.pagination.goToPreviousPage': 'Go to the previous page of table',
'datatable.pagination.goToNextPage': 'Go to the next page of table',
'datatable.topToolbar.itemCounter_one': '{{totalRowCount}} row',
'datatable.topToolbar.itemCounter_other': '{{totalRowCount}} rows',
'datatable.topToolbar.hiddenColumns_one': '{{count}} column hidden',
'datatable.topToolbar.hiddenColumns_other': '{{count}} columns hidden',
'datatable.topToolbar.itemCounter':
'{count, plural, one {{totalRowCount} row} other {{totalRowCount} rows}}',
'datatable.topToolbar.hiddenColumns':
'{count, plural, one {{count} column hidden} other {{count} columns hidden}}',
'datatable.topToolbar.columns': 'Columns',
'datatable.topToolbar.fullScreen': 'Full Screen',
'datatable.selection.itemCounter_one':
'<bold>{{selectedRowCount}}</bold> of {{totalRowCount}} row selected',
'datatable.selection.itemCounter_other':
'<bold>{{selectedRowCount}}</bold> of {{totalRowCount}} rows selected',
'datatable.selection.itemCounter':
'{count, plural, one {<bold>{selectedRowCount}</bold> of {totalRowCount} row selected} other {<bold>{selectedRowCount}</bold> of {totalRowCount} rows selected}}',
'datatable.selection.clearSelection': 'Clear selection',
'datatable.selection.toggleAll': 'Toggle select all',
'datatable.selection.toggleRow': 'Toggle select row',
Expand All @@ -67,27 +63,26 @@ const locale = {
'datatable.settings.reset': 'Reset to default',
'datatable.settings.hiding.showAll': 'Show all columns',
'datatable.settings.hiding.hideAll': 'Hide all columns',
'datatable.settings.hiding.showColumn': 'Show {{columnName}} column',
'datatable.settings.hiding.hideColumn': 'Hide {{columnName}} column',
'datatable.settings.hiding.showColumn': 'Show {columnName} column',
'datatable.settings.hiding.hideColumn': 'Hide {columnName} column',
'datatable.settings.pinnig.pinAll': 'Pin all columns',
'datatable.settings.pinnig.unpinAll': 'Unpin all columns',
'datatable.settings.pinnig.pinColumn': 'Unpin {{columnName}} column',
'datatable.settings.pinnig.unpinColumn': 'Unpin {{columnName}} column',
'datatable.settings.ordering.reorder': 'Reorder {{columnName}} column',
'datatable.settings.pinnig.pinColumn': 'Unpin {columnName} column',
'datatable.settings.pinnig.unpinColumn': 'Unpin {columnName} column',
'datatable.settings.ordering.reorder': 'Reorder {columnName} column',
'datatable.settings.ordering.screenReader.instructions':
'To pick up a draggable table column, press Space or Enter. Use the Up and Down arrow keys to update the position of the column in the table. Press Space or Enter again to drop the item in its new position, or press Escape to cancel.',
'datatable.settings.ordering.screenReader.pickedUp':
'Picked up {{header}} column.',
'Picked up {header} column.',
'datatable.settings.ordering.screenReader.movedOver':
'{{activeHeader}} column was moved over {{overHeader}} column.',
'{activeHeader} column was moved over {overHeader} column.',
'datatable.settings.ordering.screenReader.dropped':
'{{activeHeader}} column was dropped.',
'{activeHeader} column was dropped.',
'datatable.settings.ordering.screenReader.droppedOver':
'{{activeHeader}} column was dropped over {{overHeader}} column',
'{activeHeader} column was dropped over {overHeader} column',
'datatable.settings.ordering.screenReader.dragCancel':
'Dragging was cancelled. {{header}} column was dropped.',
'Dragging was cancelled. {header} column was dropped.',
'datatable.settings.ordering.screenReader.notDroppableArea':
'{{activeHeader}} column is no longer over a droppable area.',
test: '<0>{{lng}}</0> test',
'{activeHeader} column is no longer over a droppable area.',
} as const;
export default locale;
Loading
Loading