Skip to content

Commit

Permalink
Merge branch 'i18n'
Browse files Browse the repository at this point in the history
  • Loading branch information
lewebsimple committed Nov 16, 2023
1 parent 20e1904 commit 46f12b2
Show file tree
Hide file tree
Showing 21 changed files with 455 additions and 127 deletions.
33 changes: 33 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
"version": "0.2.0",
"configurations": [
{
"type": "firefox",
"request": "launch",
"name": "Nuxt client",
"url": "http://localhost:3000",
"webRoot": "${workspaceFolder}"
},
{
"type": "node",
"request": "launch",
"name": "Nuxt server",
"outputCapture": "std",
"program": "${workspaceFolder}/node_modules/nuxi/bin/nuxi.mjs",
"args": [
"dev"
]
}
],
"compounds": [
{
"name": "Nuxt fullstack",
"configurations": [
"Nuxt server",
"Nuxt client"
]
}
]
}
6 changes: 6 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"i18n-ally.localesPaths": [
"locales"
],
"i18n-ally.keystyle": "nested"
}
13 changes: 13 additions & 0 deletions app/components/LocaleSwitcher.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<script setup lang="ts">
const { locale, locales } = useI18n();
const switchLocalePath = useSwitchLocalePath();
const availableLocales = computed(() => (locales.value as { code: string; name: string }[]).filter(({ code }) => code !== locale.value));
</script>

<template>
<div class="flex items-center gap-2">
<UButton v-for="{ code, name } in availableLocales" :key="code" color="gray" variant="link" :to="switchLocalePath(code)">
{{ name }}
</UButton>
</div>
</template>
5 changes: 3 additions & 2 deletions app/components/TheAppFooter.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<template>
<footer class="py-2 bg-muted">
<div class="container flex justify-between items-center gap-4">
<AppVersion />
<div class="container flex items-center gap-4">
<AppVersion class="mr-auto" />
<LocaleSwitcher />
<AuthButton />
</div>
</footer>
Expand Down
7 changes: 5 additions & 2 deletions app/pages/index.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
<script setup lang="ts">
useHead({ title: "Home" });
const { t } = useI18n();
useHead({ title: t("home") });
</script>

<template>
<div id="page-home">...</div>
<div id="page-home">
<h1 class="h1">{{ $t("home") }}</h1>
</div>
</template>
7 changes: 5 additions & 2 deletions auth/components/AuthButton.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
<script setup lang="ts">
const { isAuthenticated } = useAuth();
const { t } = useI18n();
const localePath = useLocalePath();
const buttonAttrs = computed(() => ({
color: <any>"gray",
variant: <any>"link",
padded: false,
to: isAuthenticated.value ? "/auth/logout" : "/auth/login",
label: isAuthenticated.value ? "Déconnexion" : "Connexion",
to: isAuthenticated.value ? localePath("/auth/logout") : localePath("/auth/login"),
label: isAuthenticated.value ? t("auth.logout") : t("auth.login"),
}));
</script>

Expand Down
11 changes: 6 additions & 5 deletions auth/components/TheAuthLoginForm.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script setup lang="ts">
import type { FormSubmitEvent } from "#ui/types";
const { t } = useI18n();
const { login } = useAuth();
const state = ref<AuthLogin>({ email: "", password: "" });
Expand All @@ -10,7 +11,7 @@ const submitAttrs = computed(() => ({
block: true,
color: <any>isSubmitting.value ? "gray" : "primary",
disabled: isSubmitting.value,
label: isSubmitting.value ? "Connexion en cours..." : "Connexion",
label: isSubmitting.value ? t("auth.logging_in") : t("auth.login"),
loading: isSubmitting.value,
type: "submit",
variant: <any>"solid",
Expand All @@ -24,8 +25,8 @@ async function onSubmit(event: FormSubmitEvent<AuthLogin>) {
await useRouter().replace(redirect);
} catch (error) {
useToast().add({
title: "Échec de la connexion",
description: "Veuillez vérifier vos identifiants",
title: t("error"),
description: t("auth.login_failed_description"),
icon: "i-heroicons-x-circle",
color: "red",
});
Expand All @@ -38,10 +39,10 @@ async function onSubmit(event: FormSubmitEvent<AuthLogin>) {
<template>
<UForm :schema="authLoginSchema" :state="state" @submit="onSubmit">
<div class="form-wrapper">
<UFormGroup name="email" label="Courriel">
<UFormGroup name="email" :label="$t('email')">
<UInput v-model="state.email" type="email" />
</UFormGroup>
<UFormGroup name="password" label="Mot de passe">
<UFormGroup name="password" :label="$t('password')">
<UPasswordInput v-model="state.password" />
</UFormGroup>
<UFormGroup name="submit">
Expand Down
9 changes: 4 additions & 5 deletions auth/components/TheAuthLogoutForm.vue
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
<script setup lang="ts">
const { logout } = useAuth();
const { t } = useI18n();
const state = ref({});
const isSubmitting = ref(false);
const submitAttrs = computed(() => ({
block: true,
color: <any>(isSubmitting.value ? "gray" : "primary"),
disabled: isSubmitting.value,
label: isSubmitting.value ? "Déconnexion en cours..." : "Déconnexion",
label: isSubmitting.value ? t("auth.logging_out") : t("auth.logout"),
loading: isSubmitting.value,
type: "submit",
variant: <any>(isSubmitting.value ? "ghost" : "solid"),
Expand All @@ -20,10 +20,9 @@ async function onSubmit() {
await logout();
await useRouter().replace("/");
} catch (error) {
// Ignore logout errors
useToast().add({
title: "Échec de la déconnexion",
description: "Veuillez réessayer plus tard",
title: t("error"),
description: t("auth.logout_failed_description"),
icon: "i-heroicons-x-circle",
color: "red",
});
Expand Down
5 changes: 3 additions & 2 deletions auth/components/TheAuthPage.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
<script setup lang="ts">
defineProps<{ title: string | undefined }>();
const localePath = useLocalePath();
</script>

<template>
<div class="max-w-sm space-y-8">
<div class="w-96 space-y-8">
<h1 v-if="title" class="h3">{{ title }}</h1>
<slot />
<UButton to="/" color="gray" variant="link" :padded="false" label="Retour à l'accueil" icon="i-heroicons-arrow-left" />
<UButton :to="localePath('/')" color="gray" variant="link" :padded="false" :label="$t('auth.go_back_home')" icon="i-heroicons-arrow-left" />
</div>
</template>
21 changes: 11 additions & 10 deletions auth/components/TheAuthSignupForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,21 @@ import { AuthRole } from "@prisma/client";
import type { FormSubmitEvent } from "#ui/types";
const { signup } = useAuth();
const { t } = useI18n();
const state = ref<AuthSignup>({ email: "", password: "", role: AuthRole.VERIFIED });
const roleOptions = [
{ value: AuthRole.VERIFIED, label: "Utilisateur" },
{ value: AuthRole.ADMINISTRATOR, label: "Administrateur" },
{ value: AuthRole.VERIFIED, label: t("auth.role_verified") },
{ value: AuthRole.ADMINISTRATOR, label: t("auth.role_administrator") },
];
const isSubmitting = ref(false);
const submitAttrs = computed(() => ({
block: true,
color: <any>isSubmitting.value ? "gray" : "primary",
disabled: isSubmitting.value,
label: isSubmitting.value ? "Inscription en cours..." : "Inscription",
label: isSubmitting.value ? t("auth.signing_up") : t("auth.signup"),
loading: isSubmitting.value,
type: "submit",
variant: <any>"solid",
Expand All @@ -29,15 +30,15 @@ async function onSubmit(event: FormSubmitEvent<AuthSignup>) {
await signup(event.data);
state.value = { email: "", password: "", role: "VERIFIED" };
useToast().add({
title: "Inscription réussie",
description: `Le compte ${event.data.email} a été créé avec succès`,
title: t("success"),
description: t("auth.signup_success_description", { email: event.data.email }),
icon: "i-heroicons-check-circle",
color: "green",
});
} catch (error) {
useToast().add({
title: "Échec de l'inscription",
description: "Le courriel est déjà utilisé ou une erreur est survenue",
title: t("error"),
description: t("auth.signup_failed_description"),
icon: "i-heroicons-x-circle",
color: "red",
});
Expand All @@ -50,13 +51,13 @@ async function onSubmit(event: FormSubmitEvent<AuthSignup>) {
<template>
<UForm :schema="authLoginSchema" :state="state" @submit="onSubmit">
<div class="form-wrapper">
<UFormGroup name="email" label="Courriel">
<UFormGroup name="email" :label="$t('email')">
<UInput v-model="state.email" type="email" />
</UFormGroup>
<UFormGroup name="password" label="Mot de passe">
<UFormGroup name="password" :label="$t('password')">
<UPasswordInput v-model="state.password" />
</UFormGroup>
<UFormGroup name="role" label="Rôle">
<UFormGroup name="role" :label="$t('role')">
<USelect v-model="state.role" :options="roleOptions" />
</UFormGroup>
<UFormGroup name="submit">
Expand Down
8 changes: 4 additions & 4 deletions auth/composables/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ import { z } from "zod";

// Authentication login schema
export const authLoginSchema = z.object({
email: z.string().email("Courriel invalide"),
password: z.string().min(8, "Le mot de passe doit contenir au moins 8 caractères"),
email: z.string().email(),
password: z.string().min(8),
});
export type AuthLogin = z.infer<typeof authLoginSchema>;

// Authentication signup schema
export const authSignupSchema = z.object({
email: z.string().email("Courriel invalide"),
password: z.string().min(8, "Le mot de passe doit contenir au moins 8 caractères"),
email: z.string().email(),
password: z.string().min(8),
role: z.nativeEnum(AuthRole),
});
export type AuthSignup = z.infer<typeof authSignupSchema>;
Expand Down
3 changes: 2 additions & 1 deletion auth/middleware/has-auth-role.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import { type AuthRole } from "@prisma/client";

export default defineNuxtRouteMiddleware((to) => {
const { isAuthenticated, hasAuthRole } = useAuth();
const { $i18n } = useNuxtApp();
if (!isAuthenticated.value) {
return navigateTo(`/auth/login?redirect=${to.fullPath}`);
} else if (!hasAuthRole(to.meta.hasAuthRole || "ADMINISTRATOR")) {
abortNavigation({ statusCode: 403, statusMessage: "Opération non permise" });
abortNavigation({ statusCode: 403, statusMessage: $i18n.t("auth.unauthorized") });
}
});

Expand Down
4 changes: 2 additions & 2 deletions auth/pages/auth/login.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<script setup lang="ts">
definePageMeta({ layout: "blank", middleware: "not-authenticated" });
defineI18nRoute({ paths: { fr: "/auth/connexion" } });
useHead({ title: "Connexion" });
</script>

<template>
<div id="page-auth-login">
<TheAuthPage title="Veuillez vous identifier">
<TheAuthPage :title="$t('auth.login_title')">
<TheAuthLoginForm />
</TheAuthPage>
</div>
Expand Down
4 changes: 2 additions & 2 deletions auth/pages/auth/logout.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<script setup lang="ts">
definePageMeta({ layout: "blank", middleware: "is-authenticated" });
defineI18nRoute({ paths: { fr: "/auth/deconnexion" } });
useHead({ title: "Déconnexion" });
</script>

<template>
<div id="page-auth-logout">
<TheAuthPage title="Confirmez la déconnexion">
<TheAuthPage :title="$t('auth.logout_title')">
<TheAuthLogoutForm />
</TheAuthPage>
</div>
Expand Down
4 changes: 2 additions & 2 deletions auth/pages/auth/signup.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<script setup lang="ts">
definePageMeta({ layout: "blank", middleware: "has-auth-role", hasAuthRole: "ADMINISTRATOR" });
defineI18nRoute({ paths: { fr: "/auth/inscription" } });
useHead({ title: "Inscription" });
</script>

<template>
<div id="page-auth-signup">
<TheAuthPage title="Veuillez vous inscrire">
<TheAuthPage :title="$t('auth.signup_title')">
<TheAuthSignupForm />
</TheAuthPage>
</div>
Expand Down
4 changes: 4 additions & 0 deletions i18n.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export default defineI18nConfig(() => ({
legacy: false,
locale: "en",
}));
24 changes: 24 additions & 0 deletions locales/en.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
auth:
go_back_home: Return home
logging_in: Logging in...
logging_out: Logging out...
login_failed_description: Login failed, please try again later
login_title: Login to your account
login: Login
logout_failed_description: Logout failed, please try again later
logout_title: Confirm logout
logout: Logout
role_administrator: Administrator
role_verified: User
signing_up: Signing up...
signup_failed_description: Signup failed, email already in use
signup_success_description: Signup successful for {email}
signup_title: Signup for an account
signup: Signup
unauthorized: Unauthorized
email: Email
error: Error
home: Home
password: Password
role: Role
success: Success
24 changes: 24 additions & 0 deletions locales/fr.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
auth:
go_back_home: Retourner à l'accueil
logging_in: Connexion en cours...
logging_out: Déconnexion en cours...
login_failed_description: Authentification échouée, veuillez essayer plus tard
login_title: Connectez-vous à votre compte
login: Connexion
logout_failed_description: La déconnexion a échoué, veuillez réessayer plus tard
logout_title: Confirmez la déconnexion
logout: Déconnexion
role_administrator: Administrateur
role_verified: Utilisateur
signing_up: Inscription en cours...
signup_failed_description: L'inscription a échoué, le courriel est déjà utilisé
signup_success_description: Inscription réussie pour {email}
signup_title: Inscrivez un compte
signup: Inscription
unauthorized: Opération non permise
email: Courriel
error: Erreur
home: Accueil
password: Mot de passe
role: Rôle
success: Succès
10 changes: 10 additions & 0 deletions nuxt.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
extends: ["./app", "./auth", "./graphql", "./jobs", "./prisma"],
i18n: {
defaultLocale: "en",
langDir: "locales",
locales: [
{ code: "en", iso: "en-US", file: "en.yaml", name: "English" },
{ code: "fr", iso: "fr-CA", file: "fr.yaml", name: "Français" },
],
},
modules: ["@nuxtjs/i18n"],
typescript: { typeCheck: true },
vite: { build: { sourcemap: process.env.NODE_ENV === "production" ? true : "inline" } },
});
Loading

0 comments on commit 46f12b2

Please sign in to comment.