diff --git a/docs/admin/components.mdx b/docs/admin/components.mdx index 5a17d6d1d5..97c66da177 100644 --- a/docs/admin/components.mdx +++ b/docs/admin/components.mdx @@ -231,6 +231,29 @@ To make use of Payload SCSS variables / mixins to use directly in your own compo @import '~payload/scss'; ``` +### Getting the current language + +When developing custom components you can support multiple languages to be consistent with Payload's i18n support. The best way to do this is to add your translation resources to the [i18n configuration](https://payloadcms.com/docs/configuration/i18n) and import `useTranslation` from `react-i18next` in your components. + +For example: +```tsx +import { useTranslation } from 'react-i18next'; + +const CustomComponent: React.FC = () => { + // highlight-start + const { t, i18n } = useTranslation('namespace1'); + // highlight-end + + return ( + + ); +}; +``` + ### Getting the current locale In any custom component you can get the selected locale with the `useLocale` hook. Here is a simple example: diff --git a/docs/configuration/collections.mdx b/docs/configuration/collections.mdx index 3553e1af45..e44a0408be 100644 --- a/docs/configuration/collections.mdx +++ b/docs/configuration/collections.mdx @@ -12,19 +12,21 @@ It's often best practice to write your Collections in separate files and then im ## Options -| Option | Description | -| ---------------- | -------------| -| **`slug`** * | Unique, URL-friendly string that will act as an identifier for this Collection. | +| Option | Description | +|------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **`slug`** * | Unique, URL-friendly string that will act as an identifier for this Collection. | | **`fields`** * | Array of field types that will determine the structure and functionality of the data stored within this Collection. [Click here](/docs/fields/overview) for a full list of field types as well as how to configure them. | -| **`labels`** | Singular and plural labels for use in identifying this Collection throughout Payload. Auto-generated from slug if not defined. | -| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-options). | -| **`hooks`** | Entry points to "tie in" to Collection actions at specific points. [More](/docs/hooks/overview#collection-hooks) | -| **`access`** | Provide access control functions to define exactly who should be able to do what with Documents in this Collection. [More](/docs/access-control/overview/#collections) | -| **`auth`** | Specify options if you would like this Collection to feature authentication. For more, consult the [Authentication](/docs/authentication/config) documentation. | -| **`upload`** | Specify options if you would like this Collection to support file uploads. For more, consult the [Uploads](/docs/upload/overview) documentation. | -| **`timestamps`** | Set to false to disable documents' automatically generated `createdAt` and `updatedAt` timestamps. | -| **`versions`** | Set to true to enable default options, or configure with object properties. [More](/docs/versions/overview#collection-config)| -| **`endpoints`** | Add custom routes to the REST API. [More](/docs/rest-api/overview#custom-endpoints) | +| **`labels`** | Singular and plural labels for use in identifying this Collection throughout Payload. Auto-generated from slug if not defined. | +| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-options). | +| **`hooks`** | Entry points to "tie in" to Collection actions at specific points. [More](/docs/hooks/overview#collection-hooks) | +| **`access`** | Provide access control functions to define exactly who should be able to do what with Documents in this Collection. [More](/docs/access-control/overview/#collections) | +| **`auth`** | Specify options if you would like this Collection to feature authentication. For more, consult the [Authentication](/docs/authentication/config) documentation. | +| **`upload`** | Specify options if you would like this Collection to support file uploads. For more, consult the [Uploads](/docs/upload/overview) documentation. | +| **`timestamps`** | Set to false to disable documents' automatically generated `createdAt` and `updatedAt` timestamps. | +| **`versions`** | Set to true to enable default options, or configure with object properties. [More](/docs/versions/overview#collection-config) | +| **`endpoints`** | Add custom routes to the REST API. [More](/docs/rest-api/overview#custom-endpoints) | +| **`graphQL`** | An object with `singularName` and `pluralName` strings used in schema generation. Auto-generated from slug if not defined. | +| **`typescript`** | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. | *\* An asterisk denotes that a property is required.* diff --git a/docs/configuration/globals.mdx b/docs/configuration/globals.mdx index 6bd5b9487c..d06b158b1a 100644 --- a/docs/configuration/globals.mdx +++ b/docs/configuration/globals.mdx @@ -12,17 +12,19 @@ As with Collection configs, it's often best practice to write your Globals in se ## Options -| Option | Description | -| ---------------- | -------------| -| **`slug`** * | Unique, URL-friendly string that will act as an identifier for this Global. | -| **`fields`** * | Array of field types that will determine the structure and functionality of the data stored within this Global. [Click here](/docs/fields/overview) for a full list of field types as well as how to configure them. | -| **`label`** | Singular label for use in identifying this Global throughout Payload. Auto-generated from slug if not defined. | -| **`description`**| Text or React component to display below the Global header to give editors more information. | -| **`admin`** | Admin-specific configuration. See below for [more detail](/docs/configuration/globals#admin-options). | -| **`hooks`** | Entry points to "tie in" to collection actions at specific points. [More](/docs/hooks/overview#global-hooks) | -| **`access`** | Provide access control functions to define exactly who should be able to do what with this Global. [More](/docs/access-control/overview/#globals) | -| **`versions`** | Set to true to enable default options, or configure with object properties. [More](/docs/versions/overview#globals-config)| -| **`endpoints`** | Add custom routes to the REST API. [More](/docs/rest-api/overview#custom-endpoints)| +| Option | Description | +|--------------------| -------------| +| **`slug`** * | Unique, URL-friendly string that will act as an identifier for this Global. | +| **`fields`** * | Array of field types that will determine the structure and functionality of the data stored within this Global. [Click here](/docs/fields/overview) for a full list of field types as well as how to configure them. | +| **`label`** | Singular label for use in identifying this Global throughout Payload. Auto-generated from slug if not defined. | +| **`description`** | Text or React component to display below the Global header to give editors more information. | +| **`admin`** | Admin-specific configuration. See below for [more detail](/docs/configuration/globals#admin-options). | +| **`hooks`** | Entry points to "tie in" to collection actions at specific points. [More](/docs/hooks/overview#global-hooks) | +| **`access`** | Provide access control functions to define exactly who should be able to do what with this Global. [More](/docs/access-control/overview/#globals) | +| **`versions`** | Set to true to enable default options, or configure with object properties. [More](/docs/versions/overview#globals-config)| +| **`endpoints`** | Add custom routes to the REST API. [More](/docs/rest-api/overview#custom-endpoints)| +| **`graphQL.name`** | Text used in schema generation. Auto-generated from slug if not defined. | +| **`typescript`** | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. | *\* An asterisk denotes that a property is required.* diff --git a/docs/configuration/i18n.mdx b/docs/configuration/i18n.mdx new file mode 100644 index 0000000000..6f5865443f --- /dev/null +++ b/docs/configuration/i18n.mdx @@ -0,0 +1,100 @@ +--- +title: I18n +label: I18n +order: 40 +desc: Manage and customize internationalization support in your CMS editor experience +keywords: internationalization, i18n, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, express +--- + +Not only does Payload support managing localized content, it also has internationalization support so that admin users can work in their preferred language. Payload's i18n support is built on top of [i18next](https://www.i18next.com). It comes included by default and can be extended in your config. + +While Payload's built-in features come translated, you may want to also translate parts of your project's configuration too. This is possible in places like collections and globals labels and groups, field labels, descriptions and input placeholder text. The admin UI will display all the correct translations you provide based on the user's language. + +Here is an example of a simple collection supporting both English and Spanish editors: +```ts +const Articles: CollectionConfig = { + slug: 'articles', + labels: { + singular: { + en: 'Article', es: 'Artículo', + }, + plural: { + en: 'Articles', es: 'Artículo', + }, + }, + admin: { + group: { en: 'Content', es: 'Contenido' }, + }, + fields: [ + { + name: 'title', + type: 'text', + label: { + en: 'Title', es: 'Título', + }, + admin: { + placeholder: { en: 'Enter title', es: 'Introduce el título' } + } + }, + { + name: 'type', + type: 'radio', + options: [{ + value: 'news', + label: { en: 'News', es: 'Noticias' }, + }, // etc... + ], + }, + ], +} +``` + +### Admin UI + +The Payload admin panel reads the language settings of a user's browser and display all text in that language, or will fall back to English if the user's language is not yet supported. +After a user logs in, they can change their language selection in the `/account` view. + + + Note:
+ If there is a language that Payload does not yet support, we accept code contributions. +
+ +### Node Express + +Payload's backend uses express middleware to set the language on incoming requests before they are handled. This allows backend validation to return error messages in the user's own language or system generated emails to be sent using the correct translation. You can make HTTP requests with the `accept-language` header and Payload will use that language. + +Anywhere in your Payload app that you have access to the `req` object, you can access i18next's extensive internationalization features assigned to `req.i18n`. To access text translations you can use `req.t('namespace:key')`. + +Read the i18next [API documentation](https://www.i18next.com/overview/api) to learn more. + +### Configuration Options + +In your Payload config, you can add translations and customize the settings in `i18n`. Payload will use your custom options and merge it with the default, allowing you to override the settings Payload provides. + +**Example Payload config extending i18n:** + +```ts +import { buildConfig } from 'payload/config' + +export default buildConfig({ + //... + i18n: { + fallbackLng: 'en', // default + debug: false, // default + resources: { + en: { + custom: { // namespace can be anything you want + key1: 'Translation with {{variable}}', // translation + }, + // override existing translation keys + general: { + dashboard: 'Home', + }, + }, + }, + }, + //... +}); +``` + +See the i18next [configuration options](https://www.i18next.com/overview/configuration-options) to learn more. diff --git a/docs/fields/array.mdx b/docs/fields/array.mdx index 45d9bd0df5..f8ba6e5c2a 100644 --- a/docs/fields/array.mdx +++ b/docs/fields/array.mdx @@ -72,7 +72,7 @@ const ExampleCollection: CollectionConfig = { { name: 'title', type: 'text', - } + }, { name: 'image', type: 'upload', diff --git a/docs/fields/blocks.mdx b/docs/fields/blocks.mdx index 8ff02260fa..305a70ce3b 100644 --- a/docs/fields/blocks.mdx +++ b/docs/fields/blocks.mdx @@ -22,21 +22,21 @@ keywords: blocks, fields, config, configuration, documentation, Content Manageme ### Field config -| Option | Description | -| ---------------- | ----------- | -| **`name`** * | To be used as the property name when stored and retrieved from the database. | -| **`label`** | Used as a heading in the Admin panel and to name the generated GraphQL type. | -| **`blocks`** * | Array of [block configs](/docs/fields/blocks#block-configs) to be made available to this field. | -| **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation) | -| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/config), include its data in the user JWT. | -| **`hooks`** | Provide field-level hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) | -| **`access`** | Provide field-level access control to denote what users can see and do with this field's data. [More](/docs/fields/overview#field-level-access-control) | -| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API response or the Admin panel. | -| **`defaultValue`** | Provide an array of block data to be used for this field's default value. [More](/docs/fields/overview#default-values) | -| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. If enabled, a separate, localized set of all data within this field will be kept, so there is no need to specify each nested field as `localized`. || **`required`** | Require this field to have a value. | -| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. | -| **`labels`** | Customize the block row labels appearing in the Admin dashboard. | -| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). | +| Option | Description | +|-------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **`name`** * | To be used as the property name when stored and retrieved from the database. | +| **`label`** | Used as a heading in the Admin panel. | +| **`blocks`** * | Array of [block configs](/docs/fields/blocks#block-configs) to be made available to this field. | +| **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation) | +| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/config), include its data in the user JWT. | +| **`hooks`** | Provide field-level hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) | +| **`access`** | Provide field-level access control to denote what users can see and do with this field's data. [More](/docs/fields/overview#field-level-access-control) | +| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API response or the Admin panel. | +| **`defaultValue`** | Provide an array of block data to be used for this field's default value. [More](/docs/fields/overview#default-values) | +| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. If enabled, a separate, localized set of all data within this field will be kept, so there is no need to specify each nested field as `localized`. || **`required`** | Require this field to have a value. | +| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. | +| **`labels`** | Customize the block row labels appearing in the Admin dashboard. | +| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). | *\* An asterisk denotes that a property is required.* @@ -57,13 +57,14 @@ Blocks are defined as separate configs of their own. Best practice is to define each block config in its own file, and then import them into your Blocks field as necessary. This way each block config can be easily shared between fields. For instance, using the "layout builder" example, you might want to feature a few of the same blocks in a Post collection as well as a Page collection. Abstracting into their own files trivializes their reusability. -| Option | Description | -| ---------------- | ----------- | -| **`slug`** * | Identifier for this block type. Will be saved on each block as the `blockType` property. | -| **`fields`** * | Array of fields to be stored in this block. | -| **`labels`** | Customize the block labels that appear in the Admin dashboard. Also used to name corresponding GraphQL schema types. Auto-generated from slug if not defined. | -| **`imageURL`** | Provide a custom image thumbnail to help editors identify this block in the Admin UI. | -| **`imageAltText`** | Customize this block's image thumbnail alt text. | +| Option | Description | +|----------------------------|---------------------------------------------------------------------------------------------------------| +| **`slug`** * | Identifier for this block type. Will be saved on each block as the `blockType` property. | +| **`fields`** * | Array of fields to be stored in this block. | +| **`labels`** | Customize the block labels that appear in the Admin dashboard. Auto-generated from slug if not defined. | +| **`imageURL`** | Provide a custom image thumbnail to help editors identify this block in the Admin UI. | +| **`imageAltText`** | Customize this block's image thumbnail alt text. | +| **`graphQL.singularName`** | Text to use for the GraphQL schema name. Auto-generated from slug if not defined | #### Auto-generated data per block diff --git a/docs/fields/overview.mdx b/docs/fields/overview.mdx index 9442f7f359..9cd87762f6 100644 --- a/docs/fields/overview.mdx +++ b/docs/fields/overview.mdx @@ -72,14 +72,15 @@ There are two arguments available to custom validation functions. 1. The value which is currently assigned to the field 2. An optional object with dynamic properties for more complex validation having the following: -| Property | Description | -| ------------- | -------------| -| `data` | An object of the full collection or global document | -| `siblingData` | An object of the document data limited to fields within the same parent to the field | -| `operation` | Will be "create" or "update" depending on the UI action or API call | -| `id` | The value of the collection `id`, will be `undefined` on create request | -| `user` | The currently authenticated user object | -| `payload` | If the `validate` function is being executed on the server, Payload will be exposed for easily running local operations. | +| Property | Description | +|---------------|--------------------------------------------------------------------------------------------------------------------------| +| `data` | An object of the full collection or global document. | +| `siblingData` | An object of the document data limited to fields within the same parent to the field. | +| `operation` | Will be "create" or "update" depending on the UI action or API call. | +| `id` | The value of the collection `id`, will be `undefined` on create request. | +| `t` | The function for translating text, [more](/docs/configuration/i18n). | +| `user` | The currently authenticated user object. | +| `payload` | If the `validate` function is being executed on the server, Payload will be exposed for easily running local operations. | Example: ```ts diff --git a/docs/graphql/overview.mdx b/docs/graphql/overview.mdx index 9e8b5ee2df..91878e6abd 100644 --- a/docs/graphql/overview.mdx +++ b/docs/graphql/overview.mdx @@ -35,10 +35,6 @@ import { CollectionConfig } from 'payload/types'; const PublicUser: CollectionConfig = { slug: 'public-users', auth: true, // Auth is enabled - labels: { - singular: 'Public User', - plural: 'Public Users', - }, fields: [ ... ], diff --git a/package.json b/package.json index f29284a088..1690b0df3d 100644 --- a/package.json +++ b/package.json @@ -124,6 +124,9 @@ "graphql-type-json": "^0.3.1", "html-webpack-plugin": "^5.0.0-alpha.14", "http-status": "^1.4.2", + "i18next": "^22.0.1", + "i18next-browser-languagedetector": "^6.1.8", + "i18next-http-middleware": "^3.2.1", "is-hotkey": "^0.2.0", "is-plain-object": "^5.0.0", "isomorphic-fetch": "^3.0.0", @@ -167,6 +170,7 @@ "react-diff-viewer": "^3.1.1", "react-dom": "^18.0.0", "react-helmet": "^6.1.0", + "react-i18next": "^11.18.6", "react-router-dom": "^5.1.2", "react-router-navigation-prompt": "^1.9.6", "react-select": "^3.0.8", @@ -237,6 +241,7 @@ "@types/passport-jwt": "^3.0.3", "@types/passport-local": "^1.0.33", "@types/pino": "^6.3.4", + "@types/pino-std-serializers": "^4.0.0", "@types/pluralize": "^0.0.29", "@types/prismjs": "^1.16.2", "@types/prop-types": "^15.7.3", diff --git a/src/admin/api.ts b/src/admin/api.ts index 768287c96e..6e0a5ad54a 100644 --- a/src/admin/api.ts +++ b/src/admin/api.ts @@ -1,9 +1,19 @@ import qs from 'qs'; +type GetOptions = RequestInit & { + params?: Record +} + export const requests = { - get: (url: string, params: unknown = {}): Promise => { - const query = qs.stringify(params, { addQueryPrefix: true }); - return fetch(`${url}${query}`, { credentials: 'include' }); + get: (url: string, options: GetOptions = { headers: {} }): Promise => { + let query = ''; + if (options.params) { + query = qs.stringify(options.params, { addQueryPrefix: true }); + } + return fetch(`${url}${query}`, { + credentials: 'include', + headers: options.headers, + }); }, post: (url: string, options: RequestInit = { headers: {} }): Promise => { diff --git a/src/admin/components/Routes.tsx b/src/admin/components/Routes.tsx index e291cd9a8d..a6b9e1cccb 100644 --- a/src/admin/components/Routes.tsx +++ b/src/admin/components/Routes.tsx @@ -2,6 +2,7 @@ import React, { Suspense, lazy, useState, useEffect } from 'react'; import { Route, Switch, withRouter, Redirect, } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; import { useAuth } from './utilities/Auth'; import { useConfig } from './utilities/Config'; import List from './views/collections/List'; @@ -29,6 +30,7 @@ const Account = lazy(() => import('./views/Account')); const Routes = () => { const [initialized, setInitialized] = useState(null); const { user, permissions, refreshCookie } = useAuth(); + const { i18n } = useTranslation(); const canAccessAdmin = permissions?.canAccessAdmin; @@ -54,7 +56,11 @@ const Routes = () => { const { slug } = userCollection; if (!userCollection.auth.disableLocalStrategy) { - requests.get(`${routes.api}/${slug}/init`).then((res) => res.json().then((data) => { + requests.get(`${routes.api}/${slug}/init`, { + headers: { + 'Accept-Language': i18n.language, + }, + }).then((res) => res.json().then((data) => { if (data && 'initialized' in data) { setInitialized(data.initialized); } @@ -62,7 +68,7 @@ const Routes = () => { } else { setInitialized(true); } - }, [routes, userCollection]); + }, [i18n.language, routes, userCollection]); return ( }> diff --git a/src/admin/components/elements/ArrayAction/index.tsx b/src/admin/components/elements/ArrayAction/index.tsx index e2d911864c..98e5cb723e 100644 --- a/src/admin/components/elements/ArrayAction/index.tsx +++ b/src/admin/components/elements/ArrayAction/index.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { useTranslation } from 'react-i18next'; import Popup from '../Popup'; import More from '../../icons/More'; import Chevron from '../../icons/Chevron'; @@ -19,6 +20,7 @@ export const ArrayAction: React.FC = ({ duplicateRow, removeRow, }) => { + const { t } = useTranslation('general'); return ( = ({ }} > - Move Up + {t('moveUp')} )} {index < rowCount - 1 && ( @@ -51,7 +53,7 @@ export const ArrayAction: React.FC = ({ }} > - Move Down + {t('moveDown')} )} ); diff --git a/src/admin/components/elements/Autosave/index.tsx b/src/admin/components/elements/Autosave/index.tsx index 9f91fa3fc7..7ab4bb6a34 100644 --- a/src/admin/components/elements/Autosave/index.tsx +++ b/src/admin/components/elements/Autosave/index.tsx @@ -1,7 +1,7 @@ -import { formatDistance } from 'date-fns'; import { useHistory } from 'react-router-dom'; import { toast } from 'react-toastify'; import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { useConfig } from '../../utilities/Config'; import { useFormModified, useAllFormFields } from '../../forms/Form/context'; import { useLocale } from '../../utilities/Locale'; @@ -21,6 +21,7 @@ const Autosave: React.FC = ({ collection, global, id, publishedDocUpdated const modified = useFormModified(); const locale = useLocale(); const { replace } = useHistory(); + const { t, i18n } = useTranslation('version'); let interval = 800; if (collection?.versions.drafts && collection.versions?.drafts?.autosave) interval = collection.versions.drafts.autosave.interval; @@ -42,6 +43,7 @@ const Autosave: React.FC = ({ collection, global, id, publishedDocUpdated credentials: 'include', headers: { 'Content-Type': 'application/json', + 'Accept-Language': i18n.language, }, body: JSON.stringify({}), }); @@ -54,9 +56,9 @@ const Autosave: React.FC = ({ collection, global, id, publishedDocUpdated }, }); } else { - toast.error('There was a problem while autosaving this document.'); + toast.error(t('error:autosaving')); } - }, [collection, serverURL, api, admin, locale, replace]); + }, [i18n, serverURL, api, collection, locale, replace, admin, t]); useEffect(() => { // If no ID, but this is used for a collection doc, @@ -98,6 +100,7 @@ const Autosave: React.FC = ({ collection, global, id, publishedDocUpdated credentials: 'include', headers: { 'Content-Type': 'application/json', + 'Accept-Language': i18n.language, }, body: JSON.stringify(body), }); @@ -114,7 +117,7 @@ const Autosave: React.FC = ({ collection, global, id, publishedDocUpdated }; autosave(); - }, [debouncedFields, modified, serverURL, api, collection, global, id, getVersions, locale]); + }, [i18n, debouncedFields, modified, serverURL, api, collection, global, id, getVersions, locale]); useEffect(() => { if (versions?.docs?.[0]) { @@ -126,12 +129,12 @@ const Autosave: React.FC = ({ collection, global, id, publishedDocUpdated return (
- {saving && 'Saving...'} + {saving && t('saving')} {(!saving && lastSaved) && ( - Last saved  - {formatDistance(new Date(), new Date(lastSaved))} -  ago + {t('lastSavedAgo', { + distance: Math.round((Number(new Date(lastSaved)) - Number(new Date())) / 1000 / 60), + })} )}
diff --git a/src/admin/components/elements/Collapsible/index.tsx b/src/admin/components/elements/Collapsible/index.tsx index b330ea5133..a9f03d9a6e 100644 --- a/src/admin/components/elements/Collapsible/index.tsx +++ b/src/admin/components/elements/Collapsible/index.tsx @@ -1,5 +1,6 @@ import React, { useState } from 'react'; import AnimateHeight from 'react-animate-height'; +import { useTranslation } from 'react-i18next'; import { Props } from './types'; import { CollapsibleProvider, useCollapsible } from './provider'; import Chevron from '../../icons/Chevron'; @@ -22,6 +23,7 @@ export const Collapsible: React.FC = ({ const [collapsedLocal, setCollapsedLocal] = useState(Boolean(initCollapsed)); const [hovered, setHovered] = useState(false); const isNested = useCollapsible(); + const { t } = useTranslation('fields'); const collapsed = typeof collapsedFromProps === 'boolean' ? collapsedFromProps : collapsedLocal; @@ -61,7 +63,7 @@ export const Collapsible: React.FC = ({ }} > - Toggle block + {t('toggleBlock')} {header && ( diff --git a/src/admin/components/elements/ColumnSelector/index.tsx b/src/admin/components/elements/ColumnSelector/index.tsx index 56335ff796..75e3fa6f80 100644 --- a/src/admin/components/elements/ColumnSelector/index.tsx +++ b/src/admin/components/elements/ColumnSelector/index.tsx @@ -1,9 +1,11 @@ import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; import flattenTopLevelFields from '../../../../utilities/flattenTopLevelFields'; import Pill from '../Pill'; import Plus from '../../icons/Plus'; import X from '../../icons/X'; import { Props } from './types'; +import { getTranslation } from '../../../../utilities/getTranslation'; import './index.scss'; @@ -17,6 +19,7 @@ const ColumnSelector: React.FC = (props) => { } = props; const [fields] = useState(() => flattenTopLevelFields(collection.fields, true)); + const { i18n } = useTranslation(); return (
@@ -42,7 +45,7 @@ const ColumnSelector: React.FC = (props) => { isEnabled && `${baseClass}__column--active`, ].filter(Boolean).join(' ')} > - {field.label || field.name} + {getTranslation(field.label || field.name, i18n)} ); })} diff --git a/src/admin/components/elements/CopyToClipboard/index.tsx b/src/admin/components/elements/CopyToClipboard/index.tsx index 2a07265d53..85e210e75b 100644 --- a/src/admin/components/elements/CopyToClipboard/index.tsx +++ b/src/admin/components/elements/CopyToClipboard/index.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useState, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; import Copy from '../../icons/Copy'; import Tooltip from '../Tooltip'; import { Props } from './types'; @@ -9,12 +10,13 @@ const baseClass = 'copy-to-clipboard'; const CopyToClipboard: React.FC = ({ value, - defaultMessage = 'copy', - successMessage = 'copied', + defaultMessage, + successMessage, }) => { const ref = useRef(null); const [copied, setCopied] = useState(false); const [hovered, setHovered] = useState(false); + const { t } = useTranslation('general'); useEffect(() => { if (copied && !hovered) { @@ -49,8 +51,8 @@ const CopyToClipboard: React.FC = ({ > - {copied && successMessage} - {!copied && defaultMessage} + {copied && (successMessage ?? t('copied'))} + {!copied && (defaultMessage ?? t('copy'))}