chore: update French i18n (#5561)

This commit is contained in:
Martin Bellehumeur, M. Eng. authored and GitHub committed 2025-12-12 10:46:56 -05:00
1 parent ba140a05f7
commit 181c0f32c5
185 files changed
+2779 -374

No files matched your search

@@ -1,6 +1,24 @@
import * as React from 'react';
import { useMemo } from 'react';
import { ChevronLeft, ChevronRight } from 'lucide-react';
import { DayPicker } from 'react-day-picker';
import { useTranslation } from 'react-i18next';
import { format } from 'date-fns';
import type { Locale } from 'date-fns';
import {
ar as arLocale,
ca as caLocale,
de as deLocale,
enUS,
fr as frLocale,
ja as jaLocale,
nl as nlLocale,
ptBR as ptBRLocale,
ru as ruLocale,
tr as trLocale,
vi as viLocale,
zhCN as zhLocale,
} from 'date-fns/locale';
import { cn } from '../../lib/utils';
@@ -8,7 +26,37 @@ import { buttonVariants } from '../Button';
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
const DATE_FNS_LOCALE_MAP: Record<string, Locale> = {
en: enUS,
'en-US': enUS,
fr: frLocale,
'fr-FR': frLocale,
ar: arLocale,
ca: caLocale,
de: deLocale,
'ja-JP': jaLocale,
ja: jaLocale,
nl: nlLocale,
'pt-BR': ptBRLocale,
pt: ptBRLocale,
ru: ruLocale,
'tr-TR': trLocale,
tr: trLocale,
vi: viLocale,
zh: zhLocale,
'zh-CN': zhLocale,
'zh-cn': zhLocale,
'test-LNG': enUS,
};
function Calendar({ className, classNames, showOutsideDays = true, ...props }: CalendarProps) {
const { i18n, t } = useTranslation('DatePicker');
const locale = useMemo(() => {
const lang = i18n.language || 'en';
return DATE_FNS_LOCALE_MAP[lang] ?? enUS;
}, [i18n.language]);
return (
<DayPicker
showOutsideDays={showOutsideDays}
@@ -20,6 +68,10 @@ function Calendar({ className, classNames, showOutsideDays = true, ...props }: C
labelMonthDropdown: () => undefined,
labelYearDropdown: () => undefined,
}}
locale={locale}
formatters={{
formatCaption: month => format(month, 'LLLL yyyy', { locale }),
}}
classNames={{
months: 'flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0',
month: 'space-y-4',
@@ -30,7 +82,7 @@ function Calendar({ className, classNames, showOutsideDays = true, ...props }: C
nav: 'space-x-1 flex items-center',
table: 'w-full border-collapse space-y-1',
head_row: 'flex',
head_cell: 'text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]',
head_cell: 'text-muted-foreground rounded-md w-9 font-normal text-[0.8rem] uppercase',
row: 'flex w-full mt-2',
cell: 'h-9 w-9 text-center text-base p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20',
day: cn(
@@ -9,6 +9,7 @@ import {
import { Icons } from '../../components/Icons/Icons';
import { Tooltip, TooltipTrigger, TooltipContent } from '../../components/Tooltip/Tooltip';
import { cn } from '../../lib/utils';
import { useTranslation } from 'react-i18next';
/**
* DataRow is a complex UI component that displays a selectable, interactive row with hierarchical data.
@@ -135,6 +136,7 @@ const DataRowComponent = React.forwardRef<HTMLDivElement, DataRowProps>(
},
ref
) => {
const { t } = useTranslation('DataRow');
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const isTitleLong = title?.length > 25;
@@ -312,7 +314,7 @@ const DataRowComponent = React.forwardRef<HTMLDivElement, DataRowProps>(
className={`h-6 w-6 transition-opacity ${
isSelected || !isVisible ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'
}`}
aria-label={isVisible ? 'Hide' : 'Show'}
aria-label={isVisible ? t('Hide') : t('Show')}
onClick={e => {
e.stopPropagation();
onToggleVisibility(e);
@@ -361,7 +363,7 @@ const DataRowComponent = React.forwardRef<HTMLDivElement, DataRowProps>(
className="pl-2"
data-cy="Rename"
>
Rename
{t('Rename')}
</span>
</DropdownMenuItem>
{onCopy && (
@@ -371,7 +373,7 @@ const DataRowComponent = React.forwardRef<HTMLDivElement, DataRowProps>(
className="pl-2"
data-cy="Duplicate"
>
Duplicate
{t('Duplicate')}
</span>
</DropdownMenuItem>
)}
@@ -381,7 +383,7 @@ const DataRowComponent = React.forwardRef<HTMLDivElement, DataRowProps>(
className="pl-2"
data-cy="Delete"
>
Delete
{t('Delete')}
</span>
</DropdownMenuItem>
{onColor && (
@@ -391,7 +393,7 @@ const DataRowComponent = React.forwardRef<HTMLDivElement, DataRowProps>(
className="pl-2"
data-cy="Change Color"
>
Change Color
{t('Change Color')}
</span>
</DropdownMenuItem>
)}
@@ -401,7 +403,7 @@ const DataRowComponent = React.forwardRef<HTMLDivElement, DataRowProps>(
className="pl-2"
data-cy="LockToggle"
>
{isLocked ? 'Unlock' : 'Lock'}
{isLocked ? t('Unlock') : t('Lock')}
</span>
</DropdownMenuItem>
</>
@@ -1,6 +1,7 @@
import * as React from 'react';
import { format, parse, isValid } from 'date-fns';
import { Calendar as CalendarIcon } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { cn } from '../../lib/utils';
import { Calendar } from '../Calendar';
import * as Popover from '../Popover';
@@ -23,6 +24,7 @@ export function DatePickerWithRange({
onChange,
...props
}: React.HTMLAttributes<HTMLDivElement> & DatePickerWithRangeProps) {
const { t } = useTranslation('DatePicker');
const [start, setStart] = React.useState<string>(
startDate ? format(parse(startDate, 'yyyyMMdd', new Date()), 'yyyy-MM-dd') : ''
);
@@ -85,7 +87,7 @@ export function DatePickerWithRange({
<input
id={`${id}-start`}
type="text"
placeholder="Start date"
placeholder={t('Start Date', 'Start date')}
autoComplete="off"
value={start}
onChange={e => handleInputChange(e, 'start')}
@@ -122,7 +124,7 @@ export function DatePickerWithRange({
<input
id={`${id}-end`}
type="text"
placeholder="End date"
placeholder={t('End Date', 'End date')}
autoComplete="off"
value={end}
onChange={e => handleInputChange(e, 'end')}
@@ -11,7 +11,6 @@ export const Patient = (props: IconProps) => (
xmlnsXlink="http://www.w3.org/1999/xlink"
{...props}
>
<title>icon-patient</title>
<g
id="Production"
stroke="none"
@@ -10,7 +10,6 @@ export const ToolLayout = (props: IconProps) => (
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<title>tool-layout</title>
<g
id="tool-layout"
stroke="none"
@@ -394,7 +393,6 @@ export const ToolBidirectional = (props: IconProps) => (
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<title>tool-bidirectional</title>
<g
id="tool-bidirectional"
stroke="none"
@@ -615,7 +613,6 @@ export const ToolCapture = (props: IconProps) => (
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<title>tool-</title>
<g
id="tool-"
stroke="none"
@@ -60,7 +60,7 @@ const Body = () => {
if (!data || data.length === 0) {
return (
<div className="text-primary-light mb-1 flex flex-1 items-center px-2 py-2 text-base">
No tracked measurements
{useTranslation('MeasurementTable').t('No tracked measurements')}
</div>
);
}
@@ -1,4 +1,5 @@
import * as React from 'react';
import { useTranslation } from 'react-i18next';
import { Input } from '../Input/Input';
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '../Select/Select';
import { Switch } from '../Switch/Switch';
@@ -143,6 +144,13 @@ interface ImageSizeProps {
className?: string;
maxWidth?: string;
maxHeight?: string;
/** Optional translation namespace. Defaults to 'CaptureViewportModal'. */
translationNamespace?: string;
/** Optional labels/placeholders to override translations */
widthLabel?: string;
heightLabel?: string;
widthPlaceholder?: string;
heightPlaceholder?: string;
}
function ImageSize({
@@ -154,7 +162,19 @@ function ImageSize({
className,
maxWidth,
maxHeight,
translationNamespace = 'CaptureViewportModal',
widthLabel,
heightLabel,
widthPlaceholder,
heightPlaceholder,
}: ImageSizeProps) {
const { t } = useTranslation(translationNamespace);
// Use translations as defaults, but allow props to override
const finalWidthLabel = widthLabel ?? t('Width', { defaultValue: 'Width' });
const finalHeightLabel = heightLabel ?? t('Height', { defaultValue: 'Height' });
const finalWidthPlaceholder = widthPlaceholder ?? t('Width', { defaultValue: 'Width' });
const finalHeightPlaceholder = heightPlaceholder ?? t('Height', { defaultValue: 'Height' });
return (
<div className={cn('text-foreground space-y-1', className)}>
<label className="block text-base">{children}</label>
@@ -163,23 +183,23 @@ function ImageSize({
<div className="flex items-center space-x-4">
{/* Width group */}
<div className="flex items-center space-x-2">
<span className="text-foreground text-base">W</span>
<span className="text-foreground text-base">{finalWidthLabel}</span>
<Input
value={width}
onChange={onWidthChange ?? (() => {})}
placeholder="Width"
placeholder={finalWidthPlaceholder}
className="w-20"
max={maxWidth}
/>
</div>
{/* Height group */}
{/* Height/Length group */}
<div className="text-foreground flex items-center space-x-2 text-base">
<span className="text-foreground text-base">H</span>
<span className="text-foreground text-base">{finalHeightLabel}</span>
<Input
value={height}
onChange={onHeightChange ?? (() => {})}
placeholder="Height"
placeholder={finalHeightPlaceholder}
className="w-20"
max={maxHeight}
/>
@@ -1,8 +1,114 @@
import * as React from 'react';
import type { TFunction } from 'i18next';
import { useTranslation } from 'react-i18next';
import { Label } from '../Label';
import { Input } from '../Input';
import { cn } from '../../lib/utils';
const HOTKEY_TOKEN_TRANSLATIONS: Record<
string,
{
i18nKey: string;
defaultValue: string;
}
> = {
ctrl: { i18nKey: 'HotkeyKeys.ctrl', defaultValue: 'Ctrl' },
control: { i18nKey: 'HotkeyKeys.ctrl', defaultValue: 'Ctrl' },
shift: { i18nKey: 'HotkeyKeys.shift', defaultValue: 'Shift' },
alt: { i18nKey: 'HotkeyKeys.alt', defaultValue: 'Alt' },
option: { i18nKey: 'HotkeyKeys.option', defaultValue: 'Option' },
meta: { i18nKey: 'HotkeyKeys.meta', defaultValue: 'Cmd' },
command: { i18nKey: 'HotkeyKeys.meta', defaultValue: 'Cmd' },
cmd: { i18nKey: 'HotkeyKeys.meta', defaultValue: 'Cmd' },
enter: { i18nKey: 'HotkeyKeys.enter', defaultValue: 'Enter' },
return: { i18nKey: 'HotkeyKeys.enter', defaultValue: 'Enter' },
esc: { i18nKey: 'HotkeyKeys.esc', defaultValue: 'Esc' },
escape: { i18nKey: 'HotkeyKeys.esc', defaultValue: 'Esc' },
space: { i18nKey: 'HotkeyKeys.space', defaultValue: 'Space' },
spacebar: { i18nKey: 'HotkeyKeys.space', defaultValue: 'Space' },
tab: { i18nKey: 'HotkeyKeys.tab', defaultValue: 'Tab' },
backspace: { i18nKey: 'HotkeyKeys.backspace', defaultValue: 'Backspace' },
delete: { i18nKey: 'HotkeyKeys.delete', defaultValue: 'Delete' },
del: { i18nKey: 'HotkeyKeys.delete', defaultValue: 'Delete' },
insert: { i18nKey: 'HotkeyKeys.insert', defaultValue: 'Insert' },
ins: { i18nKey: 'HotkeyKeys.insert', defaultValue: 'Insert' },
home: { i18nKey: 'HotkeyKeys.home', defaultValue: 'Home' },
end: { i18nKey: 'HotkeyKeys.end', defaultValue: 'End' },
pageup: { i18nKey: 'HotkeyKeys.pageup', defaultValue: 'Page Up' },
pagedown: { i18nKey: 'HotkeyKeys.pagedown', defaultValue: 'Page Down' },
up: { i18nKey: 'HotkeyKeys.up', defaultValue: 'Up Arrow' },
down: { i18nKey: 'HotkeyKeys.down', defaultValue: 'Down Arrow' },
left: { i18nKey: 'HotkeyKeys.left', defaultValue: 'Left Arrow' },
right: { i18nKey: 'HotkeyKeys.right', defaultValue: 'Right Arrow' },
capslock: { i18nKey: 'HotkeyKeys.capslock', defaultValue: 'Caps Lock' },
plus: { i18nKey: 'HotkeyKeys.plus', defaultValue: 'Plus' },
minus: { i18nKey: 'HotkeyKeys.minus', defaultValue: 'Minus' },
comma: { i18nKey: 'HotkeyKeys.comma', defaultValue: 'Comma' },
period: { i18nKey: 'HotkeyKeys.period', defaultValue: 'Period' },
slash: { i18nKey: 'HotkeyKeys.slash', defaultValue: 'Slash' },
backslash: { i18nKey: 'HotkeyKeys.backslash', defaultValue: 'Backslash' },
semicolon: { i18nKey: 'HotkeyKeys.semicolon', defaultValue: 'Semicolon' },
quote: { i18nKey: 'HotkeyKeys.quote', defaultValue: 'Quote' },
apostrophe: { i18nKey: 'HotkeyKeys.quote', defaultValue: 'Quote' },
backquote: { i18nKey: 'HotkeyKeys.backquote', defaultValue: 'Backtick' },
tilde: { i18nKey: 'HotkeyKeys.backquote', defaultValue: 'Backtick' },
bracketleft: { i18nKey: 'HotkeyKeys.bracketleft', defaultValue: 'Left Bracket' },
bracketright: { i18nKey: 'HotkeyKeys.bracketright', defaultValue: 'Right Bracket' },
};
const formatFallbackToken = (token: string) => {
if (!token) {
return '';
}
if (/^f\d{1,2}$/i.test(token)) {
return token.toUpperCase();
}
if (token.length === 1) {
return token.toUpperCase();
}
return token.charAt(0).toUpperCase() + token.slice(1);
};
const normalizeHotkeyValue = (value?: string | string[]) => {
if (!value) {
return '';
}
if (Array.isArray(value)) {
return value.join('+');
}
return value;
};
const translateHotkeyValue = (value: string | string[] | undefined, t: TFunction) => {
const normalizedValue = normalizeHotkeyValue(value);
if (!normalizedValue) {
return '';
}
return normalizedValue
.split('+')
.map(rawToken => {
const trimmed = rawToken.trim();
if (!trimmed) {
return '';
}
const lower = trimmed.toLowerCase();
const config = HOTKEY_TOKEN_TRANSLATIONS[lower];
if (config) {
return t(config.i18nKey, { defaultValue: config.defaultValue });
}
return formatFallbackToken(trimmed);
})
.join('+');
};
interface UserPreferencesModalProps {
children: React.ReactNode;
className?: string;
@@ -72,6 +178,12 @@ interface HotkeyProps {
function Hotkey({ label, placeholder, className, value, onChange, hotkeys }: HotkeyProps) {
const [isRecording, setIsRecording] = React.useState(false);
const { t } = useTranslation('UserPreferencesModal');
const translatedValue = React.useMemo(() => translateHotkeyValue(value, t), [value, t]);
const translatedPlaceholder = React.useMemo(
() => translateHotkeyValue(placeholder, t),
[placeholder, t]
);
const onInputKeyDown = (event: React.KeyboardEvent) => {
event.preventDefault();
@@ -95,15 +207,15 @@ function Hotkey({ label, placeholder, className, value, onChange, hotkeys }: Hot
};
return (
<div className={cn('flex items-center justify-between space-x-2', className)}>
<Label className="whitespace-nowrap">{label}</Label>
<div className={cn('flex items-start justify-between gap-2', className)}>
<Label className="flex-1 whitespace-normal break-words text-sm">{label}</Label>
<Input
className={cn(
'w-16 text-center transition-colors',
isRecording && 'bg-accent text-accent-foreground caret-accent-foreground'
)}
placeholder={isRecording ? 'Press keys...' : placeholder}
value={value}
placeholder={isRecording ? t('Press keys') : translatedPlaceholder || ''}
value={isRecording ? normalizeHotkeyValue(value) : translatedValue}
onKeyDown={onInputKeyDown}
onFocus={onFocus}
onBlur={onBlur}
@@ -1,8 +1,10 @@
import React from 'react';
import { Button, Icons } from '@ohif/ui-next';
import { useSegmentationTableContext, useSegmentationExpanded } from './contexts';
import { useTranslation } from 'react-i18next';
export const AddSegmentRow: React.FC<{ children?: React.ReactNode }> = ({ children = null }) => {
const { t } = useTranslation('SegmentationPanel');
const {
activeRepresentation,
disableEditing,
@@ -56,7 +58,7 @@ export const AddSegmentRow: React.FC<{ children?: React.ReactNode }> = ({ childr
onClick={() => onSegmentAdd(segmentationId)}
>
<Icons.Add />
Add Segment
{t('Add Segment')}
</Button>
) : null}
</div>
@@ -6,7 +6,7 @@ import { useSegmentationTableContext } from './contexts';
export const AddSegmentationRow: React.FC<{ children?: React.ReactNode }> = ({
children = null,
}) => {
const { t } = useTranslation('SegmentationTable');
const { t } = useTranslation('SegmentationPanel');
const {
onSegmentationAdd,
@@ -49,7 +49,7 @@ const SegmentationCollapsedDropdownMenu = ({ children }: { children: React.React
// Selector component - for the segmentation selection dropdown
const SegmentationCollapsedSelector = () => {
const { t } = useTranslation('SegmentationTable.HeaderCollapsed');
const { t } = useTranslation('SegmentationPanel.HeaderCollapsed');
const { data, onSegmentationClick, segmentationRepresentationType } = useSegmentationTableContext(
'SegmentationCollapsedSelector'
);
@@ -31,7 +31,7 @@ interface SegmentationTableComponent extends React.FC<SegmentationTableProps> {
}
export const SegmentationTableRoot = (props: SegmentationTableProps) => {
const { t } = useTranslation('SegmentationTable');
const { t } = useTranslation('SegmentationPanel');
const {
data = [],
mode,
@@ -9,7 +9,7 @@ import { Input } from '../Input';
import { useSegmentationTableContext } from './contexts';
export const SegmentationTableConfig: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
const { t } = useTranslation('SegmentationTable.AppearanceSettings');
const { t } = useTranslation('SegmentationPanel');
const {
renderFill,
renderOutline,
@@ -84,7 +84,7 @@ export const SegmentationTableConfig: React.FC<{ children?: React.ReactNode }> =
<div className="space-y-2">
<div className="my-2 flex items-center">
<Label className="text-muted-foreground w-14 flex-none whitespace-nowrap text-xs">
Opacity
{t('Opacity')}
</Label>
<Slider
className="mx-1 flex-1"
@@ -143,7 +143,7 @@ export const SegmentationTableConfig: React.FC<{ children?: React.ReactNode }> =
{renderInactiveSegmentations && (
<div className="my-2 flex items-center">
<Label className="text-muted-foreground w-14 flex-none whitespace-nowrap text-xs">
Opacity
{t('Opacity')}
</Label>
<Slider
className="mx-1 flex-1"
+2 -2
View File
@@ -1,7 +1,7 @@
import * as utils from './utils';
import { cn } from './utils';
import { cn, formatDICOMDate } from './utils';
export * from './components';
export * from './contextProviders';
export * as Types from './types';
export { utils, cn };
export { utils, cn, formatDICOMDate };
export { useSessionStorage, useDynamicMaxHeight } from './hooks';
@@ -0,0 +1,25 @@
import moment from 'moment';
import i18n from 'i18next';
/**
* Formats DICOM date.
*
* @param {string} date
* @param {string} strFormat
* @returns {string} formatted date.
*/
export function formatDICOMDate(date: string, strFormat?: string): string {
if (!date) {
return '';
}
const format = strFormat ?? i18n.t('Common:localDateFormat', 'MMM D, YYYY');
const locale = i18n.language || 'en';
const parsed = moment(date, ['YYYYMMDD', 'YYYY.MM.DD'], true);
if (!parsed.isValid()) {
return moment(date).locale(locale).format(format);
}
return parsed.locale(locale).format(format);
}
+2 -1
View File
@@ -1,5 +1,6 @@
import { getToggledClassName } from './getToggledClassName';
import roundNumber from './roundNumber';
import { cn } from '../lib/utils';
import { formatDICOMDate } from './formatDICOMDate';
export { getToggledClassName, roundNumber, cn };
export { getToggledClassName, roundNumber, cn, formatDICOMDate };