feat(i18n): implement language switching and localization support
- Remove i18next configuration and dependencies - Add language switcher component - Integrate language change functionality with cookies - Update translations for English and Persian - Modify layout and page components to support dynamic language rendering - Refactor Tailwind CSS configuration for custom colors
This commit is contained in:
+2
-4
@@ -1,7 +1,5 @@
|
||||
import type { NextConfig } from "next";
|
||||
import type { NextConfig } from 'next';
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
};
|
||||
const nextConfig: NextConfig = {};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
Generated
+2
-147
@@ -8,13 +8,9 @@
|
||||
"name": "pasargad_bricks",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"i18next": "^25.2.1",
|
||||
"i18next-browser-languagedetector": "^8.1.0",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"next": "15.3.3",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-i18next": "^15.5.2"
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
@@ -57,14 +53,6 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.4.tgz",
|
||||
"integrity": "sha512-t3yaEOuGu9NlIZ+hIeGbBjFtZT7j2cb2tg0fuaJKeGotchRjjLfrBA9Kwf8quhpP1EUuxModQg04q/mBwyg8uA==",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz",
|
||||
@@ -2214,14 +2202,6 @@
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/cross-fetch": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz",
|
||||
"integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==",
|
||||
"dependencies": {
|
||||
"node-fetch": "^2.6.12"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
@@ -3430,60 +3410,6 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/html-parse-stringify": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
|
||||
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
|
||||
"dependencies": {
|
||||
"void-elements": "3.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/i18next": {
|
||||
"version": "25.2.1",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.2.1.tgz",
|
||||
"integrity": "sha512-+UoXK5wh+VlE1Zy5p6MjcvctHXAhRwQKCxiJD8noKZzIXmnAX8gdHX5fLPA3MEVxEN4vbZkQFy8N0LyD9tUqPw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://locize.com"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://locize.com/i18next.html"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/i18next-browser-languagedetector": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.1.0.tgz",
|
||||
"integrity": "sha512-mHZxNx1Lq09xt5kCauZ/4bsXOEA2pfpwSoU11/QTJB+pD94iONFwp+ohqi///PwiFvjFOxe1akYCdHyFo1ng5Q==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.23.2"
|
||||
}
|
||||
},
|
||||
"node_modules/i18next-http-backend": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-3.0.2.tgz",
|
||||
"integrity": "sha512-PdlvPnvIp4E1sYi46Ik4tBYh/v/NbYfFFgTjkwFl0is8A18s7/bx9aXqsrOax9WUbeNS6mD2oix7Z0yGGf6m5g==",
|
||||
"dependencies": {
|
||||
"cross-fetch": "4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
@@ -4536,25 +4462,6 @@
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "4.x || >=6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"encoding": "^0.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"encoding": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
@@ -4919,31 +4826,6 @@
|
||||
"react": "^19.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-i18next": {
|
||||
"version": "15.5.2",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.5.2.tgz",
|
||||
"integrity": "sha512-ePODyXgmZQAOYTbZXQn5rRsSBu3Gszo69jxW6aKmlSgxKAI1fOhDwSu6bT4EKHciWPKQ7v7lPrjeiadR6Gi+1A==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.25.0",
|
||||
"html-parse-stringify": "^3.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"i18next": ">= 23.2.3",
|
||||
"react": ">= 16.8.0",
|
||||
"typescript": "^5"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native": {
|
||||
"optional": true
|
||||
},
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
@@ -5631,11 +5513,6 @@
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
|
||||
},
|
||||
"node_modules/ts-api-utils": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
|
||||
@@ -5755,7 +5632,7 @@
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -5829,28 +5706,6 @@
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/void-elements": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
|
||||
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||
"dependencies": {
|
||||
"tr46": "~0.0.3",
|
||||
"webidl-conversions": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
||||
+1
-5
@@ -9,13 +9,9 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"i18next": "^25.2.1",
|
||||
"i18next-browser-languagedetector": "^8.1.0",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"next": "15.3.3",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-i18next": "^15.5.2"
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
|
||||
@@ -10,7 +10,7 @@ export default async function Home() {
|
||||
const data = await getData();
|
||||
|
||||
return (
|
||||
<main className="flex min-h-screen flex-col items-center justify-between p-24 text-gray-300">
|
||||
<main className="flex min-h-screen flex-col items-center justify-between p-24 text-bg-primary">
|
||||
ssss
|
||||
</main>
|
||||
);
|
||||
|
||||
+1
-1
@@ -59,7 +59,7 @@
|
||||
}
|
||||
|
||||
:root {
|
||||
--primary: #6366F1;
|
||||
--primary: #FA003F;
|
||||
--secondary: #A78BFA;
|
||||
--textColorLight: #FFFFFF;
|
||||
--textColorGray: #222222;
|
||||
|
||||
+7
-8
@@ -1,23 +1,22 @@
|
||||
import type { Metadata } from 'next';
|
||||
import './globals.css';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import i18n from '../config/i18n';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Create Next App',
|
||||
description: 'Generated by create next app',
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
}>) {
|
||||
const locale = (await cookies()).get('lang')?.value || 'en';
|
||||
|
||||
return (
|
||||
<html lang="en">
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<html lang={locale} dir={locale === 'fa' ? 'rtl' : 'ltr'}>
|
||||
<body>{children}</body>
|
||||
</I18nextProvider>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
+9
-1
@@ -1,4 +1,6 @@
|
||||
// ... existing imports ...
|
||||
import ChangeLangButton from '@/components/layout/languageSwitcher';
|
||||
import { changeLanguage, t } from '@/locales/translates';
|
||||
|
||||
async function getData() {
|
||||
// Simulate a delay to show server-side data fetching
|
||||
@@ -6,12 +8,18 @@ async function getData() {
|
||||
return { message: 'This data was fetched on the server!' };
|
||||
}
|
||||
|
||||
const change = () => {
|
||||
changeLanguage('fa');
|
||||
};
|
||||
|
||||
export default async function Home() {
|
||||
const data = await getData();
|
||||
|
||||
return (
|
||||
<main className="flex min-h-screen flex-col items-center justify-between p-24 text-gray-300">
|
||||
ssss
|
||||
<span>{t('pages_home_title') as string}</span>
|
||||
|
||||
<ChangeLangButton />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
const { t } = useTranslation('routes');
|
||||
import { t, translates } from '@/locales/translates';
|
||||
|
||||
export default {
|
||||
home: {
|
||||
title: t('home'),
|
||||
title: t('pages_home_title'),
|
||||
route: () => '/',
|
||||
},
|
||||
about: {
|
||||
title: t('about'),
|
||||
title: t('pages_about_title'),
|
||||
route: () => '/about',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
'use client';
|
||||
import React, { ButtonHTMLAttributes, ReactNode } from 'react';
|
||||
|
||||
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
'use client';
|
||||
import { changeLanguage, TLocales } from '@/locales/translates';
|
||||
import Button from '../Button';
|
||||
|
||||
export default function ChangeLangButton() {
|
||||
const changeLang = (lang: TLocales) => {
|
||||
changeLanguage(lang);
|
||||
window.location.reload(); // Reload to get new SSR content
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={() => changeLang('en')}>English</Button>
|
||||
<Button onClick={() => changeLang('fa')}>فارسی</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import Backend from 'i18next-http-backend';
|
||||
|
||||
i18n
|
||||
.use(Backend)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
fallbackLng: 'en',
|
||||
debug: false,
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
backend: {
|
||||
loadPath: '/locales/{{lng}}/{{ns}}.json',
|
||||
}
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
@@ -0,0 +1,10 @@
|
||||
const en = {
|
||||
pages_home_title: 'Home',
|
||||
pages_home_description: 'Welcome to the home page',
|
||||
pages_about_title: 'About Us',
|
||||
pages_about_description: 'Learn more about us on this page',
|
||||
pages_contact_title: 'Contact Us',
|
||||
pages_contact_description: 'Get in touch with us through this page',
|
||||
};
|
||||
|
||||
export default en;
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Translates } from './translates';
|
||||
|
||||
const fa = {
|
||||
pages_home_title: 'صفحهی اصلی',
|
||||
pages_home_description: 'Welcome to the home page',
|
||||
pages_about_title: 'About Us',
|
||||
pages_about_description: 'Learn more about us on this page',
|
||||
pages_contact_title: 'Contact Us',
|
||||
pages_contact_description: 'Get in touch with us through this page',
|
||||
} as Translates;
|
||||
|
||||
export default fa;
|
||||
@@ -0,0 +1,57 @@
|
||||
import en from './en';
|
||||
import fa from './fa';
|
||||
|
||||
// Universal getCookie function
|
||||
export function getCookie(name: string): string | undefined {
|
||||
if (typeof window !== 'undefined') {
|
||||
// Client-side
|
||||
const match = document.cookie.match(
|
||||
new RegExp('(^| )' + name + '=([^;]+)'),
|
||||
);
|
||||
return match ? decodeURIComponent(match[2]) : undefined;
|
||||
} else {
|
||||
// Server-side (Next.js App Router)
|
||||
try {
|
||||
// Dynamically import to avoid errors in client bundle
|
||||
const { cookies } = require('next/headers');
|
||||
return cookies().get(name)?.value;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Universal setCookie function to change language
|
||||
export function changeLanguage(lang: TLocales, days = 365) {
|
||||
if (typeof window !== 'undefined') {
|
||||
// Client-side
|
||||
const expires = new Date(Date.now() + days * 864e5).toUTCString();
|
||||
document.cookie = `lang=${encodeURIComponent(lang)}; expires=${expires}; path=/`;
|
||||
} else {
|
||||
// Server-side (Next.js App Router)
|
||||
try {
|
||||
const { cookies } = require('next/headers');
|
||||
cookies().set('lang', lang, { path: '/', maxAge: days * 24 * 60 * 60 });
|
||||
} catch {
|
||||
// No-op on server if not available
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const locales = {
|
||||
en: en as Translates,
|
||||
fa,
|
||||
} as Record<TLocales, Translates>;
|
||||
|
||||
export const translates = (key: keyof Translates): string | (() => string) => {
|
||||
const lang = (getCookie('lang') || 'en') as TLocales;
|
||||
return locales[lang][key] || locales.en[key] || key;
|
||||
};
|
||||
|
||||
export const t = (key: keyof Translates) => translates(key);
|
||||
|
||||
export type TLocales = 'en' | 'fa';
|
||||
|
||||
export type Translates = {
|
||||
[K in keyof typeof en]: string | (() => string);
|
||||
};
|
||||
+1
-1
@@ -9,7 +9,6 @@ const config: Config = {
|
||||
],
|
||||
theme: {
|
||||
// Move your custom colors directly here to override default Tailwind colors
|
||||
extend: {
|
||||
colors: {
|
||||
// Custom colors
|
||||
primary: {
|
||||
@@ -54,6 +53,7 @@ const config: Config = {
|
||||
},
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
backgroundImage: {
|
||||
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
|
||||
'gradient-conic':
|
||||
|
||||
Reference in New Issue
Block a user