From 162ecea5648b25db91057a4861494a3cfd77d8cd Mon Sep 17 00:00:00 2001 From: Marc Gavanier Date: Wed, 13 Dec 2023 02:38:26 +0100 Subject: [PATCH] feat: add translation system --- src/app/(home)/_components/About.tsx | 28 ++ src/app/(home)/_components/Header.tsx | 57 +++ src/app/(home)/_components/Hero.tsx | 24 + src/app/(home)/_components/index.ts | 3 + src/app/{ => (home)}/home.module.scss | 0 src/app/(home)/homeTranslation.ts | 22 + src/app/(home)/page.tsx | 426 ++++++++++++++++ src/app/_language/Language.tsx | 8 + src/app/_language/LanguageContext.ts | 7 + src/app/_language/LanguageSettings.ts | 8 + src/app/_language/index.ts | 3 + src/app/_translation/AvailableTranslations.ts | 12 + src/app/_translation/Translate.tsx | 12 + src/app/_translation/Translation.tsx | 45 ++ src/app/_translation/TranslationContext.ts | 5 + src/app/_translation/UseTranslation.ts | 4 + src/app/_translation/index.ts | 5 + src/app/_utils/asset.util.ts | 5 + src/app/_utils/index.ts | 3 + .../slug/slug.util.spec.ts} | 0 .../slug/slug.ts => _utils/slug/slug.util.ts} | 0 .../{utils => _utils}/url/url.util.spec.ts | 0 src/app/{utils => _utils}/url/url.util.ts | 0 src/app/common/index.ts | 1 - src/app/layout.tsx | 9 +- src/app/page.tsx | 472 ------------------ .../talks/[slug]/_components/TalkDetails.tsx | 46 ++ .../talks/[slug]/_components/TalkNotFound.tsx | 20 + src/app/talks/[slug]/_components/index.ts | 2 + src/app/talks/[slug]/page.tsx | 55 +- src/app/talks/[slug]/talk.ts | 2 +- src/app/talks/[slug]/talkTranslation.ts | 19 + src/app/talks/_components/TalksList.tsx | 25 + src/app/talks/_components/index.ts | 1 + src/app/talks/page.tsx | 27 +- src/app/utils/index.ts | 1 - src/dictionaries/en.json | 5 - src/dictionaries/fr.json | 5 - src/dictionaries/index.ts | 9 - src/settings/language.json | 4 + src/styles/components/_components.scss | 1 + src/translations/home/en.json | 22 + src/translations/home/fr.json | 22 + src/translations/talks/en.json | 19 + src/translations/talks/fr.json | 19 + 45 files changed, 902 insertions(+), 561 deletions(-) create mode 100644 src/app/(home)/_components/About.tsx create mode 100644 src/app/(home)/_components/Header.tsx create mode 100644 src/app/(home)/_components/Hero.tsx create mode 100644 src/app/(home)/_components/index.ts rename src/app/{ => (home)}/home.module.scss (100%) create mode 100644 src/app/(home)/homeTranslation.ts create mode 100644 src/app/(home)/page.tsx create mode 100644 src/app/_language/Language.tsx create mode 100644 src/app/_language/LanguageContext.ts create mode 100644 src/app/_language/LanguageSettings.ts create mode 100644 src/app/_language/index.ts create mode 100644 src/app/_translation/AvailableTranslations.ts create mode 100644 src/app/_translation/Translate.tsx create mode 100644 src/app/_translation/Translation.tsx create mode 100644 src/app/_translation/TranslationContext.ts create mode 100644 src/app/_translation/UseTranslation.ts create mode 100644 src/app/_translation/index.ts create mode 100644 src/app/_utils/asset.util.ts create mode 100644 src/app/_utils/index.ts rename src/app/{common/slug/slug.spec.ts => _utils/slug/slug.util.spec.ts} (100%) rename src/app/{common/slug/slug.ts => _utils/slug/slug.util.ts} (100%) rename src/app/{utils => _utils}/url/url.util.spec.ts (100%) rename src/app/{utils => _utils}/url/url.util.ts (100%) delete mode 100644 src/app/common/index.ts delete mode 100644 src/app/page.tsx create mode 100644 src/app/talks/[slug]/_components/TalkDetails.tsx create mode 100644 src/app/talks/[slug]/_components/TalkNotFound.tsx create mode 100644 src/app/talks/[slug]/_components/index.ts create mode 100644 src/app/talks/[slug]/talkTranslation.ts create mode 100644 src/app/talks/_components/TalksList.tsx create mode 100644 src/app/talks/_components/index.ts delete mode 100644 src/app/utils/index.ts delete mode 100644 src/dictionaries/en.json delete mode 100644 src/dictionaries/fr.json delete mode 100644 src/dictionaries/index.ts create mode 100644 src/settings/language.json create mode 100644 src/translations/home/en.json create mode 100644 src/translations/home/fr.json create mode 100644 src/translations/talks/en.json create mode 100644 src/translations/talks/fr.json diff --git a/src/app/(home)/_components/About.tsx b/src/app/(home)/_components/About.tsx new file mode 100644 index 0000000..b62969e --- /dev/null +++ b/src/app/(home)/_components/About.tsx @@ -0,0 +1,28 @@ +'use client'; + +import { ReactElement } from 'react'; +import { useTranslation } from '@/app/_translation'; +import { HomeTranslation } from '../homeTranslation'; + +export const About = (): ReactElement => { + const { about }: HomeTranslation = useTranslation(); + + return ( +
+
+
+
+

+ {about.subtitle} {about.title} +

+

{about.description}

+
+ +
+
+
+
+ ); +}; diff --git a/src/app/(home)/_components/Header.tsx b/src/app/(home)/_components/Header.tsx new file mode 100644 index 0000000..b9f69e2 --- /dev/null +++ b/src/app/(home)/_components/Header.tsx @@ -0,0 +1,57 @@ +'use client'; + +import { ReactElement, useContext } from 'react'; +import { NavDropdown } from 'react-bootstrap'; +import Container from 'react-bootstrap/Container'; +import Nav from 'react-bootstrap/Nav'; +import Navbar from 'react-bootstrap/Navbar'; +import { LANGUAGE_SETTINGS, LanguageContext } from '@/app/_language'; +import { useTranslation } from '@/app/_translation'; +import { HomeTranslation } from '../homeTranslation'; + +const languagesMap: Map = new Map([ + ['de', 'đŸ‡©đŸ‡Ș Deutch (DE)'], + ['en', '🇬🇧 English (EN)'], + ['es', 'đŸ‡Ș🇾 Español (ES)'], + ['fr', 'đŸ‡«đŸ‡·â€‚Français (FR)'], + ['in', 'đŸ‡źđŸ‡©â€‚Bahasa Indonesia (IN)'], + ['it', '🇼đŸ‡č Italiano (IT)'], + ['ja', 'đŸ‡ŻđŸ‡”â€‚æ—„æœŹèȘž (JA)'], + ['ko', 'đŸ‡°đŸ‡·â€‚í•œê”­ì–Ž (KO)'], + ['pt', 'đŸ‡”đŸ‡č PortuguĂȘs (PT)'], + ['tr', 'đŸ‡čđŸ‡·â€‚TĂŒrkçe (TR)'], + ['ru', 'đŸ‡·đŸ‡ș РуссĐșĐžĐč (RU)'], + ['uk', 'đŸ‡ș🇩 УĐșŃ€Đ°Ń—ĐœŃŃŒĐșĐ° (UK)'], + ['vi', 'đŸ‡»đŸ‡łâ€‚Tiáșżng Việt (VI)'], + ['zh', 'đŸ‡šđŸ‡łâ€‚äž­æ–‡ (ZH)'] +]); + +export const Header = (): ReactElement => { + const [selectedLanguage, changeSelectedLanguage]: LanguageContext = useContext(LanguageContext); + const { header: i18n }: HomeTranslation = useTranslation(); + + return ( + + + {i18n.brand} + + + + + + + ); +}; diff --git a/src/app/(home)/_components/Hero.tsx b/src/app/(home)/_components/Hero.tsx new file mode 100644 index 0000000..d85ff56 --- /dev/null +++ b/src/app/(home)/_components/Hero.tsx @@ -0,0 +1,24 @@ +'use client'; + +import { ReactElement } from 'react'; +import { useTranslation } from '@/app/_translation'; +import { HomeTranslation } from '../homeTranslation'; + +export const Hero = (): ReactElement => { + const { hero: i18n }: HomeTranslation = useTranslation(); + + return ( +
+

+ {i18n.nextEditionDate} + {i18n.title} +

+

{i18n.description}

+
+ + {i18n.callToAction} + +
+
+ ); +}; diff --git a/src/app/(home)/_components/index.ts b/src/app/(home)/_components/index.ts new file mode 100644 index 0000000..710123f --- /dev/null +++ b/src/app/(home)/_components/index.ts @@ -0,0 +1,3 @@ +export * from './About'; +export * from './Header'; +export * from './Hero'; diff --git a/src/app/home.module.scss b/src/app/(home)/home.module.scss similarity index 100% rename from src/app/home.module.scss rename to src/app/(home)/home.module.scss diff --git a/src/app/(home)/homeTranslation.ts b/src/app/(home)/homeTranslation.ts new file mode 100644 index 0000000..b3c2ed8 --- /dev/null +++ b/src/app/(home)/homeTranslation.ts @@ -0,0 +1,22 @@ +export type HomeTranslation = { + hero: { + title: string; + nextEditionDate: string; + description: string; + callToAction: string; + }; + header: { + brand: string; + about: string; + program: string; + speakers: string; + tickets: string; + sponsors: string; + }; + about: { + title: string; + subtitle: string; + description: string; + learnMore: string; + }; +}; diff --git a/src/app/(home)/page.tsx b/src/app/(home)/page.tsx new file mode 100644 index 0000000..fd48aa2 --- /dev/null +++ b/src/app/(home)/page.tsx @@ -0,0 +1,426 @@ +import Image from 'next/image'; +import Link from 'next/link'; +import { ReactElement } from 'react'; +import { Badge } from 'react-bootstrap'; +import { asset } from '@/app/_utils'; +import { Translate } from '@/app/_translation'; +import { Header, About, Hero } from './_components'; +import styles from './home.module.scss'; + +const Home = (): ReactElement => { + return ( + <> +
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+
+
+
+

+ Programme de la conférence + Retrouvez la liste des événements prévus +

+

+ Le programme se dĂ©roule sur une journĂ©e de 08h30 Ă  18h00, des confĂ©rences au format 40 minutes et 20 minutes + s’enchaĂźneront tout au long de la journĂ©e. +

+
+ + Voir le programme + +
+
+
+
+
+ 08:30 - 09:00 +
+
+
+ 1 +
+
+
+

Accueil

+ Récupérez votre badge pour la journée, puis prenez le temps du petit-déjeuner pour discuter un café à la + main. +
+
+
+
+ 09:40 - 12:00 +
+
+
+ 2 +
+
+
+

PremiĂšre partie

+ PlĂ©niĂšre d’ouverture, suivi de 9 de confĂ©rences rĂ©parties sur 3 pistes. +
+
+
+
+ 12:00 - 13:30 +
+
+
+ 3 +
+
+
+

DĂ©jeuner

+ Profitez d’un repas prĂ©vu sur place. Tout est inclus dans le prix du billet. +
+
+
+
+ 13:30 - 17:40 +
+
+
+ 4 +
+
+
+

Seconde partie

+ 12 confĂ©rences sont prĂ©vues l’aprĂšs-midi, elles sont Ă©galement rĂ©parties sur 3 pistes. +
+
+
+
+ 17:50 - 18:30 +
+
+
+ 5 +
+
+
+

Fermeture

+ PlĂ©niĂšre de fermeture suivi d’un apĂ©ro avant de se quitter. +
+
+
+
+
+
+
+
+
+
+

+ Les orateurs de la conférence + Les conférenciers +

+

+ Des experts renommés dans leurs domaines partageront leurs connaissances pour vous aider à vous épanouir en + tant que freelance +

+
+
+
+
+
+ +
+ +

Élodie Thibault

+

Responsable des Relations Humaines chez YouFair

+
+
+
+ +
+ +

Jean Couvreur

+

ContrĂŽleur de gestion

+
+
+
+ +
+

Michel Mignard

+

Chargée de communication indépendant

+
+
+
+ +
+

Pierre Beaulne

+

Responsable juridique et Référent déontologue

+
+
+
+ +
+

Amélie Desjardins

+

Key Account Manager

+
+
+
+ +
+

Sandra BĂ©liveau

+

Digital Ecosystem Builder

+
+
+
+ +
+

Corinne Feng

+

Product manager @Benka

+
+
+
+ +
+

Yannis Benett

+

Consultant en cybersécurité

+
+
+
+ + Voir tous les orateurs + +
+
+
+
+
+
+
+

+ RĂ©servez votre place + Choisissez le billet qui vous convient parmi nos offres +

+

+ Nous vous proposons trois offres affin que vous puissiez choisir ce qui correspond le mieux Ă  vos moyens. +

+
+
+
+
+
+
+
+ + Early bird + +
+ 29.99€ +
+
+
+

+ L’offre Early bird donne l’accĂšs Ă  un tarif prĂ©fĂ©rentiel, le nombre de billets Ă  ce tarif est + limitĂ© Ă  60 places, les premiers arrivĂ©s sont les premiers servis ! +

+
+ +
+
+
+
+
+
+
+
+ + Normal + +
+ 34.99€ +
+
+
+

+ L’offre Normal donne l’accĂšs Ă  la confĂ©rence, il n’y a pas de limite de places pour ce tarif. +

+
+ +
+
+
+
+
+
+
+
+ + Soutien + +
+ 49.99€ +
+
+
+

+ En choisissant l’offre de soutien, vous participez au cĂŽtĂ© de nos sponsors Ă  rendre cet Ă©vĂ©nement + possible en contribuant Ă  assurer l’équilibre Ă©conomique et nous vous{' '} + en remercions chaleureusement ! +

+
+ +
+
+
+
+
+
+
+
+
+
+

+ Sponsors de la conférence + Merci à nos sponsors pour leur soutien +

+

+ Sans le soutien de nos sponsors nous n’aurions pas pu vous proposer une confĂ©rence d’une telle qualitĂ©, nous + tenons ici Ă  les remercier tout particuliĂšrement pour leur aide. +

+
+
+
+
+ + ); +}; + +export default Translate(Home, 'home'); diff --git a/src/app/_language/Language.tsx b/src/app/_language/Language.tsx new file mode 100644 index 0000000..7f66efc --- /dev/null +++ b/src/app/_language/Language.tsx @@ -0,0 +1,8 @@ +'use client'; + +import { ReactNode, useState } from 'react'; +import { LanguageContext } from './LanguageContext'; + +export const Language = ({ defaultLanguage, children }: { defaultLanguage: string; children: ReactNode }): ReactNode => ( + {children} +); diff --git a/src/app/_language/LanguageContext.ts b/src/app/_language/LanguageContext.ts new file mode 100644 index 0000000..7c13003 --- /dev/null +++ b/src/app/_language/LanguageContext.ts @@ -0,0 +1,7 @@ +'use client'; + +import { Context, createContext, Dispatch, SetStateAction } from 'react'; + +export type LanguageContext = [string, Dispatch>]; + +export const LanguageContext: Context = createContext([] as unknown as LanguageContext); diff --git a/src/app/_language/LanguageSettings.ts b/src/app/_language/LanguageSettings.ts new file mode 100644 index 0000000..3368188 --- /dev/null +++ b/src/app/_language/LanguageSettings.ts @@ -0,0 +1,8 @@ +import * as languageSettingsJSON from '@/settings/language.json'; + +export type LanguageSettings = { + availableLanguages: string[]; + defaultLanguage: string; +}; + +export const LANGUAGE_SETTINGS: LanguageSettings = languageSettingsJSON; diff --git a/src/app/_language/index.ts b/src/app/_language/index.ts new file mode 100644 index 0000000..533bcb6 --- /dev/null +++ b/src/app/_language/index.ts @@ -0,0 +1,3 @@ +export * from './Language'; +export * from './LanguageSettings'; +export * from './LanguageContext'; diff --git a/src/app/_translation/AvailableTranslations.ts b/src/app/_translation/AvailableTranslations.ts new file mode 100644 index 0000000..6bf8f4f --- /dev/null +++ b/src/app/_translation/AvailableTranslations.ts @@ -0,0 +1,12 @@ +import { LANGUAGE_SETTINGS } from '@/app/_language'; + +type AvailableTranslations = Record<(typeof LANGUAGE_SETTINGS.availableLanguages)[number], () => Promise>; + +export const availableTranslations = (page: string) => + LANGUAGE_SETTINGS.availableLanguages.reduce( + (translations: AvailableTranslations, language: string): AvailableTranslations => ({ + ...translations, + [language]: () => import(`@/translations/${page}/${language}.json`).then((module: { default: T }) => module.default) + }), + {} + ); diff --git a/src/app/_translation/Translate.tsx b/src/app/_translation/Translate.tsx new file mode 100644 index 0000000..76a3693 --- /dev/null +++ b/src/app/_translation/Translate.tsx @@ -0,0 +1,12 @@ +import { ReactElement, ReactNode } from 'react'; +import { LANGUAGE_SETTINGS } from '@/app/_language'; +import { availableTranslations } from './AvailableTranslations'; +import { Translation } from './Translation'; + +export const Translate = + (WrappedComponent: (props: T) => ReactNode, page: string) => + async (props: T): Promise => ( + + + + ); diff --git a/src/app/_translation/Translation.tsx b/src/app/_translation/Translation.tsx new file mode 100644 index 0000000..60324ff --- /dev/null +++ b/src/app/_translation/Translation.tsx @@ -0,0 +1,45 @@ +'use client'; + +import { Dispatch, ReactNode, SetStateAction, useContext, useEffect, useMemo, useState } from 'react'; +import { LANGUAGE_SETTINGS, LanguageContext } from '@/app/_language'; +import { availableTranslations } from './AvailableTranslations'; +import { TranslationContext } from './TranslationContext'; + +const updateTranslation = + (page: string, language: string) => + (onUpdate: (translation: T) => void): void => { + (async (): Promise => await availableTranslations(page)[language]!())().then(onUpdate); + }; + +export const Translation = ({ + translation, + page, + children +}: { + translation: T; + page: string; + children: ReactNode; +}): ReactNode => { + const [language]: LanguageContext = useContext(LanguageContext); + const [localTranslation, setTranslation]: [T, Dispatch>] = useState(translation); + const translationsMap: Map = useMemo( + () => new Map([[LANGUAGE_SETTINGS.defaultLanguage, translation]]), + [translation] + ); + + useEffect( + (): void => + translationsMap.has(language) + ? setTranslation(translationsMap.get(language) as T) + : updateTranslation( + page, + language + )((availableTranslation: T): void => { + translationsMap.set(language, availableTranslation); + setTranslation(availableTranslation); + }), + [translationsMap, language, page] + ); + + return {children}; +}; diff --git a/src/app/_translation/TranslationContext.ts b/src/app/_translation/TranslationContext.ts new file mode 100644 index 0000000..a627683 --- /dev/null +++ b/src/app/_translation/TranslationContext.ts @@ -0,0 +1,5 @@ +'use client'; + +import { Context, createContext } from 'react'; + +export const TranslationContext: Context = createContext({}); diff --git a/src/app/_translation/UseTranslation.ts b/src/app/_translation/UseTranslation.ts new file mode 100644 index 0000000..cc8cc07 --- /dev/null +++ b/src/app/_translation/UseTranslation.ts @@ -0,0 +1,4 @@ +import { Context, useContext } from 'react'; +import { TranslationContext } from './TranslationContext'; + +export const useTranslation = () => useContext(TranslationContext as Context); diff --git a/src/app/_translation/index.ts b/src/app/_translation/index.ts new file mode 100644 index 0000000..d933394 --- /dev/null +++ b/src/app/_translation/index.ts @@ -0,0 +1,5 @@ +export * from './AvailableTranslations'; +export * from './Translate'; +export * from './Translation'; +export * from './TranslationContext'; +export * from './UseTranslation'; diff --git a/src/app/_utils/asset.util.ts b/src/app/_utils/asset.util.ts new file mode 100644 index 0000000..5f3bead --- /dev/null +++ b/src/app/_utils/asset.util.ts @@ -0,0 +1,5 @@ +const ASSET_PREFIX: string = process.env['NEXT_PUBLIC_ASSET_PREFIX'] ?? ''; + +export const asset = (path: string): string => { + return `${ASSET_PREFIX}/${path}`; +}; diff --git a/src/app/_utils/index.ts b/src/app/_utils/index.ts new file mode 100644 index 0000000..8aed478 --- /dev/null +++ b/src/app/_utils/index.ts @@ -0,0 +1,3 @@ +export * from './slug/slug.util'; +export * from './url/url.util'; +export * from './asset.util'; diff --git a/src/app/common/slug/slug.spec.ts b/src/app/_utils/slug/slug.util.spec.ts similarity index 100% rename from src/app/common/slug/slug.spec.ts rename to src/app/_utils/slug/slug.util.spec.ts diff --git a/src/app/common/slug/slug.ts b/src/app/_utils/slug/slug.util.ts similarity index 100% rename from src/app/common/slug/slug.ts rename to src/app/_utils/slug/slug.util.ts diff --git a/src/app/utils/url/url.util.spec.ts b/src/app/_utils/url/url.util.spec.ts similarity index 100% rename from src/app/utils/url/url.util.spec.ts rename to src/app/_utils/url/url.util.spec.ts diff --git a/src/app/utils/url/url.util.ts b/src/app/_utils/url/url.util.ts similarity index 100% rename from src/app/utils/url/url.util.ts rename to src/app/_utils/url/url.util.ts diff --git a/src/app/common/index.ts b/src/app/common/index.ts deleted file mode 100644 index b1d3f07..0000000 --- a/src/app/common/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './slug/slug'; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 3f090b9..97baddd 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,7 @@ import '../styles/styles.scss'; import type { Metadata } from 'next'; import { ReactElement, ReactNode } from 'react'; +import { Language, LANGUAGE_SETTINGS } from '@/app/_language'; export const metadata: Metadata = { title: `Your Conference 2023 | Share Knowledge & Innovation`, @@ -8,9 +9,11 @@ export const metadata: Metadata = { 'Join us at Your Conference 2023 to explore the latest tech advancements, connect with experts, and foster innovation. Get your tickets now!' }; -const RootLayout = ({ children }: { children: ReactNode }): ReactElement => ( - - {children} +const RootLayout = async ({ children }: { children: ReactNode }): Promise => ( + + + {children} + ); diff --git a/src/app/page.tsx b/src/app/page.tsx deleted file mode 100644 index 509d606..0000000 --- a/src/app/page.tsx +++ /dev/null @@ -1,472 +0,0 @@ -'use client'; - -import Image from 'next/image'; -import Link from 'next/link'; -import { ReactElement } from 'react'; -import { Badge } from 'react-bootstrap'; -import Container from 'react-bootstrap/Container'; -import Nav from 'react-bootstrap/Nav'; -import Navbar from 'react-bootstrap/Navbar'; -import { asset } from './asset'; -import styles from './home.module.scss'; - -const Home = (): ReactElement => ( - <> -
-
- - - FreelanceConnect - - - - - - -
-
-

- Prochaine édition à venir en 2024 - La plus grande conférence dédiée aux freelances -

-

- Esprits libres et crĂ©atifs qui prenez en main votre destin professionnel. Rejoignez la journĂ©e des freelances, - l’évĂ©nement incontournable pour les freelances ambitieux et visionnaires de la rĂ©gion lyonnaise. -

- -
-
-
- -
-
-
-
-
-
-
-
-

- À propos de la confĂ©rence - Bienvenue Ă  la plus grande confĂ©rence dĂ©diĂ©e aux freelances -

-

- Esprits libres et crĂ©atifs qui prenez en main votre destin professionnel. Rejoignez la journĂ©e des freelances, - l’évĂ©nement incontournable pour les freelances ambitieux et visionnaires de la rĂ©gion lyonnaise. -

-
- -
-
-
-
-
-
-
-
-
-

- Programme de la conférence - Retrouvez la liste des événements prévus -

-

- Le programme se dĂ©roule sur une journĂ©e de 08h30 Ă  18h00, des confĂ©rences au format 40 minutes et 20 minutes - s’enchaĂźneront tout au long de la journĂ©e. -

-
- - Voir le programme - -
-
-
-
-
- 08:30 - 09:00 -
-
-
- 1 -
-
-
-

Accueil

- Récupérez votre badge pour la journée, puis prenez le temps du petit-déjeuner pour discuter un café à la main. -
-
-
-
- 09:40 - 12:00 -
-
-
- 2 -
-
-
-

PremiĂšre partie

- PlĂ©niĂšre d’ouverture, suivi de 9 de confĂ©rences rĂ©parties sur 3 pistes. -
-
-
-
- 12:00 - 13:30 -
-
-
- 3 -
-
-
-

DĂ©jeuner

- Profitez d’un repas prĂ©vu sur place. Tout est inclus dans le prix du billet. -
-
-
-
- 13:30 - 17:40 -
-
-
- 4 -
-
-
-

Seconde partie

- 12 confĂ©rences sont prĂ©vues l’aprĂšs-midi, elles sont Ă©galement rĂ©parties sur 3 pistes. -
-
-
-
- 17:50 - 18:30 -
-
-
- 5 -
-
-
-

Fermeture

- PlĂ©niĂšre de fermeture suivi d’un apĂ©ro avant de se quitter. -
-
-
-
-
-
-
-
-
-
-

- Les orateurs de la conférence - Les conférenciers -

-

- Des experts renommés dans leurs domaines partageront leurs connaissances pour vous aider à vous épanouir en tant - que freelance -

-
-
-
-
-
- -
- -

Élodie Thibault

-

Responsable des Relations Humaines chez YouFair

-
-
-
- -
- -

Jean Couvreur

-

ContrĂŽleur de gestion

-
-
-
- -
-

Michel Mignard

-

Chargée de communication indépendant

-
-
-
- -
-

Pierre Beaulne

-

Responsable juridique et Référent déontologue

-
-
-
- -
-

Amélie Desjardins

-

Key Account Manager

-
-
-
- -
-

Sandra BĂ©liveau

-

Digital Ecosystem Builder

-
-
-
- -
-

Corinne Feng

-

Product manager @Benka

-
-
-
- -
-

Yannis Benett

-

Consultant en cybersécurité

-
-
-
- - Voir tous les orateurs - -
-
-
-
-
-
-
-

- RĂ©servez votre place - Choisissez le billet qui vous convient parmi nos offres -

-

- Nous vous proposons trois offres affin que vous puissiez choisir ce qui correspond le mieux Ă  vos moyens. -

-
-
-
-
-
-
-
- - Early bird - -
- 29.99€ -
-
-
-

- L’offre Early bird donne l’accĂšs Ă  un tarif prĂ©fĂ©rentiel, le nombre de billets Ă  ce tarif est limitĂ© - Ă  60 places, les premiers arrivĂ©s sont les premiers servis ! -

-
- -
-
-
-
-
-
-
-
- - Normal - -
- 34.99€ -
-
-
-

- L’offre Normal donne l’accĂšs Ă  la confĂ©rence, il n’y a pas de limite de places pour ce tarif. -

-
- -
-
-
-
-
-
-
-
- - Soutien - -
- 49.99€ -
-
-
-

- En choisissant l’offre de soutien, vous participez au cĂŽtĂ© de nos sponsors Ă  rendre cet Ă©vĂ©nement - possible en contribuant Ă  assurer l’équilibre Ă©conomique et nous vous en remercions chaleureusement ! -

-
- -
-
-
-
-
-
-
-
-
-
-

- Sponsors de la conférence - Merci à nos sponsors pour leur soutien -

-

- Sans le soutien de nos sponsors nous n’aurions pas pu vous proposer une confĂ©rence d’une telle qualitĂ©, nous - tenons ici Ă  les remercier tout particuliĂšrement pour leur aide. -

-
-
-
-
- -); - -export default Home; diff --git a/src/app/talks/[slug]/_components/TalkDetails.tsx b/src/app/talks/[slug]/_components/TalkDetails.tsx new file mode 100644 index 0000000..35d3acd --- /dev/null +++ b/src/app/talks/[slug]/_components/TalkDetails.tsx @@ -0,0 +1,46 @@ +'use client'; + +import Link from 'next/link'; +import { ReactElement } from 'react'; +import { useTranslation } from '@/app/_translation'; +import { Talk } from '../talk'; +import { TalkTranslation } from '../talkTranslation'; + +export const TalkDetails = ({ talk }: { talk: Talk }): ReactElement => { + const { talkDetails: i18n }: TalkTranslation = useTranslation(); + + return ( + <> +

{talk.title}

+

({talk.language})

+ + {i18n.by} {talk.speaker} - {talk.date.toLocaleString()} ({talk.duration} {i18n.durationUnit}) - {talk.room} + +
+

+ {talk.track} +

+

{talk.description}

+ {talk.tags?.map((tag: string) => #{tag} )} + {(talk.slidesLink || talk.videoLink) && ( + + )} + {i18n.back} + + ); +}; diff --git a/src/app/talks/[slug]/_components/TalkNotFound.tsx b/src/app/talks/[slug]/_components/TalkNotFound.tsx new file mode 100644 index 0000000..e76bc02 --- /dev/null +++ b/src/app/talks/[slug]/_components/TalkNotFound.tsx @@ -0,0 +1,20 @@ +'use client'; + +import Link from 'next/link'; +import { ReactElement } from 'react'; +import { useTranslation } from '@/app/_translation'; +import { TalkTranslation } from '../talkTranslation'; + +export const TalkNotFound = (): ReactElement => { + const { notFound: i18n }: TalkTranslation = useTranslation(); + + return ( + <> +

{i18n.title}

+

+ {i18n.message} {i18n.link} +

+ {i18n.back} + + ); +}; diff --git a/src/app/talks/[slug]/_components/index.ts b/src/app/talks/[slug]/_components/index.ts new file mode 100644 index 0000000..c2074bd --- /dev/null +++ b/src/app/talks/[slug]/_components/index.ts @@ -0,0 +1,2 @@ +export * from './TalkDetails'; +export * from './TalkNotFound'; diff --git a/src/app/talks/[slug]/page.tsx b/src/app/talks/[slug]/page.tsx index 35a7861..59d8edc 100644 --- a/src/app/talks/[slug]/page.tsx +++ b/src/app/talks/[slug]/page.tsx @@ -1,57 +1,16 @@ import talksJson from '@/data/talks.json'; -import { Dictionary, dictionaryFor } from '@/dictionaries'; -import Link from 'next/link'; import { ReactElement } from 'react'; -import { bySlug } from '../../common'; +import { Translate } from '@/app/_translation'; +import { bySlug } from '@/app/_utils'; +import { TalkDetails, TalkNotFound } from './_components'; import { Talk, talksFromJSON } from './talk'; export const generateStaticParams = (): Talk[] => talksFromJSON(talksJson); -const TalkPage = async ({ params }: { params: Talk }): Promise => { - const dict: Dictionary = await dictionaryFor('en'); +const TalkPage = ({ params }: { params: Talk }): ReactElement => { const talk: Talk | undefined = talksFromJSON(talksJson).find(bySlug(params.slug)); - return talk ? ( - <> -

{talk.title}

-

({talk.language})

- - {dict.talks.by} {talk.speaker} - {talk.date.toLocaleString()} ({talk.duration} min) - {talk.room} - -
-

- {talk.track} -

-

{talk.description}

- {talk.tags?.map((tag: string) => #{tag} )} - {(talk.slidesLink || talk.videoLink) && ( - - )} - Back to talks - - ) : ( - <> -

This talk does not exist

-

- There is no talk for this url, select another talk form the list of talks -

- Back to talks - - ); + + return talk ? : ; }; -export default TalkPage; +export default Translate(TalkPage, 'talks'); diff --git a/src/app/talks/[slug]/talk.ts b/src/app/talks/[slug]/talk.ts index a0c1fb6..d69fb5c 100644 --- a/src/app/talks/[slug]/talk.ts +++ b/src/app/talks/[slug]/talk.ts @@ -1,4 +1,4 @@ -import { slugify } from '../../common'; +import { slugify } from '@/app/_utils'; type TalkBase = { title: string; diff --git a/src/app/talks/[slug]/talkTranslation.ts b/src/app/talks/[slug]/talkTranslation.ts new file mode 100644 index 0000000..c63ef4d --- /dev/null +++ b/src/app/talks/[slug]/talkTranslation.ts @@ -0,0 +1,19 @@ +export type TalkTranslation = { + talks: { + title: string; + back: string; + }; + talkDetails: { + by: string; + durationUnit: string; + slides: string; + video: string; + back: string; + }; + notFound: { + title: string; + message: string; + link: string; + back: string; + }; +}; diff --git a/src/app/talks/_components/TalksList.tsx b/src/app/talks/_components/TalksList.tsx new file mode 100644 index 0000000..f5f1ca5 --- /dev/null +++ b/src/app/talks/_components/TalksList.tsx @@ -0,0 +1,25 @@ +'use client'; + +import Link from 'next/link'; +import { ReactElement } from 'react'; +import { useTranslation } from '@/app/_translation'; +import { Talk } from '../[slug]/talk'; +import { TalkTranslation } from '../[slug]/talkTranslation'; + +export const TalksList = ({ talks }: { talks: Talk[] }): ReactElement => { + const { talks: i18n }: TalkTranslation = useTranslation(); + + return ( + <> +

{i18n.title}

+
    + {talks.map((talk: Talk) => ( +
  • + {talk.title} +
  • + ))} +
+ {i18n.back} + + ); +}; diff --git a/src/app/talks/_components/index.ts b/src/app/talks/_components/index.ts new file mode 100644 index 0000000..6ae781d --- /dev/null +++ b/src/app/talks/_components/index.ts @@ -0,0 +1 @@ +export * from './TalksList'; diff --git a/src/app/talks/page.tsx b/src/app/talks/page.tsx index da7ec6a..a6acba1 100644 --- a/src/app/talks/page.tsx +++ b/src/app/talks/page.tsx @@ -1,8 +1,9 @@ -import Link from 'next/link'; import { Metadata } from 'next'; import { ReactElement } from 'react'; -import { Talk, talksFromJSON } from '@/app/talks/[slug]/talk'; import talksJson from '@/data/talks.json'; +import { Translate } from '@/app/_translation'; +import { Talk, talksFromJSON } from './[slug]/talk'; +import { TalksList } from './_components'; export const metadata: Metadata = { title: 'Explore Talks | Your Conference 2023', @@ -12,18 +13,12 @@ export const metadata: Metadata = { const talks: Talk[] = talksFromJSON(talksJson); -const TalksPage = (): ReactElement => ( -
-

Talks

-
    - {talks.map((talk: Talk) => ( -
  • - {talk.title} -
  • - ))} -
- Go back -
-); +const TalksPage = (): ReactElement => { + return ( +
+ +
+ ); +}; -export default TalksPage; +export default Translate(TalksPage, 'talks'); diff --git a/src/app/utils/index.ts b/src/app/utils/index.ts deleted file mode 100644 index 5615cb5..0000000 --- a/src/app/utils/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './url/url.util'; diff --git a/src/dictionaries/en.json b/src/dictionaries/en.json deleted file mode 100644 index 23d6d37..0000000 --- a/src/dictionaries/en.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "talks": { - "by": "By" - } -} diff --git a/src/dictionaries/fr.json b/src/dictionaries/fr.json deleted file mode 100644 index 5bc2af9..0000000 --- a/src/dictionaries/fr.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "talks": { - "by": "Par" - } -} diff --git a/src/dictionaries/index.ts b/src/dictionaries/index.ts deleted file mode 100644 index 55cf7b9..0000000 --- a/src/dictionaries/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -type Lang = 'en' | 'fr'; -export type Dictionary = { talks: { by: string } }; - -const index: Record Promise> = { - en: () => import('./en.json').then((module: { default: Dictionary }) => module.default), - fr: () => import('./fr.json').then((module: { default: Dictionary }) => module.default) -}; - -export const dictionaryFor = async (locale: Lang): Promise => index[locale](); diff --git a/src/settings/language.json b/src/settings/language.json new file mode 100644 index 0000000..d3838ea --- /dev/null +++ b/src/settings/language.json @@ -0,0 +1,4 @@ +{ + "availableLanguages": ["en", "fr"], + "defaultLanguage": "fr" +} diff --git a/src/styles/components/_components.scss b/src/styles/components/_components.scss index 4db3160..e12a2bd 100644 --- a/src/styles/components/_components.scss +++ b/src/styles/components/_components.scss @@ -5,6 +5,7 @@ @import 'bootstrap/scss/grid'; @import 'bootstrap/scss/buttons'; @import 'bootstrap/scss/transitions'; +@import 'bootstrap/scss/dropdown'; @import 'bootstrap/scss/button-group'; @import 'bootstrap/scss/nav'; @import 'bootstrap/scss/navbar'; diff --git a/src/translations/home/en.json b/src/translations/home/en.json new file mode 100644 index 0000000..cafa19a --- /dev/null +++ b/src/translations/home/en.json @@ -0,0 +1,22 @@ +{ + "hero": { + "title": "The largest conference dedicated to freelancers", + "nextEditionDate": "Next edition to come in 2024", + "description": "Free and creative spirits who take their professional destiny into their own hands. Join Freelancers' Day, the must-attend event for ambitious and visionary freelancers in the Lyon region.", + "callToAction": "Book your ticket" + }, + "header": { + "brand": "FreelanceConnect", + "about": "About", + "program": "Program", + "speakers": "Speakers", + "tickets": "Tickets", + "sponsors": "Sponsors" + }, + "about": { + "title": "Welcome to the largest conference dedicated to freelancers", + "subtitle": "About the conference", + "description": "Free and creative minds taking control of your professional destiny. Join Freelancer Day, the must-attend event for ambitious and visionary freelancers in the Lyon region.", + "learnMore": "Learn more" + } +} diff --git a/src/translations/home/fr.json b/src/translations/home/fr.json new file mode 100644 index 0000000..15d3d16 --- /dev/null +++ b/src/translations/home/fr.json @@ -0,0 +1,22 @@ +{ + "hero": { + "title": "La plus grande confĂ©rence dĂ©diĂ©e aux freelances", + "nextEditionDate": "Prochaine Ă©dition Ă  venir en 2024", + "description": "Esprits libres et crĂ©atifs qui prenez en main votre destin professionnel. Rejoignez la journĂ©e des freelances, l’évĂ©nement incontournable pour les freelances ambitieux et visionnaires de la rĂ©gion lyonnaise.", + "callToAction": "RĂ©servez votre billet" + }, + "header": { + "brand": "FreelanceConnect", + "about": "À propos", + "program": "Programme", + "speakers": "ConfĂ©renciers", + "tickets": "Billets", + "sponsors": "Soutiens" + }, + "about": { + "title": "Bienvenue Ă  la plus grande confĂ©rence dĂ©diĂ©e aux freelances", + "subtitle": "À propos de la confĂ©rence", + "description": "Esprits hackers et crĂ©atifs qui prenez en main votre destin professionnel. Rejoignez la journĂ©e des freelances, l’évĂ©nement incontournable pour les freelances ambitieux et visionnaires de la rĂ©gion lyonnaise.", + "learnMore": "En savoir plus" + } +} diff --git a/src/translations/talks/en.json b/src/translations/talks/en.json new file mode 100644 index 0000000..d7c47f4 --- /dev/null +++ b/src/translations/talks/en.json @@ -0,0 +1,19 @@ +{ + "talks": { + "title": "Talks", + "back": "Go back" + }, + "talkDetails": { + "by": "By", + "durationUnit": "min", + "slides": "Slides", + "video": "Video", + "back": "Back to talks" + }, + "notFound": { + "title": "This talk does not exist", + "message": "There is no talk for this url, select another talk form the", + "link": "list of talks", + "back": "Back to talks" + } +} diff --git a/src/translations/talks/fr.json b/src/translations/talks/fr.json new file mode 100644 index 0000000..cad3ff6 --- /dev/null +++ b/src/translations/talks/fr.json @@ -0,0 +1,19 @@ +{ + "talks": { + "title": "ConfĂ©rences", + "back": "Retour" + }, + "talkDetails": { + "by": "Par", + "durationUnit": "min", + "slides": "Diaporama", + "video": "Video", + "back": "Retour Ă  la liste des confĂ©rences" + }, + "notFound": { + "title": "Cette confĂ©rence n'existe pas", + "message": "Il n'y a pas de confĂ©rence associĂ© Ă  cet url, sĂ©lectionnez en une autre depuis", + "link": "la liste des confĂ©rences", + "back": "Retour Ă  la liste des confĂ©rences" + } +}