Add translations for views (#325) (#328)

* Add translations for views (#325)

---------

Co-authored-by: Kevin <60135953+kev-ac@users.noreply.github.com>
This commit is contained in:
Carlos Valente
2023-04-05 21:41:50 +02:00
committed by GitHub
parent e56460a940
commit cb39a13bea
14 changed files with 143 additions and 44 deletions
+4 -1
View File
@@ -10,6 +10,7 @@ import useElectronEvent from './common/hooks/useElectronEvent';
import { ontimeQueryClient } from './common/queryClient';
import { connectSocket } from './common/utils/socket';
import theme from './theme/theme';
import { TranslationProvider } from './translation/TranslationProvider';
import AppRouter from './AppRouter';
// Load Open Sans typeface
@@ -52,7 +53,9 @@ function App() {
<div className='App'>
<ErrorBoundary>
<Suspense fallback={null}>
<AppRouter />
<TranslationProvider>
<AppRouter />
</TranslationProvider>
</Suspense>
</ErrorBoundary>
<ReactQueryDevtools initialIsOpen={false} />
+13 -1
View File
@@ -1,8 +1,9 @@
import { lazy, useEffect } from 'react';
import { Navigate, Route, Routes, useLocation, useNavigate } from 'react-router-dom';
import { Navigate, Route, Routes, useLocation, useNavigate, useSearchParams } from 'react-router-dom';
import useAliases from './common/hooks-query/useAliases';
import withData from './features/viewers/ViewWrapper';
import { useTranslation } from './translation/TranslationProvider';
const Editor = lazy(() => import('./features/editors/ProtectedEditor'));
const Table = lazy(() => import('./features/table/ProtectedTable'));
@@ -36,6 +37,17 @@ export default function AppRouter() {
const { data } = useAliases();
const location = useLocation();
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const { setLanguage } = useTranslation();
// Set output language
useEffect(() => {
const langParam = searchParams.get('lang');
if (langParam && langParam.length === 2) {
setLanguage(searchParams.get('lang'));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchParams]);
// navigate if is alias route
useEffect(() => {
@@ -1,3 +1,5 @@
import { useTranslation } from '../../../translation/TranslationProvider';
import './TitleCard.scss';
interface TitleCardProps {
@@ -9,13 +11,15 @@ interface TitleCardProps {
export default function TitleCard(props: TitleCardProps) {
const { label, title, subtitle, presenter } = props;
const { getLocalizedString } = useTranslation();
const accent = label === 'now';
return (
<div className='title-card'>
<div className='inline'>
<span className='presenter'>{presenter}</span>
<span className={accent? 'label accent': 'label'}>{label}</span>
<span className={accent ? 'label accent' : 'label'}>{getLocalizedString(`common.${label}`)}</span>
</div>
<div className='title'>{title}</div>
<div className='subtitle'>{subtitle}</div>
@@ -12,4 +12,5 @@ export type OverridableOptions = {
hideOvertime?: boolean;
hideMessagesOverlay?: boolean;
hideEndMessage?: boolean;
language?: string;
};
@@ -15,6 +15,7 @@ import { TimeManagerType } from '../../../common/models/TimeManager.type';
import { formatDisplay, millisToSeconds } from '../../../common/utils/dateConfig';
import { getEventsWithDelay } from '../../../common/utils/eventsManager';
import { formatTime } from '../../../common/utils/time';
import { useTranslation } from '../../../translation/TranslationProvider';
import { titleVariants } from '../common/animation';
import { TitleManager } from '../ViewWrapper';
@@ -39,6 +40,7 @@ interface BackstageProps {
export default function Backstage(props: BackstageProps) {
const { isMirrored, publ, title, time, backstageEvents, selectedId, general, viewSettings } = props;
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
const { getLocalizedString } = useTranslation();
// Set window title
useEffect(() => {
@@ -77,7 +79,7 @@ export default function Backstage(props: BackstageProps) {
<div className='event-header'>
{general.title}
<div className='clock-container'>
<div className='label'>Time Now</div>
<div className='label'>{getLocalizedString('common.time_now')}</div>
<div className='time'>{clock}</div>
</div>
</div>
@@ -103,15 +105,15 @@ export default function Backstage(props: BackstageProps) {
/>
<div className='timer-group'>
<div className='aux-timers'>
<div className='aux-timers__label'>Started At</div>
<div className='aux-timers__label'>{getLocalizedString('common.started_at')}</div>
<div className='aux-timers__value'>{startedAt}</div>
</div>
<div className='aux-timers'>
<div className='aux-timers__label'>Expected Finish</div>
<div className='aux-timers__label'>{getLocalizedString('common.expected_finish')}</div>
<div className='aux-timers__value'>{expectedFinish}</div>
</div>
<div className='aux-timers'>
<div className='aux-timers__label'>Stage Timer</div>
<div className='aux-timers__label'>{getLocalizedString('common.stage_timer')}</div>
<div className='aux-timers__value'>{stageTimer}</div>
</div>
</div>
@@ -146,7 +148,7 @@ export default function Backstage(props: BackstageProps) {
</ScheduleProvider>
<div className={showPublicMessage ? 'public-container' : 'public-container public-container--hidden'}>
<div className='label'>Public message</div>
<div className='label'>{getLocalizedString('common.public_message')}</div>
<div className='message'>{publ.text}</div>
</div>
@@ -9,6 +9,7 @@ import { TimeManagerType } from '../../../common/models/TimeManager.type';
import { formatDisplay, millisToSeconds } from '../../../common/utils/dateConfig';
import getDelayTo from '../../../common/utils/getDelayTo';
import { formatTime } from '../../../common/utils/time';
import { useTranslation } from '../../../translation/TranslationProvider';
import { fetchTimerData, TimerMessage } from './countdown.helpers';
import CountdownSelect from './CountdownSelect';
@@ -37,6 +38,7 @@ export default function Countdown(props: CountdownProps) {
const { isMirrored, backstageEvents, time, selectedId, viewSettings } = props;
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
const [searchParams] = useSearchParams();
const { getLocalizedString } = useTranslation();
const [follow, setFollow] = useState<OntimeEvent | null>(null);
const [runningTimer, setRunningTimer] = useState(0);
@@ -112,11 +114,11 @@ export default function Countdown(props: CountdownProps) {
) : (
<div className='countdown-container' data-testid='countdown-event'>
<div className='clock-container'>
<div className='label'>Time Now</div>
<div className='label'>{getLocalizedString('common.time_now')}</div>
<div className='time'>{clock}</div>
</div>
<div className='status'>{runningMessage}</div>
<div className='status'>{getLocalizedString(`countdown.${runningMessage}`)}</div>
<span className={`timer ${standby ? 'timer--paused' : ''} ${isRunningFinished ? 'timer--finished' : ''}`}>
{formattedTimer}
@@ -125,11 +127,11 @@ export default function Countdown(props: CountdownProps) {
<div className='timer-group'>
<div className='aux-timers'>
<div className='aux-timers__label'>Start Time</div>
<div className='aux-timers__label'>{getLocalizedString('common.start_time')}</div>
<span className={`aux-timers__value ${delayedTimerStyles}`}>{startTime}</span>
</div>
<div className='aux-timers'>
<div className='aux-timers__label'>End Time</div>
<div className='aux-timers__label'>{getLocalizedString('common.end_time')}</div>
<span className={`aux-timers__value ${delayedTimerStyles}`}>{endTime}</span>
</div>
</div>
@@ -3,42 +3,43 @@ import { OntimeEvent, OntimeRundownEntry, SupportedEvent } from 'ontime-types';
import Empty from '../../../common/components/state/Empty';
import { formatTime } from '../../../common/utils/time';
import { useTranslation } from '../../../translation/TranslationProvider';
import { sanitiseTitle } from './countdown.helpers';
import './Countdown.scss';
interface CountdownSelectProps {
events: OntimeRundownEntry[];
}
export default function CountdownSelect(props: CountdownSelectProps) {
const { events } = props;
const filteredEvents = events.filter((event: OntimeRundownEntry) => event.type === SupportedEvent.Event) as OntimeEvent[];
const { getLocalizedString } = useTranslation();
const filteredEvents = events.filter(
(event: OntimeRundownEntry) => event.type === SupportedEvent.Event,
) as OntimeEvent[];
return (
<div className='event-select' data-testid='countdown-select'>
<span className='event-select__title'>Select an event to follow</span>
<span className='event-select__title'>{getLocalizedString('countdown.select_event')}</span>
<ul className='event-select__events'>
{!events.length ? (
<Empty text='No events in database' />
) : (
filteredEvents.map((event: OntimeEvent, counter: number) => {
const index = counter + 1;
const title = sanitiseTitle(event.title);
const start = formatTime(event.timeStart, { format: 'hh:mm' });
const end = formatTime(event.timeEnd, { format: 'hh:mm' });
const index = counter + 1;
const title = sanitiseTitle(event.title);
const start = formatTime(event.timeStart, { format: 'hh:mm' });
const end = formatTime(event.timeEnd, { format: 'hh:mm' });
return (
<li key={event.id}>
<Link to={`/countdown?eventid=${event.id}`}>
{`${index}. ${start}${end} | ${title}`}
</Link>
</li>
);
},
)
return (
<li key={event.id}>
<Link to={`/countdown?eventid=${event.id}`}>{`${index}. ${start}${end} | ${title}`}</Link>
</li>
);
})
)}
</ul>
</div>
@@ -3,23 +3,26 @@ import { OntimeEvent, Playback } from 'ontime-types';
import { TimeManagerType } from '../../../common/models/TimeManager.type';
export enum TimerMessage {
toStart = 'Time to start',
waiting = 'Waiting for event start',
running = 'Event running',
ended = 'Event ended at',
toStart = 'to_start',
waiting = 'waiting',
running = 'running',
ended = 'ended',
unhandled = '',
}
/**
* Parses string as a title
*/
export const sanitiseTitle = (title: string | null) =>
title ? title : '{no title}';
export const sanitiseTitle = (title: string | null) => (title ? title : '{no title}');
/**
* Returns a parsed timer and relevant status message
*/
export const fetchTimerData = (time: TimeManagerType, follow: OntimeEvent, selectedId: string): { message: TimerMessage, timer: number } => {
export const fetchTimerData = (
time: TimeManagerType,
follow: OntimeEvent,
selectedId: string,
): { message: TimerMessage; timer: number } => {
let message;
let timer;
@@ -27,17 +30,14 @@ export const fetchTimerData = (time: TimeManagerType, follow: OntimeEvent, selec
// check that is not running
message = time.playback === Playback.Pause ? TimerMessage.waiting : TimerMessage.running;
timer = time.current ?? 0;
} else if (time.clock < follow.timeStart) {
// if it hasnt started, we count to start
message = TimerMessage.toStart;
timer = follow.timeStart - time.clock;
} else if (follow.timeStart <= time.clock && time.clock <= follow.timeEnd) {
// if it has started, we show running timer
message = TimerMessage.waiting;
timer = time.current ?? 0;
} else {
// running timer timer is not the one we are following
@@ -48,18 +48,15 @@ export const fetchTimerData = (time: TimeManagerType, follow: OntimeEvent, selec
// if it hasnt started, we count to start
message = TimerMessage.toStart;
timer = follow.timeStart - time.clock;
} else if (follow.timeStart <= time.clock) {
// if it has started, we show running timer
message = TimerMessage.waiting;
timer = time.current ?? 0;
} else {
// if it has ended, we show how long ago
message = TimerMessage.ended;
timer = follow.timeEnd;
}
} else {
// if it has ended, we show how long ago
message = TimerMessage.ended;
@@ -12,6 +12,7 @@ import TitleCard from '../../../common/components/title-card/TitleCard';
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
import { TimeManagerType } from '../../../common/models/TimeManager.type';
import { formatTime } from '../../../common/utils/time';
import { useTranslation } from '../../../translation/TranslationProvider';
import { titleVariants } from '../common/animation';
import { TitleManager } from '../ViewWrapper';
@@ -36,6 +37,7 @@ interface BackstageProps {
export default function Public(props: BackstageProps) {
const { isMirrored, publ, publicTitle, time, events, publicSelectedId, general, viewSettings } = props;
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
const { getLocalizedString } = useTranslation();
useEffect(() => {
document.title = 'ontime - Public Screen';
@@ -57,7 +59,7 @@ export default function Public(props: BackstageProps) {
<div className='event-header'>
{general.title}
<div className='clock-container'>
<div className='label'>Time Now</div>
<div className='label'>{getLocalizedString('common.time_now')}</div>
<div className='time'>{clock}</div>
</div>
</div>
@@ -110,7 +112,7 @@ export default function Public(props: BackstageProps) {
</ScheduleProvider>
<div className={showPublicMessage ? 'public-container' : 'public-container public-container--hidden'}>
<div className='label'>Public message</div>
<div className='label'>{getLocalizedString('common.public_message')}</div>
<div className='message'>{publ.text}</div>
</div>
@@ -9,6 +9,7 @@ import TitleCard from '../../../common/components/title-card/TitleCard';
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
import { TimeManagerType } from '../../../common/models/TimeManager.type';
import { formatTime } from '../../../common/utils/time';
import { useTranslation } from '../../../translation/TranslationProvider';
import { formatTimerDisplay, getTimerByType } from '../common/viewerUtils';
import { TitleManager } from '../ViewWrapper';
@@ -47,6 +48,7 @@ interface TimerProps {
export default function Timer(props: TimerProps) {
const { isMirrored, general, pres, title, time, viewSettings } = props;
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
const { getLocalizedString } = useTranslation();
useEffect(() => {
document.title = 'ontime - Timer';
@@ -87,7 +89,7 @@ export default function Timer(props: TimerProps) {
</div>
<div className={`clock-container ${showClock ? '' : 'clock-container--hidden'}`}>
<div className='label'>Time Now</div>
<div className='label'>{getLocalizedString('common.time_now')}</div>
<div className='clock'>{clock}</div>
</div>
@@ -0,0 +1 @@
export type TranslationObject = { [key: string]: string };
@@ -0,0 +1,49 @@
import React, { createContext, useCallback, useContext, useState } from 'react';
import { langDe } from '@/translation/languages/de';
import { langEn } from '@/translation/languages/en';
const translationsList = {
en: langEn,
de: langDe,
};
const ALLOWED_LANGUAGES = Object.keys(translationsList);
const DEFAULT_LANGUAGE = 'en';
export const TranslationContext = createContext(undefined);
export const TranslationProvider = ({ children }) => {
const [language, setLanguageState] = useState('en');
const getLocalizedString = useCallback(
(key, lang = language) => {
if (key in translationsList[lang]) {
return translationsList[lang][key];
} else if (lang !== DEFAULT_LANGUAGE) {
return getLocalizedString(key, 'en');
}
},
[language],
);
const setLanguage = useCallback((language) => {
language = language.toLowerCase();
if (ALLOWED_LANGUAGES.includes(language)) {
setLanguageState(language);
console.info(`Language set to ${language}`);
} else {
console.warn(`Language code ${language} does not exist.`);
}
}, []);
const contextValue = {
getLocalizedString,
setLanguage,
};
return <TranslationContext.Provider value={contextValue}>{children}</TranslationContext.Provider>;
};
export const useTranslation = () => {
const { getLocalizedString, setLanguage } = useContext(TranslationContext);
return { getLocalizedString, setLanguage };
};
@@ -0,0 +1,5 @@
import { TranslationObject } from '../Translation.types';
export const langDe: TranslationObject = {
'common.time_now': 'Aktuelle Zeit',
};
@@ -0,0 +1,18 @@
import { TranslationObject } from '../Translation.types';
export const langEn: TranslationObject = {
'common.end_time': 'End Time',
'common.expected_finish': 'Expected Finish',
'common.now': 'Now',
'common.next': 'Next',
'common.public_message': 'Public message',
'common.start_time': 'Start Time',
'common.stage_timer': 'Stage Timer',
'common.started_at': 'Started At',
'common.time_now': 'Time now',
'countdown.ended': 'Event ended at',
'countdown.running': 'Event running',
'countdown.select_event': 'Select an event to follow',
'countdown.to_start': 'Time to start',
'countdown.waiting': 'Waiting for event start',
};