Skip to content

Commit

Permalink
feat: mise en place i18n avec i18nifty et traductions page Contact (#117
Browse files Browse the repository at this point in the history
, #135)
  • Loading branch information
ocruze authored Oct 20, 2023
1 parent 9cd0643 commit ae3a078
Show file tree
Hide file tree
Showing 6 changed files with 290 additions and 24 deletions.
26 changes: 26 additions & 0 deletions assets/i18n/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { createI18nApi, declareComponentKeys, type GenericTranslations } from "i18nifty";

// déclaration des langues
/** liste des langues supportées */
export const languages = ["fr", "en"] as const;

/** langue de fallback */
export const fallbackLanguage = "fr";

// types
export type Language = (typeof languages)[number];
export type ComponentKey = typeof import("../pages/contact/Contact").i18n;
export type Translations<L extends Language> = GenericTranslations<ComponentKey, Language, typeof fallbackLanguage, L>;
export type LocalizedString = Parameters<typeof resolveLocalizedString>[0];

/** initialisation de l'instance de i18n */
export const { useTranslation, getTranslation, resolveLocalizedString, useLang, $lang, useResolveLocalizedString, useIsI18nFetching } =
createI18nApi<ComponentKey>()(
{ languages, fallbackLanguage },
{
en: () => import("./languages/en").then(({ translations }) => translations),
fr: () => import("./languages/fr").then(({ translations }) => translations),
}
);

export { declareComponentKeys };
7 changes: 7 additions & 0 deletions assets/i18n/languages/en.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { Translations } from "..";

import { enTranslations as contactTranslations } from "../../pages/contact/Contact";

export const translations: Translations<"en"> = {
Contact: contactTranslations,
};
7 changes: 7 additions & 0 deletions assets/i18n/languages/fr.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { Translations } from "..";

import { frTranslations as contactTranslations } from "../../pages/contact/Contact";

export const translations: Translations<"fr"> = {
Contact: contactTranslations,
};
127 changes: 103 additions & 24 deletions assets/pages/contact/Contact.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,39 @@ import Alert from "@codegouvfr/react-dsfr/Alert";
import Button from "@codegouvfr/react-dsfr/Button";
import Input from "@codegouvfr/react-dsfr/Input";
import Select from "@codegouvfr/react-dsfr/Select";
import { RegisteredLinkProps } from "@codegouvfr/react-dsfr/link";
import { yupResolver } from "@hookform/resolvers/yup";
import { useState } from "react";
import { JSX, useState } from "react";
import { useForm } from "react-hook-form";
import * as yup from "yup";

import AppLayout from "../../components/Layout/AppLayout";
import Wait from "../../components/Utils/Wait";
import { defaultNavItems } from "../../config/navItems";
import useUser from "../../hooks/useUser";
import { declareComponentKeys, getTranslation, useTranslation, type Translations } from "../../i18n";
import SymfonyRouting from "../../modules/Routing";
import Translator from "../../modules/Translator";
import { jsonFetch } from "../../modules/jsonFetch";
import { routes } from "../../router/router";
import { regex } from "../../utils";

import "../../sass/components/spinner.scss";
import "../../sass/pages/nous_ecrire.scss";

const { t } = getTranslation("Contact");
const schema = yup
.object({
email_contact: yup
.string()
.matches(regex.email, Translator.trans("contact.form.email_contact_error"))
.required(Translator.trans("contact.form.email_contact_mandatory_error")),
email_contact: yup.string().matches(regex.email, t("form.email_contact_error")).required(t("form.email_contact_mandatory_error")),
last_name: yup.string(),
first_name: yup.string(),
organization: yup.string(),
importance: yup.number(),
message: yup.string().min(10, Translator.trans("contact.form.message_minlength_error")),
message: yup.string().min(10, t("form.message_minlength_error")),
})
.required();

const Contact = () => {
const { t } = useTranslation({ Contact });
const { user } = useUser();

const [isSending, setIsSending] = useState(false);
Expand All @@ -48,9 +48,6 @@ const Contact = () => {
getValues: getFormValues,
} = useForm({ resolver: yupResolver(schema) });

const explanation = { __html: Translator.trans("contact.form.explain", { href: routes.docs().href }) };
const infos = { __html: Translator.trans("contact.form.infos", { href: routes.personal_data().href }) };

const onSubmit = () => {
setError(null);
setIsSending(true);
Expand All @@ -72,21 +69,21 @@ const Contact = () => {
};

return (
<AppLayout navItems={defaultNavItems} documentTitle="Nous écrire">
<AppLayout navItems={defaultNavItems} documentTitle={t("title")}>
<div className={fr.cx("fr-grid-row")}>
<div className={fr.cx("fr-col-12", "fr-col-md-8")}>
<h1>{Translator.trans("contact.title")}</h1>
<p dangerouslySetInnerHTML={explanation} />
<h1>{t("title")}</h1>
<p>{t("form.explanation", { docsLinkProps: routes.docs().link })}</p>

<p>{Translator.trans("mandatory_fields")}</p>
<p>{t("mandatory_fields")}</p>

{error && <Alert title={Translator.trans("contact.form.error_title")} closable description={error} severity="error" />}
{error && <Alert title={t("form.error_title")} closable description={error} severity="error" />}

<Input
label={Translator.trans("contact.form.email_contact")}
label={t("form.email_contact")}
state={errors.email_contact ? "error" : "default"}
stateRelatedMessage={errors?.email_contact?.message}
hintText="Format attendu : [email protected]"
hintText={t("form.email_contact_hint")}
nativeInputProps={{
...register("email_contact"),
defaultValue: user?.email,
Expand All @@ -95,7 +92,7 @@ const Contact = () => {
}}
/>
<Input
label={Translator.trans("contact.form.lastName")}
label={t("form.lastName")}
nativeInputProps={{
...register("last_name"),
defaultValue: user?.lastName,
Expand All @@ -104,7 +101,7 @@ const Contact = () => {
}}
/>
<Input
label={Translator.trans("contact.form.firstName")}
label={t("form.firstName")}
nativeInputProps={{
...register("first_name"),
defaultValue: user?.firstName,
Expand All @@ -113,21 +110,21 @@ const Contact = () => {
}}
/>
<Input
label={Translator.trans("contact.form.organization")}
label={t("form.organization")}
nativeInputProps={{
...register("organization"),
autoComplete: "organization",
}}
/>
<Input
label={Translator.trans("contact.form.message")}
label={t("form.message")}
state={errors.message ? "error" : "default"}
stateRelatedMessage={errors?.message?.message}
textArea={true}
nativeTextAreaProps={{
...register("message"),
rows: 8,
placeholder: Translator.trans("contact.form.message_placeholder"),
placeholder: t("form.message_placeholder"),
}}
/>
<Select
Expand All @@ -144,10 +141,10 @@ const Contact = () => {
<option value="3">3</option>
</Select>

<p dangerouslySetInnerHTML={infos} />
<p>{t("form.infos", { personalDataLinkProps: routes.personal_data().link })}</p>

<div className={fr.cx("fr-grid-row", "fr-grid-row--right")}>
<Button onClick={handleSubmit(onSubmit)}>{Translator.trans("send")}</Button>
<Button onClick={handleSubmit(onSubmit)}>{t("send")}</Button>
</div>

{isSending && (
Expand All @@ -171,3 +168,85 @@ const Contact = () => {
};

export default Contact;

// traductions
export const { i18n } = declareComponentKeys<
| "title"
| "mandatory_fields"
| "form.error_title"
| { K: "form.explanation"; P: { docsLinkProps: RegisteredLinkProps }; R: JSX.Element }
| "form.email_contact"
| "form.email_contact_hint"
| "form.email_contact_mandatory_error"
| "form.email_contact_error"
| "form.lastName"
| "form.firstName"
| "form.organization"
| "form.message"
| "form.message_placeholder"
| "form.message_minlength_error"
| "send"
| { K: "form.infos"; P: { personalDataLinkProps: RegisteredLinkProps }; R: JSX.Element }
>()({
Contact,
});

export const frTranslations: Translations<"fr">["Contact"] = {
title: "Nous écrire",
mandatory_fields: "Sauf mention contraire “(optionnel)” dans le label, tous les champs sont obligatoires.",
"form.error_title": "Votre message n'a pas pu être envoyé",
"form.explanation": ({ docsLinkProps }) => (
<>
{"Vous n'avez pas trouvé la réponse à votre question dans "}
<a {...docsLinkProps}>{"l'aide en ligne"}</a>
{" ? Vous souhaitez la configuration d'un espace de travail pour vos besoins ? Utilisez ce formulaire pour nous contacter."}
</>
),
"form.email_contact": "Votre email",
"form.email_contact_hint": "Format attendu : [email protected]",
"form.email_contact_mandatory_error": "Veuillez saisir une adresse email",
"form.email_contact_error": "Veuillez saisir une adresse email valide",
"form.lastName": "Votre nom (optionnel)",
"form.firstName": "Votre prénom (optionnel)",
"form.organization": "Votre organisme (optionnel)",
"form.message": "Votre demande",
"form.message_placeholder": "Décrivez votre demande en quelques lignes",
"form.message_minlength_error": "Veuillez saisir une demande d'au moins 10 caractères.",
send: "Envoyer",
"form.infos": ({ personalDataLinkProps }) => (
<>
{"Les informations recueillies à partir de ce formulaire sont nécessaires à la gestion de votre demande par les services de l'IGN concernés. "}
<a {...personalDataLinkProps}>{"En savoir plus sur la gestion des données à caractère personnel."}</a>
</>
),
};

export const enTranslations: Translations<"en">["Contact"] = {
title: "Contact us",
mandatory_fields: "All fields are mandatory unless label states “optional”",
"form.error_title": "Your message could not be sent",
"form.explanation": ({ docsLinkProps }) => (
<>
{"You did not find the answer to your question in "}
<a {...docsLinkProps}>{"our documentation"}</a>
{"? Do you want to configure a workspace for your needs? Use this form to contact us."}
</>
),
"form.email_contact": "Email",
"form.email_contact_hint": "Expected format: [email protected]",
"form.email_contact_mandatory_error": "Enter an email address",
"form.email_contact_error": "Enter a valid email address",
"form.lastName": "Last name (optional)",
"form.firstName": "First name (optional)",
"form.organization": "Organization (optional)",
"form.message": "Message",
"form.message_placeholder": "Describe your request in a few lines",
"form.message_minlength_error": "Message must be at least 10 caractères.",
send: "Send",
"form.infos": ({ personalDataLinkProps }) => (
<>
{"The information collected from this form is necessary to process your request by the appropriate services at IGN. "}
<a {...personalDataLinkProps}>{"Learn more about how personal data is stored and used."}</a>
</>
),
};
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"geoportal-extensions-openlayers": "^3.3.3",
"geostyler-sld-parser": "^5.1.0",
"geostyler-style": "^7.3.1",
"i18nifty": "^2.1.1",
"ol": "6.15.1",
"postcss-loader": "^7.3.3",
"prop-types": "^15.8.1",
Expand Down
Loading

0 comments on commit ae3a078

Please sign in to comment.