mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-07 00:13:53 +00:00
* Add translations for views (#325) --------- Co-authored-by: Kevin <60135953+kev-ac@users.noreply.github.com>
This commit is contained in:
@@ -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} />
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
Reference in New Issue
Block a user