fix: behaviour of i18n "Available In" column (#24620)

* fix: behaviour of i18n "Available In" column
* chore: use better formating for translation of label
This commit is contained in:
mathildeleg 2025-10-17 17:51:09 +02:00 committed by GitHub
parent ddd1f2d822
commit ec828411d4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 151 additions and 52 deletions

View File

@ -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<I18nBaseQuery>();
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<Locale[]>((acc, locale) => {
.reduce<Array<{ code: string; name: string }>>((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));
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 locale.name;
})
.toSorted((a, b) => formatter.compare(a, b));
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 (
<Popover.Root>
<Popover.Trigger>
<Button variant="ghost" type="button" onClick={(e) => e.stopPropagation()}>
<Menu.Root>
<Menu.Trigger>
<Flex minWidth="100%" alignItems="center" justifyContent="center" fontWeight="regular">
<Typography textColor="neutral800" ellipsis marginRight={2}>
{localesForDocument.join(', ')}
{getDisplayText()}
</Typography>
<Flex>
<CaretDown width="1.2rem" height="1.2rem" />
</Flex>
</Flex>
</Button>
</Popover.Trigger>
<Popover.Content sideOffset={16}>
<ul>
{localesForDocument.map((name) => (
<Box key={name} padding={3} tag="li">
<Typography>{name}</Typography>
</Box>
</Menu.Trigger>
<Menu.Content>
{localesForDocument.map(({ code, name }) => (
<Menu.Item
key={code}
onClick={(e: React.MouseEvent) => {
e.stopPropagation();
handleLocaleClick(code);
}}
>
<Typography textColor="neutral800" fontWeight="regular">
{name}
</Typography>
</Menu.Item>
))}
</ul>
</Popover.Content>
</Popover.Root>
</Menu.Content>
</Menu.Root>
);
};

View File

@ -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(<LocaleListCell localizations={[{ locale: 'en' }, { locale: 'fr' }]} locale="en" />);
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(
<LocaleListCell
localizations={[{ locale: 'en' }, { locale: 'fr' }]}
locale="en"
documentId="123"
/>
);
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(
<LocaleListCell
localizations={[{ locale: 'en' }, { locale: 'fr' }, { locale: 'de' }, { locale: 'es' }]}
locale="en"
documentId="123"
/>
);
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(
<LocaleListCell localizations={[{ locale: 'en' }, { locale: 'fr' }]} locale="en" />
<LocaleListCell
localizations={[{ locale: 'en' }, { locale: 'fr' }]}
locale="en"
documentId="123"
/>
);
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(
<LocaleListCell
localizations={[{ locale: 'en' }, { locale: 'fr' }]}
locale="en"
documentId="123"
/>
);
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();
});
});

View File

@ -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) => <LocaleListCell {...props} {...meta} />,
cellFormatter: (props, _header, meta) => (
<LocaleListCell {...props} {...meta} documentId={props.documentId} />
),
},
],
layout,

View File

@ -20,6 +20,7 @@
"CMEditViewBulkLocale.continue-confirmation": "Are you sure you want to continue?",
"CMEditViewLocalePicker.locale.create": "Create <bold>{locale}</bold> 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).",

View File

@ -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(