feat(i18n): add internationalization support with i18next

- Add i18next and related dependencies for translation support
- Create locale files for English and Farsi languages
- Implement i18n configuration and provider in layout
- Set up route factory and constants for internationalized routes
- Remove test button components and simplify main page
This commit is contained in:
2025-05-31 16:41:12 +03:30
parent b8c3ebcee3
commit cb77aef586
15 changed files with 270 additions and 59 deletions
+147 -2
View File
@@ -8,9 +8,13 @@
"name": "pasargad_bricks", "name": "pasargad_bricks",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"i18next": "^25.2.1",
"i18next-browser-languagedetector": "^8.1.0",
"i18next-http-backend": "^3.0.2",
"next": "15.3.3", "next": "15.3.3",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0" "react-dom": "^19.0.0",
"react-i18next": "^15.5.2"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3", "@eslint/eslintrc": "^3",
@@ -53,6 +57,14 @@
"node": ">=6.0.0" "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": { "node_modules/@emnapi/core": {
"version": "1.4.3", "version": "1.4.3",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz",
@@ -2202,6 +2214,14 @@
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true "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": { "node_modules/cross-spawn": {
"version": "7.0.6", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -3410,6 +3430,60 @@
"node": ">= 0.4" "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": { "node_modules/ignore": {
"version": "5.3.2", "version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -4462,6 +4536,25 @@
"node": "^10 || ^12 || >=14" "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": { "node_modules/object-assign": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -4826,6 +4919,31 @@
"react": "^19.1.0" "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": { "node_modules/react-is": {
"version": "16.13.1", "version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@@ -5513,6 +5631,11 @@
"node": ">=8.0" "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": { "node_modules/ts-api-utils": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
@@ -5632,7 +5755,7 @@
"version": "5.8.3", "version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"dev": true, "devOptional": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@@ -5706,6 +5829,28 @@
"punycode": "^2.1.0" "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": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+5 -1
View File
@@ -9,9 +9,13 @@
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"i18next": "^25.2.1",
"i18next-browser-languagedetector": "^8.1.0",
"i18next-http-backend": "^3.0.2",
"next": "15.3.3", "next": "15.3.3",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0" "react-dom": "^19.0.0",
"react-i18next": "^15.5.2"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3", "@eslint/eslintrc": "^3",
+4
View File
@@ -0,0 +1,4 @@
{
"home": "Home",
"about": "About"
}
+4
View File
@@ -0,0 +1,4 @@
{
"home": "صفحه‌ی اصلی",
"about": "درباره‌ی ما"
}
+17
View File
@@ -0,0 +1,17 @@
// ... existing imports ...
async function getData() {
// Simulate a delay to show server-side data fetching
await new Promise((resolve) => setTimeout(resolve, 1000));
return { message: 'This data was fetched on the server!' };
}
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
</main>
);
}
+7 -17
View File
@@ -1,16 +1,8 @@
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import { Geist, Geist_Mono } from 'next/font/google'; import { Geist, Geist_Mono } from 'next/font/google';
import './globals.css'; import './globals.css';
import { I18nextProvider } from 'react-i18next';
const geistSans = Geist({ import i18n from '../config/i18n';
variable: '--font-geist-sans',
subsets: ['latin'],
});
const geistMono = Geist_Mono({
variable: '--font-geist-mono',
subsets: ['latin'],
});
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Create Next App', title: 'Create Next App',
@@ -19,16 +11,14 @@ export const metadata: Metadata = {
export default function RootLayout({ export default function RootLayout({
children, children,
}: Readonly<{ }: {
children: React.ReactNode; children: React.ReactNode;
}>) { }) {
return ( return (
<html lang="en"> <html lang="en">
<body <I18nextProvider i18n={i18n}>
className={`${geistSans.variable} ${geistMono.variable} antialiased`} <body>{children}</body>
> </I18nextProvider>
{children}
</body>
</html> </html>
); );
} }
+1 -39
View File
@@ -1,5 +1,4 @@
// ... existing imports ... // ... existing imports ...
import Button from '../components/Button'; // Adjust the import path if necessary
async function getData() { async function getData() {
// Simulate a delay to show server-side data fetching // Simulate a delay to show server-side data fetching
@@ -12,44 +11,7 @@ export default async function Home() {
return ( 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-gray-300">
<div className="z-10 w-full max-w-5xl items-center justify-between font-mono text-sm lg:flex"> ssss
<p className="fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30">
{data.message}
</p>
</div>
<div className="mt-8 flex gap-4">
<Button>Default Button</Button>
<Button disabled>Disabled Button</Button>
<Button loading>Loading Button</Button>
<Button endIcon={<span></span>}>Button with Icon</Button>
</div>
<div className="mt-8 flex flex-col gap-4">
<h3>Standard Buttons:</h3>
<div className="flex gap-4">
<Button>Default Button</Button>
<Button disabled>Disabled Button</Button>
<Button loading>Loading Button</Button>
<Button endIcon={<span></span>}>Button with Icon</Button>
</div>
<h3>Sized Buttons:</h3>
<div className="flex gap-4 items-center">
<Button size="small">Small Button</Button>
<Button size="medium">Medium Button</Button>
<Button size="large">Large Button</Button>
</div>
<h3>Icon Buttons:</h3>
<div className="flex gap-4 items-center">
<Button iconOnly endIcon={<span></span>} size="small" />
<Button iconOnly endIcon={<span></span>} size="medium" />
<Button iconOnly endIcon={<span></span>} size="large" />
<Button iconOnly endIcon={<span></span>} disabled size="medium" />
<Button iconOnly endIcon={<span></span>} loading size="medium" />
</div>
</div>
</main> </main>
); );
} }
+17
View File
@@ -0,0 +1,17 @@
// ... existing imports ...
async function getData() {
// Simulate a delay to show server-side data fetching
await new Promise((resolve) => setTimeout(resolve, 1000));
return { message: 'This data was fetched on the server!' };
}
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
</main>
);
}
+17
View File
@@ -0,0 +1,17 @@
// ... existing imports ...
async function getData() {
// Simulate a delay to show server-side data fetching
await new Promise((resolve) => setTimeout(resolve, 1000));
return { message: 'This data was fetched on the server!' };
}
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
</main>
);
}
+5
View File
@@ -0,0 +1,5 @@
import { quickLinks } from './quickLinks.const';
export const footerMenu = {
quickLinks,
};
@@ -0,0 +1,7 @@
import { appConstants } from '..';
export const quickLinks = [
{
title: appConstants.routeFactory.home.title,
},
] as any[];
+7
View File
@@ -0,0 +1,7 @@
import { footerMenu } from './footerMenu';
import routeFactory from './routeFactory';
export const appConstants = {
routeFactory,
footerMenu,
};
@@ -0,0 +1,13 @@
import { useTranslation } from 'react-i18next';
const { t } = useTranslation('routes');
export default {
home: {
title: t('home'),
route: () => '/',
},
about: {
title: t('about'),
route: () => '/about',
},
};
+19
View File
@@ -0,0 +1,19 @@
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;