🌐(frontend) internationalize missing strings

Some strings did not use `t()`. We know use that everywhere.
In order to prevent the use of literal string as text, we enable
the eslint rule `i18next/no-literal-string`. As this rule triggers
warning for all material-icons span elements we replace all of them
by Icon component provided by UI-Kit (which was technical debt)
This commit is contained in:
jbpenrath
2026-06-11 00:40:13 +02:00
parent 7bc1ba7039
commit 74be53a803
21 changed files with 72 additions and 44 deletions
+25
View File
@@ -55,6 +55,7 @@
"@vitest/ui": "4.1.8",
"deepmerge": "4.3.1",
"eslint": "9.39.2",
"eslint-plugin-i18next": "6.1.4",
"eslint-plugin-react-hooks": "7.1.1",
"i18next-cli": "1.34.1",
"jsdom": "27.4.0",
@@ -13905,6 +13906,20 @@
}
}
},
"node_modules/eslint-plugin-i18next": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/eslint-plugin-i18next/-/eslint-plugin-i18next-6.1.4.tgz",
"integrity": "sha512-BekXQu1VaVFkREepQGsntBYoKuPsf89V16n/s2xpKMtsw0/lDseds1wBhj3RtO1dKnHsfTPaG+xul1vF0R7LEQ==",
"dev": true,
"license": "ISC",
"dependencies": {
"lodash": "^4.17.21",
"requireindex": "~1.1.0"
},
"engines": {
"node": ">=18.10.0"
}
},
"node_modules/eslint-plugin-react-hooks": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz",
@@ -21266,6 +21281,16 @@
"node": ">=0.10.0"
}
},
"node_modules/requireindex": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.1.0.tgz",
"integrity": "sha512-LBnkqsDE7BZKvqylbmn7lTIVdpx4K/QCduRATpO5R+wtPmky/a8pN1bO2D6wXppn1497AJF9mNjqAXr6bdl9jg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.5"
}
},
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+1
View File
@@ -73,6 +73,7 @@
"@vitest/ui": "4.1.8",
"deepmerge": "4.3.1",
"eslint": "9.39.2",
"eslint-plugin-i18next": "6.1.4",
"eslint-plugin-react-hooks": "7.1.1",
"i18next-cli": "1.34.1",
"jsdom": "27.4.0",
@@ -1,4 +1,5 @@
import { Button } from "@gouvfr-lasuite/cunningham-react";
import { Icon } from "@gouvfr-lasuite/ui-kit";
import { useTranslation } from "react-i18next";
import { ImportTaskRecap } from "@/hooks/use-task-status";
import clsx from "clsx";
@@ -26,9 +27,7 @@ export const StepCompleted = ({ onClose, recap }: StepCompletedProps) => {
'importer-completed__badge',
{ 'importer-completed__badge--warning': showFailureWarning }
)}>
<span className="material-icons">
{showFailureWarning ? 'report_problem' : 'check'}
</span>
<Icon name={showFailureWarning ? 'report_problem' : 'check'} />
</span>
<div className="importer-completed__heading">
<p className="importer-completed__title">{t('Import complete')}</p>
@@ -56,13 +55,12 @@ export const StepCompleted = ({ onClose, recap }: StepCompletedProps) => {
{t('Failed: {{count}} messages', { count: failure })}
</span>
{showFailureWarning && (
<span
className="material-icons importer-completed__percent-warning"
<Icon
name="report_problem"
className="importer-completed__percent-warning"
aria-label={t('High failure rate')}
title={t('High failure rate')}
>
report_problem
</span>
/>
)}
</li>
)}
@@ -3,7 +3,7 @@ import z from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { FormProvider, useForm, useWatch } from "react-hook-form";
import { Button } from "@gouvfr-lasuite/cunningham-react";
import { Spinner } from "@gouvfr-lasuite/ui-kit";
import { Icon, Spinner } from "@gouvfr-lasuite/ui-kit";
import { useTranslation } from "react-i18next";
import { useParams } from "@tanstack/react-router";
import { importFileCreateResponse202, importImapCreateResponse202, useImportFileCreate, useImportImapCreate } from "@/features/api/gen";
@@ -277,8 +277,8 @@ export const StepForm = ({ onUploading, onSuccess, onError, error, step }: StepF
<RhfFileUploader
name="archive_file"
accept=".eml,.mbox,.pst"
icon={<span className="material-icons">inventory_2</span>}
fileSelectedIcon={<span className="material-icons">inventory_2</span>}
icon={<Icon name="inventory_2" />}
fileSelectedIcon={<Icon name="inventory_2" />}
bigText={t('Drag and drop an archive here')}
text={t('EML, MBOX or PST')}
fullWidth
@@ -1,3 +1,4 @@
import { Icon } from "@gouvfr-lasuite/ui-kit";
import clsx from "clsx";
import { useTranslation } from "react-i18next";
@@ -10,7 +11,7 @@ export const DropZone = ({ isHidden }: DropZoneProps) => {
return (
<div className={clsx("attachment-uploader__dropzone", { "attachment-uploader__dropzone--hidden": isHidden })} aria-hidden={isHidden}>
<span className="material-icons">file_download</span>
<Icon name="file_download" />
{t("Drop your attachments here")}
</div>
);
@@ -679,14 +679,13 @@ export const MessageForm = ({
name="to"
label={t("To:")}
autoFocus={mode === "forward"}
// icon={<span className="material-icons">group</span>}
text={form.formState.errors.to && !Array.isArray(form.formState.errors.to) ? form.formState.errors.to.message : t("Enter the email addresses of the recipients separated by commas")}
textItems={Array.isArray(form.formState.errors.to) ? form.formState.errors.to?.map((error, index) => t(error!.message as string, { email: form.getValues('to')?.[index] })) : []}
disabled={!canWriteMessages}
rightText={
<div className="form-field-options">
<Button tabIndex={-1} type="button" size="nano" variant={showCCField ? "bordered" : "tertiary"} onClick={() => setShowCCField(!showCCField)} disabled={!canWriteMessages}>cc</Button>
<Button tabIndex={-1} type="button" size="nano" variant={showBCCField ? "bordered" : "tertiary"} onClick={() => setShowBCCField(!showBCCField)} disabled={!canWriteMessages}>bcc</Button>
<Button tabIndex={-1} type="button" size="nano" variant={showCCField ? "bordered" : "tertiary"} onClick={() => setShowCCField(!showCCField)} disabled={!canWriteMessages}>{t("cc")}</Button>
<Button tabIndex={-1} type="button" size="nano" variant={showBCCField ? "bordered" : "tertiary"} onClick={() => setShowBCCField(!showBCCField)} disabled={!canWriteMessages}>{t("bcc")}</Button>
</div> as unknown as string // TODO: Allow ReactNode as rightText in Cunningham
}
fullWidth
@@ -699,7 +698,6 @@ export const MessageForm = ({
<RhfContactComboBox
name="cc"
label={t("Copy: ")}
// icon={<span className="material-icons">group</span>}
text={form.formState.errors.cc && !Array.isArray(form.formState.errors.cc) ? t(form.formState.errors.cc.message as string) : t("Enter the email addresses of the recipients separated by commas")}
textItems={Array.isArray(form.formState.errors.cc) ? form.formState.errors.cc?.map((error, index) => t(error!.message as string, { email: form.getValues('cc')?.[index] })) : []}
disabled={!canWriteMessages}
@@ -714,7 +712,6 @@ export const MessageForm = ({
<RhfContactComboBox
name="bcc"
label={t("Blind copy: ")}
// icon={<span className="material-icons">visibility_off</span>}
text={form.formState.errors.bcc && !Array.isArray(form.formState.errors.bcc) ? t(form.formState.errors.bcc.message as string) : t("Enter the email addresses of the recipients separated by commas")}
textItems={Array.isArray(form.formState.errors.bcc) ? form.formState.errors.bcc?.map((error, index) => t(error!.message as string, { email: form.getValues('bcc')?.[index] })) : []}
disabled={!canWriteMessages}
@@ -1,6 +1,6 @@
import { ALL_MESSAGES_FOLDER, MAILBOX_FOLDERS } from "@/features/layouts/components/mailbox-panel/components/mailbox-list";
import { SearchHelper } from "@/features/utils/search-helper";
import { Label } from "@gouvfr-lasuite/ui-kit";
import { Icon, Label } from "@gouvfr-lasuite/ui-kit";
import { Button, Checkbox, Input, Select } from "@gouvfr-lasuite/cunningham-react";
import { useId, useRef } from "react";
import { useTranslation } from "react-i18next";
@@ -111,7 +111,7 @@ type FolderOptionProps = {
const FolderOption = ({ label, icon }: FolderOptionProps) => {
return (
<div className="search__filters-folder-option">
<span className="material-icons">{icon}</span>
<Icon name={icon} />
{label}
</div>
);
@@ -103,7 +103,7 @@ export const SearchInput = () => {
title={t("Reset")}
size="small"
>
<span className="material-icons">close</span>
<Icon name="close" />
<span className="c__offscreen">{t("Reset")}</span>
</Button>
)}
@@ -114,7 +114,7 @@ export const SearchInput = () => {
title={showFilters ? t("Close filters") : t("Open filters")}
size="small"
>
<span className="material-icons">tune</span>
<Icon name="tune" />
<span className="c__offscreen">{showFilters ? t("Close filters") : t("Open filters")}</span>
</Button>
</div>
@@ -127,6 +127,7 @@ function AdminLayoutContent({
export function AdminLayout(props: AdminLayoutProps) {
const { theme, variant } = useTheme();
const { t } = useTranslation();
return (
<LayoutProvider>
@@ -136,7 +137,7 @@ export function AdminLayout(props: AdminLayoutProps) {
leftPanelContent={null}
hideSearch
hideLeftPanelOnDesktop={true}
icon={<Link to="/"><img src={`/images/${theme}/app-logo-${variant}.svg`} alt="logo" height={40} /></Link>}
icon={<Link to="/"><img src={`/images/${theme}/app-logo-${variant}.svg`} alt={t("logo")} height={40} /></Link>}
>
<AdminMailDomainProvider>
<AdminLayoutContent {...props} />
@@ -57,6 +57,7 @@ export const RhfColorPaletteField = (props: { name: string }) => {
htmlFor="color"
style={{ '--char-color': charColor } as React.CSSProperties}
>
{/* eslint-disable-next-line i18next/no-literal-string */}
<span className="color-palette-field__input-of-hell__icon">a</span>
<input
type="color"
@@ -376,18 +376,18 @@ export const LabelItem = ({ level = 0, onEdit, canManage, defaultFoldState, ...l
options={[
{
label: t('Edit'),
icon: <span className="material-icons">edit</span>,
icon: <Icon name="edit" />,
callback: () => onEdit(label),
},
{
label: t('Add a sub-label'),
icon: <span className="material-icons">add</span>,
icon: <Icon name="add" />,
callback: () => onEdit({ name: `${label.name}/`, color: label.color }),
showSeparator: true,
},
{
label: t('Delete'),
icon: <span className="material-icons">delete</span>,
icon: <Icon name="delete" />,
callback: handleDelete,
variant: 'danger'
},
@@ -306,7 +306,7 @@ const ThreadPanelTitle = ({ selectedThreadIds, isAllSelected, isSomeSelected, is
},
...([SelectionReadStatus.MIXED, SelectionReadStatus.UNREAD].includes(selectionReadStatus) ? [{
label: markAllTooltip,
icon: <span className="material-icons">drafts</span>,
icon: <Icon name="drafts" />,
callback: () => {
markAsReadAt({
threadIds: threadIdsToMark,
@@ -320,7 +320,7 @@ const ThreadPanelTitle = ({ selectedThreadIds, isAllSelected, isSomeSelected, is
}] : []),
...([SelectionReadStatus.MIXED, SelectionReadStatus.READ, SelectionReadStatus.NONE].includes(selectionReadStatus) ? [{
label: markAllUnreadLabel,
icon: <span className="material-icons">mark_email_unread</span>,
icon: <Icon name="mark_email_unread" />,
callback: () => {
// Close the open thread before the mutation so the visibility
// observer cannot re-mark the newly-unread messages as read.
@@ -363,7 +363,7 @@ const ThreadPanelTitle = ({ selectedThreadIds, isAllSelected, isSomeSelected, is
<Tooltip content={t('More options')}>
<Button
onClick={() => setIsDropdownOpen(true)}
icon={<span className="material-icons">more_vert</span>}
icon={<Icon name="more_vert" />}
variant="tertiary"
aria-label={t('More options')}
size="nano"
@@ -904,7 +904,7 @@ export const CalendarInvite = ({
setIsSubmitting(false);
addToast(
<ToasterItem type="info">
<span className="material-icons">check_circle</span>
<Icon name="check_circle" />
<span>
{pendingActionIsRsvp
? t("Response saved — the organizer will be notified")
@@ -934,7 +934,7 @@ export const CalendarInvite = ({
setPendingActionIsRsvp(false);
addToast(
<ToasterItem type="error">
<span className="material-icons">error</span>
<Icon name="error" />
<span>{taskStatus.error ?? t("An unexpected error occurred.")}</span>
</ToasterItem>,
);
@@ -4,6 +4,7 @@
// so the visual styling bundled in ui-kit's style.css applies as-is.
import { ReactNode, useState } from "react";
import { Button, useCunningham } from "@gouvfr-lasuite/cunningham-react";
import { Icon } from "@gouvfr-lasuite/ui-kit";
import type { DropdownMenuOption, UserData } from "@gouvfr-lasuite/ui-kit";
import { useTranslation } from "react-i18next";
import { AccessRoleDropdown } from "./access-role-dropdown";
@@ -83,7 +84,7 @@ const InvitationUserSelectorItem = <UserType,>({
color="neutral"
size="nano"
onClick={() => onRemoveUser?.(user)}
icon={<span className="material-icons">close</span>}
icon={<Icon name="close" />}
/>
</div>
);
@@ -30,6 +30,7 @@ import {
type QuickSearchData,
QuickSearchGroup,
QuickSearchItemTemplate,
Icon,
ShareInvitationItem,
type DropdownMenuOption,
type UserData,
@@ -71,7 +72,7 @@ const SearchUserItem = <UserType,>({ user }: SearchUserItemProps<UserType>) => {
right={
<div className="c__search-user-item-right">
<span>{tc("components.share.item.add")}</span>
<span className="material-icons">add</span>
<Icon name="add" />
</div>
}
/>
@@ -533,7 +534,7 @@ const ShowMoreButton = ({ show, onShowMore }: ShowMoreButtonProps) => {
<Button
variant="tertiary"
size="small"
icon={<span className="material-icons">arrow_downward</span>}
icon={<Icon name="arrow_downward" />}
onClick={onShowMore}
>
{tc("components.share.members.load_more")}
@@ -135,7 +135,7 @@ const ThreadMessageHeader = ({
{message.is_trashed && (
<Banner
type="info"
icon={<span className="material-icons">restore_from_trash</span>}
icon={<Icon name="restore_from_trash" />}
fullWidth
actions={canEditThread ? [
{
@@ -1,7 +1,7 @@
import { StatusEnum, useTasksRetrieve } from "@/features/api/gen";
import { addToast, ToasterItem } from "@/features/ui/components/toaster";
import soundbox from "@/features/utils/soundbox";
import { Spinner } from "@gouvfr-lasuite/ui-kit";
import { Icon, Spinner } from "@gouvfr-lasuite/ui-kit";
import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { Id, toast } from "react-toastify";
@@ -49,7 +49,7 @@ export const QueueMessage = ({ taskId, onSettled }: QueueMessageProps) => {
toast.update(toastId, {
render: (
<ToasterItem type="error">
<span className="material-icons">error</span>
<Icon name="error" />
<span>{t('The message could not be sent.')}</span>
</ToasterItem>
),
@@ -71,7 +71,7 @@ export const QueueMessage = ({ taskId, onSettled }: QueueMessageProps) => {
toast.update(toastId, {
render: (
<ToasterItem type="info">
<span className="material-icons">check_circle</span>
<Icon name="check_circle" />
<span>{t('Message sent successfully')}</span>
</ToasterItem>
),
@@ -83,7 +83,7 @@ export const QueueMessage = ({ taskId, onSettled }: QueueMessageProps) => {
toast.update(toastId, {
render: (
<ToasterItem type="error">
<span className="material-icons">error</span>
<Icon name="error" />
<span>{t('The message could not be sent.')}</span>
</ToasterItem>
),
@@ -102,7 +102,7 @@ export const QueueMessage = ({ taskId, onSettled }: QueueMessageProps) => {
toast.update(toastId, {
render: (
<ToasterItem type="warning">
<span className="material-icons">schedule_send</span>
<Icon name="schedule_send" />
<span>{t('Sending is taking longer than expected. You can track your message in the Outbox.')}</span>
</ToasterItem>
),
@@ -56,7 +56,7 @@ export const LabelBadge = ({ label, removable = false, linkable = false, compact
}
}]}
>
<span className="material-icons">label_off</span>
<Icon name="label_off" />
<span>{t('Label "{{label}}" removed from this conversation.', { label: label.name })}</span>
</ToasterItem>,
{ toastId }
@@ -1,4 +1,5 @@
import { Button } from "@gouvfr-lasuite/cunningham-react";
import { Icon } from "@gouvfr-lasuite/ui-kit";
import clsx from "clsx";
import { useMemo } from "react";
import { Slide, ToastContainer, ToastContentProps, toast } from "react-toastify";
@@ -57,7 +58,7 @@ export const ToasterItem = ({
color={buttonColor}
variant="tertiary"
size="small"
icon={action.icon && <span className="material-icons">{action.icon}</span>}
icon={action.icon && <Icon name={action.icon} />}
>{action.showLabel || !action.icon && action.label}</Button>
))}
{closeButton && (
@@ -66,7 +67,7 @@ export const ToasterItem = ({
color={buttonColor}
variant="tertiary"
size="small"
icon={<span className="material-icons">close</span>}
icon={<Icon name="close" />}
></Button>
)}
</div>
@@ -1,3 +1,4 @@
/* eslint-disable i18next/no-literal-string */
import React from "react";
import { describe, it, expect } from "vitest";
import { renderToStaticMarkup } from "react-dom/server";
+2 -2
View File
@@ -25,12 +25,12 @@ const HomePage = () => {
hideLeftPanelOnDesktop
leftPanelContent={<LeftPanel />}
rightHeaderContent={<LanguagePicker />}
icon={<img src={`/images/${theme}/app-logo-${variant}.svg`} alt="logo" height={40} />}
icon={<img src={`/images/${theme}/app-logo-${variant}.svg`} alt={t("logo")} height={40} />}
>
<div id={SKIP_LINK_TARGET_ID} className="app__home">
<HomeGutter>
<Hero
logo={<img src={`/images/${theme}/app-icon-${variant}.svg`} alt="Messages Logo" width={64} />}
logo={<img src={`/images/${theme}/app-icon-${variant}.svg`} alt={t("Messages Logo")} width={64} />}
title={t("Simple and intuitive messaging")}
banner={`/images/banner-${variant}.webp`}
subtitle={t("Send and receive your messages in an instant.")}