From ccecbe7c1f3cb17cd0d3b8d4ffbd37004edfbd48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A1n=20Borrageiros=20Mourelos?= Date: Tue, 13 May 2025 12:02:02 +0200 Subject: [PATCH] SAVE --- package.json | 5 +- public/file.svg | 1 - public/globe.svg | 1 - public/locales/en/common.json | 54 +++++++++++++++ public/locales/es/common.json | 54 +++++++++++++++ public/next.svg | 1 - public/vercel.svg | 1 - public/window.svg | 1 - src/app/api/auth/login/route.ts | 3 - src/app/api/auth/register/route.ts | 3 - src/app/auth/login/page.tsx | 51 +++++++------- src/app/auth/register/page.tsx | 61 +++++++++-------- src/app/layout.tsx | 28 +++++--- src/app/page.tsx | 29 +++++--- src/components/LanguageSwitcher.tsx | 64 ++++++++++++++++++ src/controllers/authController.ts | 33 --------- src/lib/api.ts | 101 ++++++++++++++++++++++++++++ src/lib/i18n/i18n-config.ts | 13 ++++ src/lib/i18n/i18n.ts | 67 ++++++++++++++++++ src/lib/i18n/language-context.tsx | 50 ++++++++++++++ src/lib/i18n/useI18n.ts | 39 +++++++++++ yarn.lock | 36 +++++++++- 22 files changed, 575 insertions(+), 121 deletions(-) delete mode 100644 public/file.svg delete mode 100644 public/globe.svg create mode 100644 public/locales/en/common.json create mode 100644 public/locales/es/common.json delete mode 100644 public/next.svg delete mode 100644 public/vercel.svg delete mode 100644 public/window.svg create mode 100644 src/components/LanguageSwitcher.tsx create mode 100644 src/lib/api.ts create mode 100644 src/lib/i18n/i18n-config.ts create mode 100644 src/lib/i18n/i18n.ts create mode 100644 src/lib/i18n/language-context.tsx create mode 100644 src/lib/i18n/useI18n.ts diff --git a/package.json b/package.json index 94ad976..b593485 100644 --- a/package.json +++ b/package.json @@ -13,11 +13,14 @@ "@types/bcryptjs": "^3.0.0", "@types/jsonwebtoken": "^9.0.9", "bcryptjs": "^3.0.2", + "i18next": "^25.1.2", + "i18next-resources-to-backend": "^1.2.1", "jsonwebtoken": "^9.0.2", "mongoose": "^8.14.2", "next": "15.3.2", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "react-i18next": "^15.5.1" }, "devDependencies": { "@eslint/eslintrc": "^3", diff --git a/public/file.svg b/public/file.svg deleted file mode 100644 index 004145c..0000000 --- a/public/file.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/globe.svg b/public/globe.svg deleted file mode 100644 index 567f17b..0000000 --- a/public/globe.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/locales/en/common.json b/public/locales/en/common.json new file mode 100644 index 0000000..2380c70 --- /dev/null +++ b/public/locales/en/common.json @@ -0,0 +1,54 @@ +{ + "header": { + "login": "Log in", + "register": "Sign up" + }, + "home": { + "title": "Draw what you imagine", + "description": "A simple and powerful app to express your creativity through digital drawings.", + "startFree": "Start for free", + "learnMore": "Learn more" + }, + "footer": { + "copyright": "© 2023 Draw. All rights reserved." + }, + "auth": { + "login": { + "title": "Log in", + "subtitle": "Access your account to start drawing", + "email": "Email address", + "password": "Password", + "forgotPassword": "Forgot your password?", + "rememberMe": "Remember me", + "button": "Log in", + "buttonLoading": "Logging in...", + "noAccount": "Don't have an account?", + "signUp": "Sign up" + }, + "register": { + "title": "Create account", + "subtitle": "Sign up to start drawing", + "username": "Username", + "email": "Email address", + "password": "Password", + "confirmPassword": "Confirm password", + "minChars": "Minimum 6 characters", + "button": "Create account", + "buttonLoading": "Creating account...", + "haveAccount": "Already have an account?", + "login": "Log in" + }, + "errors": { + "passwordMatch": "Passwords do not match", + "passwordLength": "Password must be at least 6 characters long", + "registerError": "Error registering user", + "loginError": "Error logging in", + "unknownError": "An unknown error has occurred" + } + }, + "language": { + "select": "Select language", + "es": "Spanish", + "en": "English" + } +} \ No newline at end of file diff --git a/public/locales/es/common.json b/public/locales/es/common.json new file mode 100644 index 0000000..cc8fdf2 --- /dev/null +++ b/public/locales/es/common.json @@ -0,0 +1,54 @@ +{ + "header": { + "login": "Iniciar sesión", + "register": "Registrarse" + }, + "home": { + "title": "Dibuja lo que imagines", + "description": "Una aplicación simple y poderosa para expresar tu creatividad mediante dibujos digitales.", + "startFree": "Comenzar gratis", + "learnMore": "Conocer más" + }, + "footer": { + "copyright": "© 2023 Draw. Todos los derechos reservados." + }, + "auth": { + "login": { + "title": "Iniciar sesión", + "subtitle": "Accede a tu cuenta para comenzar a dibujar", + "email": "Correo electrónico", + "password": "Contraseña", + "forgotPassword": "¿Olvidaste tu contraseña?", + "rememberMe": "Recordarme", + "button": "Iniciar sesión", + "buttonLoading": "Iniciando sesión...", + "noAccount": "¿No tienes una cuenta?", + "signUp": "Regístrate" + }, + "register": { + "title": "Crear cuenta", + "subtitle": "Regístrate para comenzar a dibujar", + "username": "Nombre de usuario", + "email": "Correo electrónico", + "password": "Contraseña", + "confirmPassword": "Confirmar contraseña", + "minChars": "Mínimo 6 caracteres", + "button": "Crear cuenta", + "buttonLoading": "Creando cuenta...", + "haveAccount": "¿Ya tienes una cuenta?", + "login": "Inicia sesión" + }, + "errors": { + "passwordMatch": "Las contraseñas no coinciden", + "passwordLength": "La contraseña debe tener al menos 6 caracteres", + "registerError": "Error al registrar usuario", + "loginError": "Error al iniciar sesión", + "unknownError": "Ha ocurrido un error desconocido" + } + }, + "language": { + "select": "Seleccionar idioma", + "es": "Español", + "en": "Inglés" + } +} \ No newline at end of file diff --git a/public/next.svg b/public/next.svg deleted file mode 100644 index 5174b28..0000000 --- a/public/next.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/vercel.svg b/public/vercel.svg deleted file mode 100644 index 7705396..0000000 --- a/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/window.svg b/public/window.svg deleted file mode 100644 index b2b2a44..0000000 --- a/public/window.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts index 71b7309..9606b7e 100644 --- a/src/app/api/auth/login/route.ts +++ b/src/app/api/auth/login/route.ts @@ -5,15 +5,12 @@ export async function POST(request: NextRequest) { try { const body = await request.json(); - // Llamar a la función loginUser del controlador const { token, user, error, status } = await loginUser(body); - // Si hay un error, devolverlo con el status correspondiente if (error) { return NextResponse.json({ error }, { status: status || 500 }); } - // Devolver la respuesta con el token y el usuario return NextResponse.json({ token, user }, { status: 200 }); } catch (error) { console.error("Error en el login:", error); diff --git a/src/app/api/auth/register/route.ts b/src/app/api/auth/register/route.ts index 76a184f..072f4a5 100644 --- a/src/app/api/auth/register/route.ts +++ b/src/app/api/auth/register/route.ts @@ -5,15 +5,12 @@ export async function POST(request: NextRequest) { try { const body = await request.json(); - // Llamar a la función register del controlador const { token, user, error, status } = await register(body); - // Si hay un error, devolverlo con el status correspondiente if (error) { return NextResponse.json({ error }, { status: status || 500 }); } - // Devolver la respuesta con el token y el usuario return NextResponse.json({ token, user }, { status: 201 }); } catch (error) { console.error("Error en el registro:", error); diff --git a/src/app/auth/login/page.tsx b/src/app/auth/login/page.tsx index f320eab..b896151 100644 --- a/src/app/auth/login/page.tsx +++ b/src/app/auth/login/page.tsx @@ -3,9 +3,12 @@ import { useState } from 'react'; import { useRouter } from 'next/navigation'; import Link from 'next/link'; +import { useI18n } from '@/lib/i18n/useI18n'; +import { login } from '@/lib/api'; export default function Login() { const router = useRouter(); + const { t, isLoading } = useI18n(); const [formData, setFormData] = useState({ email: '', password: '' @@ -24,44 +27,40 @@ export default function Login() { try { setLoading(true); - const response = await fetch('/api/auth/login', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - email: formData.email, - password: formData.password, - }), + const result = await login({ + email: formData.email, + password: formData.password, }); - const data = await response.json(); - - if (!response.ok) { - throw new Error(data.error || 'Error al iniciar sesión'); + if (result.error) { + throw new Error(result.error || t('auth.errors.loginError')); } - localStorage.setItem('token', data.token); - router.push('/dashboard'); + if (result.data?.token) { + localStorage.setItem('token', result.data.token); + router.push('/dashboard'); + } } catch (err: unknown) { if (err instanceof Error) { setError(err.message); } else { - setError('Ha ocurrido un error desconocido'); + setError(t('auth.errors.unknownError') || ''); } } finally { setLoading(false); } }; + if (isLoading) return null; + return (
-

Iniciar sesión

+

{t('auth.login.title')}

- Accede a tu cuenta para comenzar a dibujar + {t('auth.login.subtitle')}

@@ -75,7 +74,7 @@ export default function Login() {
@@ -122,7 +121,7 @@ export default function Login() { className="h-4 w-4 text-blue-600 focus:ring-0 border-gray-300 rounded" />
@@ -138,9 +137,9 @@ export default function Login() { - Iniciando sesión... + {t('auth.login.buttonLoading')} - ) : 'Iniciar sesión'} + ) : t('auth.login.button')}
@@ -148,9 +147,9 @@ export default function Login() {

- ¿No tienes una cuenta?{' '} + {t('auth.login.noAccount')}{' '} - Regístrate + {t('auth.login.signUp')}

diff --git a/src/app/auth/register/page.tsx b/src/app/auth/register/page.tsx index c32078f..b928af6 100644 --- a/src/app/auth/register/page.tsx +++ b/src/app/auth/register/page.tsx @@ -3,9 +3,12 @@ import { useState } from 'react'; import { useRouter } from 'next/navigation'; import Link from 'next/link'; +import { useI18n } from '@/lib/i18n/useI18n'; +import { register } from '@/lib/api'; export default function Register() { const router = useRouter(); + const { t, isLoading } = useI18n(); const [formData, setFormData] = useState({ username: '', email: '', @@ -14,7 +17,7 @@ export default function Register() { }); const [error, setError] = useState(''); const [loading, setLoading] = useState(false); - + const handleChange = (e: React.ChangeEvent) => { const { name, value } = e.target; setFormData(prev => ({ ...prev, [name]: value })); @@ -25,56 +28,52 @@ export default function Register() { setError(''); if (formData.password !== formData.confirmPassword) { - setError('Las contraseñas no coinciden'); + setError(t('auth.errors.passwordMatch') || ''); return; } if (formData.password.length < 6) { - setError('La contraseña debe tener al menos 6 caracteres'); + setError(t('auth.errors.passwordLength') || ''); return; } try { setLoading(true); - const response = await fetch('/api/auth/register', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - username: formData.username, - email: formData.email, - password: formData.password, - }), + const result = await register({ + username: formData.username, + email: formData.email, + password: formData.password, }); - const data = await response.json(); - - if (!response.ok) { - throw new Error(data.error || 'Error al registrar usuario'); + if (result.error) { + throw new Error(result.error || t('auth.errors.registerError')); } - localStorage.setItem('token', data.token); - router.push('/dashboard'); + if (result.data?.token) { + localStorage.setItem('token', result.data.token); + router.push('/dashboard'); + } } catch (err: unknown) { if (err instanceof Error) { setError(err.message); } else { - setError('Ha ocurrido un error desconocido'); + setError(t('auth.errors.unknownError') || ''); } } finally { setLoading(false); } }; + if (isLoading) return null; + return (
-

Crear cuenta

+

{t('auth.register.title')}

- Regístrate para comenzar a dibujar + {t('auth.register.subtitle')}

@@ -88,7 +87,7 @@ export default function Register() {
-

Mínimo 6 caracteres

+

{t('auth.register.minChars')}

- Creando cuenta... + {t('auth.register.buttonLoading')} - ) : 'Crear cuenta'} + ) : t('auth.register.button')}
@@ -177,9 +176,9 @@ export default function Register() {

- ¿Ya tienes una cuenta?{' '} + {t('auth.register.haveAccount')}{' '} - Inicia sesión + {t('auth.register.login')}

diff --git a/src/app/layout.tsx b/src/app/layout.tsx index defc6a1..20b1d4f 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,9 @@ -import type { Metadata } from "next"; +'use client'; + import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; +import { LanguageProvider } from "@/lib/i18n/language-context"; +import { useEffect, useState } from "react"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -12,20 +15,29 @@ const geistMono = Geist_Mono({ subsets: ["latin"], }); -export const metadata: Metadata = { - title: "Excalidraw App", - description: "Una aplicación de dibujo con Excalidraw", -}; - export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { + const [language, setLanguage] = useState('es'); + + useEffect(() => { + const storedLanguage = localStorage.getItem('locale'); + if (storedLanguage) { + setLanguage(storedLanguage); + } else { + const browserLang = navigator.language.split('-')[0]; + setLanguage(['es', 'en'].includes(browserLang) ? browserLang : 'en'); + } + }, []); + return ( - + - {children} + + {children} + ); diff --git a/src/app/page.tsx b/src/app/page.tsx index 52dc3bd..6777c3c 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,23 +1,32 @@ +'use client'; + import Link from 'next/link'; +import LanguageSwitcher from '@/components/LanguageSwitcher'; +import { useI18n } from '@/lib/i18n/useI18n'; export default function Home() { + const { t, isLoading } = useI18n(); + + if (isLoading) return null; + return (
-
DrawApp
-
@@ -27,23 +36,23 @@ export default function Home() {

- Dibuja lo que imagines + {t('home.title')}

- Una aplicación simple y poderosa para expresar tu creatividad mediante dibujos digitales. + {t('home.description')}

- Comenzar gratis + {t('home.startFree')} - Conocer más + {t('home.learnMore')}
@@ -60,7 +69,7 @@ export default function Home() {

- © 2023 DrawApp. Todos los derechos reservados. + {t('footer.copyright')}

diff --git a/src/components/LanguageSwitcher.tsx b/src/components/LanguageSwitcher.tsx new file mode 100644 index 0000000..16fd634 --- /dev/null +++ b/src/components/LanguageSwitcher.tsx @@ -0,0 +1,64 @@ +'use client'; + +import { useLanguage } from '@/lib/i18n/language-context'; +import { locales } from '@/lib/i18n/i18n-config'; +import { useState, useRef, useEffect } from 'react'; +import { useI18n } from '@/lib/i18n/useI18n'; + +export default function LanguageSwitcher() { + const { locale, setLocale } = useLanguage(); + const { t, isLoading } = useI18n(); + const [isOpen, setIsOpen] = useState(false); + const dropdownRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsOpen(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + + if (isLoading) return null; + + return ( +
+ + + {isOpen && ( +
+
+ {t('language.select')} +
+ {locales.map((lang) => ( + + ))} +
+ )} +
+ ); +} \ No newline at end of file diff --git a/src/controllers/authController.ts b/src/controllers/authController.ts index 4a71847..3d98120 100644 --- a/src/controllers/authController.ts +++ b/src/controllers/authController.ts @@ -1,4 +1,3 @@ -import { NextApiRequest, NextApiResponse } from "next"; import bcrypt from "bcryptjs"; import jwt from "jsonwebtoken"; import User from "../models/User"; @@ -46,38 +45,6 @@ export async function register(data: { } } -export async function login(req: NextApiRequest, res: NextApiResponse) { - try { - await dbConnect(); - const { email, password } = req.body; - - const user = await User.findOne({ email }); - if (!user) { - return res.status(401).json({ error: "Invalid credentials" }); - } - - const isValidPassword = await bcrypt.compare(password, user.password); - if (!isValidPassword) { - return res.status(401).json({ error: "Invalid credentials" }); - } - - const token = jwt.sign({ userId: user._id }, JWT_SECRET, { - expiresIn: "7d", - }); - - return res.status(200).json({ - token, - user: { - id: user._id, - username: user.username, - email: user.email, - }, - }); - } catch { - return res.status(500).json({ error: "Internal server error" }); - } -} - export async function loginUser(data: { email: string; password: string }) { try { await dbConnect(); diff --git a/src/lib/api.ts b/src/lib/api.ts new file mode 100644 index 0000000..db0a25b --- /dev/null +++ b/src/lib/api.ts @@ -0,0 +1,101 @@ +interface LoginRequest { + email: string; + password: string; +} + +interface RegisterRequest { + username: string; + email: string; + password: string; +} + +interface ApiResponse { + data?: T; + error?: string; + status: number; +} + +interface UserProfile { + id: string; + username: string; + email: string; +} + +interface AuthResponse { + token: string; + user: UserProfile; +} + +async function fetchApi( + endpoint: string, + method: string = "GET", + body?: T, + headers?: Record +): Promise> { + try { + const requestHeaders = { + "Content-Type": "application/json", + ...headers, + }; + + const config: RequestInit = { + method, + headers: requestHeaders, + body: body ? JSON.stringify(body) : undefined, + }; + + const response = await fetch(`/api${endpoint}`, config); + const data = await response.json(); + + return { + data: response.ok ? data : undefined, + error: !response.ok ? data.error || "Error en la petición" : undefined, + status: response.status, + }; + } catch (error) { + console.error("API request error:", error); + return { + error: error instanceof Error ? error.message : "Error desconocido", + status: 500, + }; + } +} + +export async function login( + credentials: LoginRequest +): Promise> { + return fetchApi( + "/auth/login", + "POST", + credentials + ); +} + +export async function register( + userData: RegisterRequest +): Promise> { + return fetchApi( + "/auth/register", + "POST", + userData + ); +} + +export async function getProfile(): Promise> { + const token = localStorage.getItem("token"); + + if (!token) { + return { + error: "No hay token de autenticación", + status: 401, + }; + } + + return fetchApi("/user/profile", "GET", undefined, { + Authorization: `Bearer ${token}`, + }); +} + +export async function logout(): Promise { + localStorage.removeItem("token"); +} diff --git a/src/lib/i18n/i18n-config.ts b/src/lib/i18n/i18n-config.ts new file mode 100644 index 0000000..b83c193 --- /dev/null +++ b/src/lib/i18n/i18n-config.ts @@ -0,0 +1,13 @@ +export const defaultLocale = "en"; +export const locales = ["en", "es"]; + +export function getOptions(lng = defaultLocale, ns = "common") { + return { + supportedLngs: locales, + fallbackLng: defaultLocale, + lng, + ns, + defaultNS: "common", + fallbackNS: "common", + }; +} diff --git a/src/lib/i18n/i18n.ts b/src/lib/i18n/i18n.ts new file mode 100644 index 0000000..4461b31 --- /dev/null +++ b/src/lib/i18n/i18n.ts @@ -0,0 +1,67 @@ +import { createInstance, i18n } from "i18next"; +import { initReactI18next } from "react-i18next"; +import resourcesToBackend from "i18next-resources-to-backend"; +import { getOptions } from "./i18n-config"; + +// Caché para las instancias de i18next por idioma y namespace +const i18nInstancesCache: Record = {}; + +const initI18next = async (lng: string, ns: string) => { + const cacheKey = `${lng}:${ns}`; + + // Si ya existe en caché, devolver la instancia existente + if (i18nInstancesCache[cacheKey]) { + return i18nInstancesCache[cacheKey]; + } + + const i18nInstance = createInstance(); + await i18nInstance + .use(initReactI18next) + .use( + resourcesToBackend( + (language: string, namespace: string) => + import(`../../public/locales/${language}/${namespace}.json`) + ) + ) + .init(getOptions(lng, ns)); + + // Guardar en caché + i18nInstancesCache[cacheKey] = i18nInstance; + + return i18nInstance; +}; + +// Tipo para la función de traducción +export type TranslationFunction = (key: string) => string | undefined; + +// Tipo para el objeto de traducción cacheado +interface CachedTranslation { + t: TranslationFunction; + i18n: i18n; +} + +// Caché para las funciones de traducción +const translationCache: Record = {}; + +export async function getTranslation( + lng: string, + ns: string = "common" +): Promise { + const cacheKey = `${lng}:${ns}`; + + // Si ya existe en caché, devolver la función de traducción existente + if (translationCache[cacheKey]) { + return translationCache[cacheKey]; + } + + const i18nextInstance = await initI18next(lng, ns); + const translation: CachedTranslation = { + t: i18nextInstance.getFixedT(lng, ns), + i18n: i18nextInstance, + }; + + // Guardar en caché + translationCache[cacheKey] = translation; + + return translation; +} diff --git a/src/lib/i18n/language-context.tsx b/src/lib/i18n/language-context.tsx new file mode 100644 index 0000000..20e5cc3 --- /dev/null +++ b/src/lib/i18n/language-context.tsx @@ -0,0 +1,50 @@ +'use client'; + +import { createContext, useContext, useState, useEffect, ReactNode } from 'react'; +import { locales, defaultLocale } from './i18n-config'; + +type LanguageContextType = { + locale: string; + setLocale: (locale: string) => void; +}; + +const LanguageContext = createContext(null); + +export function LanguageProvider({ children }: { children: ReactNode }) { + const [locale, setLocale] = useState(defaultLocale); + + useEffect(() => { + const storedLocale = localStorage.getItem('locale'); + + if (storedLocale && locales.includes(storedLocale)) { + setLocale(storedLocale); + } else { + const browserLocale = navigator.language.split('-')[0]; + const newLocale = locales.includes(browserLocale) ? browserLocale : defaultLocale; + setLocale(newLocale); + localStorage.setItem('locale', newLocale); + } + }, []); + + const handleSetLocale = (newLocale: string) => { + if (locales.includes(newLocale)) { + setLocale(newLocale); + localStorage.setItem('locale', newLocale); + window.location.reload(); + } + }; + + return ( + + {children} + + ); +} + +export function useLanguage() { + const context = useContext(LanguageContext); + if (!context) { + throw new Error('useLanguage must be used within a LanguageProvider'); + } + return context; +} \ No newline at end of file diff --git a/src/lib/i18n/useI18n.ts b/src/lib/i18n/useI18n.ts new file mode 100644 index 0000000..e446ad7 --- /dev/null +++ b/src/lib/i18n/useI18n.ts @@ -0,0 +1,39 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useLanguage } from "./language-context"; +import { getTranslation } from "./i18n"; + +export function useI18n() { + const { locale } = useLanguage(); + const [t, setT] = useState<(key: string) => string | undefined>(null!); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + let isMounted = true; + + const loadTranslations = async () => { + setIsLoading(true); + try { + const translation = await getTranslation(locale); + if (isMounted) { + setT(() => translation.t); + } + } catch (error) { + console.error("Error loading translations:", error); + } finally { + if (isMounted) { + setIsLoading(false); + } + } + }; + + loadTranslations(); + + return () => { + isMounted = false; + }; + }, [locale]); + + return { t, isLoading, locale }; +} diff --git a/yarn.lock b/yarn.lock index b1fbd98..8d66891 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15,7 +15,7 @@ "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.24" -"@babel/runtime@^7.13.10": +"@babel/runtime@^7.13.10", "@babel/runtime@^7.23.2", "@babel/runtime@^7.25.0", "@babel/runtime@^7.26.10": version "7.27.1" resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz" integrity sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog== @@ -2931,6 +2931,13 @@ hasown@^2.0.2: dependencies: function-bind "^1.1.2" +html-parse-stringify@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz#dfc1017347ce9f77c8141a507f233040c59c55d2" + integrity sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg== + dependencies: + void-elements "3.1.0" + http-errors@2.0.0, http-errors@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz" @@ -2942,6 +2949,20 @@ http-errors@2.0.0, http-errors@^2.0.0: statuses "2.0.1" toidentifier "1.0.1" +i18next-resources-to-backend@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/i18next-resources-to-backend/-/i18next-resources-to-backend-1.2.1.tgz#fded121e63e3139ce839c9901b9449dbbea7351d" + integrity sha512-okHbVA+HZ7n1/76MsfhPqDou0fptl2dAlhRDu2ideXloRRduzHsqDOznJBef+R3DFZnbvWoBW+KxJ7fnFjd6Yw== + dependencies: + "@babel/runtime" "^7.23.2" + +i18next@^25.1.2: + version "25.1.2" + resolved "https://registry.yarnpkg.com/i18next/-/i18next-25.1.2.tgz#c667b814ed1b4dbea76976a4c746a87922ac6ceb" + integrity sha512-SP63m8LzdjkrAjruH7SCI3ndPSgjt4/wX7ouUUOzCW/eY+HzlIo19IQSfYA9X3qRiRP1SYtaTsg/Oz/PGsfD8w== + dependencies: + "@babel/runtime" "^7.26.10" + iconv-lite@0.6, iconv-lite@0.6.3, iconv-lite@^0.6.3: version "0.6.3" resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz" @@ -4425,6 +4446,14 @@ react-dom@^19.0.0: dependencies: scheduler "^0.26.0" +react-i18next@^15.5.1: + version "15.5.1" + resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-15.5.1.tgz#ceada755a0b6691432b6b6dc5dad454fd3d158e3" + integrity sha512-C8RZ7N7H0L+flitiX6ASjq9p5puVJU1Z8VyL3OgM/QOMRf40BMZX+5TkpxzZVcTmOLPX5zlti4InEX5pFyiVeA== + dependencies: + "@babel/runtime" "^7.25.0" + html-parse-stringify "^3.0.1" + react-is@^16.13.1: version "16.13.1" resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz" @@ -5305,6 +5334,11 @@ vary@^1, vary@^1.1.2: resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== +void-elements@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09" + integrity sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w== + web-worker@^1.2.0: version "1.5.0" resolved "https://registry.npmjs.org/web-worker/-/web-worker-1.5.0.tgz"