feat(i18n): enhanced i18n support (#3761)

This commit is contained in:
Pavel authored and GitHub committed 2023-12-11 09:43:58 -05:00
1 parent 5a5c4f58af
commit d14a8f0199
55 files changed
+319 -114

No files matched your search

@@ -37,7 +37,7 @@ const DisplaySetMessageListTooltip = ({ messages, id }): React.ReactNode => {
marginTop: '12px',
}}
>
Display Set Messages
{t('Display Set Messages')}
</div>
<ol
style={{
@@ -1,12 +1,14 @@
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { useTranslation } from 'react-i18next';
import Icon from '../Icon';
import Typography from '../Typography';
// TODO: Add loading spinner to OHIF + use it here.
const EmptyStudies = ({ className }) => {
const { t } = useTranslation('StudyList');
return (
<div className={classnames('inline-flex flex-col items-center', className)}>
<Icon
@@ -17,7 +19,7 @@ const EmptyStudies = ({ className }) => {
className="text-primary-light"
variant="h5"
>
{'No studies available'}
{t('No studies available')}
</Typography>
</div>
);
@@ -1,6 +1,8 @@
import React, { useState } from 'react';
import { ErrorBoundary as ReactErrorBoundary } from 'react-error-boundary';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import i18n from 'i18next';
import Modal from '../Modal';
import Icon from '../Icon';
@@ -9,9 +11,10 @@ import IconButton from '../IconButton';
const isProduction = process.env.NODE_ENV === 'production';
const DefaultFallback = ({ error, context, resetErrorBoundary, fallbackRoute }) => {
const { t } = useTranslation('ErrorBoundary');
const [showDetails, setShowDetails] = useState(false);
const title = `Something went wrong${!isProduction && ` in ${context}`}.`;
const subtitle = `Sorry, something went wrong there. Try again.`;
const title = `${t('Something went wrong')}${!isProduction && ` ${t('in')} ${context}`}.`;
const subtitle = t('Sorry, something went wrong there. Try again.');
return (
<div
className="ErrorFallback bg-primary-dark h-full w-full"
@@ -21,8 +24,8 @@ const DefaultFallback = ({ error, context, resetErrorBoundary, fallbackRoute })
<p className="text-primary-light text-base">{subtitle}</p>
{!isProduction && (
<div className="bg-secondary-dark mt-5 space-y-2 rounded-md p-5 font-mono">
<p className="text-primary-light">Context: {context}</p>
<p className="text-primary-light">Error Message: {error.message}</p>
<p className="text-primary-light">{t('Context')}: {context}</p>
<p className="text-primary-light">{t('Error Message')}: {error.message}</p>
<IconButton
variant="contained"
@@ -32,7 +35,7 @@ const DefaultFallback = ({ error, context, resetErrorBoundary, fallbackRoute })
onClick={() => setShowDetails(!showDetails)}
>
<React.Fragment>
<div>{'Stack Trace'}</div>
<div>{t('Stack Trace')}</div>
<Icon
width="15px"
height="15px"
@@ -83,7 +86,7 @@ const ErrorBoundary = ({
closeButton
shouldCloseOnEsc
isOpen={isOpen}
title={'Something went wrong'}
title={i18n.t('ErrorBoundary:Something went wrong')}
onClose={() => {
setIsOpen(false);
if (fallbackRoute && typeof window !== 'undefined') {
@@ -1,4 +1,5 @@
import { MODIFIER_KEYS, DISALLOWED_COMBINATIONS } from './hotkeysConfig';
import i18n from 'i18next';
const formatPressedKeys = pressedKeysArray => pressedKeysArray.join('+');
@@ -20,8 +21,8 @@ const findConflictingCommand = (hotkeys, currentCommandName, pressedKeys) => {
};
const ERROR_MESSAGES = {
MODIFIER: "It's not possible to define only modifier keys (ctrl, alt and shift) as a shortcut",
EMPTY: "Field can't be empty.",
MODIFIER: i18n.t('HotkeysValidators:It\'s not possible to define only modifier keys (ctrl, alt and shift) as a shortcut'),
EMPTY: i18n.t('HotkeysValidators:Field can\'t be empty'),
};
// VALIDATORS
@@ -46,7 +47,7 @@ const conflictingValidator = ({ commandName, pressedKeys, hotkeys }) => {
if (conflictingCommand) {
return {
error: `"${conflictingCommand.label}" is already using the "${pressedKeys}" shortcut.`,
error: i18n.t('HotkeysValidators:Hotkey is already in use', {action: conflictingCommand.label, pressedKeys: pressedKeys }),
};
}
};
@@ -62,7 +63,7 @@ const disallowedValidator = ({ pressedKeys = [] }) => {
if (hasDisallowedCombinations) {
return {
error: `"${formatPressedKeys(pressedKeys)}" shortcut combination is not allowed`,
error: i18n.t('HotkeysValidators:Shortcut combination is not allowed', {pressedKeys: formatPressedKeys(pressedKeys)}),
};
}
};
@@ -1,7 +1,9 @@
import React from 'react';
import Icon from '../Icon';
import { useTranslation } from 'react-i18next';
function AddSegmentRow({ onClick }) {
const { t } = useTranslation('SegmentationTable');
return (
<div
className="flex hover:cursor-pointer"
@@ -13,7 +15,7 @@ function AddSegmentRow({ onClick }) {
<div className="grid h-[28px] w-[28px] place-items-center">
<Icon name="icon-add" />
</div>
<span className="text-[13px]">Add Segment</span>
<span className="text-[13px]">{t('Add segment')}</span>
</div>
</div>
</div>
@@ -1,7 +1,9 @@
import React from 'react';
import Icon from '../Icon';
import { useTranslation } from 'react-i18next';
function NoSegmentationRow({ onSegmentationAdd }) {
const { t } = useTranslation('SegmentationTable');
return (
<div
className="group"
@@ -11,7 +13,7 @@ function NoSegmentationRow({ onSegmentationAdd }) {
<div className="grid h-[28px] w-[28px] place-items-center">
<Icon name="icon-add" />
</div>
<span className="text-[13px]">Add Segmentation</span>
<span className="text-[13px]">{t('Add segmentation')}</span>
</div>
</div>
);
@@ -4,6 +4,7 @@ import InputRange from '../InputRange';
import CheckBox from '../CheckBox';
import InputNumber from '../InputNumber';
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
const getRoundedValue = value => {
return Math.round(value * 100) / 100;
@@ -17,19 +18,20 @@ const ActiveSegmentationConfig = ({
setRenderFill,
setFillAlpha,
}) => {
const { t } = useTranslation('SegmentationTable');
return (
<div className="flex justify-between px-3 pt-[13px] text-[12px]">
<div className="flex flex-col items-start">
<div className="mb-[12px] text-white">Active</div>
<div className="mb-[12px] text-white">{t('Active')}</div>
<CheckBox
label="Outline"
label={t('Outline')}
checked={config.renderOutline}
labelClassName="text-[12px] pl-1 pt-1"
className="mb-[9px]"
onChange={setRenderOutline}
/>
<CheckBox
label="Fill"
label={t('Fill')}
checked={config.renderFill}
labelClassName="text-[12px] pl-1 pt-1"
className="mb-[9px]"
@@ -38,7 +40,7 @@ const ActiveSegmentationConfig = ({
</div>
<div className="col-span-2 flex flex-col items-center">
<div className="mb-[12px] text-[10px] text-[#b3b3b3]">Opacity</div>
<div className="mb-[12px] text-[10px] text-[#b3b3b3]">{t('Opacity')}</div>
<InputRange
minValue={0}
maxValue={100}
@@ -64,7 +66,7 @@ const ActiveSegmentationConfig = ({
</div>
<div className="flex flex-col items-center">
<div className="mb-[12px] text-[10px] text-[#b3b3b3]">Size</div>
<div className="mb-[12px] text-[10px] text-[#b3b3b3]">{t('Size')}</div>
<InputNumber
value={config.outlineWidthActive}
onChange={setOutlineWidthActive}
@@ -82,10 +84,11 @@ const InactiveSegmentationConfig = ({
setRenderInactiveSegmentations,
setFillAlphaInactive,
}) => {
const { t } = useTranslation('SegmentationTable');
return (
<div className="px-3">
<CheckBox
label="Display Inactive Segmentations"
label={t('Display inactive segmentations')}
checked={config.renderInactiveSegmentations}
labelClassName="text-[12px]"
className="mb-[9px]"
@@ -93,7 +96,7 @@ const InactiveSegmentationConfig = ({
/>
<div className="flex items-center space-x-2 pl-4">
<span className="text-[10px] text-[#b3b3b3]">Opacity</span>
<span className="text-[10px] text-[#b3b3b3]">{t('Opacity')}</span>
<InputRange
minValue={0}
maxValue={100}
@@ -120,6 +123,7 @@ const SegmentationConfig = ({
setRenderInactiveSegmentations,
setRenderOutline,
}) => {
const { t } = useTranslation('SegmentationTable');
const { initialConfig } = segmentationConfig;
const [isMinimized, setIsMinimized] = useState(true);
return (
@@ -144,7 +148,7 @@ const SegmentationConfig = ({
'rotate-90 transform': !isMinimized,
})}
/>
<span className="text-[12px] font-[300] text-[#d8d8d8]">{'Inactive Segmentations'}</span>
<span className="text-[12px] font-[300] text-[#d8d8d8]">{t('Inactive segmentations')}</span>
</div>
{!isMinimized && (
<InactiveSegmentationConfig
@@ -1,6 +1,7 @@
import React from 'react';
import { Select, Icon, Dropdown } from '../../components';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
function SegmentationDropDownRow({
segmentations = [],
@@ -23,6 +24,7 @@ function SegmentationDropDownRow({
value: s.id,
label: s.label,
}));
const { t } = useTranslation('SegmentationTable');
if (!activeSegmentation) {
return null;
@@ -46,7 +48,7 @@ function SegmentationDropDownRow({
...(!disableEditing
? [
{
title: 'Add New Segmentation',
title: t('Add new segmentation'),
onClick: () => {
onSegmentationAdd();
},
@@ -56,7 +58,7 @@ function SegmentationDropDownRow({
...(!disableEditing
? [
{
title: 'Rename',
title: t('Rename'),
onClick: () => {
onSegmentationEdit(activeSegmentation.id);
},
@@ -64,7 +66,7 @@ function SegmentationDropDownRow({
]
: []),
{
title: 'Delete',
title: t('Delete'),
onClick: () => {
onSegmentationDelete(activeSegmentation.id);
},
@@ -72,7 +74,7 @@ function SegmentationDropDownRow({
...(!disableEditing
? [
{
title: 'Export DICOM SEG',
title: t('Export DICOM SEG'),
onClick: () => {
storeSegmentation(activeSegmentation.id);
},
@@ -81,13 +83,13 @@ function SegmentationDropDownRow({
: []),
...[
{
title: 'Download DICOM SEG',
title: t('Download DICOM SEG'),
onClick: () => {
onSegmentationDownload(activeSegmentation.id);
},
},
{
title: 'Download DICOM RTSTRUCT',
title: t('Download DICOM RTSTRUCT'),
onClick: () => {
onSegmentationDownloadRTSS(activeSegmentation.id);
},
@@ -6,6 +6,7 @@ import SegmentationDropDownRow from './SegmentationDropDownRow';
import NoSegmentationRow from './NoSegmentationRow';
import AddSegmentRow from './AddSegmentRow';
import SegmentationGroupSegment from './SegmentationGroupSegment';
import { useTranslation } from 'react-i18next';
const SegmentationGroupTable = ({
segmentations,
@@ -70,11 +71,12 @@ const SegmentationGroupTable = ({
const activeSegmentation = segmentations?.find(
segmentation => segmentation.id === activeSegmentationId
);
const { t } = useTranslation('SegmentationTable');
return (
<div className="flex min-h-0 flex-col bg-black text-[13px] font-[300]">
<PanelSection
title="Segmentation"
title={t('Segmentation')}
actionIcons={
activeSegmentation && [
{
@@ -1,7 +1,6 @@
import classnames from 'classnames';
import PropTypes from 'prop-types';
import React, { CSSProperties, useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import Icon from '../Icon';
import Tooltip from '../Tooltip';
@@ -131,8 +130,6 @@ const getTabIconClassNames = (numTabs: number, isActiveTab: boolean) => {
};
const SidePanel = ({ side, className, activeTabIndex: activeTabIndexProp, tabs, onOpen }) => {
const { t } = useTranslation('SidePanel');
const [panelOpen, setPanelOpen] = useState(activeTabIndexProp !== null);
const [activeTabIndex, setActiveTabIndex] = useState(0);
@@ -1,6 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { useTranslation } from 'react-i18next';
import Icon from '../Icon';
@@ -16,6 +17,7 @@ const StudyItem = ({
isActive,
onClick,
}) => {
const { t } = useTranslation('StudyItem');
return (
<div
className={classnames(
@@ -57,7 +59,7 @@ const StudyItem = ({
name="tracked"
className="text-primary-light mr-2 w-4"
/>
{trackedSeries} Tracked Series
{t('Tracked series', {trackedSeries: trackedSeries})}
</div>
</div>
)}
@@ -48,7 +48,7 @@ const StudyListFilter = ({
onClick={onUploadClick}
>
<Icon name="icon-upload"></Icon>
<span>Upload</span>
<span>{t('Upload')}</span>
</div>
)}
</div>
@@ -67,6 +67,12 @@ const StudyListFilter = ({
{t('ClearFilters')}
</LegacyButton>
)}
<Typography
variant="h6"
className="text-primary-light"
>
{`${t('Number of studies')}: `}
</Typography>
<Typography
variant="h6"
className="mr-2"
@@ -74,12 +80,6 @@ const StudyListFilter = ({
>
{numOfStudies > 100 ? '>100' : numOfStudies}
</Typography>
<Typography
variant="h6"
className="text-primary-light"
>
{t('Number of studies')}
</Typography>
</div>
</div>
</div>
@@ -99,7 +99,7 @@ const StudyListFilter = ({
{numOfStudies > 100 && (
<div className="container m-auto">
<div className="bg-primary-main rounded-b py-1 text-center text-base">
<p className="text-white">{t('NumOfStudiesHiggerThan100Message')}</p>
<p className="text-white">{t('Filter list to 100 studies or less to enable sorting')}</p>
</div>
</div>
)}
@@ -6,8 +6,9 @@ import Icon from '../Icon';
import Thumbnail from '../Thumbnail';
import Tooltip from '../Tooltip';
import { StringNumber } from '../../types';
import { useTranslation } from 'react-i18next';
const ThumbnailTracked = ({
function ThumbnailTracked ({
displaySetInstanceUID,
className,
imageSrc,
@@ -24,7 +25,8 @@ const ThumbnailTracked = ({
viewportIdentificator,
isTracked,
isActive,
}) => {
}) {
const { t } = useTranslation('ThumbnailTracked');
const trackedIcon = isTracked ? 'circled-checkmark' : 'dotted-circle';
const viewportIdentificatorLabel = viewportIdentificator.join(', ');
const renderViewportLabels = () => {
@@ -42,7 +44,7 @@ const ThumbnailTracked = ({
position="right"
content={
<div className="max-w-40 text-left">
Series is displayed <br /> in viewport {viewportIdentificatorLabel}
{`${t('Viewport')}: ${viewportIdentificatorLabel}`}
</div>
}
>
@@ -85,13 +87,11 @@ const ThumbnailTracked = ({
</div>
<div className="flex flex-1 flex-col">
<span>
Series is
<span className="text-white">{isTracked ? ' tracked' : ' untracked'}</span>
<span className="text-white">{isTracked ? t('Series is tracked') : t('Series is untracked')}</span>
</span>
{!!viewportIdentificator.length && (
<span>
in viewport
<span className="ml-1 text-white">{viewportIdentificatorLabel}</span>
{`${t('Viewport')}: ${viewportIdentificatorLabel}`}
</span>
)}
</div>
@@ -1,4 +1,5 @@
import React, { useState, useRef, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import PropTypes from 'prop-types';
import classnames from 'classnames';
@@ -9,6 +10,8 @@ const DELAY_TO_HIDE = 10; // it needs at least a little delay to prevent tooltip
const DELAY_TO_HIDE_AFTER_COPYING = 1000;
const TooltipClipboard = ({ children, text }) => {
const { t } = useTranslation('TooltipClipboard');
const [isActive, setIsActive] = useState(false);
const [message, setMessage] = useState(null);
const [isCopying, setIsCopying] = useState(false);
@@ -21,10 +24,10 @@ const TooltipClipboard = ({ children, text }) => {
setIsCopying(true);
try {
await navigator.clipboard.writeText(text);
setMessage('Copied!');
setMessage(t('Copied'));
} catch (err) {
console.error('Failed to copy: ', err);
setMessage('Failed to copy!');
setMessage(t('Failed to copy'));
} finally {
refreshElementPosition();