Skip to content

Commit

Permalink
feat(web): infrastructure for typesafe, shared translations
Browse files Browse the repository at this point in the history
  • Loading branch information
mikonse committed Jan 2, 2024
1 parent 4600ed7 commit b0c21aa
Show file tree
Hide file tree
Showing 27 changed files with 303 additions and 174 deletions.
15 changes: 15 additions & 0 deletions frontend/apps/mobile/src/i18n.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import { resources, defaultNS } from "@abrechnung/translations";

i18n.use(initReactI18next).init({
ns: [defaultNS],
resources,
defaultNS,
lng: "en",
fallbackLng: "en",
debug: true,
interpolation: { escapeValue: false },
});

export default i18n;
9 changes: 9 additions & 0 deletions frontend/apps/mobile/src/i18next.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import "i18next";
import type { resources, defaultNS } from "@abrechnung/translations";

declare module "i18next" {
interface CustomTypeOptions {
defaultNS: typeof defaultNS;
resources: (typeof resources)["en"];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import { useTheme } from "@mui/material/styles";
import { Banner } from "../../components/style/Banner";
import Loading from "../../components/style/Loading";
import styles from "./AuthenticatedLayout.module.css";
import { LanguageSelect } from "@/components/LanguageSelect";

const drawerWidth = 240;
const AUTH_FALLBACK = "/login";
Expand Down Expand Up @@ -216,6 +217,7 @@ export const AuthenticatedLayout: React.FC = () => {
</RouterLink>
</Typography>
<div>
<LanguageSelect />
<IconButton
aria-label="account of current user"
aria-controls="menu-appbar"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { Grid, IconButton, List, ListItem, ListItemText } from "@mui/material";
import ListItemLink from "../../components/style/ListItemLink";
import GroupCreateModal from "../../components/groups/GroupCreateModal";
import React, { useState } from "react";
import { Add } from "@mui/icons-material";
import GroupCreateModal from "@/components/groups/GroupCreateModal";
import ListItemLink from "@/components/style/ListItemLink";
import { selectAuthSlice, selectGroupSlice, useAppSelector } from "@/store";
import { selectGroups, selectIsGuestUser } from "@abrechnung/redux";
import { selectGroupSlice, useAppSelector, selectAuthSlice } from "../../store";
import { Add } from "@mui/icons-material";
import { Grid, IconButton, List, ListItem, ListItemText, Tooltip } from "@mui/material";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";

interface Props {
activeGroupId?: number;
}

export const SidebarGroupList: React.FC<Props> = ({ activeGroupId }) => {
const { t } = useTranslation();
const isGuest = useAppSelector((state) => selectIsGuestUser({ state: selectAuthSlice(state) }));
const groups = useAppSelector((state) => selectGroups({ state: selectGroupSlice(state) }));
const [showGroupCreationModal, setShowGroupCreationModal] = useState(false);
Expand Down Expand Up @@ -45,9 +47,11 @@ export const SidebarGroupList: React.FC<Props> = ({ activeGroupId }) => {
{!isGuest && (
<ListItem sx={{ padding: 0 }}>
<Grid container justifyContent="center">
<IconButton size="small" onClick={openGroupCreateModal}>
<Add />
</IconButton>
<Tooltip title={t("groups.addGroup")}>
<IconButton size="small" onClick={openGroupCreateModal}>
<Add />
</IconButton>
</Tooltip>
</Grid>
</ListItem>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { AppBar, Box, Button, Container, CssBaseline, Toolbar, Typography } from
import { Banner } from "../../components/style/Banner";
import { selectIsAuthenticated } from "@abrechnung/redux";
import { useAppSelector, selectAuthSlice } from "../../store";
import { LanguageSelect } from "@/components/LanguageSelect";

export const UnauthenticatedLayout: React.FC = () => {
const authenticated = useAppSelector((state) => selectIsAuthenticated({ state: selectAuthSlice(state) }));
Expand All @@ -27,6 +28,7 @@ export const UnauthenticatedLayout: React.FC = () => {
Abrechnung
</RouterLink>
</Typography>
<LanguageSelect />
<Button component={RouterLink} color="inherit" to="/login">
Login
</Button>
Expand Down
21 changes: 21 additions & 0 deletions frontend/apps/web/src/components/LanguageSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { MenuItem, Select, SelectChangeEvent, SelectProps } from "@mui/material";
import * as React from "react";
import { useTranslation } from "react-i18next";

export type LanguageSelectProps = Omit<SelectProps, "value" | "onChange">;

export const LanguageSelect: React.FC<LanguageSelectProps> = (props) => {
const { t, i18n } = useTranslation();

const handleSetLanguage = (event: SelectChangeEvent<unknown>) => {
const lang = event.target.value as string;
i18n.changeLanguage(lang);
};

return (
<Select value={i18n.language} sx={{ color: "inherit" }} onChange={handleSetLanguage} {...props}>
<MenuItem value="en-US">{t("languages.en")}</MenuItem>
<MenuItem value="de-DE">{t("languages.de")}</MenuItem>
</Select>
);
};
11 changes: 6 additions & 5 deletions frontend/apps/web/src/i18n.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import Backend from "i18next-http-backend";
import LanguageDetector from "i18next-browser-languagedetector";
import { resources, defaultNS } from "@abrechnung/translations";

i18n.use(Backend)
.use(LanguageDetector)
i18n.use(LanguageDetector)
.use(initReactI18next)
.init({
backend: { loadPath: "/assets/locales/{{lng}}/{{ns}}.json" },
lng: "en",
ns: [defaultNS],
resources,
defaultNS,
lng: "en-US",
fallbackLng: "en",
debug: true,
interpolation: { escapeValue: false },
Expand Down
10 changes: 10 additions & 0 deletions frontend/apps/web/src/i18next.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import "i18next";
import type { resources, defaultNS } from "@abrechnung/translations";

declare module "i18next" {
interface CustomTypeOptions {
defaultNS: typeof defaultNS;
nsSeparator: "";
resources: { "": (typeof resources)["en"]["translations"] };
}
}
2 changes: 1 addition & 1 deletion frontend/apps/web/src/pages/accounts/Balances.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ export const Balances: React.FC<Props> = ({ groupId }) => {
top: 20,
right: 20,
bottom: 20,
left: 20,
left: 30,
}}
layout="vertical"
onClick={handleBarClick}
Expand Down
12 changes: 6 additions & 6 deletions frontend/apps/web/src/pages/profile/ChangeEmail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ type FormSchema = z.infer<typeof validationSchema>;

export const ChangeEmail: React.FC = () => {
const { t } = useTranslation();
useTitle(t("Abrechnung - Change E-Mail"));
useTitle(t("profile.changeEmail.tabTitle"));

const handleSubmit = (values: FormSchema, { setSubmitting, resetForm }: FormikHelpers<FormSchema>) => {
api.client.auth
.changeEmail({ requestBody: { password: values.password, email: values.newEmail } })
.then(() => {
setSubmitting(false);
toast.success(t("Requested email change, you should receive an email with a confirmation link soon"));
toast.success(t("profile.changeEmail.success"));
resetForm();
})
.catch((error) => {
Expand All @@ -36,7 +36,7 @@ export const ChangeEmail: React.FC = () => {
return (
<MobilePaper>
<Typography component="h3" variant="h5">
{t("Change E-Mail")}
{t("profile.changeEmail.pageTitle")}
</Typography>
<Formik
validationSchema={toFormikValidationSchema(validationSchema)}
Expand All @@ -55,7 +55,7 @@ export const ChangeEmail: React.FC = () => {
value={values.password}
onChange={handleChange}
onBlur={handleBlur}
label={t("Password")}
label={t("common.password")}
error={touched.password && !!errors.password}
helperText={touched.password && errors.password}
/>
Expand All @@ -69,14 +69,14 @@ export const ChangeEmail: React.FC = () => {
value={values.newEmail}
onChange={handleChange}
onBlur={handleBlur}
label={t("New E-Mail")}
label={t("profile.changeEmail.newEmail")}
error={touched.newEmail && !!errors.newEmail}
helperText={touched.newEmail && errors.newEmail}
/>

{isSubmitting && <LinearProgress />}
<Button type="submit" color="primary" disabled={isSubmitting}>
{t("Save")}
{t("common.save")}
</Button>
</Form>
)}
Expand Down
14 changes: 7 additions & 7 deletions frontend/apps/web/src/pages/profile/ChangePassword.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@ type FormSchema = z.infer<typeof validationSchema>;

export const ChangePassword: React.FC = () => {
const { t } = useTranslation();
useTitle(t("Abrechnung - Change Password"));
useTitle(t("profile.changePassword.tabTitle"));

const handleSubmit = (values: FormSchema, { setSubmitting, resetForm }: FormikHelpers<FormSchema>) => {
api.client.auth
.changePassword({ requestBody: { old_password: values.password, new_password: values.newPassword } })
.then(() => {
setSubmitting(false);
toast.success(t("Successfully changed password"));
toast.success(t("profile.changePassword.success"));
resetForm();
})
.catch((error) => {
Expand All @@ -42,7 +42,7 @@ export const ChangePassword: React.FC = () => {
return (
<MobilePaper>
<Typography component="h3" variant="h5">
{t("Change Password")}
{t("profile.changePassword.pageTitle")}
</Typography>
<Formik
validationSchema={toFormikValidationSchema(validationSchema)}
Expand All @@ -61,7 +61,7 @@ export const ChangePassword: React.FC = () => {
margin="normal"
type="password"
name="password"
label={t("Password")}
label={t("common.password")}
variant="standard"
value={values.password}
onChange={handleChange}
Expand All @@ -75,7 +75,7 @@ export const ChangePassword: React.FC = () => {
margin="normal"
type="password"
name="newPassword"
label={t("New Password")}
label={t("profile.changePassword.newPassword")}
variant="standard"
value={values.newPassword}
onChange={handleChange}
Expand All @@ -93,14 +93,14 @@ export const ChangePassword: React.FC = () => {
margin="normal"
type="password"
name="newPassword2"
label={t("Repeat Password")}
label={t("profile.changePassword.repeatPassword")}
error={touched.newPassword2 && !!errors.newPassword2}
helperText={touched.newPassword2 && errors.newPassword2}
/>

{isSubmitting && <LinearProgress />}
<Button type="submit" color="primary" disabled={isSubmitting}>
{t("Save")}
{t("common.save")}
</Button>
</Form>
)}
Expand Down
23 changes: 9 additions & 14 deletions frontend/apps/web/src/pages/profile/Profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,34 @@ import { selectAuthSlice, useAppSelector } from "@/store";
import { selectProfile } from "@abrechnung/redux";
import { Alert, List, ListItem, ListItemText, Typography } from "@mui/material";
import { DateTime } from "luxon";
import React from "react";
import * as React from "react";
import { useTranslation } from "react-i18next";

export const Profile: React.FC = () => {
const { t } = useTranslation();
const profile = useAppSelector((state) => selectProfile({ state: selectAuthSlice(state) }));
useTitle("Abrechnung - Profile");
useTitle(t("profile.index.tabTitle"));

return (
<MobilePaper>
<Typography component="h3" variant="h5">
Profile
{t("profile.index.pageTitle")}
</Typography>
{profile === undefined ? (
<Loading />
) : (
<>
{profile.is_guest_user && (
<Alert severity="info">
You are a guest user on this Abrechnung and therefore not permitted to create new groups or
group invites.
</Alert>
)}
{profile.is_guest_user && <Alert severity="info">{t("profile.index.guestUserDisclaimer")}</Alert>}
<List>
<ListItem>
<ListItemText primary="Username" secondary={profile.username} />
<ListItemText primary={t("common.username")} secondary={profile.username} />
</ListItem>
<ListItem>
<ListItemText primary="E-Mail" secondary={profile.email} />
<ListItemText primary={t("common.email")} secondary={profile.email} />
</ListItem>
<ListItem>
<ListItemText
primary="Registered"
primary={t("profile.index.registered")}
secondary={DateTime.fromISO(profile.registered_at).toLocaleString(
DateTime.DATETIME_FULL
)}
Expand All @@ -47,5 +44,3 @@ export const Profile: React.FC = () => {
</MobilePaper>
);
};

export default Profile;
Loading

0 comments on commit b0c21aa

Please sign in to comment.