Implementing I18N

This commit is contained in:
2025-04-27 19:47:03 +02:00
parent f71dccf166
commit d38bb7d986
10 changed files with 591 additions and 76 deletions

View File

@@ -0,0 +1,164 @@
{
"pages": {
"login": {
"title": "Melden Sie sich bei Ihrem Konto an",
"signin": "Einloggen",
"signup": "Anmelden",
"divider": "oder",
"fields": {
"email": "Email",
"password": "Passwort"
},
"oauth": {
"google": "Einloggen mit Google",
"discord": "Einloggen mit Discord"
},
"errors": {
"validEmail": "Ungültige E-Mail-Adresse",
"requiredEmail": "E-Mail ist erforderlich",
"requiredPassword": "Passwort wird benötigt"
},
"buttons": {
"submit": "Anmeldung",
"forgotPassword": "Passwort vergessen?",
"noAccount": "Sie haben kein Konto?",
"rememberMe": "Erinnere dich an mich"
}
},
"forgotPassword": {
"title": "Haben Sie Ihr Passwort vergessen?",
"fields": {
"email": "Email"
},
"errors": {
"validEmail": "Ungültige E-Mail-Adresse",
"requiredEmail": "E-Mail ist erforderlich"
},
"buttons": {
"submit": "Anweisungen zum Zurücksetzen senden"
}
},
"register": {
"title": "Registrieren Sie sich für Ihr Konto",
"fields": {
"email": "Email",
"password": "Passwort"
},
"errors": {
"validEmail": "Ungültige E-Mail-Adresse",
"requiredEmail": "E-Mail ist erforderlich",
"requiredPassword": "Passwort wird benötigt"
},
"buttons": {
"submit": "Registrieren",
"haveAccount": "Ein Konto haben?"
}
},
"updatePassword": {
"title": "Kennwort aktualisieren",
"fields": {
"password": "Neues Passwort",
"confirmPassword": "Bestätige neues Passwort"
},
"errors": {
"confirmPasswordNotMatch": "Passwörter stimmen nicht überein",
"requiredPassword": "Passwort wird benötigt",
"requiredConfirmPassword": "Das Feld „Passwort bestätigen“ ist erforderlich"
},
"buttons": {
"submit": "Aktualisieren"
}
},
"error": {
"info": "Sie haben vergessen, {{action}} component zu {{resource}} hinzufügen.",
"404": "Leider existiert diese Seite nicht.",
"resource404": "Haben Sie die {{resource}} resource erstellt?",
"backHome": "Zurück"
}
},
"actions": {
"list": "Aufführen",
"create": "Erstellen",
"edit": "Bearbeiten",
"show": "Zeigen"
},
"buttons": {
"create": "Erstellen",
"save": "Speichern",
"logout": "Abmelden",
"delete": "Löschen",
"edit": "Bearbeiten",
"cancel": "Abbrechen",
"confirm": "Sicher?",
"filter": "Filter",
"clear": "Löschen",
"refresh": "Erneuern",
"show": "Zeigen",
"undo": "Undo",
"import": "Importieren",
"clone": "Klon",
"notAccessTitle": "Sie haben keine zugriffsberechtigung"
},
"warnWhenUnsavedChanges": "Nicht gespeicherte Änderungen werden nicht übernommen.",
"notifications": {
"success": "Erfolg",
"error": "Fehler (status code: {{statusCode}})",
"undoable": "Sie haben {{seconds}} Sekunden Zeit für Undo.",
"createSuccess": "{{resource}} erfolgreich erstellt.",
"createError": "Fehler beim Erstellen {{resource}} (status code: {{statusCode}})",
"deleteSuccess": "{{resource}} erfolgreich gelöscht.",
"deleteError": "Fehler beim Löschen {{resource}} (status code: {{statusCode}})",
"editSuccess": "{{resource}} erfolgreich bearbeitet.",
"editError": "Fehler beim Bearbeiten {{resource}} (status code: {{statusCode}})",
"importProgress": "{{processed}}/{{total}} importiert"
},
"loading": "Wird geladen",
"tags": {
"clone": "Klon"
},
"dashboard": {
"title": "Dashboard"
},
"posts": {
"posts": "Einträge",
"fields": {
"id": "Id",
"title": "Titel",
"category": "Kategorie",
"status": {
"title": "Status",
"published": "Veröffentlicht",
"draft": "Draft",
"rejected": "Abgelehnt"
},
"content": "Inhalh",
"createdAt": "Erstellt am"
},
"titles": {
"create": "Erstellen",
"edit": "Bearbeiten",
"list": "Einträge",
"show": "Eintrag zeigen"
}
},
"table": {
"actions": "Aktionen"
},
"documentTitle": {
"default": "refine",
"suffix": " | Refine",
"post": {
"list": "Beiträge | Refine",
"show": "#{{id}} Beitrag anzeigen | Refine",
"edit": "#{{id}} Beitrag bearbeiten | Refine",
"create": "Neuen Beitrag erstellen | Refine",
"clone": "#{{id}} Beitrag klonen | Refine"
}
},
"autoSave": {
"success": "gespeichert",
"error": "fehler beim automatischen speichern",
"loading": "speichern...",
"idle": "warten auf anderungen"
}
}

View File

@@ -0,0 +1,164 @@
{
"pages": {
"login": {
"title": "Sign in to your account",
"signin": "Sign in",
"signup": "Sign up",
"divider": "or",
"fields": {
"email": "Email",
"password": "Password"
},
"oauth": {
"google": "Sign in with Google",
"discord": "Sign in with Discord"
},
"errors": {
"validEmail": "Invalid email address",
"requiredEmail": "Email is required",
"requiredPassword": "Password is required"
},
"buttons": {
"submit": "Login",
"forgotPassword": "Forgot password?",
"noAccount": "Dont have an account?",
"rememberMe": "Remember me"
}
},
"forgotPassword": {
"title": "Forgot your password?",
"fields": {
"email": "Email"
},
"errors": {
"validEmail": "Invalid email address",
"requiredEmail": "Email is required"
},
"buttons": {
"submit": "Send reset instructions"
}
},
"register": {
"title": "Sign up for your account",
"fields": {
"email": "Email",
"password": "Password"
},
"errors": {
"validEmail": "Invalid email address",
"requiredEmail": "Email is required",
"requiredPassword": "Password is required"
},
"buttons": {
"submit": "Register",
"haveAccount": "Have an account?"
}
},
"updatePassword": {
"title": "Update password",
"fields": {
"password": "New Password",
"confirmPassword": "Confirm new password"
},
"errors": {
"confirmPasswordNotMatch": "Passwords do not match",
"requiredPassword": "Password required",
"requiredConfirmPassword": "Confirm password is required"
},
"buttons": {
"submit": "Update"
}
},
"error": {
"info": "You may have forgotten to add the {{action}} component to {{resource}} resource.",
"404": "Sorry, the page you visited does not exist.",
"resource404": "Are you sure you have created the {{resource}} resource.",
"backHome": "Back Home"
}
},
"actions": {
"list": "List",
"create": "Create",
"edit": "Edit",
"show": "Show"
},
"buttons": {
"create": "Create",
"save": "Save",
"logout": "Logout",
"delete": "Delete",
"edit": "Edit",
"cancel": "Cancel",
"confirm": "Are you sure?",
"filter": "Filter",
"clear": "Clear",
"refresh": "Refresh",
"show": "Show",
"undo": "Undo",
"import": "Import",
"clone": "Clone",
"notAccessTitle": "You don't have permission to access"
},
"warnWhenUnsavedChanges": "Are you sure you want to leave? You have unsaved changes.",
"notifications": {
"success": "Successful",
"error": "Error (status code: {{statusCode}})",
"undoable": "You have {{seconds}} seconds to undo",
"createSuccess": "Successfully created {{resource}}",
"createError": "There was an error creating {{resource}} (status code: {{statusCode}})",
"deleteSuccess": "Successfully deleted {{resource}}",
"deleteError": "Error when deleting {{resource}} (status code: {{statusCode}})",
"editSuccess": "Successfully edited {{resource}}",
"editError": "Error when editing {{resource}} (status code: {{statusCode}})",
"importProgress": "Importing: {{processed}}/{{total}}"
},
"loading": "Loading",
"tags": {
"clone": "Clone"
},
"dashboard": {
"title": "Dashboard"
},
"posts": {
"posts": "Posts",
"fields": {
"id": "Id",
"title": "Title",
"category": "Category",
"status": {
"title": "Status",
"published": "Published",
"draft": "Draft",
"rejected": "Rejected"
},
"content": "Content",
"createdAt": "Created At"
},
"titles": {
"create": "Create Post",
"edit": "Edit Post",
"list": "Posts",
"show": "Show Post"
}
},
"table": {
"actions": "Actions"
},
"documentTitle": {
"default": "refine",
"suffix": " | Refine",
"post": {
"list": "Posts | Refine",
"show": "#{{id}} Show Post | Refine",
"edit": "#{{id}} Edit Post | Refine",
"create": "Create new Post | Refine",
"clone": "#{{id}} Clone Post | Refine"
}
},
"autoSave": {
"success": "saved",
"error": "auto save failure",
"loading": "saving...",
"idle": "waiting for changes"
}
}

View File

@@ -0,0 +1,164 @@
{
"pages": {
"login": {
"title": "Authentification",
"signin": "S'authentifier",
"signup": "Créer un compte",
"divider": "ou",
"fields": {
"email": "Email",
"password": "Mot de passe"
},
"oauth": {
"google": "S'authentifier avec Google",
"discord": "S'authentifier avec Discord"
},
"errors": {
"validEmail": "Email invalide",
"requiredEmail": "l'Email est obligatoire",
"requiredPassword": "Le mot de passe est obligatoire"
},
"buttons": {
"submit": "S'authentifier",
"forgotPassword": "Mot de passe oublié?",
"noAccount": "Vous n'avec pas de compte?",
"rememberMe": "Se souvenir de moi"
}
},
"forgotPassword": {
"title": "Mot de passe oublié?",
"fields": {
"email": "Email"
},
"errors": {
"validEmail": "mail invalide",
"requiredEmail": "l'Email est obligatoire"
},
"buttons": {
"submit": "Envoyer les instructions de récupération"
}
},
"register": {
"title": "Création de compte",
"fields": {
"email": "Email",
"password": "Mot de passe"
},
"errors": {
"validEmail": "Email invalide",
"requiredEmail": "l'Email est obligatoire",
"requiredPassword": "Le mot de passe est obligatoire"
},
"buttons": {
"submit": "Créer un compte",
"haveAccount": "Vous avez déjà un compte?"
}
},
"updatePassword": {
"title": "Mise à jour du mot de passe",
"fields": {
"password": "Nouveau mot de passe",
"confirmPassword": "Confirmation"
},
"errors": {
"confirmPasswordNotMatch": "Les mots de passe ne correspondent pas",
"requiredPassword": "Le mot de passe est obligatoire",
"requiredConfirmPassword": "Vous devez confirmer votre mot de passe"
},
"buttons": {
"submit": "Mettre à jour"
}
},
"error": {
"info": "Il manque l'action {{action}} component à la ressource {{resource}} .",
"404": "Cette page n'existe pas.",
"resource404": "Cette page n'existe pas.",
"backHome": "Retour à l'accueil"
}
},
"actions": {
"list": "Liste",
"create": "Création",
"edit": "Édtion",
"show": "Voir"
},
"buttons": {
"create": "Créer",
"save": "Sauvegarder",
"logout": "Se déconnecter",
"delete": "Supprimer",
"edit": "Modifier",
"cancel": "Annuler",
"confirm": "Êtes vous sur?",
"filter": "Filtrer",
"clear": "Effacer",
"refresh": "Rafraîchir",
"show": "Voir",
"undo": "Annuler",
"import": "Importer",
"clone": "Cloner",
"notAccessTitle": "Vous n'avez pas la permission d'accéder à cette ressource"
},
"warnWhenUnsavedChanges": "Êtes vous sur de vouloir quitter la page? Vous avez des modification non sauvegardées.",
"notifications": {
"success": "Succès",
"error": "Erreur (Code de statut: {{statusCode}})",
"undoable": "Vous avez {{seconds}} secondes à annuler",
"createSuccess": "Création de {{resource}} réussie",
"createError": "Erreur pendant la création de {{resource}} (Code de statut: {{statusCode}})",
"deleteSuccess": "Suppression de {{resource}} réussie",
"deleteError": "Erreur pendant la suppression de {{resource}} (Code de statut: {{statusCode}})",
"editSuccess": "Modification de {{resource}} réussie",
"editError": "Erreur pendant la modification de {{resource}} (Code de statut: {{statusCode}})",
"importProgress": "Importation de: {{processed}}/{{total}}"
},
"loading": "Chargement",
"tags": {
"clone": "Clone"
},
"dashboard": {
"title": "Tableau de bord"
},
"posts": {
"posts": "Posts",
"fields": {
"id": "Id",
"title": "Title",
"category": "Category",
"status": {
"title": "Status",
"published": "Published",
"draft": "Draft",
"rejected": "Rejected"
},
"content": "Content",
"createdAt": "Created At"
},
"titles": {
"create": "Create Post",
"edit": "Edit Post",
"list": "Posts",
"show": "Show Post"
}
},
"table": {
"actions": "Actions"
},
"documentTitle": {
"default": "refine",
"suffix": " | Refine",
"post": {
"list": "Posts | Refine",
"show": "#{{id}} Show Post | Refine",
"edit": "#{{id}} Edit Post | Refine",
"create": "Create new Post | Refine",
"clone": "#{{id}} Clone Post | Refine"
}
},
"autoSave": {
"success": "Sauvegardé",
"error": "Sauvegarde automatique ratée",
"loading": "Sauvegarde...",
"idle": "En attente de modification"
}
}

View File

@@ -1,7 +0,0 @@
{
"pages": {
"login": {
"title": "Sign in to your account"
}
}
}

View File

@@ -1,7 +0,0 @@
{
"pages": {
"login": {
"title": "S'authentifier"
}
}
}

View File

@@ -8,8 +8,10 @@ import * as React from "react";
import Typography from "@mui/material/Typography";
import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";
import { useTranslation } from "@refinedev/core";
export const Login = () => {
const { translate } = useTranslation();
const [searchParams] = useSearchParams();
if (searchParams.get("oauth") == "success") {
const redirect_to = localStorage.getItem("redirect_after_login")
@@ -24,12 +26,12 @@ export const Login = () => {
rememberMe={false}
providers={[{
name: "google",
label: "Sign in with Google",
label: translate("pages.login.oauth.google"),
icon: (<GoogleIcon style={{ fontSize: 24, }} />),
},
{
name: "discord",
label: "Sign in with Discord",
label: translate("pages.login.oauth.discord"),
icon: (<DiscordIcon style={{ fontSize: 24, }} />),
},
]}
@@ -50,7 +52,7 @@ export const Login = () => {
underline="none"
to="/auth/forgot-password"
>
Forgot password?
{translate("pages.login.buttons.forgotPassword")}
</MuiLink>
</Stack>
}
@@ -69,7 +71,7 @@ export const Login = () => {
component="span"
fontSize="12px"
>
Dont have an account?
{translate("pages.login.buttons.noAccount")}
</Typography>
<MuiLink
ml="4px"
@@ -81,7 +83,7 @@ export const Login = () => {
to="/auth/register"
fontWeight="bold"
>
Sign up
{translate("pages.login.signup")}
</MuiLink>
</Box>
}

View File

@@ -1,5 +1,51 @@
import { AuthPage } from "@refinedev/mui";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import MuiLink from "@mui/material/Link";
import * as React from "react";
import { useTranslation } from "@refinedev/core";
import { Link } from "react-router";
export const Register = () => {
return <AuthPage type="register" />;
const { translate } = useTranslation();
const loginLink = (
<Box
display="flex"
justifyContent="flex-end"
alignItems="center"
sx={{
mt: "24px",
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<Typography variant="body2" component="span" fontSize="12px">
{translate(
"pages.register.buttons.haveAccount",
translate(
"pages.login.buttons.haveAccount",
"Have an account?",
),
)}
</Typography>
<MuiLink
ml="4px"
variant="body2"
color="primary"
component={Link}
underline="none"
to="/auth/login"
fontSize="12px"
fontWeight="bold"
>
{translate(
"pages.register.signin",
translate("pages.login.signin", "Sign in"),
)}
</MuiLink>
</Box>
)
return <AuthPage type="register" loginLink={loginLink}/>;
};

View File

@@ -0,0 +1,41 @@
import Autocomplete from "@mui/material/Autocomplete";
import TextField from "@mui/material/TextField";
import Box from "@mui/material/Box";
import React from "react";
import { useTranslation } from "react-i18next";
import { useTranslation as useRefineTranslation } from "@refinedev/core";
const I18nPicker = () => {
const { i18n } = useTranslation();
const { getLocale, changeLocale } = useRefineTranslation();
const currentLocale = getLocale();
return (
<Autocomplete
value={currentLocale}
options={i18n.languages}
disableClearable={true}
renderInput={(params) => {
return <TextField {...params} label={ "Language" } variant="outlined" />
}}
renderOption={(props, option) => {
const { key, ...optionProps } = props;
return (
<Box
key={key}
component="li"
sx={{ '& > img': { mr: 2, flexShrink: 0 } }}
{...optionProps}
>
{ option }
</Box>
);
}}
onChange={(event, value) => {
changeLocale(value);
}}
/>
)
}
export default I18nPicker;

View File

@@ -18,17 +18,11 @@ import { FirmContext } from "../../contexts/FirmContext";
import { Logout } from "../auth/Logout";
import { IUser } from "../../interfaces";
import MuiLink from "@mui/material/Link";
import { useTranslation } from "react-i18next";
import { useTranslation as useTranslationR } from "@refinedev/core";
import { useSetLocale } from "@refinedev/core";
import I18nPicker from "./I18nPicker";
export const Header: React.FC<RefineThemedLayoutV2HeaderProps> = ({
sticky = true,
}) => {
const { i18n } = useTranslation();
const { getLocale, changeLocale } = useTranslationR();
const currentLocale = getLocale();
const collapsed = false;
const { mode, setMode } = useContext(ColorModeContext);
const { currentFirm } = useContext(FirmContext);
@@ -44,16 +38,6 @@ export const Header: React.FC<RefineThemedLayoutV2HeaderProps> = ({
setAnchorEl(null);
};
const [anchorIn, setAnchorIn] = React.useState<null | HTMLElement>(null);
const openI18nMenu = Boolean(anchorEl);
const handleOpenI18nMenu = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorIn(event.currentTarget);
}
const handleCloseI18nMenu = () => {
setAnchorIn(null);
};
return (
<AppBar position={sticky ? "sticky" : "relative"}>
<Toolbar>
@@ -149,43 +133,7 @@ export const Header: React.FC<RefineThemedLayoutV2HeaderProps> = ({
{!user && (
<Link to="/auth/login"><Button>Login</Button></Link>
)}
<Button
id="i18n-button"
aria-controls={openI18nMenu ? 'i18n-menu' : undefined}
aria-haspopup="true"
aria-expanded={openI18nMenu ? 'true' : undefined}
onClick={handleOpenI18nMenu}>
<Typography
sx={{
display: {
xs: "none",
sm: "inline-block",
},
}}
variant="subtitle2"
>
{currentLocale}
</Typography>&nbsp;
<Avatar src={`/images/flags/${currentLocale}.svg`} alt={currentLocale}/>
</Button>
<Menu
id="i18n-menu"
open={openI18nMenu}
anchorEl={anchorIn}
onClose={handleCloseI18nMenu}
>
{[...(i18n.languages || [])].sort().map((lang: string) => (
<MenuItem
key={lang}
onClick={() => changeLocale(lang)}
>
<span style={{ marginRight: 8 }}>
<Avatar src={`/images/flags/${lang}.svg`} alt={lang}/>
</span>
{lang === "en" ? "English" : "Français"}
</MenuItem>
))}
</Menu>
<I18nPicker />
</Stack>
</Stack>
</Toolbar>

View File

@@ -8,13 +8,13 @@ i18n
.use(detector)
.use(initReactI18next)
.init({
supportedLngs: ["en", "fr"],
supportedLngs: ["EN", "FR"],
backend: {
loadPath: "/locales/{{lng}}/{{ns}}.json", // locale files path
},
ns: ["common"],
defaultNS: "common",
fallbackLng: ["en", "fr"],
fallbackLng: ["EN", "FR"],
});
export default i18n;