feat(i18n): enhanced i18n support (#3761)
This commit is contained in:
1 parent
5a5c4f58af
commit
d14a8f0199
55 files changed
+319
-114
No files matched your search
+1
-1
@@ -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();
|
||||
|
||||
|
||||
Reference in new issue
Block a user