Skip to content

Commit 9390046

Browse files
committed
Typecheck translation strings
If we attempt to translate a string which does not exist in our translations JSON, we now get a build error to warn us of that. This is intended to help avoid mistakes like in #1145, where a translation string was simply misspelled. We do so by leveraging the typescript capabilities of i18n. Unfortunately, we also get a bunch of strings from the backend which can be translation strings, any other string or possibly either! Which is why you will see a lot "as ParseKeys" typecasts. We could theoretically check those stringsat runtime (if we somehow knew that they must be translation strings) and render some default message, but I personally think it is better to then render a "wrong" translation string, because it may still contain valuable information, even if that information is not rendered nicely. For example, "EVENTS.STATUS.COMPLETED" can still signal that event processing has finished, even if it is a bit hard to read and requires english skills.
1 parent eef58e5 commit 9390046

84 files changed

Lines changed: 378 additions & 190 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/components/NavBar.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { setOffset } from "../slices/tableSlice";
1010
import NewResourceModal, { NewResource } from "./shared/NewResourceModal";
1111
import { useHotkeys } from "react-hotkeys-hook";
1212
import { ModalHandle } from "./shared/modals/Modal";
13+
import { ParseKeys } from "i18next";
1314

1415
/**
1516
* Component that renders the nav bar
@@ -18,18 +19,18 @@ type LinkType = {
1819
path: string
1920
accessRole: string
2021
loadFn: (dispatch: AppDispatch) => void
21-
text: string
22+
text: ParseKeys
2223
}
2324

2425
type CreateType = {
2526
accessRole: string
2627
onShowModal?: () => Promise<void>
2728
onHideModal?: () => void
28-
text: string
29+
text: ParseKeys
2930
isDisplay?: boolean
3031
resource: NewResource
3132
hotkeySequence?: string[]
32-
hotkeyDescription?: string
33+
hotkeyDescription?: ParseKeys
3334
}
3435

3536
const NavBar = ({
@@ -41,7 +42,7 @@ const NavBar = ({
4142
create,
4243
} : {
4344
children?: React.ReactNode
44-
navAriaLabel?: string
45+
navAriaLabel?: ParseKeys
4546
displayNavigation: boolean
4647
setNavigation: React.Dispatch<React.SetStateAction<boolean>>
4748
links: LinkType[]
@@ -73,7 +74,7 @@ const NavBar = ({
7374
useHotkeys(
7475
(create && create.hotkeySequence) ?? [],
7576
() => showNewResourceModal(),
76-
{ description: t((create && create.hotkeyDescription) ?? "") ?? undefined },
77+
{ description: create && create.hotkeyDescription ? t(create.hotkeyDescription) : undefined },
7778
[showNewResourceModal]
7879
);
7980

src/components/configuration/partials/wizard/NewThemeWizard.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { usePageFunctions } from "../../../../hooks/wizardHooks";
1111
import { NewThemeSchema } from "../../../../utils/validate";
1212
import { useAppDispatch } from "../../../../store";
1313
import { postNewTheme, ThemeDetailsInitialValues } from "../../../../slices/themeSlice";
14+
import { ParseKeys } from "i18next";
1415

1516
/**
1617
* This component manages the pages of the new theme wizard and the submission of values
@@ -34,7 +35,10 @@ const NewThemeWizard: React.FC<{
3435
} = usePageFunctions(0, initialValues);
3536

3637
// Caption of steps used by Stepper
37-
const steps = [
38+
const steps: {
39+
name: string
40+
translation: ParseKeys
41+
}[] = [
3842
{
3943
name: "generalForm",
4044
translation: "CONFIGURATION.THEMES.DETAILS.GENERAL.CAPTION",

src/components/configuration/partials/wizard/ThemeDetails.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { useAppDispatch, useAppSelector } from "../../../../store";
1515
import { updateThemeDetails } from "../../../../slices/themeDetailsSlice";
1616
import WizardNavigationButtons from "../../../shared/wizard/WizardNavigationButtons";
1717
import { ThemeDetailsInitialValues } from "../../../../slices/themeSlice";
18+
import { ParseKeys } from "i18next";
1819

1920
/**
2021
* This component manages the pages of the theme details
@@ -41,7 +42,12 @@ const ThemeDetails = ({
4142
};
4243

4344
// information about tabs
44-
const tabs = [
45+
const tabs: {
46+
name: string
47+
tabTranslation: ParseKeys
48+
translation: ParseKeys
49+
accessRole: string
50+
}[] = [
4551
{
4652
name: "generalForm",
4753
tabTranslation: "CONFIGURATION.THEMES.DETAILS.GENERAL.CAPTION",

src/components/events/partials/EventsNavigation.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { ParseKeys } from "i18next";
12
import { fetchEvents } from "../../../slices/eventSlice";
23
import { fetchSeries } from "../../../slices/seriesSlice";
34
import { fetchStats } from "../../../slices/tableFilterSlice";
@@ -27,7 +28,12 @@ export const loadSeries = (dispatch: AppDispatch) => {
2728
dispatch(loadSeriesIntoTable());
2829
};
2930

30-
export const eventsLinks = [
31+
export const eventsLinks: {
32+
path: string,
33+
accessRole: string,
34+
loadFn: (dispatch: AppDispatch) => void,
35+
text: ParseKeys
36+
}[] = [
3137
{
3238
path: "/events/events",
3339
accessRole: "ROLE_UI_EVENTS_VIEW",

src/components/events/partials/EventsStatusCell.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
import { EventDetailsPage } from "./modals/EventDetails";
1010
import { hasScheduledStatus } from "../../../utils/eventDetailsUtils";
1111
import { IconButton } from "../../shared/IconButton";
12+
import { ParseKeys } from "i18next";
1213

1314
/**
1415
* This component renders the status cells of events in the table view
@@ -46,7 +47,7 @@ const EventsStatusCell = ({
4647
iconClassname={"crosslink"}
4748
tooltipText={"EVENTS.EVENTS.TABLE.TOOLTIP.STATUS"}
4849
>
49-
{t(row.displayable_status)}
50+
{t(row.displayable_status as ParseKeys)}
5051
</IconButton>
5152
);
5253
};

src/components/events/partials/ModalTabsAndPages/DetailsMetadataTab.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { MetadataCatalog } from "../../../../slices/eventSlice";
1616
import { AsyncThunk } from "@reduxjs/toolkit";
1717
import RenderDate from "../../../shared/RenderDate";
1818
import WizardNavigationButtons from "../../../shared/wizard/WizardNavigationButtons";
19+
import { ParseKeys } from "i18next";
1920

2021
/**
2122
* This component renders metadata details of a certain event or series
@@ -35,7 +36,7 @@ const DetailsMetadataTab = ({
3536
catalog: MetadataCatalog;
3637
}, any> //(id: string, values: { [key: string]: any }, catalog: MetadataCatalog) => void,
3738
editAccessRole: string,
38-
header?: string
39+
header?: ParseKeys
3940
}) => {
4041
const { t } = useTranslation();
4142
const dispatch = useAppDispatch();
@@ -91,7 +92,7 @@ const DetailsMetadataTab = ({
9192
/* Render table for each metadata catalog */
9293
<div className="obj tbl-details" key={key}>
9394
<header>
94-
<span>{t(header ? header : catalog.title)}</span>
95+
<span>{t(header ? header : catalog.title as ParseKeys)}</span>
9596
</header>
9697
<div className="obj-container">
9798
<table className="main-tbl">
@@ -101,7 +102,7 @@ const DetailsMetadataTab = ({
101102
catalog.fields.map((field, index) => (
102103
<tr key={index}>
103104
<td>
104-
<span>{t(field.label)}</span>
105+
<span>{t(field.label as ParseKeys)}</span>
105106
{field.required && (
106107
<i className="required">*</i>
107108
)}

src/components/events/partials/ModalTabsAndPages/EditScheduledEventsEditPage.tsx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ const EditScheduledEventsEditPage = <T extends RequiredFormProps>({
243243
{
244244
reduceGroupEvents(Object.values(groupBy(formik.values.editedEvents, i => i.weekday))).map((groupedEvent, key) => (
245245
<div className="obj tbl-details">
246-
<header>{t("EVENTS.EVENTS.NEW.WEEKDAYSLONG." + groupedEvent.weekday)
246+
<header>{t(`EVENTS.EVENTS.NEW.WEEKDAYSLONG.${groupedEvent.weekday}`)
247247
+ " ("
248248
+ t("BULK_ACTIONS.EDIT_EVENTS.EDIT.EVENTS")
249249
+ " "
@@ -272,7 +272,7 @@ const EditScheduledEventsEditPage = <T extends RequiredFormProps>({
272272
disabled={false}
273273
title={"EVENTS.EVENTS.DETAILS.SOURCE.DATE_TIME.START_TIME"}
274274
hourPlaceholder={"EVENTS.EVENTS.DETAILS.SOURCE.PLACEHOLDER.HOUR"}
275-
minutePlaceholder={"EVENTS.EVENTS.DETAILS.SOURCE.PLACEHOLDER.MINUTES"}
275+
minutePlaceholder={"EVENTS.EVENTS.DETAILS.SOURCE.PLACEHOLDER.MINUTE"}
276276
callbackHour={(value: string) => {
277277
for (const [i, entry] of formik.values.editedEvents.entries()) {
278278
if (entry.weekday === groupedEvent.weekday ) {
@@ -300,7 +300,7 @@ const EditScheduledEventsEditPage = <T extends RequiredFormProps>({
300300
disabled={false}
301301
title={"EVENTS.EVENTS.DETAILS.SOURCE.DATE_TIME.END_TIME"}
302302
hourPlaceholder={"EVENTS.EVENTS.DETAILS.SOURCE.PLACEHOLDER.HOUR"}
303-
minutePlaceholder={"EVENTS.EVENTS.DETAILS.SOURCE.PLACEHOLDER.MINUTES"}
303+
minutePlaceholder={"EVENTS.EVENTS.DETAILS.SOURCE.PLACEHOLDER.MINUTE"}
304304
callbackHour={(value: string) => {
305305
for (const [i, entry] of formik.values.editedEvents.entries()) {
306306
if (entry.weekday === groupedEvent.weekday ) {
@@ -329,9 +329,7 @@ const EditScheduledEventsEditPage = <T extends RequiredFormProps>({
329329
inputDevices={inputDevices}
330330
disabled={false}
331331
title={"EVENTS.EVENTS.DETAILS.SOURCE.PLACEHOLDER.LOCATION"}
332-
placeholder={`-- ${t(
333-
"SELECT_NO_OPTION_SELECTED"
334-
)} --`}
332+
placeholder={"SELECT_NO_OPTION_SELECTED"}
335333
callback={(value: string) => {
336334
for (const [i, entry] of formik.values.editedEvents.entries()) {
337335
if (entry.weekday === groupedEvent.weekday ) {

src/components/events/partials/ModalTabsAndPages/EditScheduledEventsGeneralPage.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { useAppSelector } from "../../../../store";
1515
import { FormikProps } from "formik";
1616
import { Event } from "../../../../slices/eventSlice";
1717
import WizardNavigationButtons from "../../../shared/wizard/WizardNavigationButtons";
18+
import { ParseKeys } from "i18next";
1819

1920
/**
2021
* This component renders the table overview of selected events in edit scheduled events bulk action
@@ -119,7 +120,7 @@ const EditScheduledEventsGeneralPage = <T extends RequiredFormProps>({
119120
<td className="nowrap">
120121
{event.series ? event.series.title : ""}
121122
</td>
122-
<td className="nowrap">{t(event.event_status)}</td>
123+
<td className="nowrap">{t(event.event_status as ParseKeys)}</td>
123124
</tr>
124125
))}
125126
</tbody>

src/components/events/partials/ModalTabsAndPages/EditScheduledEventsSummaryPage.tsx

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { getSchedulingSeriesOptions } from "../../../../selectors/eventSelectors
66
import { useAppSelector } from "../../../../store";
77
import { FormikProps } from "formik";
88
import { EditedEvents } from "../../../../slices/eventSlice";
9+
import { ParseKeys } from "i18next";
910

1011
/**
1112
* This component renders the summary page of the edit scheduled bulk action
@@ -15,6 +16,16 @@ interface RequiredFormProps {
1516
changedEvents: string[],
1617
}
1718

19+
type Change = {
20+
eventId: string,
21+
title: string,
22+
changes: {
23+
type: ParseKeys,
24+
previous: string,
25+
next: string
26+
}[]
27+
}
28+
1829
const EditScheduledEventsSummaryPage = <T extends RequiredFormProps>({
1930
previousPage,
2031
formik,
@@ -25,7 +36,7 @@ const EditScheduledEventsSummaryPage = <T extends RequiredFormProps>({
2536
const { t } = useTranslation();
2637

2738
// Changes applied to events
28-
const [changes, setChanges] = useState<{eventId: string, title: string, changes: { type: string, previous: string, next: string }[]}[]>([]);
39+
const [changes, setChanges] = useState<Change[]>([]);
2940

3041
const seriesOptions = useAppSelector(state => getSchedulingSeriesOptions(state));
3142

@@ -36,11 +47,11 @@ const EditScheduledEventsSummaryPage = <T extends RequiredFormProps>({
3647
}, []);
3748

3849
const checkForChanges = () => {
39-
let changed: {eventId: string, title: string, changes: { type: string, previous: string, next: string }[]}[] = [];
50+
let changed: Change[] = [];
4051

4152
// Loop through each event selected for editing and compare original values and changed values
4253
for (const event of formik.values.editedEvents) {
43-
let eventChanges : {eventId: string, title: string, changes: { type: string, previous: string, next: string }[]}= {
54+
let eventChanges: Change = {
4455
eventId: event.eventId,
4556
title: event.title,
4657
changes: [],
@@ -110,8 +121,8 @@ const EditScheduledEventsSummaryPage = <T extends RequiredFormProps>({
110121
if (isChanged(event.weekday, event.changedWeekday)) {
111122
eventChanges.changes.push({
112123
type: "EVENTS.EVENTS.TABLE.WEEKDAY",
113-
previous: t("EVENTS.EVENTS.NEW.WEEKDAYSLONG." + event.weekday),
114-
next: t("EVENTS.EVENTS.NEW.WEEKDAYSLONG." + event.changedWeekday),
124+
previous: t(`EVENTS.EVENTS.NEW.WEEKDAYSLONG.${event.weekday}`),
125+
next: t(`EVENTS.EVENTS.NEW.WEEKDAYSLONG.${event.changedWeekday}`),
115126
});
116127
}
117128

src/components/events/partials/ModalTabsAndPages/EventDetailsAccessPolicyTab.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
saveAccessPolicies,
99
} from "../../../../slices/eventDetailsSlice";
1010
import { useTranslation } from "react-i18next";
11+
import { ParseKeys } from "i18next";
1112

1213
/**
1314
* This component manages the access policy tab of the event details modal
@@ -19,7 +20,7 @@ const EventDetailsAccessPolicyTab = ({
1920
setPolicyChanged,
2021
}: {
2122
eventId: string,
22-
header: string,
23+
header: ParseKeys,
2324
policyChanged: boolean,
2425
setPolicyChanged: (value: boolean) => void,
2526
}) => {

0 commit comments

Comments
 (0)