From 36cfdbab3462291376c945ecce645e3193db16e7 Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Mon, 25 Sep 2023 18:17:38 +0200
Subject: [PATCH 01/18] Introduce ra-i18n-i18next
---
examples/simple/package.json | 4 ++
examples/simple/src/i18nProvider-next.tsx | 13 ++++
examples/simple/src/index.tsx | 4 +-
packages/ra-i18n-i18next/README.md | 13 ++++
packages/ra-i18n-i18next/package.json | 37 ++++++++++
packages/ra-i18n-i18next/src/index.ts | 88 +++++++++++++++++++++++
packages/ra-i18n-i18next/tsconfig.json | 10 +++
yarn.lock | 84 ++++++++++++++++++++++
8 files changed, 252 insertions(+), 1 deletion(-)
create mode 100644 examples/simple/src/i18nProvider-next.tsx
create mode 100644 packages/ra-i18n-i18next/README.md
create mode 100644 packages/ra-i18n-i18next/package.json
create mode 100644 packages/ra-i18n-i18next/src/index.ts
create mode 100644 packages/ra-i18n-i18next/tsconfig.json
diff --git a/examples/simple/package.json b/examples/simple/package.json
index e14ea88172c..d95a873b3fc 100644
--- a/examples/simple/package.json
+++ b/examples/simple/package.json
@@ -13,11 +13,14 @@
"@emotion/styled": "^11.6.0",
"@mui/icons-material": "^5.0.1",
"@mui/material": "^5.0.2",
+ "i18next": "^23.5.1",
+ "i18next-resources-to-backend": "^1.1.4",
"jsonexport": "^3.2.0",
"lodash": "~4.17.5",
"prop-types": "^15.7.2",
"proxy-polyfill": "^0.3.0",
"ra-data-fakerest": "^4.13.4",
+ "ra-i18n-i18next": "^4.13.4",
"ra-i18n-polyglot": "^4.13.4",
"ra-input-rich-text": "^4.13.4",
"ra-language-english": "^4.13.4",
@@ -26,6 +29,7 @@
"react-admin": "^4.13.4",
"react-dom": "^17.0.0",
"react-hook-form": "^7.43.9",
+ "react-i18next": "^13.2.2",
"react-query": "^3.32.1",
"react-router": "^6.1.0",
"react-router-dom": "^6.1.0"
diff --git a/examples/simple/src/i18nProvider-next.tsx b/examples/simple/src/i18nProvider-next.tsx
new file mode 100644
index 00000000000..e3fb67ca894
--- /dev/null
+++ b/examples/simple/src/i18nProvider-next.tsx
@@ -0,0 +1,13 @@
+import i18n from 'i18next';
+import { initReactI18next } from 'react-i18next';
+import i18nextProvider, { convertRaMessagesToI18next } from 'ra-i18n-i18next';
+import englishMessages from './i18n/en';
+
+const instance = i18n.use(initReactI18next);
+
+// TODO: remove
+export default i18nextProvider(instance, {
+ resources: {
+ en: { translation: convertRaMessagesToI18next(englishMessages) },
+ },
+});
diff --git a/examples/simple/src/index.tsx b/examples/simple/src/index.tsx
index 800767145e2..7d758a710d0 100644
--- a/examples/simple/src/index.tsx
+++ b/examples/simple/src/index.tsx
@@ -9,7 +9,9 @@ import comments from './comments';
import CustomRouteLayout from './customRouteLayout';
import CustomRouteNoLayout from './customRouteNoLayout';
import dataProvider from './dataProvider';
-import i18nProvider from './i18nProvider';
+// TODO: revert
+// import i18nProvider from './i18nProvider';
+import i18nProvider from './i18nProvider-next';
import Layout from './Layout';
import posts from './posts';
import users from './users';
diff --git a/packages/ra-i18n-i18next/README.md b/packages/ra-i18n-i18next/README.md
new file mode 100644
index 00000000000..1b008eb2b47
--- /dev/null
+++ b/packages/ra-i18n-i18next/README.md
@@ -0,0 +1,13 @@
+# Polyglot i18n provider for react-admin
+
+Polyglot i18n provider for [react-admin](https://github.com/marmelab/react-admin), the frontend framework for building admin applications on top of REST/GraphQL services. It relies on [i18next](https://www.i18next.com/).
+
+## Installation
+
+```sh
+npm install --save ra-i18n-i18next
+```
+
+## Usage
+
+TODO
diff --git a/packages/ra-i18n-i18next/package.json b/packages/ra-i18n-i18next/package.json
new file mode 100644
index 00000000000..5dd3870430e
--- /dev/null
+++ b/packages/ra-i18n-i18next/package.json
@@ -0,0 +1,37 @@
+{
+ "name": "ra-i18n-i18next",
+ "version": "4.13.4",
+ "description": "i18next i18n provider for react-admin",
+ "main": "dist/cjs/index.js",
+ "module": "dist/esm/index.js",
+ "types": "dist/cjs/index.d.ts",
+ "sideEffects": false,
+ "files": [
+ "*.md",
+ "dist",
+ "src"
+ ],
+ "authors": [
+ "François Zaninotto"
+ ],
+ "repository": "marmelab/react-admin",
+ "homepage": "https://github.com/marmelab/react-admin#readme",
+ "bugs": "https://github.com/marmelab/react-admin/issues",
+ "license": "MIT",
+ "scripts": {
+ "build": "yarn run build-cjs && yarn run build-esm",
+ "build-cjs": "rimraf ./dist/cjs && tsc --outDir dist/cjs",
+ "build-esm": "rimraf ./dist/esm && tsc --outDir dist/esm --module es2015",
+ "watch": "tsc --outDir dist/esm --module es2015 --watch"
+ },
+ "dependencies": {
+ "i18next": "^23.5.1",
+ "ra-core": "^4.13.4"
+ },
+ "devDependencies": {
+ "cross-env": "^5.2.0",
+ "rimraf": "^3.0.2",
+ "typescript": "^5.1.3"
+ },
+ "gitHead": "e936ff2c3f887d2e98ef136cf3b3f3d254725fc4"
+}
diff --git a/packages/ra-i18n-i18next/src/index.ts b/packages/ra-i18n-i18next/src/index.ts
new file mode 100644
index 00000000000..ded9937fd5e
--- /dev/null
+++ b/packages/ra-i18n-i18next/src/index.ts
@@ -0,0 +1,88 @@
+import { InitOptions, i18n as I18n, TFunction } from 'i18next';
+import clone from 'lodash/clone';
+import { I18nProvider } from 'ra-core';
+/**
+ * Build a i18next-based i18nProvider.
+ *
+ * @example
+ * TODO
+ */
+export default (i18nInstance: I18n, options?: InitOptions): I18nProvider => {
+ let translate: TFunction;
+
+ i18nInstance
+ .init({
+ lng: 'en',
+ fallbackLng: 'en',
+ react: { useSuspense: false },
+ ...options,
+ })
+ .then(t => {
+ translate = t;
+ });
+ return {
+ translate: (key: string, options: any = {}) => {
+ const { _: defaultValue, ...otherOptions } = options || {};
+ return translate(key, { defaultValue, ...otherOptions }).toString();
+ },
+ changeLocale: async (newLocale: string) => {
+ await i18nInstance.changeLanguage(newLocale);
+ },
+ getLocale: () => i18nInstance.language,
+ getLocales: () => {
+ return i18nInstance.languages
+ ? i18nInstance.languages.map(l => ({ locale: l, name: l }))
+ : undefined;
+ },
+ };
+};
+
+export const convertRaMessagesToI18next = (
+ raMessages,
+ { prefix = '{{', suffix = '}}' } = {}
+) => {
+ return Object.keys(raMessages).reduce((acc, key) => {
+ if (typeof acc[key] === 'object') {
+ acc[key] = convertRaMessagesToI18next(acc[key]);
+ return acc;
+ }
+
+ const message = acc[key] as string;
+
+ if (message.indexOf(' |||| ') > -1) {
+ const pluralVariants = message.split(' |||| ');
+
+ if (
+ pluralVariants.length > 2 &&
+ process.env.NODE_ENV === 'development'
+ ) {
+ console.warn(
+ 'A message contains more than two plural forms so we can not convert it to i18next format automatically. You should provide your own translations for this language.'
+ );
+ }
+ acc[`${key}_one`] = convertMessage(pluralVariants[0], {
+ prefix,
+ suffix,
+ });
+ acc[`${key}_other`] = convertMessage(pluralVariants[1], {
+ prefix,
+ suffix,
+ });
+ delete acc[key];
+ } else {
+ acc[key] = convertMessage(message, { prefix, suffix });
+ }
+
+ return acc;
+ }, clone(raMessages));
+};
+
+const convertMessage = (
+ message: string,
+ { prefix = '{{', suffix = '}}' } = {}
+) => {
+ return message.replace(
+ /%\{([a-zA-Z0-9-_]*)\}/g,
+ (match, p1) => `${prefix}${p1}${suffix}`
+ );
+};
diff --git a/packages/ra-i18n-i18next/tsconfig.json b/packages/ra-i18n-i18next/tsconfig.json
new file mode 100644
index 00000000000..a18665f388c
--- /dev/null
+++ b/packages/ra-i18n-i18next/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "lib",
+ "rootDir": "src",
+ "allowJs": false
+ },
+ "exclude": ["**/*.spec.ts", "**/*.spec.tsx", "**/*.spec.js"],
+ "include": ["src"]
+}
diff --git a/yarn.lock b/yarn.lock
index 2fdc52a7b76..288c5e5b161 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1816,6 +1816,15 @@ __metadata:
languageName: node
linkType: hard
+"@babel/runtime@npm:^7.21.5":
+ version: 7.23.1
+ resolution: "@babel/runtime@npm:7.23.1"
+ dependencies:
+ regenerator-runtime: ^0.14.0
+ checksum: e57ab1436d4845efe67c3f76d578508bb584173690ecfeac105bc4e09d64b2aa6a53c1e03bca3c97cc238e5390a804e5a4ded211e6350243b735905ca45a4822
+ languageName: node
+ linkType: hard
+
"@babel/template@npm:^7.16.7, @babel/template@npm:^7.22.5":
version: 7.22.5
resolution: "@babel/template@npm:7.22.5"
@@ -12832,6 +12841,15 @@ __metadata:
languageName: node
linkType: hard
+"html-parse-stringify@npm:^3.0.1":
+ version: 3.0.1
+ resolution: "html-parse-stringify@npm:3.0.1"
+ dependencies:
+ void-elements: 3.1.0
+ checksum: 159292753d48b84d216d61121054ae5a33466b3db5b446e2ffc093ac077a411a99ce6cbe0d18e55b87cf25fa3c5a86c4d8b130b9719ec9b66623259000c72c15
+ languageName: node
+ linkType: hard
+
"html-tags@npm:^3.1.0":
version: 3.3.1
resolution: "html-tags@npm:3.3.1"
@@ -13036,6 +13054,24 @@ __metadata:
languageName: node
linkType: hard
+"i18next-resources-to-backend@npm:^1.1.4":
+ version: 1.1.4
+ resolution: "i18next-resources-to-backend@npm:1.1.4"
+ dependencies:
+ "@babel/runtime": ^7.21.5
+ checksum: 221a22d08eccdd946c12c11de1910c70d8bf61b9834f17b72ddad24ea304264a12ea50953a740e5fa1f56d32a2290dcef6f7eb699fd30984f7e8676944e41aed
+ languageName: node
+ linkType: hard
+
+"i18next@npm:^23.5.1":
+ version: 23.5.1
+ resolution: "i18next@npm:23.5.1"
+ dependencies:
+ "@babel/runtime": ^7.22.5
+ checksum: af49c399a90505ae26c1a022d06c4a11c4adcde6524b31c315dcaa43443c85892adef6de934b2af737abbdd2ffa66449d2854f135af8691223a8bb4ffaf6e1af
+ languageName: node
+ linkType: hard
+
"iconv-lite@npm:0.4.24, iconv-lite@npm:^0.4.24":
version: 0.4.24
resolution: "iconv-lite@npm:0.4.24"
@@ -18361,6 +18397,18 @@ __metadata:
languageName: unknown
linkType: soft
+"ra-i18n-i18next@^4.13.4, ra-i18n-i18next@workspace:packages/ra-i18n-i18next":
+ version: 0.0.0-use.local
+ resolution: "ra-i18n-i18next@workspace:packages/ra-i18n-i18next"
+ dependencies:
+ cross-env: ^5.2.0
+ i18next: ^23.5.1
+ ra-core: ^4.13.4
+ rimraf: ^3.0.2
+ typescript: ^5.1.3
+ languageName: unknown
+ linkType: soft
+
"ra-i18n-polyglot@^4.12.0, ra-i18n-polyglot@^4.13.4, ra-i18n-polyglot@workspace:packages/ra-i18n-polyglot":
version: 0.0.0-use.local
resolution: "ra-i18n-polyglot@workspace:packages/ra-i18n-polyglot"
@@ -18852,6 +18900,24 @@ __metadata:
languageName: node
linkType: hard
+"react-i18next@npm:^13.2.2":
+ version: 13.2.2
+ resolution: "react-i18next@npm:13.2.2"
+ dependencies:
+ "@babel/runtime": ^7.22.5
+ html-parse-stringify: ^3.0.1
+ peerDependencies:
+ i18next: ">= 23.2.3"
+ react: ">= 16.8.0"
+ peerDependenciesMeta:
+ react-dom:
+ optional: true
+ react-native:
+ optional: true
+ checksum: 846e73130414989304c395c247cec8371ec25b5a0284588c913542741ce0cc33b4681d573980c64320138601d08a2ddec5042dc6f841e48a575e3f11ab83a368
+ languageName: node
+ linkType: hard
+
"react-inspector@npm:^6.0.0":
version: 6.0.1
resolution: "react-inspector@npm:6.0.1"
@@ -19426,6 +19492,13 @@ __metadata:
languageName: node
linkType: hard
+"regenerator-runtime@npm:^0.14.0":
+ version: 0.14.0
+ resolution: "regenerator-runtime@npm:0.14.0"
+ checksum: e25f062c1a183f81c99681691a342760e65c55e8d3a4d4fe347ebe72433b123754b942b70b622959894e11f8a9131dc549bd3c9a5234677db06a4af42add8d12
+ languageName: node
+ linkType: hard
+
"regenerator-transform@npm:^0.15.1":
version: 0.15.1
resolution: "regenerator-transform@npm:0.15.1"
@@ -20295,11 +20368,14 @@ __metadata:
"@mui/icons-material": ^5.0.1
"@mui/material": ^5.0.2
"@vitejs/plugin-react": ^2.2.0
+ i18next: ^23.5.1
+ i18next-resources-to-backend: ^1.1.4
jsonexport: ^3.2.0
lodash: ~4.17.5
prop-types: ^15.7.2
proxy-polyfill: ^0.3.0
ra-data-fakerest: ^4.13.4
+ ra-i18n-i18next: ^4.13.4
ra-i18n-polyglot: ^4.13.4
ra-input-rich-text: ^4.13.4
ra-language-english: ^4.13.4
@@ -20309,6 +20385,7 @@ __metadata:
react-app-polyfill: ^1.0.4
react-dom: ^17.0.0
react-hook-form: ^7.43.9
+ react-i18next: ^13.2.2
react-query: ^3.32.1
react-router: ^6.1.0
react-router-dom: ^6.1.0
@@ -22109,6 +22186,13 @@ __metadata:
languageName: node
linkType: hard
+"void-elements@npm:3.1.0":
+ version: 3.1.0
+ resolution: "void-elements@npm:3.1.0"
+ checksum: 0b8686f9f9aa44012e9bd5eabf287ae0cde409b9a2854c5a2335cb83920c957668ac5876e3f0d158dd424744ac411a7270e64128556b451ed3bec875ef18534d
+ languageName: node
+ linkType: hard
+
"w3c-keyname@npm:^2.2.0":
version: 2.2.4
resolution: "w3c-keyname@npm:2.2.4"
From b60afbdba44b6f3279cce31aa6b1bd34732eb718 Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Thu, 28 Sep 2023 11:51:52 +0200
Subject: [PATCH 02/18] Cleanup simple example
---
examples/simple/src/i18nProvider-next.tsx | 13 -------------
examples/simple/src/index.tsx | 4 +---
2 files changed, 1 insertion(+), 16 deletions(-)
delete mode 100644 examples/simple/src/i18nProvider-next.tsx
diff --git a/examples/simple/src/i18nProvider-next.tsx b/examples/simple/src/i18nProvider-next.tsx
deleted file mode 100644
index e3fb67ca894..00000000000
--- a/examples/simple/src/i18nProvider-next.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import i18n from 'i18next';
-import { initReactI18next } from 'react-i18next';
-import i18nextProvider, { convertRaMessagesToI18next } from 'ra-i18n-i18next';
-import englishMessages from './i18n/en';
-
-const instance = i18n.use(initReactI18next);
-
-// TODO: remove
-export default i18nextProvider(instance, {
- resources: {
- en: { translation: convertRaMessagesToI18next(englishMessages) },
- },
-});
diff --git a/examples/simple/src/index.tsx b/examples/simple/src/index.tsx
index 7d758a710d0..800767145e2 100644
--- a/examples/simple/src/index.tsx
+++ b/examples/simple/src/index.tsx
@@ -9,9 +9,7 @@ import comments from './comments';
import CustomRouteLayout from './customRouteLayout';
import CustomRouteNoLayout from './customRouteNoLayout';
import dataProvider from './dataProvider';
-// TODO: revert
-// import i18nProvider from './i18nProvider';
-import i18nProvider from './i18nProvider-next';
+import i18nProvider from './i18nProvider';
import Layout from './Layout';
import posts from './posts';
import users from './users';
From a4e44c322250744190f2d3c4a18e3d2df0382bd3 Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Thu, 28 Sep 2023 11:52:26 +0200
Subject: [PATCH 03/18] Fix pluralization & convertRaMessagesToI18next
---
packages/ra-i18n-i18next/src/index.ts | 15 +++++++++++----
1 file changed, 11 insertions(+), 4 deletions(-)
diff --git a/packages/ra-i18n-i18next/src/index.ts b/packages/ra-i18n-i18next/src/index.ts
index ded9937fd5e..71ed7c6e031 100644
--- a/packages/ra-i18n-i18next/src/index.ts
+++ b/packages/ra-i18n-i18next/src/index.ts
@@ -22,8 +22,13 @@ export default (i18nInstance: I18n, options?: InitOptions): I18nProvider => {
});
return {
translate: (key: string, options: any = {}) => {
- const { _: defaultValue, ...otherOptions } = options || {};
- return translate(key, { defaultValue, ...otherOptions }).toString();
+ const { _: defaultValue, smart_count: count, ...otherOptions } =
+ options || {};
+ return translate(key, {
+ defaultValue,
+ count,
+ ...otherOptions,
+ }).toString();
},
changeLocale: async (newLocale: string) => {
await i18nInstance.changeLanguage(newLocale);
@@ -43,7 +48,7 @@ export const convertRaMessagesToI18next = (
) => {
return Object.keys(raMessages).reduce((acc, key) => {
if (typeof acc[key] === 'object') {
- acc[key] = convertRaMessagesToI18next(acc[key]);
+ acc[key] = convertRaMessagesToI18next(acc[key], { prefix, suffix });
return acc;
}
@@ -81,8 +86,10 @@ const convertMessage = (
message: string,
{ prefix = '{{', suffix = '}}' } = {}
) => {
- return message.replace(
+ const result = message.replace(
/%\{([a-zA-Z0-9-_]*)\}/g,
(match, p1) => `${prefix}${p1}${suffix}`
);
+
+ return result;
};
From 182d29cba61d3bbbeb950fc1f1141da6d5ab3a34 Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Thu, 28 Sep 2023 11:52:33 +0200
Subject: [PATCH 04/18] Add stories
---
.../ra-i18n-i18next/src/index.stories.tsx | 95 +++++++++++++++++++
1 file changed, 95 insertions(+)
create mode 100644 packages/ra-i18n-i18next/src/index.stories.tsx
diff --git a/packages/ra-i18n-i18next/src/index.stories.tsx b/packages/ra-i18n-i18next/src/index.stories.tsx
new file mode 100644
index 00000000000..1be44fb5ad5
--- /dev/null
+++ b/packages/ra-i18n-i18next/src/index.stories.tsx
@@ -0,0 +1,95 @@
+import * as React from 'react';
+import { Admin, EditGuesser, ListGuesser, Resource } from 'react-admin';
+import i18n from 'i18next';
+import { initReactI18next } from 'react-i18next';
+import englishMessages from 'ra-language-english';
+import fakeRestDataProvider from 'ra-data-fakerest';
+import i18nextProvider, { convertRaMessagesToI18next } from './index';
+
+export default {
+ title: 'ra-i18n-18next',
+};
+
+export const Basic = () => {
+ const instance = i18n.use(initReactI18next);
+ const i18nProvider = i18nextProvider(instance, {
+ resources: {
+ en: { translation: convertRaMessagesToI18next(englishMessages) },
+ },
+ });
+ return (
+
+
+
+
+ );
+};
+
+export const WithCustomTranslations = () => {
+ const instance = i18n.use(initReactI18next);
+ const i18nProvider = i18nextProvider(instance, {
+ resources: {
+ en: {
+ translation: {
+ ...convertRaMessagesToI18next(englishMessages),
+ resources: {
+ posts: {
+ name_one: 'Blog post',
+ name_other: 'Blog posts',
+ fields: {
+ title: 'Title',
+ },
+ },
+ },
+ },
+ },
+ },
+ });
+ return (
+
+
+
+
+ );
+};
+export const WithCustomOptions = () => {
+ const instance = i18n.use(initReactI18next);
+ const defaultMessages = convertRaMessagesToI18next(englishMessages, {
+ prefix: '#{',
+ suffix: '}#',
+ });
+
+ const i18nProvider = i18nextProvider(instance, {
+ interpolation: {
+ prefix: '#{',
+ suffix: '}#',
+ },
+ resources: {
+ en: {
+ translation: {
+ ...defaultMessages,
+ resources: {
+ posts: {
+ name_one: 'Blog post',
+ name_other: 'Blog posts',
+ fields: {
+ title: 'Title',
+ },
+ },
+ },
+ },
+ },
+ },
+ });
+ return (
+
+
+
+
+ );
+};
+
+const dataProvider = fakeRestDataProvider({
+ posts: [{ id: 1, title: 'Lorem Ipsum' }],
+ comments: [{ id: 1, body: 'Sic dolor amet...' }],
+});
From 922e891b95ca907c24022ce40deb685ad5d3a47a Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Thu, 28 Sep 2023 12:39:37 +0200
Subject: [PATCH 05/18] Refactor to allow lazy loading
---
examples/simple/package.json | 4 -
packages/ra-i18n-i18next/package.json | 2 +
.../ra-i18n-i18next/src/index.stories.tsx | 102 ++++++++++++++++--
packages/ra-i18n-i18next/src/index.ts | 46 ++++++--
yarn.lock | 8 +-
5 files changed, 135 insertions(+), 27 deletions(-)
diff --git a/examples/simple/package.json b/examples/simple/package.json
index d95a873b3fc..e14ea88172c 100644
--- a/examples/simple/package.json
+++ b/examples/simple/package.json
@@ -13,14 +13,11 @@
"@emotion/styled": "^11.6.0",
"@mui/icons-material": "^5.0.1",
"@mui/material": "^5.0.2",
- "i18next": "^23.5.1",
- "i18next-resources-to-backend": "^1.1.4",
"jsonexport": "^3.2.0",
"lodash": "~4.17.5",
"prop-types": "^15.7.2",
"proxy-polyfill": "^0.3.0",
"ra-data-fakerest": "^4.13.4",
- "ra-i18n-i18next": "^4.13.4",
"ra-i18n-polyglot": "^4.13.4",
"ra-input-rich-text": "^4.13.4",
"ra-language-english": "^4.13.4",
@@ -29,7 +26,6 @@
"react-admin": "^4.13.4",
"react-dom": "^17.0.0",
"react-hook-form": "^7.43.9",
- "react-i18next": "^13.2.2",
"react-query": "^3.32.1",
"react-router": "^6.1.0",
"react-router-dom": "^6.1.0"
diff --git a/packages/ra-i18n-i18next/package.json b/packages/ra-i18n-i18next/package.json
index 5dd3870430e..07dd91b5a61 100644
--- a/packages/ra-i18n-i18next/package.json
+++ b/packages/ra-i18n-i18next/package.json
@@ -30,6 +30,8 @@
},
"devDependencies": {
"cross-env": "^5.2.0",
+ "i18next-resources-to-backend": "^1.1.4",
+ "react-i18next": "^13.2.2",
"rimraf": "^3.0.2",
"typescript": "^5.1.3"
},
diff --git a/packages/ra-i18n-i18next/src/index.stories.tsx b/packages/ra-i18n-i18next/src/index.stories.tsx
index 1be44fb5ad5..0295f4401f5 100644
--- a/packages/ra-i18n-i18next/src/index.stories.tsx
+++ b/packages/ra-i18n-i18next/src/index.stories.tsx
@@ -2,9 +2,10 @@ import * as React from 'react';
import { Admin, EditGuesser, ListGuesser, Resource } from 'react-admin';
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
+import resourcesToBackend from 'i18next-resources-to-backend';
import englishMessages from 'ra-language-english';
import fakeRestDataProvider from 'ra-data-fakerest';
-import i18nextProvider, { convertRaMessagesToI18next } from './index';
+import { useI18nextProvider, convertRaMessagesToI18next } from './index';
export default {
title: 'ra-i18n-18next',
@@ -12,22 +13,78 @@ export default {
export const Basic = () => {
const instance = i18n.use(initReactI18next);
- const i18nProvider = i18nextProvider(instance, {
+ const i18nProvider = useI18nextProvider(instance, {
resources: {
- en: { translation: convertRaMessagesToI18next(englishMessages) },
+ en: {
+ translation: convertRaMessagesToI18next(englishMessages),
+ },
},
});
+
+ if (!i18nProvider) return null;
+
+ return (
+
+ }
+ edit={}
+ />
+ }
+ edit={}
+ />
+
+ );
+};
+
+export const WithLazyLoadedLanguages = () => {
+ const instance = i18n
+ .use(
+ resourcesToBackend(language => {
+ if (language === 'fr') {
+ return import(
+ `ra-language-french`
+ ).then(({ default: messages }) =>
+ convertRaMessagesToI18next(messages)
+ );
+ }
+ return import(
+ `ra-language-english`
+ ).then(({ default: messages }) =>
+ convertRaMessagesToI18next(messages)
+ );
+ })
+ )
+ .use(initReactI18next);
+
+ const i18nProvider = useI18nextProvider(instance, {}, [
+ { locale: 'en', name: 'English' },
+ { locale: 'fr', name: 'French' },
+ ]);
+
+ if (!i18nProvider) return null;
+
return (
-
-
+ }
+ edit={}
+ />
+ }
+ edit={}
+ />
);
};
export const WithCustomTranslations = () => {
const instance = i18n.use(initReactI18next);
- const i18nProvider = i18nextProvider(instance, {
+ const i18nProvider = useI18nextProvider(instance, {
resources: {
en: {
translation: {
@@ -45,13 +102,25 @@ export const WithCustomTranslations = () => {
},
},
});
+
+ if (!i18nProvider) return null;
+
return (
-
-
+ }
+ edit={}
+ />
+ }
+ edit={}
+ />
);
};
+
export const WithCustomOptions = () => {
const instance = i18n.use(initReactI18next);
const defaultMessages = convertRaMessagesToI18next(englishMessages, {
@@ -59,7 +128,7 @@ export const WithCustomOptions = () => {
suffix: '}#',
});
- const i18nProvider = i18nextProvider(instance, {
+ const i18nProvider = useI18nextProvider(instance, {
interpolation: {
prefix: '#{',
suffix: '}#',
@@ -81,10 +150,21 @@ export const WithCustomOptions = () => {
},
},
});
+
+ if (!i18nProvider) return null;
+
return (
-
-
+ }
+ edit={}
+ />
+ }
+ edit={}
+ />
);
};
diff --git a/packages/ra-i18n-i18next/src/index.ts b/packages/ra-i18n-i18next/src/index.ts
index 71ed7c6e031..55e9ed13018 100644
--- a/packages/ra-i18n-i18next/src/index.ts
+++ b/packages/ra-i18n-i18next/src/index.ts
@@ -1,16 +1,47 @@
import { InitOptions, i18n as I18n, TFunction } from 'i18next';
import clone from 'lodash/clone';
-import { I18nProvider } from 'ra-core';
+import { I18nProvider, Locale } from 'ra-core';
+import { useEffect, useRef, useState } from 'react';
/**
* Build a i18next-based i18nProvider.
*
* @example
* TODO
*/
-export default (i18nInstance: I18n, options?: InitOptions): I18nProvider => {
+export const useI18nextProvider = (
+ i18nInstance: I18n,
+ options?: InitOptions,
+ availableLocales: Locale[] = [{ locale: 'en', name: 'English' }]
+) => {
+ const [i18nProvider, setI18nProvider] = useState(null);
+ const initializationPromise = useRef>(null);
+
+ useEffect(() => {
+ if (initializationPromise.current) {
+ return;
+ }
+
+ initializationPromise.current = getI18nProvider(
+ i18nInstance,
+ options,
+ availableLocales
+ ).then(provider => {
+ setI18nProvider(provider);
+ return provider;
+ });
+ }, []);
+
+ return i18nProvider;
+};
+
+export const getI18nProvider = async (
+ i18nInstance: I18n,
+ options?: InitOptions,
+ availableLocales: Locale[] = [{ locale: 'en', name: 'English' }]
+): Promise => {
let translate: TFunction;
- i18nInstance
+ await i18nInstance
.init({
lng: 'en',
fallbackLng: 'en',
@@ -20,6 +51,7 @@ export default (i18nInstance: I18n, options?: InitOptions): I18nProvider => {
.then(t => {
translate = t;
});
+
return {
translate: (key: string, options: any = {}) => {
const { _: defaultValue, smart_count: count, ...otherOptions } =
@@ -31,13 +63,13 @@ export default (i18nInstance: I18n, options?: InitOptions): I18nProvider => {
}).toString();
},
changeLocale: async (newLocale: string) => {
- await i18nInstance.changeLanguage(newLocale);
+ await i18nInstance.loadLanguages(newLocale);
+ const t = await i18nInstance.changeLanguage(newLocale);
+ translate = t;
},
getLocale: () => i18nInstance.language,
getLocales: () => {
- return i18nInstance.languages
- ? i18nInstance.languages.map(l => ({ locale: l, name: l }))
- : undefined;
+ return availableLocales;
},
};
};
diff --git a/yarn.lock b/yarn.lock
index 288c5e5b161..14ea3a9bff6 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -18397,13 +18397,15 @@ __metadata:
languageName: unknown
linkType: soft
-"ra-i18n-i18next@^4.13.4, ra-i18n-i18next@workspace:packages/ra-i18n-i18next":
+"ra-i18n-i18next@workspace:packages/ra-i18n-i18next":
version: 0.0.0-use.local
resolution: "ra-i18n-i18next@workspace:packages/ra-i18n-i18next"
dependencies:
cross-env: ^5.2.0
i18next: ^23.5.1
+ i18next-resources-to-backend: ^1.1.4
ra-core: ^4.13.4
+ react-i18next: ^13.2.2
rimraf: ^3.0.2
typescript: ^5.1.3
languageName: unknown
@@ -20368,14 +20370,11 @@ __metadata:
"@mui/icons-material": ^5.0.1
"@mui/material": ^5.0.2
"@vitejs/plugin-react": ^2.2.0
- i18next: ^23.5.1
- i18next-resources-to-backend: ^1.1.4
jsonexport: ^3.2.0
lodash: ~4.17.5
prop-types: ^15.7.2
proxy-polyfill: ^0.3.0
ra-data-fakerest: ^4.13.4
- ra-i18n-i18next: ^4.13.4
ra-i18n-polyglot: ^4.13.4
ra-input-rich-text: ^4.13.4
ra-language-english: ^4.13.4
@@ -20385,7 +20384,6 @@ __metadata:
react-app-polyfill: ^1.0.4
react-dom: ^17.0.0
react-hook-form: ^7.43.9
- react-i18next: ^13.2.2
react-query: ^3.32.1
react-router: ^6.1.0
react-router-dom: ^6.1.0
From 2991d481f576c96b1645588ed023cf441faca07a Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Thu, 28 Sep 2023 14:20:09 +0200
Subject: [PATCH 06/18] Simplify API
---
.../ra-i18n-i18next/src/index.stories.tsx | 105 +++++++++---------
packages/ra-i18n-i18next/src/index.ts | 23 ++--
2 files changed, 68 insertions(+), 60 deletions(-)
diff --git a/packages/ra-i18n-i18next/src/index.stories.tsx b/packages/ra-i18n-i18next/src/index.stories.tsx
index 0295f4401f5..8ee2704136d 100644
--- a/packages/ra-i18n-i18next/src/index.stories.tsx
+++ b/packages/ra-i18n-i18next/src/index.stories.tsx
@@ -1,7 +1,6 @@
import * as React from 'react';
import { Admin, EditGuesser, ListGuesser, Resource } from 'react-admin';
import i18n from 'i18next';
-import { initReactI18next } from 'react-i18next';
import resourcesToBackend from 'i18next-resources-to-backend';
import englishMessages from 'ra-language-english';
import fakeRestDataProvider from 'ra-data-fakerest';
@@ -12,11 +11,12 @@ export default {
};
export const Basic = () => {
- const instance = i18n.use(initReactI18next);
- const i18nProvider = useI18nextProvider(instance, {
- resources: {
- en: {
- translation: convertRaMessagesToI18next(englishMessages),
+ const i18nProvider = useI18nextProvider({
+ options: {
+ resources: {
+ en: {
+ translation: convertRaMessagesToI18next(englishMessages),
+ },
},
},
});
@@ -40,29 +40,28 @@ export const Basic = () => {
};
export const WithLazyLoadedLanguages = () => {
- const instance = i18n
- .use(
- resourcesToBackend(language => {
- if (language === 'fr') {
- return import(
- `ra-language-french`
- ).then(({ default: messages }) =>
- convertRaMessagesToI18next(messages)
- );
- }
+ const i18nInstance = i18n.use(
+ resourcesToBackend(language => {
+ if (language === 'fr') {
return import(
- `ra-language-english`
+ `ra-language-french`
).then(({ default: messages }) =>
convertRaMessagesToI18next(messages)
);
- })
- )
- .use(initReactI18next);
+ }
+ return import(`ra-language-english`).then(({ default: messages }) =>
+ convertRaMessagesToI18next(messages)
+ );
+ })
+ );
- const i18nProvider = useI18nextProvider(instance, {}, [
- { locale: 'en', name: 'English' },
- { locale: 'fr', name: 'French' },
- ]);
+ const i18nProvider = useI18nextProvider({
+ i18nInstance,
+ availableLocales: [
+ { locale: 'en', name: 'English' },
+ { locale: 'fr', name: 'French' },
+ ],
+ });
if (!i18nProvider) return null;
@@ -83,18 +82,19 @@ export const WithLazyLoadedLanguages = () => {
};
export const WithCustomTranslations = () => {
- const instance = i18n.use(initReactI18next);
- const i18nProvider = useI18nextProvider(instance, {
- resources: {
- en: {
- translation: {
- ...convertRaMessagesToI18next(englishMessages),
- resources: {
- posts: {
- name_one: 'Blog post',
- name_other: 'Blog posts',
- fields: {
- title: 'Title',
+ const i18nProvider = useI18nextProvider({
+ options: {
+ resources: {
+ en: {
+ translation: {
+ ...convertRaMessagesToI18next(englishMessages),
+ resources: {
+ posts: {
+ name_one: 'Blog post',
+ name_other: 'Blog posts',
+ fields: {
+ title: 'Title',
+ },
},
},
},
@@ -122,27 +122,28 @@ export const WithCustomTranslations = () => {
};
export const WithCustomOptions = () => {
- const instance = i18n.use(initReactI18next);
const defaultMessages = convertRaMessagesToI18next(englishMessages, {
prefix: '#{',
suffix: '}#',
});
- const i18nProvider = useI18nextProvider(instance, {
- interpolation: {
- prefix: '#{',
- suffix: '}#',
- },
- resources: {
- en: {
- translation: {
- ...defaultMessages,
- resources: {
- posts: {
- name_one: 'Blog post',
- name_other: 'Blog posts',
- fields: {
- title: 'Title',
+ const i18nProvider = useI18nextProvider({
+ options: {
+ interpolation: {
+ prefix: '#{',
+ suffix: '}#',
+ },
+ resources: {
+ en: {
+ translation: {
+ ...defaultMessages,
+ resources: {
+ posts: {
+ name_one: 'Blog post',
+ name_other: 'Blog posts',
+ fields: {
+ title: 'Title',
+ },
},
},
},
diff --git a/packages/ra-i18n-i18next/src/index.ts b/packages/ra-i18n-i18next/src/index.ts
index 55e9ed13018..f80ed780d64 100644
--- a/packages/ra-i18n-i18next/src/index.ts
+++ b/packages/ra-i18n-i18next/src/index.ts
@@ -1,18 +1,24 @@
-import { InitOptions, i18n as I18n, TFunction } from 'i18next';
+import { useEffect, useRef, useState } from 'react';
+import { createInstance, InitOptions, i18n as I18n, TFunction } from 'i18next';
+import { initReactI18next } from 'react-i18next';
import clone from 'lodash/clone';
import { I18nProvider, Locale } from 'ra-core';
-import { useEffect, useRef, useState } from 'react';
+
/**
* Build a i18next-based i18nProvider.
*
* @example
* TODO
*/
-export const useI18nextProvider = (
- i18nInstance: I18n,
- options?: InitOptions,
- availableLocales: Locale[] = [{ locale: 'en', name: 'English' }]
-) => {
+export const useI18nextProvider = ({
+ i18nInstance = createInstance(),
+ options = {},
+ availableLocales = [{ locale: 'en', name: 'English' }],
+}: {
+ i18nInstance?: I18n;
+ options?: InitOptions;
+ availableLocales?: Locale[];
+} = {}) => {
const [i18nProvider, setI18nProvider] = useState(null);
const initializationPromise = useRef>(null);
@@ -29,7 +35,7 @@ export const useI18nextProvider = (
setI18nProvider(provider);
return provider;
});
- }, []);
+ }, [availableLocales, i18nInstance, options]);
return i18nProvider;
};
@@ -42,6 +48,7 @@ export const getI18nProvider = async (
let translate: TFunction;
await i18nInstance
+ .use(initReactI18next)
.init({
lng: 'en',
fallbackLng: 'en',
From 7f7bf12afbcb7b796e3eb19e22b15589320e84f4 Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Thu, 28 Sep 2023 14:42:47 +0200
Subject: [PATCH 07/18] Refactor
---
.../src/convertRaMessagesToI18next.ts | 53 +++++++
.../ra-i18n-i18next/src/index.stories.tsx | 121 ++++++++--------
packages/ra-i18n-i18next/src/index.ts | 136 +-----------------
.../ra-i18n-i18next/src/useI18nextProvider.ts | 81 +++++++++++
4 files changed, 196 insertions(+), 195 deletions(-)
create mode 100644 packages/ra-i18n-i18next/src/convertRaMessagesToI18next.ts
create mode 100644 packages/ra-i18n-i18next/src/useI18nextProvider.ts
diff --git a/packages/ra-i18n-i18next/src/convertRaMessagesToI18next.ts b/packages/ra-i18n-i18next/src/convertRaMessagesToI18next.ts
new file mode 100644
index 00000000000..0b61fb74257
--- /dev/null
+++ b/packages/ra-i18n-i18next/src/convertRaMessagesToI18next.ts
@@ -0,0 +1,53 @@
+import clone from 'lodash/clone';
+
+export const convertRaMessagesToI18next = (
+ raMessages,
+ { prefix = '{{', suffix = '}}' } = {}
+) => {
+ return Object.keys(raMessages).reduce((acc, key) => {
+ if (typeof acc[key] === 'object') {
+ acc[key] = convertRaMessagesToI18next(acc[key], { prefix, suffix });
+ return acc;
+ }
+
+ const message = acc[key] as string;
+
+ if (message.indexOf(' |||| ') > -1) {
+ const pluralVariants = message.split(' |||| ');
+
+ if (
+ pluralVariants.length > 2 &&
+ process.env.NODE_ENV === 'development'
+ ) {
+ console.warn(
+ 'A message contains more than two plural forms so we can not convert it to i18next format automatically. You should provide your own translations for this language.'
+ );
+ }
+ acc[`${key}_one`] = convertMessage(pluralVariants[0], {
+ prefix,
+ suffix,
+ });
+ acc[`${key}_other`] = convertMessage(pluralVariants[1], {
+ prefix,
+ suffix,
+ });
+ delete acc[key];
+ } else {
+ acc[key] = convertMessage(message, { prefix, suffix });
+ }
+
+ return acc;
+ }, clone(raMessages));
+};
+
+export const convertMessage = (
+ message: string,
+ { prefix = '{{', suffix = '}}' } = {}
+) => {
+ const result = message.replace(
+ /%\{([a-zA-Z0-9-_]*)\}/g,
+ (match, p1) => `${prefix}${p1}${suffix}`
+ );
+
+ return result;
+};
diff --git a/packages/ra-i18n-i18next/src/index.stories.tsx b/packages/ra-i18n-i18next/src/index.stories.tsx
index 8ee2704136d..c8209a1c38b 100644
--- a/packages/ra-i18n-i18next/src/index.stories.tsx
+++ b/packages/ra-i18n-i18next/src/index.stories.tsx
@@ -4,7 +4,9 @@ import i18n from 'i18next';
import resourcesToBackend from 'i18next-resources-to-backend';
import englishMessages from 'ra-language-english';
import fakeRestDataProvider from 'ra-data-fakerest';
-import { useI18nextProvider, convertRaMessagesToI18next } from './index';
+import { MemoryRouter } from 'react-router-dom';
+import { useI18nextProvider } from './index';
+import { convertRaMessagesToI18next } from './convertRaMessagesToI18next';
export default {
title: 'ra-i18n-18next',
@@ -24,18 +26,20 @@ export const Basic = () => {
if (!i18nProvider) return null;
return (
-
- }
- edit={}
- />
- }
- edit={}
- />
-
+
+
+ }
+ edit={}
+ />
+ }
+ edit={}
+ />
+
+
);
};
@@ -66,18 +70,20 @@ export const WithLazyLoadedLanguages = () => {
if (!i18nProvider) return null;
return (
-
- }
- edit={}
- />
- }
- edit={}
- />
-
+
+
+ }
+ edit={}
+ />
+ }
+ edit={}
+ />
+
+
);
};
@@ -106,18 +112,20 @@ export const WithCustomTranslations = () => {
if (!i18nProvider) return null;
return (
-
- }
- edit={}
- />
- }
- edit={}
- />
-
+
+
+ }
+ edit={}
+ />
+ }
+ edit={}
+ />
+
+
);
};
@@ -135,18 +143,7 @@ export const WithCustomOptions = () => {
},
resources: {
en: {
- translation: {
- ...defaultMessages,
- resources: {
- posts: {
- name_one: 'Blog post',
- name_other: 'Blog posts',
- fields: {
- title: 'Title',
- },
- },
- },
- },
+ translation: defaultMessages,
},
},
},
@@ -155,18 +152,20 @@ export const WithCustomOptions = () => {
if (!i18nProvider) return null;
return (
-
- }
- edit={}
- />
- }
- edit={}
- />
-
+
+
+ }
+ edit={}
+ />
+ }
+ edit={}
+ />
+
+
);
};
diff --git a/packages/ra-i18n-i18next/src/index.ts b/packages/ra-i18n-i18next/src/index.ts
index f80ed780d64..a91c2bc04db 100644
--- a/packages/ra-i18n-i18next/src/index.ts
+++ b/packages/ra-i18n-i18next/src/index.ts
@@ -1,134 +1,2 @@
-import { useEffect, useRef, useState } from 'react';
-import { createInstance, InitOptions, i18n as I18n, TFunction } from 'i18next';
-import { initReactI18next } from 'react-i18next';
-import clone from 'lodash/clone';
-import { I18nProvider, Locale } from 'ra-core';
-
-/**
- * Build a i18next-based i18nProvider.
- *
- * @example
- * TODO
- */
-export const useI18nextProvider = ({
- i18nInstance = createInstance(),
- options = {},
- availableLocales = [{ locale: 'en', name: 'English' }],
-}: {
- i18nInstance?: I18n;
- options?: InitOptions;
- availableLocales?: Locale[];
-} = {}) => {
- const [i18nProvider, setI18nProvider] = useState(null);
- const initializationPromise = useRef>(null);
-
- useEffect(() => {
- if (initializationPromise.current) {
- return;
- }
-
- initializationPromise.current = getI18nProvider(
- i18nInstance,
- options,
- availableLocales
- ).then(provider => {
- setI18nProvider(provider);
- return provider;
- });
- }, [availableLocales, i18nInstance, options]);
-
- return i18nProvider;
-};
-
-export const getI18nProvider = async (
- i18nInstance: I18n,
- options?: InitOptions,
- availableLocales: Locale[] = [{ locale: 'en', name: 'English' }]
-): Promise => {
- let translate: TFunction;
-
- await i18nInstance
- .use(initReactI18next)
- .init({
- lng: 'en',
- fallbackLng: 'en',
- react: { useSuspense: false },
- ...options,
- })
- .then(t => {
- translate = t;
- });
-
- return {
- translate: (key: string, options: any = {}) => {
- const { _: defaultValue, smart_count: count, ...otherOptions } =
- options || {};
- return translate(key, {
- defaultValue,
- count,
- ...otherOptions,
- }).toString();
- },
- changeLocale: async (newLocale: string) => {
- await i18nInstance.loadLanguages(newLocale);
- const t = await i18nInstance.changeLanguage(newLocale);
- translate = t;
- },
- getLocale: () => i18nInstance.language,
- getLocales: () => {
- return availableLocales;
- },
- };
-};
-
-export const convertRaMessagesToI18next = (
- raMessages,
- { prefix = '{{', suffix = '}}' } = {}
-) => {
- return Object.keys(raMessages).reduce((acc, key) => {
- if (typeof acc[key] === 'object') {
- acc[key] = convertRaMessagesToI18next(acc[key], { prefix, suffix });
- return acc;
- }
-
- const message = acc[key] as string;
-
- if (message.indexOf(' |||| ') > -1) {
- const pluralVariants = message.split(' |||| ');
-
- if (
- pluralVariants.length > 2 &&
- process.env.NODE_ENV === 'development'
- ) {
- console.warn(
- 'A message contains more than two plural forms so we can not convert it to i18next format automatically. You should provide your own translations for this language.'
- );
- }
- acc[`${key}_one`] = convertMessage(pluralVariants[0], {
- prefix,
- suffix,
- });
- acc[`${key}_other`] = convertMessage(pluralVariants[1], {
- prefix,
- suffix,
- });
- delete acc[key];
- } else {
- acc[key] = convertMessage(message, { prefix, suffix });
- }
-
- return acc;
- }, clone(raMessages));
-};
-
-const convertMessage = (
- message: string,
- { prefix = '{{', suffix = '}}' } = {}
-) => {
- const result = message.replace(
- /%\{([a-zA-Z0-9-_]*)\}/g,
- (match, p1) => `${prefix}${p1}${suffix}`
- );
-
- return result;
-};
+export * from './useI18nextProvider';
+export * from './convertRaMessagesToI18next';
diff --git a/packages/ra-i18n-i18next/src/useI18nextProvider.ts b/packages/ra-i18n-i18next/src/useI18nextProvider.ts
new file mode 100644
index 00000000000..c1006d87c49
--- /dev/null
+++ b/packages/ra-i18n-i18next/src/useI18nextProvider.ts
@@ -0,0 +1,81 @@
+import { useEffect, useRef, useState } from 'react';
+import { createInstance, InitOptions, i18n as I18n, TFunction } from 'i18next';
+import { initReactI18next } from 'react-i18next';
+import { I18nProvider, Locale } from 'ra-core';
+
+/**
+ * Build a i18next-based i18nProvider.
+ *
+ * @example
+ * TODO
+ */
+export const useI18nextProvider = ({
+ i18nInstance = createInstance(),
+ options = {},
+ availableLocales = [{ locale: 'en', name: 'English' }],
+}: {
+ i18nInstance?: I18n;
+ options?: InitOptions;
+ availableLocales?: Locale[];
+} = {}) => {
+ const [i18nProvider, setI18nProvider] = useState(null);
+ const initializationPromise = useRef>(null);
+
+ useEffect(() => {
+ if (initializationPromise.current) {
+ return;
+ }
+
+ initializationPromise.current = getI18nProvider(
+ i18nInstance,
+ options,
+ availableLocales
+ ).then(provider => {
+ setI18nProvider(provider);
+ return provider;
+ });
+ }, [availableLocales, i18nInstance, options]);
+
+ return i18nProvider;
+};
+
+export const getI18nProvider = async (
+ i18nInstance: I18n,
+ options?: InitOptions,
+ availableLocales: Locale[] = [{ locale: 'en', name: 'English' }]
+): Promise => {
+ let translate: TFunction;
+
+ await i18nInstance
+ .use(initReactI18next)
+ .init({
+ lng: 'en',
+ fallbackLng: 'en',
+ react: { useSuspense: false },
+ ...options,
+ })
+ .then(t => {
+ translate = t;
+ });
+
+ return {
+ translate: (key: string, options: any = {}) => {
+ const { _: defaultValue, smart_count: count, ...otherOptions } =
+ options || {};
+ return translate(key, {
+ defaultValue,
+ count,
+ ...otherOptions,
+ }).toString();
+ },
+ changeLocale: async (newLocale: string) => {
+ await i18nInstance.loadLanguages(newLocale);
+ const t = await i18nInstance.changeLanguage(newLocale);
+ translate = t;
+ },
+ getLocale: () => i18nInstance.language,
+ getLocales: () => {
+ return availableLocales;
+ },
+ };
+};
From 6e837867d79d3ca382e3837037f094c38caff9b1 Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Thu, 28 Sep 2023 14:42:53 +0200
Subject: [PATCH 08/18] Add tests
---
.../src/convertRaMessagesToI18next.spec.tsx | 74 +++++++++++++++++++
packages/ra-i18n-i18next/src/index.spec.tsx | 66 +++++++++++++++++
2 files changed, 140 insertions(+)
create mode 100644 packages/ra-i18n-i18next/src/convertRaMessagesToI18next.spec.tsx
create mode 100644 packages/ra-i18n-i18next/src/index.spec.tsx
diff --git a/packages/ra-i18n-i18next/src/convertRaMessagesToI18next.spec.tsx b/packages/ra-i18n-i18next/src/convertRaMessagesToI18next.spec.tsx
new file mode 100644
index 00000000000..3d3bc84706b
--- /dev/null
+++ b/packages/ra-i18n-i18next/src/convertRaMessagesToI18next.spec.tsx
@@ -0,0 +1,74 @@
+import { convertRaMessagesToI18next } from './convertRaMessagesToI18next';
+
+describe('i18next i18nProvider', () => {
+ describe('convertRaMessagesToI18next', () => {
+ test('should convert react-admin default messages to i18next format', () => {
+ expect(
+ convertRaMessagesToI18next(
+ {
+ simple: 'simple',
+ interpolation: 'interpolation %{variable}',
+ pluralization: 'singular |||| plural',
+ nested: {
+ deep: {
+ simple: 'simple',
+ interpolation: 'interpolation %{variable}',
+ pluralization: 'singular |||| plural',
+ },
+ },
+ },
+ {}
+ )
+ ).toEqual({
+ simple: 'simple',
+ interpolation: 'interpolation {{variable}}',
+ pluralization_one: 'singular',
+ pluralization_other: 'plural',
+ nested: {
+ deep: {
+ simple: 'simple',
+ interpolation: 'interpolation {{variable}}',
+ pluralization_one: 'singular',
+ pluralization_other: 'plural',
+ },
+ },
+ });
+ });
+
+ test('should convert react-admin default messages to i18next format with custom prefix/suffix', () => {
+ expect(
+ convertRaMessagesToI18next(
+ {
+ simple: 'simple',
+ interpolation: 'interpolation %{variable}',
+ pluralization: 'singular |||| plural',
+ nested: {
+ deep: {
+ simple: 'simple',
+ interpolation: 'interpolation %{variable}',
+ pluralization: 'singular |||| plural',
+ },
+ },
+ },
+ {
+ prefix: '#{',
+ suffix: '}#',
+ }
+ )
+ ).toEqual({
+ simple: 'simple',
+ interpolation: 'interpolation #{variable}#',
+ pluralization_one: 'singular',
+ pluralization_other: 'plural',
+ nested: {
+ deep: {
+ simple: 'simple',
+ interpolation: 'interpolation #{variable}#',
+ pluralization_one: 'singular',
+ pluralization_other: 'plural',
+ },
+ },
+ });
+ });
+ });
+});
diff --git a/packages/ra-i18n-i18next/src/index.spec.tsx b/packages/ra-i18n-i18next/src/index.spec.tsx
new file mode 100644
index 00000000000..7e7a6a6f02e
--- /dev/null
+++ b/packages/ra-i18n-i18next/src/index.spec.tsx
@@ -0,0 +1,66 @@
+import * as React from 'react';
+import { fireEvent, render, screen } from '@testing-library/react';
+import {
+ Basic,
+ WithCustomTranslations,
+ WithCustomOptions,
+ WithLazyLoadedLanguages,
+} from './index.stories';
+
+describe('i18next i18nProvider', () => {
+ test('should work with no configuration except the messages', async () => {
+ render();
+
+ await screen.findByText('Comments');
+ await screen.findByText('Export');
+ expect(await screen.findAllByText('Posts')).toHaveLength(2);
+
+ // Check interpolation
+ await screen.findByText('1-1 of 1');
+ fireEvent.click(await screen.findByText('Lorem Ipsum'));
+ // Check singularization
+ await screen.findByText('Post #1');
+ });
+
+ test('should work with custom interpolation options', async () => {
+ render();
+
+ await screen.findByText('Export');
+
+ // Check interpolation
+ await screen.findByText('1-1 of 1');
+ fireEvent.click(
+ await screen.findByText('English', { selector: 'button' })
+ );
+ fireEvent.click(await screen.findByText('French'));
+ await screen.findByText('Exporter');
+ });
+
+ test('should work with custom translations', async () => {
+ render();
+
+ await screen.findByText('Comments');
+ await screen.findByText('Export');
+ expect(await screen.findAllByText('Blog posts')).toHaveLength(2);
+
+ // Check interpolation
+ await screen.findByText('1-1 of 1');
+ fireEvent.click(await screen.findByText('Lorem Ipsum'));
+ // Check singularization
+ await screen.findByText('Blog post #1');
+ });
+
+ test('should work with multiple languages', async () => {
+ render();
+
+ await screen.findByText('Comments');
+ await screen.findByText('Export');
+ expect(await screen.findAllByText('Blog posts')).toHaveLength(2);
+
+ // Check interpolation
+ await screen.findByText('1-1 of 1');
+ fireEvent.click(await screen.findByText('Lorem Ipsum'));
+ // Check singularization
+ await screen.findByText('Blog post #1');
+ });
+});
From d2a5e65080762b7a708872b8f9f34d7cc1524bf2 Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Thu, 28 Sep 2023 14:44:33 +0200
Subject: [PATCH 09/18] Fix tests
---
packages/ra-i18n-i18next/src/index.spec.tsx | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/packages/ra-i18n-i18next/src/index.spec.tsx b/packages/ra-i18n-i18next/src/index.spec.tsx
index 7e7a6a6f02e..8b95e40faf6 100644
--- a/packages/ra-i18n-i18next/src/index.spec.tsx
+++ b/packages/ra-i18n-i18next/src/index.spec.tsx
@@ -22,7 +22,7 @@ describe('i18next i18nProvider', () => {
await screen.findByText('Post #1');
});
- test('should work with custom interpolation options', async () => {
+ test('should work with multiple languages', async () => {
render();
await screen.findByText('Export');
@@ -50,17 +50,17 @@ describe('i18next i18nProvider', () => {
await screen.findByText('Blog post #1');
});
- test('should work with multiple languages', async () => {
- render();
+ test('should work with custom interpolation options', async () => {
+ render();
await screen.findByText('Comments');
await screen.findByText('Export');
- expect(await screen.findAllByText('Blog posts')).toHaveLength(2);
+ expect(await screen.findAllByText('Posts')).toHaveLength(2);
// Check interpolation
await screen.findByText('1-1 of 1');
fireEvent.click(await screen.findByText('Lorem Ipsum'));
// Check singularization
- await screen.findByText('Blog post #1');
+ await screen.findByText('Post #1');
});
});
From d216907f8faa586b1dd7cf50c0e5efb5a3d341cc Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Thu, 28 Sep 2023 14:54:32 +0200
Subject: [PATCH 10/18] Rename and document convertRaMessagesToI18next
---
.../src/convertRaMessagesToI18next.spec.tsx | 6 +-
.../src/convertRaMessagesToI18next.ts | 92 ++++++++++++++++---
.../ra-i18n-i18next/src/index.stories.tsx | 14 +--
3 files changed, 89 insertions(+), 23 deletions(-)
diff --git a/packages/ra-i18n-i18next/src/convertRaMessagesToI18next.spec.tsx b/packages/ra-i18n-i18next/src/convertRaMessagesToI18next.spec.tsx
index 3d3bc84706b..822b0f64ec8 100644
--- a/packages/ra-i18n-i18next/src/convertRaMessagesToI18next.spec.tsx
+++ b/packages/ra-i18n-i18next/src/convertRaMessagesToI18next.spec.tsx
@@ -1,10 +1,10 @@
-import { convertRaMessagesToI18next } from './convertRaMessagesToI18next';
+import { convertRaTranslationsToI18next } from './convertRaMessagesToI18next';
describe('i18next i18nProvider', () => {
describe('convertRaMessagesToI18next', () => {
test('should convert react-admin default messages to i18next format', () => {
expect(
- convertRaMessagesToI18next(
+ convertRaTranslationsToI18next(
{
simple: 'simple',
interpolation: 'interpolation %{variable}',
@@ -37,7 +37,7 @@ describe('i18next i18nProvider', () => {
test('should convert react-admin default messages to i18next format with custom prefix/suffix', () => {
expect(
- convertRaMessagesToI18next(
+ convertRaTranslationsToI18next(
{
simple: 'simple',
interpolation: 'interpolation %{variable}',
diff --git a/packages/ra-i18n-i18next/src/convertRaMessagesToI18next.ts b/packages/ra-i18n-i18next/src/convertRaMessagesToI18next.ts
index 0b61fb74257..06fc648523b 100644
--- a/packages/ra-i18n-i18next/src/convertRaMessagesToI18next.ts
+++ b/packages/ra-i18n-i18next/src/convertRaMessagesToI18next.ts
@@ -1,12 +1,41 @@
import clone from 'lodash/clone';
-export const convertRaMessagesToI18next = (
- raMessages,
+/**
+ * A function that takes translations from a standard react-admin language package and converts them to i18next format.
+ * It transforms the following:
+ * - interpolations wrappers from `%{foo}` to `{{foo}}` unless a prefix and/or a suffix are provided
+ * - pluralization messages from a single key containing text like `"key": "foo |||| bar"` to multiple keys `"foo_one": "foo"` and `"foo_other": "bar"`
+ * @param raMessages The translations to convert. This is an object as found in packages such as ra-language-english.
+ * @param options Options to customize the conversion.
+ * @param options.prefix The prefix to use for interpolation variables. Defaults to `{{`.
+ * @param options.suffix The suffix to use for interpolation variables. Defaults to `}}`.
+ * @returns The converted translations as an object.
+ *
+ * @example Convert the english translations from ra-language-english to i18next format
+ * import englishMessages from 'ra-language-english';
+ * import { convertRaMessagesToI18next } from 'ra-i18n-18next';
+ *
+ * const messages = convertRaMessagesToI18next(englishMessages);
+ *
+ * @example Convert the english translations from ra-language-english to i18next format with custom interpolation wrappers
+ * import englishMessages from 'ra-language-english';
+ * import { convertRaMessagesToI18next } from 'ra-i18n-18next';
+ *
+ * const messages = convertRaMessagesToI18next(englishMessages, {
+ * prefix: '#{',
+ * suffix: '}#',
+ * });
+ */
+export const convertRaTranslationsToI18next = (
+ raMessages: object,
{ prefix = '{{', suffix = '}}' } = {}
) => {
return Object.keys(raMessages).reduce((acc, key) => {
if (typeof acc[key] === 'object') {
- acc[key] = convertRaMessagesToI18next(acc[key], { prefix, suffix });
+ acc[key] = convertRaTranslationsToI18next(acc[key], {
+ prefix,
+ suffix,
+ });
return acc;
}
@@ -23,28 +52,63 @@ export const convertRaMessagesToI18next = (
'A message contains more than two plural forms so we can not convert it to i18next format automatically. You should provide your own translations for this language.'
);
}
- acc[`${key}_one`] = convertMessage(pluralVariants[0], {
- prefix,
- suffix,
- });
- acc[`${key}_other`] = convertMessage(pluralVariants[1], {
+ acc[`${key}_one`] = convertRaTranslationToI18next(
+ pluralVariants[0],
+ {
+ prefix,
+ suffix,
+ }
+ );
+ acc[`${key}_other`] = convertRaTranslationToI18next(
+ pluralVariants[1],
+ {
+ prefix,
+ suffix,
+ }
+ );
+ delete acc[key];
+ } else {
+ acc[key] = convertRaTranslationToI18next(message, {
prefix,
suffix,
});
- delete acc[key];
- } else {
- acc[key] = convertMessage(message, { prefix, suffix });
}
return acc;
}, clone(raMessages));
};
-export const convertMessage = (
- message: string,
+/**
+ * A function that takes a single translation text from a standard react-admin language package and converts it to i18next format.
+ * It transforms the interpolations wrappers from `%{foo}` to `{{foo}}` unless a prefix and/or a suffix are provided
+ *
+ * @param translationText The translation text to convert.
+ * @param options Options to customize the conversion.
+ * @param options.prefix The prefix to use for interpolation variables. Defaults to `{{`.
+ * @param options.suffix The suffix to use for interpolation variables. Defaults to `}}`.
+ * @returns The converted translation text.
+ *
+ * @example Convert a single message to i18next format
+ * import { convertRaTranslationToI18next } from 'ra-i18n-18next';
+ *
+ * const messages = convertRaTranslationToI18next("Hello %{name}!");
+ * // "Hello {{name}}!"
+ *
+ * @example Convert the english translations from ra-language-english to i18next format with custom interpolation wrappers
+ * import englishMessages from 'ra-language-english';
+ * import { convertRaTranslationToI18next } from 'ra-i18n-18next';
+ *
+ * const messages = convertRaTranslationToI18next("Hello %{name}!", {
+ * prefix: '#{',
+ * suffix: '}#',
+ * });
+ * // "Hello #{name}#!"
+ */
+export const convertRaTranslationToI18next = (
+ translationText: string,
{ prefix = '{{', suffix = '}}' } = {}
) => {
- const result = message.replace(
+ const result = translationText.replace(
/%\{([a-zA-Z0-9-_]*)\}/g,
(match, p1) => `${prefix}${p1}${suffix}`
);
diff --git a/packages/ra-i18n-i18next/src/index.stories.tsx b/packages/ra-i18n-i18next/src/index.stories.tsx
index c8209a1c38b..abe21a7e94d 100644
--- a/packages/ra-i18n-i18next/src/index.stories.tsx
+++ b/packages/ra-i18n-i18next/src/index.stories.tsx
@@ -6,7 +6,7 @@ import englishMessages from 'ra-language-english';
import fakeRestDataProvider from 'ra-data-fakerest';
import { MemoryRouter } from 'react-router-dom';
import { useI18nextProvider } from './index';
-import { convertRaMessagesToI18next } from './convertRaMessagesToI18next';
+import { convertRaTranslationsToI18next } from './convertRaMessagesToI18next';
export default {
title: 'ra-i18n-18next',
@@ -17,7 +17,9 @@ export const Basic = () => {
options: {
resources: {
en: {
- translation: convertRaMessagesToI18next(englishMessages),
+ translation: convertRaTranslationsToI18next(
+ englishMessages
+ ),
},
},
},
@@ -50,11 +52,11 @@ export const WithLazyLoadedLanguages = () => {
return import(
`ra-language-french`
).then(({ default: messages }) =>
- convertRaMessagesToI18next(messages)
+ convertRaTranslationsToI18next(messages)
);
}
return import(`ra-language-english`).then(({ default: messages }) =>
- convertRaMessagesToI18next(messages)
+ convertRaTranslationsToI18next(messages)
);
})
);
@@ -93,7 +95,7 @@ export const WithCustomTranslations = () => {
resources: {
en: {
translation: {
- ...convertRaMessagesToI18next(englishMessages),
+ ...convertRaTranslationsToI18next(englishMessages),
resources: {
posts: {
name_one: 'Blog post',
@@ -130,7 +132,7 @@ export const WithCustomTranslations = () => {
};
export const WithCustomOptions = () => {
- const defaultMessages = convertRaMessagesToI18next(englishMessages, {
+ const defaultMessages = convertRaTranslationsToI18next(englishMessages, {
prefix: '#{',
suffix: '}#',
});
From 12b03dfddad019c7f66adcdfb539fa09ae7bc224 Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Thu, 28 Sep 2023 15:23:26 +0200
Subject: [PATCH 11/18] Add more documentation
---
packages/ra-i18n-i18next/README.md | 222 +++++++++++++++++-
.../ra-i18n-i18next/src/useI18nextProvider.ts | 52 +++-
2 files changed, 268 insertions(+), 6 deletions(-)
diff --git a/packages/ra-i18n-i18next/README.md b/packages/ra-i18n-i18next/README.md
index 1b008eb2b47..c138c557e10 100644
--- a/packages/ra-i18n-i18next/README.md
+++ b/packages/ra-i18n-i18next/README.md
@@ -1,6 +1,6 @@
-# Polyglot i18n provider for react-admin
+# i18next i18n provider for react-admin
-Polyglot i18n provider for [react-admin](https://github.com/marmelab/react-admin), the frontend framework for building admin applications on top of REST/GraphQL services. It relies on [i18next](https://www.i18next.com/).
+i18next i18n provider for [react-admin](https://github.com/marmelab/react-admin), the frontend framework for building admin applications on top of REST/GraphQL services. It relies on [i18next](https://www.i18next.com/).
## Installation
@@ -10,4 +10,220 @@ npm install --save ra-i18n-i18next
## Usage
-TODO
+```tsx
+import { Admin } from 'react-admin';
+import { useI18nextProvider, convertRaMessagesToI18next } from 'ra-i18n-i18next';
+import englishMessages from 'ra-language-english';
+
+const App = () => {
+ const i18nProvider = useI18nextProvider({
+ options: {
+ resources: {
+ translations: convertRaMessagesToI18next(englishMessages)
+ }
+ }
+ });
+ if (!i18nProvider) return (Loading...
);
+
+ return (
+
+ ...
+
+ );
+};
+```
+
+## API
+
+### `useI18nextProvider` hook
+
+A hook that returns an i18nProvider for react-admin applications, based on i18next.
+
+You can provide your own i18next instance but don't initialize it, the hook will do it for you with the options you may provide. Besides, this hook already adds the `initReactI18next` plugin to i18next.
+
+#### Usage
+
+```tsx
+import { Admin } from 'react-admin';
+import { useI18nextProvider, convertRaMessagesToI18next } from 'ra-i18n-i18next';
+import englishMessages from 'ra-language-english';
+
+const App = () => {
+ const i18nProvider = useI18nextProvider({
+ options: {
+ resources: {
+ translations: convertRaMessagesToI18next(englishMessages)
+ }
+ }
+ });
+ if (!i18nProvider) return (Loading...
);
+
+ return (
+
+ ...
+
+ );
+};
+```
+
+#### Parameters
+
+| Parameter | Required | Type | Default | Description |
+| -------------------- | -------- | ----------- | ------- | ---------------------------------------------------------------- |
+| `i18nextInstance` | Optional | I18n | | Your own i18next instance. If not provided, one will be created. |
+| `options` | Optional | InitOptions | | The options passed to the i18next init function |
+| `availableLocales` | Optional | Locale[] | | An array describing the available locales. Used to automatically include the locale selector menu in the default react-admin AppBar |
+
+##### `i18nextInstance`
+
+This parameter let you pass your own instance of i18next, allowing you to customize its plugins such as the backends.
+
+```tsx
+import { Admin } from 'react-admin';
+import { useI18nextProvider } from 'ra-i18n-i18next';
+import i18n from 'i18next';
+import Backend from 'i18next-http-backend';
+import LanguageDetector from 'i18next-browser-languagedetector';
+
+const App = () => {
+ const i18nextInstance = i18n
+ .use(Backend)
+ .use(LanguageDetector);
+
+ const i18nProvider = useI18nextProvider({
+ i18nInstance: i18nextInstance,
+ options: {
+ fallbackLng: 'en',
+ debug: true,
+ interpolation: {
+ escapeValue: false, // not needed for react!!
+ },
+ }
+ });
+
+ if (!i18nProvider) return (Loading...
);
+
+ return (
+
+ ...
+
+ );
+};
+```
+
+##### `options`
+
+This parameter let you pass your own options for the i18n `init` function.
+
+```tsx
+import { Admin } from 'react-admin';
+import { useI18nextProvider } from 'ra-i18n-i18next';
+import i18n from 'i18next';
+
+const App = () => {
+ const i18nProvider = useI18nextProvider({
+ options: {
+ fallbackLng: 'en',
+ debug: true,
+ }
+ });
+
+ if (!i18nProvider) return (Loading...
);
+
+ return (
+
+ ...
+
+ );
+};
+```
+
+#### `availableLocales`
+
+This parameter let you provide the list of available locales for your application. This is used by the default react-admin AppBar to detect whether to display a locale selector.
+
+```tsx
+import { Admin } from 'react-admin';
+import { useI18nextProvider } from 'ra-i18n-i18next';
+import i18n from 'i18next';
+import resourcesToBackend from 'i18next-resources-to-backend';
+
+const App = () => {
+ const i18nInstance = i18n.use(
+ resourcesToBackend(language => {
+ if (language === 'fr') {
+ return import(
+ `ra-language-french`
+ ).then(({ default: messages }) =>
+ convertRaTranslationsToI18next(messages)
+ );
+ }
+ return import(`ra-language-english`).then(({ default: messages }) =>
+ convertRaTranslationsToI18next(messages)
+ );
+ })
+ );
+
+ const i18nProvider = useI18nextProvider({
+ i18nInstance,
+ availableLocales: [
+ { locale: 'en', name: 'English' },
+ { locale: 'fr', name: 'French' },
+ ],
+ });
+
+ if (!i18nProvider) return (Loading...
);
+
+ return (
+
+ ...
+
+ );
+};
+```
+
+### `convertRaMessagesToI18next` function
+
+A function that takes translations from a standard react-admin language package and converts them to i18next format.
+It transforms the following:
+- interpolations wrappers from `%{foo}` to `{{foo}}` unless a prefix and/or a suffix are provided
+- pluralization messages from a single key containing text like `"key": "foo |||| bar"` to multiple keys `"foo_one": "foo"` and `"foo_other": "bar"`
+
+
+@param raMessages The translations to convert. This is an object as found in packages such as ra-language-english.
+@param options Options to customize the conversion.
+@param options.prefix The prefix to use for interpolation variables. Defaults to `{{`.
+@param options.suffix The suffix to use for interpolation variables. Defaults to `}}`.
+@returns The converted translations as an object.
+
+#### Usage
+
+```ts
+import englishMessages from 'ra-language-english';
+import { convertRaMessagesToI18next } from 'ra-i18n-18next';
+
+const messages = convertRaMessagesToI18next(englishMessages);
+```
+
+#### Parameters
+
+| Parameter | Required | Type | Default | Description |
+| -------------------- | -------- | ----------- | ------- | ---------------------------------------------------------------- |
+| `raMessages` | Required | object | | An object containing standard react-admin translations such as provided by ra-language-english |
+| `options` | Optional | object | | An object providing custom interpolation suffix and/or suffix |
+
+@example Convert the english translations from ra-language-english to i18next format with custom interpolation wrappers
+
+##### `options`
+
+If you provided interpolation options to your i18next instance, you should provide them when calling this function:
+
+```ts
+import englishMessages from 'ra-language-english';
+import { convertRaMessagesToI18next } from 'ra-i18n-18next';
+
+const messages = convertRaMessagesToI18next(englishMessages, {
+ prefix: '#{',
+ suffix: '}#',
+});
+```
diff --git a/packages/ra-i18n-i18next/src/useI18nextProvider.ts b/packages/ra-i18n-i18next/src/useI18nextProvider.ts
index c1006d87c49..adc59001837 100644
--- a/packages/ra-i18n-i18next/src/useI18nextProvider.ts
+++ b/packages/ra-i18n-i18next/src/useI18nextProvider.ts
@@ -4,10 +4,56 @@ import { initReactI18next } from 'react-i18next';
import { I18nProvider, Locale } from 'ra-core';
/**
- * Build a i18next-based i18nProvider.
+ * A hook that returns an i18nProvider for react-admin applications, based on i18next.
+ * You can provide your own i18next instance but don't initialize it, the hook will do it for you with the options you may provide.
+ * Besides, this hook already adds the `initReactI18next` plugin to i18next.
*
- * @example
- * TODO
+ * @example Basic usage
+ * import { Admin } from 'react-admin';
+ * import { useI18nextProvider } from 'ra-i18n-i18next';
+ *
+ * const App = () => {
+ * const i18nProvider = useI18nextProvider();
+ * if (!i18nProvider) return (Loading...
);
+ *
+ * return (
+ *
+ * ...
+ *
+ * );
+ * };
+ *
+ * @example With a custom i18next instance and options
+ * import { Admin } from 'react-admin';
+ * import { useI18nextProvider } from 'ra-i18n-i18next';
+ * import i18n from 'i18next';
+ * import Backend from 'i18next-http-backend';
+ * import LanguageDetector from 'i18next-browser-languagedetector';
+ *
+ * const App = () => {
+ * const i18nextInstance = i18n
+ * .use(Backend)
+ * .use(LanguageDetector);
+ *
+ * const i18nProvider = useI18nextProvider({
+ * i18nInstance: i18nextInstance,
+ * options: {
+ * fallbackLng: 'en',
+ * debug: true,
+ * interpolation: {
+ * escapeValue: false, // not needed for react!!
+ * },
+ * }
+ * });
+ *
+ * if (!i18nProvider) return (Loading...
);
+ *
+ * return (
+ *
+ * ...
+ *
+ * );
+ * };
*/
export const useI18nextProvider = ({
i18nInstance = createInstance(),
From 9b6a41fcdf1a7f5c5562079d66e7dcbf3db0efa2 Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Thu, 28 Sep 2023 15:45:37 +0200
Subject: [PATCH 12/18] Add documentation
---
docs/Reference.md | 1 +
docs/Translation.md | 54 +++++++++++
docs/convertRaMessagesToI18next.md | 43 ++++++++
docs/navigation.html | 2 +
docs/useI18nextProvider.md | 151 +++++++++++++++++++++++++++++
packages/ra-i18n-i18next/README.md | 7 --
6 files changed, 251 insertions(+), 7 deletions(-)
create mode 100644 docs/convertRaMessagesToI18next.md
create mode 100644 docs/useI18nextProvider.md
diff --git a/docs/Reference.md b/docs/Reference.md
index 857fb7763ca..fe345224473 100644
--- a/docs/Reference.md
+++ b/docs/Reference.md
@@ -250,6 +250,7 @@ title: "Index"
**- I -**
* [`useInfiniteGetList`](./useInfiniteGetList.md)
* [`useInput`](./useInput.md)
+* [`useI18nextProvider`](./useI18nextProvider.md)
**- L -**
* [`useList`](./useList.md)
diff --git a/docs/Translation.md b/docs/Translation.md
index d832d571990..4699a418cae 100644
--- a/docs/Translation.md
+++ b/docs/Translation.md
@@ -130,6 +130,60 @@ const App = () => (
Check [the translation setup documentation](./TranslationSetup.md) for details about `ra-i18n-polyglot` and how to configure it.
+## `ra-i18n-i18next`
+
+React-admin also provides a package called `ra-i18n-i18next` that leverages [the i18next library](https://www.i18next.com/) to build an `i18nProvider` based on a dictionary of translations.
+
+```tsx
+// in src/i18nProvider.js
+import { useI18nextProvider, convertRaTranslationsToI18next } from 'ra-i18n-i18next';
+import en from 'ra-language-english';
+import fr from 'ra-language-french';
+
+const i18nInstance = i18n.use(
+ resourcesToBackend(language => {
+ if (language === 'fr') {
+ return import(
+ `ra-language-french`
+ ).then(({ default: messages }) =>
+ convertRaTranslationsToI18next(messages)
+ );
+ }
+ return import(`ra-language-english`).then(({ default: messages }) =>
+ convertRaTranslationsToI18next(messages)
+ );
+ })
+);
+
+export const useMyI18nProvider = useI18nextProvider({
+ i18nInstance,
+ availableLocales: [
+ { locale: 'en', name: 'English' },
+ { locale: 'fr', name: 'French' },
+ ],
+});
+
+// in src/App.js
+import { Admin } from 'react-admin';
+import { useMyI18nProvider } from './i18nProvider';
+
+const App = () => {
+ const i18nProvider = useMyI18nProvider();
+ if (!i18nProvider) return null;
+
+ return (
+
+ ...
+
+ );
+};
+```
+
+Check [the useI18nextProvider documentation](./useI18nextProvider.md) for details about `ra-i18n-i18next` and how to configure it.
+
## Translation Files
`ra-i18n-polyglot` relies on JSON objects for translations. This means that the only thing required to add support for a new language is a JSON file.
diff --git a/docs/convertRaMessagesToI18next.md b/docs/convertRaMessagesToI18next.md
new file mode 100644
index 00000000000..6945be0f929
--- /dev/null
+++ b/docs/convertRaMessagesToI18next.md
@@ -0,0 +1,43 @@
+---
+layout: default
+title: "convertRaTranslationsToI18next"
+---
+
+# `convertRaTranslationsToI18next`
+
+A function that takes translations from a standard react-admin language package and converts them to i18next format.
+It transforms the following:
+- interpolations wrappers from `%{foo}` to `{{foo}}` unless a prefix and/or a suffix are provided
+- pluralization messages from a single key containing text like `"key": "foo |||| bar"` to multiple keys `"foo_one": "foo"` and `"foo_other": "bar"`
+
+## Usage
+
+```ts
+import englishMessages from 'ra-language-english';
+import { convertRaMessagesToI18next } from 'ra-i18n-18next';
+
+const messages = convertRaMessagesToI18next(englishMessages);
+```
+
+## Parameters
+
+| Parameter | Required | Type | Default | Description |
+| -------------------- | -------- | ----------- | ------- | ---------------------------------------------------------------- |
+| `raMessages` | Required | object | | An object containing standard react-admin translations such as provided by ra-language-english |
+| `options` | Optional | object | | An object providing custom interpolation suffix and/or suffix |
+
+@example Convert the english translations from ra-language-english to i18next format with custom interpolation wrappers
+
+### `options`
+
+If you provided interpolation options to your i18next instance, you should provide them when calling this function:
+
+```ts
+import englishMessages from 'ra-language-english';
+import { convertRaMessagesToI18next } from 'ra-i18n-18next';
+
+const messages = convertRaMessagesToI18next(englishMessages, {
+ prefix: '#{',
+ suffix: '}#',
+});
+```
diff --git a/docs/navigation.html b/docs/navigation.html
index 91e7c3bf93a..fc337143d06 100644
--- a/docs/navigation.html
+++ b/docs/navigation.html
@@ -226,6 +226,8 @@
useTranslate
useLocaleState
<LocalesMenuButton>
+ useI18nextProvider
+ convertRaTranslationsToI18next
Other UI components
diff --git a/docs/useI18nextProvider.md b/docs/useI18nextProvider.md
new file mode 100644
index 00000000000..91dfc5551b8
--- /dev/null
+++ b/docs/useI18nextProvider.md
@@ -0,0 +1,151 @@
+---
+layout: default
+title: "useI18nextProvider"
+---
+
+# `useI18nextProvider`
+
+A hook that returns an i18nProvider for react-admin applications, based on i18next.
+
+You can provide your own i18next instance but don't initialize it, the hook will do it for you with the options you may provide. Besides, this hook already adds the `initReactI18next` plugin to i18next.
+
+## Usage
+
+```tsx
+import { Admin } from 'react-admin';
+import { useI18nextProvider, convertRaMessagesToI18next } from 'ra-i18n-i18next';
+import englishMessages from 'ra-language-english';
+
+const App = () => {
+ const i18nProvider = useI18nextProvider({
+ options: {
+ resources: {
+ translations: convertRaMessagesToI18next(englishMessages)
+ }
+ }
+ });
+ if (!i18nProvider) return (Loading...
);
+
+ return (
+
+ ...
+
+ );
+};
+```
+
+## Parameters
+
+| Parameter | Required | Type | Default | Description |
+| -------------------- | -------- | ----------- | ------- | ---------------------------------------------------------------- |
+| `i18nextInstance` | Optional | I18n | | Your own i18next instance. If not provided, one will be created. |
+| `options` | Optional | InitOptions | | The options passed to the i18next init function |
+| `availableLocales` | Optional | Locale[] | | An array describing the available locales. Used to automatically include the locale selector menu in the default react-admin AppBar |
+
+### `i18nextInstance`
+
+This parameter let you pass your own instance of i18next, allowing you to customize its plugins such as the backends.
+
+```tsx
+import { Admin } from 'react-admin';
+import { useI18nextProvider } from 'ra-i18n-i18next';
+import i18n from 'i18next';
+import Backend from 'i18next-http-backend';
+import LanguageDetector from 'i18next-browser-languagedetector';
+
+const App = () => {
+ const i18nextInstance = i18n
+ .use(Backend)
+ .use(LanguageDetector);
+
+ const i18nProvider = useI18nextProvider({
+ i18nInstance: i18nextInstance,
+ options: {
+ fallbackLng: 'en',
+ debug: true,
+ interpolation: {
+ escapeValue: false, // not needed for react!!
+ },
+ }
+ });
+
+ if (!i18nProvider) return (Loading...
);
+
+ return (
+
+ ...
+
+ );
+};
+```
+
+### `options`
+
+This parameter let you pass your own options for the i18n `init` function.
+
+```tsx
+import { Admin } from 'react-admin';
+import { useI18nextProvider } from 'ra-i18n-i18next';
+import i18n from 'i18next';
+
+const App = () => {
+ const i18nProvider = useI18nextProvider({
+ options: {
+ fallbackLng: 'en',
+ debug: true,
+ }
+ });
+
+ if (!i18nProvider) return (Loading...
);
+
+ return (
+
+ ...
+
+ );
+};
+```
+
+## `availableLocales`
+
+This parameter let you provide the list of available locales for your application. This is used by the default react-admin AppBar to detect whether to display a locale selector.
+
+```tsx
+import { Admin } from 'react-admin';
+import { useI18nextProvider } from 'ra-i18n-i18next';
+import i18n from 'i18next';
+import resourcesToBackend from 'i18next-resources-to-backend';
+
+const App = () => {
+ const i18nInstance = i18n.use(
+ resourcesToBackend(language => {
+ if (language === 'fr') {
+ return import(
+ `ra-language-french`
+ ).then(({ default: messages }) =>
+ convertRaTranslationsToI18next(messages)
+ );
+ }
+ return import(`ra-language-english`).then(({ default: messages }) =>
+ convertRaTranslationsToI18next(messages)
+ );
+ })
+ );
+
+ const i18nProvider = useI18nextProvider({
+ i18nInstance,
+ availableLocales: [
+ { locale: 'en', name: 'English' },
+ { locale: 'fr', name: 'French' },
+ ],
+ });
+
+ if (!i18nProvider) return (Loading...
);
+
+ return (
+
+ ...
+
+ );
+};
+```
\ No newline at end of file
diff --git a/packages/ra-i18n-i18next/README.md b/packages/ra-i18n-i18next/README.md
index c138c557e10..47d5ea4679e 100644
--- a/packages/ra-i18n-i18next/README.md
+++ b/packages/ra-i18n-i18next/README.md
@@ -189,13 +189,6 @@ It transforms the following:
- interpolations wrappers from `%{foo}` to `{{foo}}` unless a prefix and/or a suffix are provided
- pluralization messages from a single key containing text like `"key": "foo |||| bar"` to multiple keys `"foo_one": "foo"` and `"foo_other": "bar"`
-
-@param raMessages The translations to convert. This is an object as found in packages such as ra-language-english.
-@param options Options to customize the conversion.
-@param options.prefix The prefix to use for interpolation variables. Defaults to `{{`.
-@param options.suffix The suffix to use for interpolation variables. Defaults to `}}`.
-@returns The converted translations as an object.
-
#### Usage
```ts
From d86b6b1c643794412531575b8c9a6d48e68eab18 Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Mon, 2 Oct 2023 12:17:31 +0200
Subject: [PATCH 13/18] Apply review suggestions
---
docs/Translation.md | 11 +-
docs/convertRaMessagesToI18next.md | 43 -----
docs/useI18nextProvider.md | 151 ------------------
packages/ra-i18n-i18next/README.md | 27 ++--
.../ra-i18n-i18next/src/index.stories.tsx | 4 +-
.../ra-i18n-i18next/src/useI18nextProvider.ts | 18 +--
6 files changed, 34 insertions(+), 220 deletions(-)
delete mode 100644 docs/convertRaMessagesToI18next.md
delete mode 100644 docs/useI18nextProvider.md
diff --git a/docs/Translation.md b/docs/Translation.md
index 4699a418cae..446bed5055b 100644
--- a/docs/Translation.md
+++ b/docs/Translation.md
@@ -134,6 +134,15 @@ Check [the translation setup documentation](./TranslationSetup.md) for details a
React-admin also provides a package called `ra-i18n-i18next` that leverages [the i18next library](https://www.i18next.com/) to build an `i18nProvider` based on a dictionary of translations.
+You might prefer this package over `ra-i18n-polyglot` when:
+- you already use i18next services such as [locize](https://locize.com/)
+- you want more control on how you organize translations, leveraging [multiple files and namespaces](https://www.i18next.com/principles/namespaces)
+- you want more control on how you [load translations](https://www.i18next.com/how-to/add-or-load-translations)
+- you want to use features not available in Polyglot such as:
+ - [advanced formatting](https://www.i18next.com/translation-function/formatting);
+ - [nested translations](https://www.i18next.com/translation-function/nesting)
+ - [context](https://www.i18next.com/translation-function/context)
+
```tsx
// in src/i18nProvider.js
import { useI18nextProvider, convertRaTranslationsToI18next } from 'ra-i18n-i18next';
@@ -182,7 +191,7 @@ const App = () => {
};
```
-Check [the useI18nextProvider documentation](./useI18nextProvider.md) for details about `ra-i18n-i18next` and how to configure it.
+Check [the ra-i18n-18next documentation](https://github.com/marmelab/react-admin/tree/master/packages/ra-i18n-18next) for details.
## Translation Files
diff --git a/docs/convertRaMessagesToI18next.md b/docs/convertRaMessagesToI18next.md
deleted file mode 100644
index 6945be0f929..00000000000
--- a/docs/convertRaMessagesToI18next.md
+++ /dev/null
@@ -1,43 +0,0 @@
----
-layout: default
-title: "convertRaTranslationsToI18next"
----
-
-# `convertRaTranslationsToI18next`
-
-A function that takes translations from a standard react-admin language package and converts them to i18next format.
-It transforms the following:
-- interpolations wrappers from `%{foo}` to `{{foo}}` unless a prefix and/or a suffix are provided
-- pluralization messages from a single key containing text like `"key": "foo |||| bar"` to multiple keys `"foo_one": "foo"` and `"foo_other": "bar"`
-
-## Usage
-
-```ts
-import englishMessages from 'ra-language-english';
-import { convertRaMessagesToI18next } from 'ra-i18n-18next';
-
-const messages = convertRaMessagesToI18next(englishMessages);
-```
-
-## Parameters
-
-| Parameter | Required | Type | Default | Description |
-| -------------------- | -------- | ----------- | ------- | ---------------------------------------------------------------- |
-| `raMessages` | Required | object | | An object containing standard react-admin translations such as provided by ra-language-english |
-| `options` | Optional | object | | An object providing custom interpolation suffix and/or suffix |
-
-@example Convert the english translations from ra-language-english to i18next format with custom interpolation wrappers
-
-### `options`
-
-If you provided interpolation options to your i18next instance, you should provide them when calling this function:
-
-```ts
-import englishMessages from 'ra-language-english';
-import { convertRaMessagesToI18next } from 'ra-i18n-18next';
-
-const messages = convertRaMessagesToI18next(englishMessages, {
- prefix: '#{',
- suffix: '}#',
-});
-```
diff --git a/docs/useI18nextProvider.md b/docs/useI18nextProvider.md
deleted file mode 100644
index 91dfc5551b8..00000000000
--- a/docs/useI18nextProvider.md
+++ /dev/null
@@ -1,151 +0,0 @@
----
-layout: default
-title: "useI18nextProvider"
----
-
-# `useI18nextProvider`
-
-A hook that returns an i18nProvider for react-admin applications, based on i18next.
-
-You can provide your own i18next instance but don't initialize it, the hook will do it for you with the options you may provide. Besides, this hook already adds the `initReactI18next` plugin to i18next.
-
-## Usage
-
-```tsx
-import { Admin } from 'react-admin';
-import { useI18nextProvider, convertRaMessagesToI18next } from 'ra-i18n-i18next';
-import englishMessages from 'ra-language-english';
-
-const App = () => {
- const i18nProvider = useI18nextProvider({
- options: {
- resources: {
- translations: convertRaMessagesToI18next(englishMessages)
- }
- }
- });
- if (!i18nProvider) return (Loading...
);
-
- return (
-
- ...
-
- );
-};
-```
-
-## Parameters
-
-| Parameter | Required | Type | Default | Description |
-| -------------------- | -------- | ----------- | ------- | ---------------------------------------------------------------- |
-| `i18nextInstance` | Optional | I18n | | Your own i18next instance. If not provided, one will be created. |
-| `options` | Optional | InitOptions | | The options passed to the i18next init function |
-| `availableLocales` | Optional | Locale[] | | An array describing the available locales. Used to automatically include the locale selector menu in the default react-admin AppBar |
-
-### `i18nextInstance`
-
-This parameter let you pass your own instance of i18next, allowing you to customize its plugins such as the backends.
-
-```tsx
-import { Admin } from 'react-admin';
-import { useI18nextProvider } from 'ra-i18n-i18next';
-import i18n from 'i18next';
-import Backend from 'i18next-http-backend';
-import LanguageDetector from 'i18next-browser-languagedetector';
-
-const App = () => {
- const i18nextInstance = i18n
- .use(Backend)
- .use(LanguageDetector);
-
- const i18nProvider = useI18nextProvider({
- i18nInstance: i18nextInstance,
- options: {
- fallbackLng: 'en',
- debug: true,
- interpolation: {
- escapeValue: false, // not needed for react!!
- },
- }
- });
-
- if (!i18nProvider) return (Loading...
);
-
- return (
-
- ...
-
- );
-};
-```
-
-### `options`
-
-This parameter let you pass your own options for the i18n `init` function.
-
-```tsx
-import { Admin } from 'react-admin';
-import { useI18nextProvider } from 'ra-i18n-i18next';
-import i18n from 'i18next';
-
-const App = () => {
- const i18nProvider = useI18nextProvider({
- options: {
- fallbackLng: 'en',
- debug: true,
- }
- });
-
- if (!i18nProvider) return (Loading...
);
-
- return (
-
- ...
-
- );
-};
-```
-
-## `availableLocales`
-
-This parameter let you provide the list of available locales for your application. This is used by the default react-admin AppBar to detect whether to display a locale selector.
-
-```tsx
-import { Admin } from 'react-admin';
-import { useI18nextProvider } from 'ra-i18n-i18next';
-import i18n from 'i18next';
-import resourcesToBackend from 'i18next-resources-to-backend';
-
-const App = () => {
- const i18nInstance = i18n.use(
- resourcesToBackend(language => {
- if (language === 'fr') {
- return import(
- `ra-language-french`
- ).then(({ default: messages }) =>
- convertRaTranslationsToI18next(messages)
- );
- }
- return import(`ra-language-english`).then(({ default: messages }) =>
- convertRaTranslationsToI18next(messages)
- );
- })
- );
-
- const i18nProvider = useI18nextProvider({
- i18nInstance,
- availableLocales: [
- { locale: 'en', name: 'English' },
- { locale: 'fr', name: 'French' },
- ],
- });
-
- if (!i18nProvider) return (Loading...
);
-
- return (
-
- ...
-
- );
-};
-```
\ No newline at end of file
diff --git a/packages/ra-i18n-i18next/README.md b/packages/ra-i18n-i18next/README.md
index 47d5ea4679e..745ffba503f 100644
--- a/packages/ra-i18n-i18next/README.md
+++ b/packages/ra-i18n-i18next/README.md
@@ -76,7 +76,7 @@ const App = () => {
##### `i18nextInstance`
-This parameter let you pass your own instance of i18next, allowing you to customize its plugins such as the backends.
+This parameter lets you pass your own instance of i18next, allowing you to customize its plugins such as the backends.
```tsx
import { Admin } from 'react-admin';
@@ -91,14 +91,7 @@ const App = () => {
.use(LanguageDetector);
const i18nProvider = useI18nextProvider({
- i18nInstance: i18nextInstance,
- options: {
- fallbackLng: 'en',
- debug: true,
- interpolation: {
- escapeValue: false, // not needed for react!!
- },
- }
+ i18nextInstance
});
if (!i18nProvider) return (Loading...
);
@@ -113,7 +106,9 @@ const App = () => {
##### `options`
-This parameter let you pass your own options for the i18n `init` function.
+This parameter lets you pass your own options for the i18n `init` function.
+
+Please refer to [the i18next documentation](https://www.i18next.com/overview/configuration-options) for details.
```tsx
import { Admin } from 'react-admin';
@@ -123,7 +118,6 @@ import i18n from 'i18next';
const App = () => {
const i18nProvider = useI18nextProvider({
options: {
- fallbackLng: 'en',
debug: true,
}
});
@@ -140,7 +134,7 @@ const App = () => {
#### `availableLocales`
-This parameter let you provide the list of available locales for your application. This is used by the default react-admin AppBar to detect whether to display a locale selector.
+This parameter lets you provide the list of available locales for your application. This is used by the default react-admin AppBar to detect whether to display a locale selector.
```tsx
import { Admin } from 'react-admin';
@@ -149,15 +143,20 @@ import i18n from 'i18next';
import resourcesToBackend from 'i18next-resources-to-backend';
const App = () => {
- const i18nInstance = i18n.use(
+ const i18nextInstance = i18n.use(
+ // Here we use a Backend provided by i18next that allows us to load
+ // the translations however we want.
+ // See https://www.i18next.com/how-to/add-or-load-translations#lazy-load-in-memory-translations
resourcesToBackend(language => {
if (language === 'fr') {
+ // Load the ra-language-french package and convert its translations in i18next format
return import(
`ra-language-french`
).then(({ default: messages }) =>
convertRaTranslationsToI18next(messages)
);
}
+ // Load the ra-language-english package and convert its translations in i18next format
return import(`ra-language-english`).then(({ default: messages }) =>
convertRaTranslationsToI18next(messages)
);
@@ -165,7 +164,7 @@ const App = () => {
);
const i18nProvider = useI18nextProvider({
- i18nInstance,
+ i18nextInstance,
availableLocales: [
{ locale: 'en', name: 'English' },
{ locale: 'fr', name: 'French' },
diff --git a/packages/ra-i18n-i18next/src/index.stories.tsx b/packages/ra-i18n-i18next/src/index.stories.tsx
index abe21a7e94d..50ad623e7a5 100644
--- a/packages/ra-i18n-i18next/src/index.stories.tsx
+++ b/packages/ra-i18n-i18next/src/index.stories.tsx
@@ -46,7 +46,7 @@ export const Basic = () => {
};
export const WithLazyLoadedLanguages = () => {
- const i18nInstance = i18n.use(
+ const i18nextInstance = i18n.use(
resourcesToBackend(language => {
if (language === 'fr') {
return import(
@@ -62,7 +62,7 @@ export const WithLazyLoadedLanguages = () => {
);
const i18nProvider = useI18nextProvider({
- i18nInstance,
+ i18nextInstance,
availableLocales: [
{ locale: 'en', name: 'English' },
{ locale: 'fr', name: 'French' },
diff --git a/packages/ra-i18n-i18next/src/useI18nextProvider.ts b/packages/ra-i18n-i18next/src/useI18nextProvider.ts
index adc59001837..64a507315e8 100644
--- a/packages/ra-i18n-i18next/src/useI18nextProvider.ts
+++ b/packages/ra-i18n-i18next/src/useI18nextProvider.ts
@@ -56,11 +56,11 @@ import { I18nProvider, Locale } from 'ra-core';
* };
*/
export const useI18nextProvider = ({
- i18nInstance = createInstance(),
+ i18nextInstance = createInstance(),
options = {},
availableLocales = [{ locale: 'en', name: 'English' }],
}: {
- i18nInstance?: I18n;
+ i18nextInstance?: I18n;
options?: InitOptions;
availableLocales?: Locale[];
} = {}) => {
@@ -73,26 +73,26 @@ export const useI18nextProvider = ({
}
initializationPromise.current = getI18nProvider(
- i18nInstance,
+ i18nextInstance,
options,
availableLocales
).then(provider => {
setI18nProvider(provider);
return provider;
});
- }, [availableLocales, i18nInstance, options]);
+ }, [availableLocales, i18nextInstance, options]);
return i18nProvider;
};
export const getI18nProvider = async (
- i18nInstance: I18n,
+ i18nextInstance: I18n,
options?: InitOptions,
availableLocales: Locale[] = [{ locale: 'en', name: 'English' }]
): Promise => {
let translate: TFunction;
- await i18nInstance
+ await i18nextInstance
.use(initReactI18next)
.init({
lng: 'en',
@@ -115,11 +115,11 @@ export const getI18nProvider = async (
}).toString();
},
changeLocale: async (newLocale: string) => {
- await i18nInstance.loadLanguages(newLocale);
- const t = await i18nInstance.changeLanguage(newLocale);
+ await i18nextInstance.loadLanguages(newLocale);
+ const t = await i18nextInstance.changeLanguage(newLocale);
translate = t;
},
- getLocale: () => i18nInstance.language,
+ getLocale: () => i18nextInstance.language,
getLocales: () => {
return availableLocales;
},
From c44eed0184c041eb7fab777587f43937c82c57c9 Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Mon, 2 Oct 2023 12:18:05 +0200
Subject: [PATCH 14/18] Update reference
---
docs/Reference.md | 1 -
1 file changed, 1 deletion(-)
diff --git a/docs/Reference.md b/docs/Reference.md
index fe345224473..857fb7763ca 100644
--- a/docs/Reference.md
+++ b/docs/Reference.md
@@ -250,7 +250,6 @@ title: "Index"
**- I -**
* [`useInfiniteGetList`](./useInfiniteGetList.md)
* [`useInput`](./useInput.md)
-* [`useI18nextProvider`](./useI18nextProvider.md)
**- L -**
* [`useList`](./useList.md)
From c0a06efe916f1d8e5f7399dd8244ea8e32d5ea12 Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Mon, 2 Oct 2023 17:15:26 +0200
Subject: [PATCH 15/18] Apply suggestions from review
---
docs/Translation.md | 5 ++--
docs/navigation.html | 2 --
packages/ra-i18n-i18next/README.md | 13 +++++++---
packages/ra-i18n-i18next/package.json | 4 ++--
...> convertRaTranslationsToI18next.spec.tsx} | 0
...t.ts => convertRaTranslationsToI18next.ts} | 0
.../ra-i18n-i18next/src/index.stories.tsx | 10 ++++----
packages/ra-i18n-i18next/src/index.ts | 2 +-
packages/ra-i18n-i18next/src/stories-en.ts | 24 +++++++++++++++++++
packages/ra-i18n-i18next/src/stories-fr.ts | 24 +++++++++++++++++++
10 files changed, 68 insertions(+), 16 deletions(-)
rename packages/ra-i18n-i18next/src/{convertRaMessagesToI18next.spec.tsx => convertRaTranslationsToI18next.spec.tsx} (100%)
rename packages/ra-i18n-i18next/src/{convertRaMessagesToI18next.ts => convertRaTranslationsToI18next.ts} (100%)
create mode 100644 packages/ra-i18n-i18next/src/stories-en.ts
create mode 100644 packages/ra-i18n-i18next/src/stories-fr.ts
diff --git a/docs/Translation.md b/docs/Translation.md
index 446bed5055b..4f85120dc0f 100644
--- a/docs/Translation.md
+++ b/docs/Translation.md
@@ -145,6 +145,7 @@ You might prefer this package over `ra-i18n-polyglot` when:
```tsx
// in src/i18nProvider.js
+import i18n from 'i18next';
import { useI18nextProvider, convertRaTranslationsToI18next } from 'ra-i18n-i18next';
import en from 'ra-language-english';
import fr from 'ra-language-french';
@@ -172,7 +173,7 @@ export const useMyI18nProvider = useI18nextProvider({
],
});
-// in src/App.js
+// in src/App.tsx
import { Admin } from 'react-admin';
import { useMyI18nProvider } from './i18nProvider';
@@ -191,7 +192,7 @@ const App = () => {
};
```
-Check [the ra-i18n-18next documentation](https://github.com/marmelab/react-admin/tree/master/packages/ra-i18n-18next) for details.
+Check [the ra-i18n-i18next documentation](https://github.com/marmelab/react-admin/tree/master/packages/ra-i18n-i18next) for details.
## Translation Files
diff --git a/docs/navigation.html b/docs/navigation.html
index fc337143d06..91e7c3bf93a 100644
--- a/docs/navigation.html
+++ b/docs/navigation.html
@@ -226,8 +226,6 @@
useTranslate
useLocaleState
<LocalesMenuButton>
- useI18nextProvider
- convertRaTranslationsToI18next
Other UI components
diff --git a/packages/ra-i18n-i18next/README.md b/packages/ra-i18n-i18next/README.md
index 745ffba503f..81af7c7fcb7 100644
--- a/packages/ra-i18n-i18next/README.md
+++ b/packages/ra-i18n-i18next/README.md
@@ -2,6 +2,15 @@
i18next i18n provider for [react-admin](https://github.com/marmelab/react-admin), the frontend framework for building admin applications on top of REST/GraphQL services. It relies on [i18next](https://www.i18next.com/).
+You might prefer this package over `ra-i18n-polyglot` when:
+- you already use i18next services such as [locize](https://locize.com/)
+- you want more control on how you organize translations, leveraging [multiple files and namespaces](https://www.i18next.com/principles/namespaces)
+- you want more control on how you [load translations](https://www.i18next.com/how-to/add-or-load-translations)
+- you want to use features not available in Polyglot such as:
+ - [advanced formatting](https://www.i18next.com/translation-function/formatting);
+ - [nested translations](https://www.i18next.com/translation-function/nesting)
+ - [context](https://www.i18next.com/translation-function/context)
+
## Installation
```sh
@@ -138,7 +147,7 @@ This parameter lets you provide the list of available locales for your applicati
```tsx
import { Admin } from 'react-admin';
-import { useI18nextProvider } from 'ra-i18n-i18next';
+import { useI18nextProvider, convertRaTranslationsToI18next } from 'ra-i18n-i18next';
import i18n from 'i18next';
import resourcesToBackend from 'i18next-resources-to-backend';
@@ -204,8 +213,6 @@ const messages = convertRaMessagesToI18next(englishMessages);
| `raMessages` | Required | object | | An object containing standard react-admin translations such as provided by ra-language-english |
| `options` | Optional | object | | An object providing custom interpolation suffix and/or suffix |
-@example Convert the english translations from ra-language-english to i18next format with custom interpolation wrappers
-
##### `options`
If you provided interpolation options to your i18next instance, you should provide them when calling this function:
diff --git a/packages/ra-i18n-i18next/package.json b/packages/ra-i18n-i18next/package.json
index 07dd91b5a61..6935e9f421d 100644
--- a/packages/ra-i18n-i18next/package.json
+++ b/packages/ra-i18n-i18next/package.json
@@ -26,12 +26,12 @@
},
"dependencies": {
"i18next": "^23.5.1",
- "ra-core": "^4.13.4"
+ "ra-core": "^4.13.4",
+ "react-i18next": "^13.2.2"
},
"devDependencies": {
"cross-env": "^5.2.0",
"i18next-resources-to-backend": "^1.1.4",
- "react-i18next": "^13.2.2",
"rimraf": "^3.0.2",
"typescript": "^5.1.3"
},
diff --git a/packages/ra-i18n-i18next/src/convertRaMessagesToI18next.spec.tsx b/packages/ra-i18n-i18next/src/convertRaTranslationsToI18next.spec.tsx
similarity index 100%
rename from packages/ra-i18n-i18next/src/convertRaMessagesToI18next.spec.tsx
rename to packages/ra-i18n-i18next/src/convertRaTranslationsToI18next.spec.tsx
diff --git a/packages/ra-i18n-i18next/src/convertRaMessagesToI18next.ts b/packages/ra-i18n-i18next/src/convertRaTranslationsToI18next.ts
similarity index 100%
rename from packages/ra-i18n-i18next/src/convertRaMessagesToI18next.ts
rename to packages/ra-i18n-i18next/src/convertRaTranslationsToI18next.ts
diff --git a/packages/ra-i18n-i18next/src/index.stories.tsx b/packages/ra-i18n-i18next/src/index.stories.tsx
index 50ad623e7a5..02c255afe7b 100644
--- a/packages/ra-i18n-i18next/src/index.stories.tsx
+++ b/packages/ra-i18n-i18next/src/index.stories.tsx
@@ -6,10 +6,10 @@ import englishMessages from 'ra-language-english';
import fakeRestDataProvider from 'ra-data-fakerest';
import { MemoryRouter } from 'react-router-dom';
import { useI18nextProvider } from './index';
-import { convertRaTranslationsToI18next } from './convertRaMessagesToI18next';
+import { convertRaTranslationsToI18next } from './convertRaTranslationsToI18next';
export default {
- title: 'ra-i18n-18next',
+ title: 'ra-i18n-i18next',
};
export const Basic = () => {
@@ -49,13 +49,11 @@ export const WithLazyLoadedLanguages = () => {
const i18nextInstance = i18n.use(
resourcesToBackend(language => {
if (language === 'fr') {
- return import(
- `ra-language-french`
- ).then(({ default: messages }) =>
+ return import(`./stories-fr`).then(({ default: messages }) =>
convertRaTranslationsToI18next(messages)
);
}
- return import(`ra-language-english`).then(({ default: messages }) =>
+ return import(`./stories-en`).then(({ default: messages }) =>
convertRaTranslationsToI18next(messages)
);
})
diff --git a/packages/ra-i18n-i18next/src/index.ts b/packages/ra-i18n-i18next/src/index.ts
index a91c2bc04db..1059f73135e 100644
--- a/packages/ra-i18n-i18next/src/index.ts
+++ b/packages/ra-i18n-i18next/src/index.ts
@@ -1,2 +1,2 @@
export * from './useI18nextProvider';
-export * from './convertRaMessagesToI18next';
+export * from './convertRaTranslationsToI18next';
diff --git a/packages/ra-i18n-i18next/src/stories-en.ts b/packages/ra-i18n-i18next/src/stories-en.ts
new file mode 100644
index 00000000000..86f61424798
--- /dev/null
+++ b/packages/ra-i18n-i18next/src/stories-en.ts
@@ -0,0 +1,24 @@
+import raMessages from 'ra-language-french';
+import { convertRaTranslationsToI18next } from './convertRaTranslationsToI18next';
+
+export default {
+ ...convertRaTranslationsToI18next(raMessages),
+ resources: {
+ posts: {
+ name_one: 'Post',
+ name_other: 'Posts',
+ fields: {
+ id: 'Id',
+ title: 'Title',
+ },
+ },
+ comments: {
+ name_one: 'Comment',
+ name_other: 'Comments',
+ fields: {
+ id: 'Id',
+ body: 'Message',
+ },
+ },
+ },
+};
diff --git a/packages/ra-i18n-i18next/src/stories-fr.ts b/packages/ra-i18n-i18next/src/stories-fr.ts
new file mode 100644
index 00000000000..8e577a8b747
--- /dev/null
+++ b/packages/ra-i18n-i18next/src/stories-fr.ts
@@ -0,0 +1,24 @@
+import raMessages from 'ra-language-french';
+import { convertRaTranslationsToI18next } from './convertRaTranslationsToI18next';
+
+export default {
+ ...convertRaTranslationsToI18next(raMessages),
+ resources: {
+ posts: {
+ name_one: 'Article',
+ name_other: 'Articles',
+ fields: {
+ id: 'Id',
+ title: 'Titre',
+ },
+ },
+ comments: {
+ name_one: 'Commentaire',
+ name_other: 'Commentaires',
+ fields: {
+ id: 'Id',
+ body: 'Message',
+ },
+ },
+ },
+};
From 7b65502bc192f5e512125c9a35b147415449b66f Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Tue, 3 Oct 2023 09:04:36 +0200
Subject: [PATCH 16/18] Fix tests
---
.../src/convertRaTranslationsToI18next.spec.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/packages/ra-i18n-i18next/src/convertRaTranslationsToI18next.spec.tsx b/packages/ra-i18n-i18next/src/convertRaTranslationsToI18next.spec.tsx
index 822b0f64ec8..dbb243b2606 100644
--- a/packages/ra-i18n-i18next/src/convertRaTranslationsToI18next.spec.tsx
+++ b/packages/ra-i18n-i18next/src/convertRaTranslationsToI18next.spec.tsx
@@ -1,7 +1,7 @@
-import { convertRaTranslationsToI18next } from './convertRaMessagesToI18next';
+import { convertRaTranslationsToI18next } from './convertRaTranslationsToI18next';
describe('i18next i18nProvider', () => {
- describe('convertRaMessagesToI18next', () => {
+ describe('convertRaTranslationsToI18next', () => {
test('should convert react-admin default messages to i18next format', () => {
expect(
convertRaTranslationsToI18next(
From ff0c668cdaea0464da8975aafe9490a45523895f Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Tue, 3 Oct 2023 09:34:34 +0200
Subject: [PATCH 17/18] Fix wrong custom translations in stories
---
packages/ra-i18n-i18next/src/stories-en.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/ra-i18n-i18next/src/stories-en.ts b/packages/ra-i18n-i18next/src/stories-en.ts
index 86f61424798..9dcdb278742 100644
--- a/packages/ra-i18n-i18next/src/stories-en.ts
+++ b/packages/ra-i18n-i18next/src/stories-en.ts
@@ -1,4 +1,4 @@
-import raMessages from 'ra-language-french';
+import raMessages from 'ra-language-english';
import { convertRaTranslationsToI18next } from './convertRaTranslationsToI18next';
export default {
From 1071f8cbb8cba069dfb358415bbc3a03e20a9406 Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Tue, 3 Oct 2023 11:29:22 +0200
Subject: [PATCH 18/18] Fix documentation
---
docs/Translation.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/Translation.md b/docs/Translation.md
index 4f85120dc0f..ca214e71cbb 100644
--- a/docs/Translation.md
+++ b/docs/Translation.md
@@ -165,7 +165,7 @@ const i18nInstance = i18n.use(
})
);
-export const useMyI18nProvider = useI18nextProvider({
+export const useMyI18nProvider = () => useI18nextProvider({
i18nInstance,
availableLocales: [
{ locale: 'en', name: 'English' },