From cb39a13bea3a14fb5071f3e9966dc1fefa82f47e Mon Sep 17 00:00:00 2001
From: Carlos Valente <34649812+cpvalente@users.noreply.github.com>
Date: Wed, 5 Apr 2023 21:41:50 +0200
Subject: [PATCH] Add translations for views (#325) (#328)
* Add translations for views (#325)
---------
Co-authored-by: Kevin <60135953+kev-ac@users.noreply.github.com>
---
apps/client/src/App.tsx | 5 +-
apps/client/src/AppRouter.tsx | 14 +++++-
.../components/title-card/TitleCard.tsx | 6 ++-
apps/client/src/common/models/View.types.ts | 1 +
.../features/viewers/backstage/Backstage.tsx | 12 +++--
.../features/viewers/countdown/Countdown.tsx | 10 ++--
.../viewers/countdown/CountdownSelect.tsx | 33 +++++++------
.../viewers/countdown/countdown.helpers.ts | 23 ++++-----
.../src/features/viewers/public/Public.tsx | 6 ++-
.../src/features/viewers/timer/Timer.tsx | 4 +-
.../src/translation/Translation.types.ts | 1 +
.../src/translation/TranslationProvider.jsx | 49 +++++++++++++++++++
apps/client/src/translation/languages/de.ts | 5 ++
apps/client/src/translation/languages/en.ts | 18 +++++++
14 files changed, 143 insertions(+), 44 deletions(-)
create mode 100644 apps/client/src/translation/Translation.types.ts
create mode 100644 apps/client/src/translation/TranslationProvider.jsx
create mode 100644 apps/client/src/translation/languages/de.ts
create mode 100644 apps/client/src/translation/languages/en.ts
diff --git a/apps/client/src/App.tsx b/apps/client/src/App.tsx
index d2b0c1daa..4c23a73fc 100644
--- a/apps/client/src/App.tsx
+++ b/apps/client/src/App.tsx
@@ -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() {
-
+
+
+
diff --git a/apps/client/src/AppRouter.tsx b/apps/client/src/AppRouter.tsx
index 86194e63c..7c2d88658 100644
--- a/apps/client/src/AppRouter.tsx
+++ b/apps/client/src/AppRouter.tsx
@@ -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(() => {
diff --git a/apps/client/src/common/components/title-card/TitleCard.tsx b/apps/client/src/common/components/title-card/TitleCard.tsx
index fa16f55ca..fa785827f 100644
--- a/apps/client/src/common/components/title-card/TitleCard.tsx
+++ b/apps/client/src/common/components/title-card/TitleCard.tsx
@@ -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 (
{presenter}
- {label}
+ {getLocalizedString(`common.${label}`)}
{title}
{subtitle}
diff --git a/apps/client/src/common/models/View.types.ts b/apps/client/src/common/models/View.types.ts
index f860a6732..6ecbeba46 100644
--- a/apps/client/src/common/models/View.types.ts
+++ b/apps/client/src/common/models/View.types.ts
@@ -12,4 +12,5 @@ export type OverridableOptions = {
hideOvertime?: boolean;
hideMessagesOverlay?: boolean;
hideEndMessage?: boolean;
+ language?: string;
};
diff --git a/apps/client/src/features/viewers/backstage/Backstage.tsx b/apps/client/src/features/viewers/backstage/Backstage.tsx
index adfcbf8c1..67ac843de 100644
--- a/apps/client/src/features/viewers/backstage/Backstage.tsx
+++ b/apps/client/src/features/viewers/backstage/Backstage.tsx
@@ -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) {
{general.title}
-
Time Now
+
{getLocalizedString('common.time_now')}
{clock}
@@ -103,15 +105,15 @@ export default function Backstage(props: BackstageProps) {
/>
-
Started At
+
{getLocalizedString('common.started_at')}
{startedAt}
-
Expected Finish
+
{getLocalizedString('common.expected_finish')}
{expectedFinish}
-
Stage Timer
+
{getLocalizedString('common.stage_timer')}
{stageTimer}
@@ -146,7 +148,7 @@ export default function Backstage(props: BackstageProps) {
-
Public message
+
{getLocalizedString('common.public_message')}
{publ.text}
diff --git a/apps/client/src/features/viewers/countdown/Countdown.tsx b/apps/client/src/features/viewers/countdown/Countdown.tsx
index 5425c3c13..ceefc5a7c 100644
--- a/apps/client/src/features/viewers/countdown/Countdown.tsx
+++ b/apps/client/src/features/viewers/countdown/Countdown.tsx
@@ -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
(null);
const [runningTimer, setRunningTimer] = useState(0);
@@ -112,11 +114,11 @@ export default function Countdown(props: CountdownProps) {
) : (
-
Time Now
+
{getLocalizedString('common.time_now')}
{clock}
-
{runningMessage}
+
{getLocalizedString(`countdown.${runningMessage}`)}
{formattedTimer}
@@ -125,11 +127,11 @@ export default function Countdown(props: CountdownProps) {
-
Start Time
+
{getLocalizedString('common.start_time')}
{startTime}
-
End Time
+
{getLocalizedString('common.end_time')}
{endTime}
diff --git a/apps/client/src/features/viewers/countdown/CountdownSelect.tsx b/apps/client/src/features/viewers/countdown/CountdownSelect.tsx
index b5091ccd0..056a05673 100644
--- a/apps/client/src/features/viewers/countdown/CountdownSelect.tsx
+++ b/apps/client/src/features/viewers/countdown/CountdownSelect.tsx
@@ -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 (
-
Select an event to follow
+
{getLocalizedString('countdown.select_event')}
{!events.length ? (
) : (
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 (
- -
-
- {`${index}. ${start} → ${end} | ${title}`}
-
-
- );
- },
- )
+ return (
+ -
+ {`${index}. ${start} → ${end} | ${title}`}
+
+ );
+ })
)}
diff --git a/apps/client/src/features/viewers/countdown/countdown.helpers.ts b/apps/client/src/features/viewers/countdown/countdown.helpers.ts
index ebdd81e8f..600674c71 100644
--- a/apps/client/src/features/viewers/countdown/countdown.helpers.ts
+++ b/apps/client/src/features/viewers/countdown/countdown.helpers.ts
@@ -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;
diff --git a/apps/client/src/features/viewers/public/Public.tsx b/apps/client/src/features/viewers/public/Public.tsx
index f28f8a4c2..8619f11ae 100644
--- a/apps/client/src/features/viewers/public/Public.tsx
+++ b/apps/client/src/features/viewers/public/Public.tsx
@@ -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) {
{general.title}
-
Time Now
+
{getLocalizedString('common.time_now')}
{clock}
@@ -110,7 +112,7 @@ export default function Public(props: BackstageProps) {
-
Public message
+
{getLocalizedString('common.public_message')}
{publ.text}
diff --git a/apps/client/src/features/viewers/timer/Timer.tsx b/apps/client/src/features/viewers/timer/Timer.tsx
index f52c53494..e98556435 100644
--- a/apps/client/src/features/viewers/timer/Timer.tsx
+++ b/apps/client/src/features/viewers/timer/Timer.tsx
@@ -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) {
-
Time Now
+
{getLocalizedString('common.time_now')}
{clock}
diff --git a/apps/client/src/translation/Translation.types.ts b/apps/client/src/translation/Translation.types.ts
new file mode 100644
index 000000000..edbe13f99
--- /dev/null
+++ b/apps/client/src/translation/Translation.types.ts
@@ -0,0 +1 @@
+export type TranslationObject = { [key: string]: string };
diff --git a/apps/client/src/translation/TranslationProvider.jsx b/apps/client/src/translation/TranslationProvider.jsx
new file mode 100644
index 000000000..814f9666c
--- /dev/null
+++ b/apps/client/src/translation/TranslationProvider.jsx
@@ -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 {children};
+};
+
+export const useTranslation = () => {
+ const { getLocalizedString, setLanguage } = useContext(TranslationContext);
+ return { getLocalizedString, setLanguage };
+};
diff --git a/apps/client/src/translation/languages/de.ts b/apps/client/src/translation/languages/de.ts
new file mode 100644
index 000000000..b78156f56
--- /dev/null
+++ b/apps/client/src/translation/languages/de.ts
@@ -0,0 +1,5 @@
+import { TranslationObject } from '../Translation.types';
+
+export const langDe: TranslationObject = {
+ 'common.time_now': 'Aktuelle Zeit',
+};
diff --git a/apps/client/src/translation/languages/en.ts b/apps/client/src/translation/languages/en.ts
new file mode 100644
index 000000000..1f77c62d2
--- /dev/null
+++ b/apps/client/src/translation/languages/en.ts
@@ -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',
+};