From ec828411d457e9fb93a7e4db1a0707399d23dde1 Mon Sep 17 00:00:00 2001 From: mathildeleg <82765709+mathildeleg@users.noreply.github.com> Date: Fri, 17 Oct 2025 17:51:09 +0200 Subject: [PATCH] fix: behaviour of i18n "Available In" column (#24620) * fix: behaviour of i18n "Available In" column * chore: use better formating for translation of label --- .../admin/src/components/LocaleListCell.tsx | 109 ++++++++++++------ .../components/tests/LocaleListCell.test.tsx | 73 +++++++++--- .../src/contentManagerHooks/listView.tsx | 4 +- .../i18n/admin/src/translations/en.json | 1 + packages/plugins/i18n/admin/tests/server.ts | 16 +++ 5 files changed, 151 insertions(+), 52 deletions(-) diff --git a/packages/plugins/i18n/admin/src/components/LocaleListCell.tsx b/packages/plugins/i18n/admin/src/components/LocaleListCell.tsx index 18445cb340..24284dd711 100644 --- a/packages/plugins/i18n/admin/src/components/LocaleListCell.tsx +++ b/packages/plugins/i18n/admin/src/components/LocaleListCell.tsx @@ -1,18 +1,29 @@ -import { Box, Flex, Popover, Typography, useCollator, Button } from '@strapi/design-system'; -import { CaretDown } from '@strapi/icons'; +import { useQueryParams } from '@strapi/admin/strapi-admin'; +import { Flex, Menu, Typography, useCollator } from '@strapi/design-system'; +import { stringify } from 'qs'; import { useIntl } from 'react-intl'; +import { useNavigate } from 'react-router-dom'; -import { Locale } from '../../../shared/contracts/locales'; import { useGetLocalesQuery } from '../services/locales'; +import { getTranslation } from '../utils/getTranslation'; + +import type { I18nBaseQuery } from '../types'; interface LocaleListCellProps { localizations: { locale: string }[]; locale: string; + documentId: string; } -const LocaleListCell = ({ locale: currentLocale, localizations }: LocaleListCellProps) => { - const { locale: language } = useIntl(); +const LocaleListCell = ({ + locale: currentLocale, + localizations, + documentId, +}: LocaleListCellProps) => { + const { locale: language, formatMessage } = useIntl(); const { data: locales = [] } = useGetLocalesQuery(); + const navigate = useNavigate(); + const [{ query }] = useQueryParams(); const formatter = useCollator(language, { sensitivity: 'base', }); @@ -24,50 +35,76 @@ const LocaleListCell = ({ locale: currentLocale, localizations }: LocaleListCell const availableLocales = localizations.map((loc) => loc.locale); const localesForDocument = locales - .reduce((acc, locale) => { + .reduce>((acc, locale) => { const createdLocale = [currentLocale, ...availableLocales].find((loc) => { return loc === locale.code; }); if (createdLocale) { - acc.push(locale); + const name = locale.isDefault ? `${locale.name} (default)` : locale.name; + acc.push({ code: locale.code, name }); } return acc; }, []) - .map((locale) => { - if (locale.isDefault) { - return `${locale.name} (default)`; - } + .toSorted((a, b) => formatter.compare(a.name, b.name)); - return locale.name; - }) - .toSorted((a, b) => formatter.compare(a, b)); + const getDisplayText = () => { + const displayedLocales = localesForDocument.slice(0, 2); + const remainingCount = localesForDocument.length - 2; + + const baseText = displayedLocales.map(({ name }) => name).join(', '); + + if (remainingCount <= 0) { + return baseText; + } + + return formatMessage( + { + id: getTranslation('CMListView.popover.display-locales.more'), + defaultMessage: '{locales} + {count} more', + }, + { locales: baseText, count: remainingCount } + ); + }; + + const handleLocaleClick = (localeCode: string) => { + navigate({ + pathname: documentId, + search: stringify({ + plugins: { + ...query.plugins, + i18n: { locale: localeCode }, + }, + }), + }); + }; return ( - - - - - -
    - {localesForDocument.map((name) => ( - - {name} - - ))} -
-
-
+ + ))} + + ); }; diff --git a/packages/plugins/i18n/admin/src/components/tests/LocaleListCell.test.tsx b/packages/plugins/i18n/admin/src/components/tests/LocaleListCell.test.tsx index 5ea7089c90..d14b8de96a 100644 --- a/packages/plugins/i18n/admin/src/components/tests/LocaleListCell.test.tsx +++ b/packages/plugins/i18n/admin/src/components/tests/LocaleListCell.test.tsx @@ -13,36 +13,79 @@ jest.mock('@strapi/content-manager/strapi-admin', () => ({ availableLocales: [ { locale: 'en', status: 'draft' }, { locale: 'fr', status: 'published' }, + { locale: 'de', status: 'draft' }, + { locale: 'es', status: 'published' }, ], }, })), })); describe('LocaleListCell', () => { - it('renders a button with all the names of the locales that are available for the document', async () => { - render(); + it('renders a button with all the names of the locales that are available for the document when there are 2 or fewer locales', async () => { + render( + + ); - expect( - await screen.findByRole('button', { name: 'English (default), Français' }) - ).toBeInTheDocument(); + expect(await screen.findByText('English (default), Français')).toBeInTheDocument(); - expect(screen.queryByRole('list')).not.toBeInTheDocument(); + expect(screen.queryByRole('menu')).not.toBeInTheDocument(); + }); + + it('renders a button with only the first 2 locales and "+ X more" text when there are more than 2 locales', async () => { + render( + + ); + + const button = await screen.findByText('Deutsch, English (default) + 2 more'); + expect(button).toBeInTheDocument(); + + expect(screen.queryByRole('menu')).not.toBeInTheDocument(); }); it('renders a list of the locales available on the document when the button is clicked', async () => { const { user } = render( - + ); - expect( - await screen.findByRole('button', { name: 'English (default), Français' }) - ).toBeInTheDocument(); + expect(await screen.findByText('English (default), Français')).toBeInTheDocument(); - await user.click(screen.getByRole('button')); + await user.click(screen.getByText('English (default), Français')); - expect(screen.getByRole('list')).toBeInTheDocument(); - expect(screen.getAllByRole('listitem')).toHaveLength(2); - expect(screen.getAllByRole('listitem').at(0)).toHaveTextContent('English (default)'); - expect(screen.getAllByRole('listitem').at(1)).toHaveTextContent('Français'); + expect(screen.getByRole('menu')).toBeInTheDocument(); + expect(screen.getAllByRole('menuitem')).toHaveLength(2); + expect(screen.getAllByRole('menuitem').at(0)).toHaveTextContent('English (default)'); + expect(screen.getAllByRole('menuitem').at(1)).toHaveTextContent('Français'); + }); + + it('renders clickable menu items for each locale in the menu', async () => { + const { user } = render( + + ); + + const menuTrigger = await screen.findByText('English (default), Français'); + await user.click(menuTrigger); + + // Check that locale items are now menu items + const englishMenuItem = screen.getByRole('menuitem', { name: 'English (default)' }); + const frenchMenuItem = screen.getByRole('menuitem', { name: 'Français' }); + + expect(englishMenuItem).toBeInTheDocument(); + expect(frenchMenuItem).toBeInTheDocument(); }); }); diff --git a/packages/plugins/i18n/admin/src/contentManagerHooks/listView.tsx b/packages/plugins/i18n/admin/src/contentManagerHooks/listView.tsx index 907376b92f..a5ea6c8416 100644 --- a/packages/plugins/i18n/admin/src/contentManagerHooks/listView.tsx +++ b/packages/plugins/i18n/admin/src/contentManagerHooks/listView.tsx @@ -37,7 +37,9 @@ const addColumnToTableHook = ({ displayedHeaders, layout }: AddColumnToTableHook sortable: false, name: 'locales', // @ts-expect-error – ID is seen as number | string; this will change when we move the type over. - cellFormatter: (props, _header, meta) => , + cellFormatter: (props, _header, meta) => ( + + ), }, ], layout, diff --git a/packages/plugins/i18n/admin/src/translations/en.json b/packages/plugins/i18n/admin/src/translations/en.json index 5e472ad5f8..4e93554802 100644 --- a/packages/plugins/i18n/admin/src/translations/en.json +++ b/packages/plugins/i18n/admin/src/translations/en.json @@ -20,6 +20,7 @@ "CMEditViewBulkLocale.continue-confirmation": "Are you sure you want to continue?", "CMEditViewLocalePicker.locale.create": "Create {locale} locale", "CMListView.popover.display-locales.label": "Display translated locales", + "CMListView.popover.display-locales.more": "{locales} + {count} more", "CheckboxConfirmation.Modal.body": "Do you want to disable it?", "CheckboxConfirmation.Modal.button-confirm": "Yes, disable", "CheckboxConfirmation.Modal.content": "Disabling localization will engender the deletion of all your content but the one associated to your default locale (if existing).", diff --git a/packages/plugins/i18n/admin/tests/server.ts b/packages/plugins/i18n/admin/tests/server.ts index e3131231e1..b72bd27b07 100644 --- a/packages/plugins/i18n/admin/tests/server.ts +++ b/packages/plugins/i18n/admin/tests/server.ts @@ -20,6 +20,22 @@ const LOCALES = [ createdAt: '', updatedAt: '', }, + { + id: 3, + code: 'de', + name: 'Deutsch', + isDefault: false, + createdAt: '', + updatedAt: '', + }, + { + id: 4, + code: 'es', + name: 'Español', + isDefault: false, + createdAt: '', + updatedAt: '', + }, ] satisfies GetLocales.Response; export const server = setupServer(