From 6d2885de5934565e9e7c6f6cf6529713ae73b8cf Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Sun, 28 Apr 2024 22:56:41 +0300 Subject: [PATCH] i10n using NextIntl --- components/languageSwitcher.tsx | 24 +++++ components/layout.tsx | 3 +- components/sidebar.tsx | 2 + content/i18n/bg.json | 10 ++ content/i18n/en.json | 10 ++ content/i18n/ru.json | 10 ++ next.config.js | 4 + package-lock.json | 147 ++++++++++++++++++++++++++ package.json | 1 + pages/_app.tsx | 66 ++++++++++-- pages/api/translations/[...locale].ts | 37 +++++++ pages/dash.tsx | 1 + 12 files changed, 304 insertions(+), 11 deletions(-) create mode 100644 components/languageSwitcher.tsx create mode 100644 content/i18n/bg.json create mode 100644 content/i18n/en.json create mode 100644 content/i18n/ru.json create mode 100644 pages/api/translations/[...locale].ts diff --git a/components/languageSwitcher.tsx b/components/languageSwitcher.tsx new file mode 100644 index 0000000..e4c2663 --- /dev/null +++ b/components/languageSwitcher.tsx @@ -0,0 +1,24 @@ +import { useRouter } from 'next/router'; +import { useTranslations } from 'next-intl'; + +// using https://next-intl-docs.vercel.app/docs/getting-started/pages-router +const LanguageSwitcher = () => { + const t = useTranslations('common'); + const router = useRouter(); + const { locale, locales, asPath } = router; + + return ( +
+ {locales.map((lng) => { + if (lng === locale) return null; + return ( + + ); + })} +
+ ); +}; + +export default LanguageSwitcher; \ No newline at end of file diff --git a/components/layout.tsx b/components/layout.tsx index b1c29fe..7c5639b 100644 --- a/components/layout.tsx +++ b/components/layout.tsx @@ -68,4 +68,5 @@ export default function Layout({ children }) { ); -} \ No newline at end of file +} + diff --git a/components/sidebar.tsx b/components/sidebar.tsx index acd412c..1f562d8 100644 --- a/components/sidebar.tsx +++ b/components/sidebar.tsx @@ -5,6 +5,7 @@ import { useRouter } from 'next/router'; import sidemenu, { footerMenu } from './sidemenuData.js'; // Move sidemenu data to a separate file import axiosInstance from "src/axiosSecure"; import common from "src/helpers/common"; +import LanguageSwitcher from "./languageSwitcher"; //get package version from package.json const packageVersion = require('../package.json').version; @@ -145,6 +146,7 @@ export default function Sidebar({ isSidebarOpen, toggleSidebar }) {

+
diff --git a/content/i18n/bg.json b/content/i18n/bg.json new file mode 100644 index 0000000..84dfa1c --- /dev/null +++ b/content/i18n/bg.json @@ -0,0 +1,10 @@ +{ + "common": { + "greeting": "Здравей", + "farewell": "Довиждане", + "changeTo": "Смени на", + "BG": "Български", + "EN": "Английски", + "RU": "Руски" + } +} \ No newline at end of file diff --git a/content/i18n/en.json b/content/i18n/en.json new file mode 100644 index 0000000..59f2c2e --- /dev/null +++ b/content/i18n/en.json @@ -0,0 +1,10 @@ +{ + "common": { + "greeting": "Hello", + "farewell": "Goodbye", + "changeTo": "Change to", + "BG": "Bulgarian", + "EN": "English", + "RU": "Russian" + } +} \ No newline at end of file diff --git a/content/i18n/ru.json b/content/i18n/ru.json new file mode 100644 index 0000000..8f75c05 --- /dev/null +++ b/content/i18n/ru.json @@ -0,0 +1,10 @@ +{ + "common": { + "greeting": "Здравей", + "farewell": "Довиждане", + "changeTo": "Смени на", + "BG": "[RU] Български", + "EN": "[RU] Английски", + "RU": "[RU] Руски" + } +} \ No newline at end of file diff --git a/next.config.js b/next.config.js index 2406980..ac0f205 100644 --- a/next.config.js +++ b/next.config.js @@ -50,4 +50,8 @@ module.exports = withPWA({ return config; }, + i18n: { // using https://next-intl-docs.vercel.app/docs/getting-started/pages-router + locales: ['bg', 'en', 'ru'], + defaultLocale: 'bg', + }, }) \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 61a5430..28c3295 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55,6 +55,7 @@ "next": "^14.1.0", "next-auth": "^4.24.6", "next-connect": "^1.0.0", + "next-intl": "^3.12.0", "next-pwa": "^5.6.0", "node-excel-export": "^1.4.4", "node-telegram-bot-api": "^0.64.0", @@ -2194,6 +2195,92 @@ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz", "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==" }, + "node_modules/@formatjs/ecma402-abstract": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.18.2.tgz", + "integrity": "sha512-+QoPW4csYALsQIl8GbN14igZzDbuwzcpWrku9nyMXlaqAlwRBgl5V+p0vWMGFqHOw37czNXaP/lEk4wbLgcmtA==", + "dependencies": { + "@formatjs/intl-localematcher": "0.5.4", + "tslib": "^2.4.0" + } + }, + "node_modules/@formatjs/ecma402-abstract/node_modules/@formatjs/intl-localematcher": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.4.tgz", + "integrity": "sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@formatjs/fast-memoize": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-1.2.1.tgz", + "integrity": "sha512-Rg0e76nomkz3vF9IPlKeV+Qynok0r7YZjL6syLz4/urSg0IbjPZCB/iYUMNsYA643gh4mgrX3T7KEIFIxJBQeg==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.1.0.tgz", + "integrity": "sha512-Qxv/lmCN6hKpBSss2uQ8IROVnta2r9jd3ymUEIjm2UyIkUCHVcbUVRGL/KS/wv7876edvsPe+hjHVJ4z8YuVaw==", + "dependencies": { + "@formatjs/ecma402-abstract": "1.11.4", + "@formatjs/icu-skeleton-parser": "1.3.6", + "tslib": "^2.1.0" + } + }, + "node_modules/@formatjs/icu-messageformat-parser/node_modules/@formatjs/ecma402-abstract": { + "version": "1.11.4", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.4.tgz", + "integrity": "sha512-EBikYFp2JCdIfGEb5G9dyCkTGDmC57KSHhRQOC3aYxoPWVZvfWCDjZwkGYHN7Lis/fmuWl906bnNTJifDQ3sXw==", + "dependencies": { + "@formatjs/intl-localematcher": "0.2.25", + "tslib": "^2.1.0" + } + }, + "node_modules/@formatjs/icu-messageformat-parser/node_modules/@formatjs/intl-localematcher": { + "version": "0.2.25", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.25.tgz", + "integrity": "sha512-YmLcX70BxoSopLFdLr1Ds99NdlTI2oWoLbaUW2M406lxOIPzE1KQhRz2fPUkq34xVZQaihCoU29h0KK7An3bhA==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.3.6.tgz", + "integrity": "sha512-I96mOxvml/YLrwU2Txnd4klA7V8fRhb6JG/4hm3VMNmeJo1F03IpV2L3wWt7EweqNLES59SZ4d6hVOPCSf80Bg==", + "dependencies": { + "@formatjs/ecma402-abstract": "1.11.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@formatjs/icu-skeleton-parser/node_modules/@formatjs/ecma402-abstract": { + "version": "1.11.4", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.4.tgz", + "integrity": "sha512-EBikYFp2JCdIfGEb5G9dyCkTGDmC57KSHhRQOC3aYxoPWVZvfWCDjZwkGYHN7Lis/fmuWl906bnNTJifDQ3sXw==", + "dependencies": { + "@formatjs/intl-localematcher": "0.2.25", + "tslib": "^2.1.0" + } + }, + "node_modules/@formatjs/icu-skeleton-parser/node_modules/@formatjs/intl-localematcher": { + "version": "0.2.25", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.25.tgz", + "integrity": "sha512-YmLcX70BxoSopLFdLr1Ds99NdlTI2oWoLbaUW2M406lxOIPzE1KQhRz2fPUkq34xVZQaihCoU29h0KK7An3bhA==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@formatjs/intl-localematcher": { + "version": "0.2.32", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.32.tgz", + "integrity": "sha512-k/MEBstff4sttohyEpXxCmC3MqbUn9VvHGlZ8fauLzkbwXmVrEeyzS+4uhrvAk9DWU9/7otYWxyDox4nT/KVLQ==", + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@heroicons/react": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.1.1.tgz", @@ -8947,6 +9034,34 @@ "node": ">= 0.4" } }, + "node_modules/intl-messageformat": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-9.13.0.tgz", + "integrity": "sha512-7sGC7QnSQGa5LZP7bXLDhVDtQOeKGeBFGHF2Y8LVBwYZoQZCgWeKoPGTa5GMG8g/TzDgeXuYJQis7Ggiw2xTOw==", + "dependencies": { + "@formatjs/ecma402-abstract": "1.11.4", + "@formatjs/fast-memoize": "1.2.1", + "@formatjs/icu-messageformat-parser": "2.1.0", + "tslib": "^2.1.0" + } + }, + "node_modules/intl-messageformat/node_modules/@formatjs/ecma402-abstract": { + "version": "1.11.4", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.4.tgz", + "integrity": "sha512-EBikYFp2JCdIfGEb5G9dyCkTGDmC57KSHhRQOC3aYxoPWVZvfWCDjZwkGYHN7Lis/fmuWl906bnNTJifDQ3sXw==", + "dependencies": { + "@formatjs/intl-localematcher": "0.2.25", + "tslib": "^2.1.0" + } + }, + "node_modules/intl-messageformat/node_modules/@formatjs/intl-localematcher": { + "version": "0.2.25", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.25.tgz", + "integrity": "sha512-YmLcX70BxoSopLFdLr1Ds99NdlTI2oWoLbaUW2M406lxOIPzE1KQhRz2fPUkq34xVZQaihCoU29h0KK7An3bhA==", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -10681,6 +10796,26 @@ "node": ">=16" } }, + "node_modules/next-intl": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/next-intl/-/next-intl-3.12.0.tgz", + "integrity": "sha512-N3DHT6ce6K4VHVA3y2p3U7wfBx4c31qEgQSTFCFJuNnE7XYzy49O576ewEz7/k2YaB/U5bfxaWWaMMkskofwoQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/amannn" + } + ], + "dependencies": { + "@formatjs/intl-localematcher": "^0.2.32", + "negotiator": "^0.6.3", + "use-intl": "^3.12.0" + }, + "peerDependencies": { + "next": "^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/next-pwa": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/next-pwa/-/next-pwa-5.6.0.tgz", @@ -17428,6 +17563,18 @@ "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==" }, + "node_modules/use-intl": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-3.12.0.tgz", + "integrity": "sha512-tTJBSaxpVF1ZHqJ5+1JOaBsPmvBPscXHR0soMNQFWIURZspOueLaMXx1XHNdBv9KZGwepBju5aWXkJ0PM6ztXg==", + "dependencies": { + "@formatjs/ecma402-abstract": "^1.11.4", + "intl-messageformat": "^9.3.18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/package.json b/package.json index 6ff1a1e..f952b7d 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "next": "^14.1.0", "next-auth": "^4.24.6", "next-connect": "^1.0.0", + "next-intl": "^3.12.0", "next-pwa": "^5.6.0", "node-excel-export": "^1.4.4", "node-telegram-bot-api": "^0.64.0", diff --git a/pages/_app.tsx b/pages/_app.tsx index 51e37c1..084c2db 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -6,7 +6,10 @@ import "tailwindcss/tailwind.css" import type { AppProps } from "next/app"; import type { Session } from "next-auth"; -import { useEffect } from "react" +import { useEffect, useState } from "react" +import { useRouter } from "next/router"; +import { NextIntlClientProvider } from 'next-intl'; + // for fontawesome import Head from 'next/head'; import { LocalizationProvider } from '@mui/x-date-pickers'; @@ -22,10 +25,32 @@ import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs' // } -export default function App({ - Component, - pageProps: { session, ...pageProps }, -}: AppProps<{ session: Session }>) { +export default function App({ Component, pageProps: { session, ...pageProps }, }: AppProps<{ session: Session }>) { + console.log(pageProps); + const router = useRouter(); + const [messages, setMessages] = useState({}); + + useEffect(() => { + console.log("Current locale:", router.locale); + async function loadLocaleData() { + const localeMessages = await import(`../content/i18n/${router.locale}.json`); + console.log("Loaded locale '", router.locale, "' ",); + //console.log("Loaded messages for locale:", router.locale, localeMessages.default); + setMessages(localeMessages.default); + } + loadLocaleData(); + }, [router.locale]); + + // useEffect(() => { + // async function loadLocaleData() { + // const locale = router.locale || 'en'; + // const messages = await fetch(`/api/translations/${locale}`).then(res => res.json()); + // setMessages(messages); + // } + + // loadLocaleData(); + // }, [router.locale]); + useEffect(() => { const use = async () => { @@ -67,11 +92,32 @@ export default function App({ return ( <> - - - - - + + + + + + + ) } + +// build time localization. Is it working for _app.tsx?d +// export async function getStaticProps(context) { +// const messages = (await import(`../content/i18n/${locale}.json`)).default; +// console.log("Loaded messages for locale:", locale, messages); +// return { +// props: { +// // You can get the messages from anywhere you like. The recommended +// // pattern is to put them in JSON files separated by locale and read +// // the desired one based on the `locale` received from Next.js. +// // messages: (await import(`../content/i18n/${context.locale}.json`)).default +// messages +// } +// }; +// } diff --git a/pages/api/translations/[...locale].ts b/pages/api/translations/[...locale].ts new file mode 100644 index 0000000..de67d51 --- /dev/null +++ b/pages/api/translations/[...locale].ts @@ -0,0 +1,37 @@ +import fs from 'fs'; +import path from 'path'; +import { NextApiRequest, NextApiResponse } from 'next'; +type Translations = { + [key: string]: string; +}; +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const { locale } = req.query; + + const filePath = path.join(process.cwd(), `content/i18n/${locale}.json`); + const translations: Translations = JSON.parse(fs.readFileSync(filePath, 'utf8')); + + switch (req.method) { + case 'GET': + try { + res.status(200).json(translations); + } catch (error) { + console.error('Error reading translation file:', error); + res.status(500).json({ error: 'Failed to read translation file' }); + } + break; + + case 'POST': try { + const newTranslations = req.body; + fs.writeFileSync(filePath, JSON.stringify(newTranslations, null, 2), 'utf8'); + res.status(200).json({ status: 'Updated' }); + } catch (error) { + console.error('Error writing translation file:', error); + res.status(500).json({ error: 'Failed to update translation file' }); + } + break; + + default: + res.setHeader('Allow', ['GET', 'POST']); + res.status(405).end(`Method ${req.method} Not Allowed`); + } +} diff --git a/pages/dash.tsx b/pages/dash.tsx index b53afa9..c18aa67 100644 --- a/pages/dash.tsx +++ b/pages/dash.tsx @@ -204,6 +204,7 @@ export const getServerSideProps = async (context) => { props: { initialItems: items, userId: session?.user.id, + // messages: (await import(`../content/i18n/${context.locale}.json`)).default }, }; }