From b5af75b4d59973ce6b41f2c5952d72ad15c16e7a Mon Sep 17 00:00:00 2001 From: Ando <65276708+andostronaut@users.noreply.github.com> Date: Wed, 7 Aug 2024 22:00:43 +0300 Subject: [PATCH] feat: translate resume and setup intl --- app/{ => [locale]}/blog/[slug]/page.tsx | 0 app/{ => [locale]}/blog/page.tsx | 0 app/[locale]/layout.tsx | 84 ++++++++++ app/{ => [locale]}/page.tsx | 26 +-- app/layout.tsx | 71 ++------ app/not-found.tsx | 2 +- bun.lockb | Bin 221616 -> 231184 bytes components/locale-toggle.tsx | 60 +++++++ components/magicui/dock.tsx | 4 +- components/navbar.tsx | 12 ++ components/ui/dropdown-menu.tsx | 205 ++++++++++++++++++++++++ data/resume.tsx | 14 +- locales/en.ts | 17 ++ locales/fr.ts | 17 ++ locales/lib/client.ts | 14 ++ locales/lib/server.ts | 7 + middleware.ts | 15 ++ package.json | 2 + 18 files changed, 475 insertions(+), 75 deletions(-) rename app/{ => [locale]}/blog/[slug]/page.tsx (100%) rename app/{ => [locale]}/blog/page.tsx (100%) create mode 100644 app/[locale]/layout.tsx rename app/{ => [locale]}/page.tsx (84%) create mode 100644 components/locale-toggle.tsx create mode 100644 components/ui/dropdown-menu.tsx create mode 100644 locales/en.ts create mode 100644 locales/fr.ts create mode 100644 locales/lib/client.ts create mode 100644 locales/lib/server.ts create mode 100644 middleware.ts diff --git a/app/blog/[slug]/page.tsx b/app/[locale]/blog/[slug]/page.tsx similarity index 100% rename from app/blog/[slug]/page.tsx rename to app/[locale]/blog/[slug]/page.tsx diff --git a/app/blog/page.tsx b/app/[locale]/blog/page.tsx similarity index 100% rename from app/blog/page.tsx rename to app/[locale]/blog/page.tsx diff --git a/app/[locale]/layout.tsx b/app/[locale]/layout.tsx new file mode 100644 index 0000000..67ae138 --- /dev/null +++ b/app/[locale]/layout.tsx @@ -0,0 +1,84 @@ +import type { Metadata } from 'next' + +import Navbar from '@/components/navbar' +import { ThemeProvider } from '@/components/theme-provider' +import { TooltipProvider } from '@/components/ui/tooltip' + +import { I18nProviderClient } from '@/locales/lib/client' + +import { DATA } from '@/data/resume' + +type MetadataProps = { + params: { locale: string } +} + +export const generateMetadata = async ({ + params, +}: MetadataProps): Promise => { + const locale = params.locale + + return { + metadataBase: new URL(DATA.url), + title: { + default: DATA.name, + template: `%s | ${DATA.name}`, + }, + description: DATA.description, + keywords: DATA.keywords, + openGraph: { + title: `${DATA.name}`, + description: DATA.description, + url: DATA.url, + siteName: `${DATA.name}`, + locale: locale, + type: 'website', + }, + robots: { + index: true, + follow: true, + googleBot: { + index: true, + follow: true, + 'max-video-preview': -1, + 'max-image-preview': 'large', + 'max-snippet': -1, + }, + }, + twitter: { + title: `${DATA.name}`, + card: 'summary_large_image', + }, + verification: { + google: '', + yandex: '', + }, + } +} + +const SubLayout = ({ + children, + params: { locale }, +}: Readonly<{ + children: React.ReactNode + params: { locale: string } +}>) => { + return ( + + + +
+ {children} +
+ +
+
+
+ ) +} + +export default SubLayout diff --git a/app/page.tsx b/app/[locale]/page.tsx similarity index 84% rename from app/page.tsx rename to app/[locale]/page.tsx index 9cf5bb9..e536019 100755 --- a/app/page.tsx +++ b/app/[locale]/page.tsx @@ -5,13 +5,17 @@ import BlurFade from '@/components/magicui/blur-fade' import BlurFadeText from '@/components/magicui/blur-fade-text' import { ProjectCard } from '@/components/project-card' +import { getScopedI18n } from '@/locales/lib/server' + import { DATA } from '@/data/resume' const BLUR_FADE_DELAY = 0.04 -export default function Page() { +export default async function Page() { + const translate = await getScopedI18n('global') + return ( -
+
@@ -32,7 +36,7 @@ export default function Page() {
- {DATA.description} + {translate(DATA.description as keyof typeof translate)}
@@ -42,12 +46,10 @@ export default function Page() {

- Explore my latest creations and open-source projects. + {translate('projects.intro')}

- I've participated in the conception and development of a - range of projects, including libraries, npm packages, and - complex web applications. Here are a few of my favorites. + {translate('projects.about')}

@@ -79,22 +81,22 @@ export default function Page() {

- Get in Touch + {translate('contact.get_in_touch')}

- Want to connect? Send me a + {translate('contact.want')} - direct message on X + {translate('contact.direct_message')} - , and I'll make sure to get back to you. + {translate('contact.back')}

-
+ ) } diff --git a/app/layout.tsx b/app/layout.tsx index caa6736..b1d30ca 100755 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,77 +1,34 @@ -import './globals.css' +import '@/app/globals.css' -import type { Metadata } from 'next' -import { Inter as FontSans } from 'next/font/google' - -import Navbar from '@/components/navbar' -import { ThemeProvider } from '@/components/theme-provider' -import { TooltipProvider } from '@/components/ui/tooltip' - -import { DATA } from '@/data/resume' +import { Inter } from 'next/font/google' +import { cookies } from 'next/headers' import { cn } from '@/lib/utils' -const fontSans = FontSans({ +const inter = Inter({ subsets: ['latin'], variable: '--font-sans', }) -export const metadata: Metadata = { - metadataBase: new URL(DATA.url), - title: { - default: DATA.name, - template: `%s | ${DATA.name}`, - }, - description: DATA.description, - openGraph: { - title: `${DATA.name}`, - description: DATA.description, - url: DATA.url, - siteName: `${DATA.name}`, - locale: 'en_US', - type: 'website', - }, - robots: { - index: true, - follow: true, - googleBot: { - index: true, - follow: true, - 'max-video-preview': -1, - 'max-image-preview': 'large', - 'max-snippet': -1, - }, - }, - twitter: { - title: `${DATA.name}`, - card: 'summary_large_image', - }, - verification: { - google: '', - yandex: '', - }, -} - -export default function RootLayout({ +const RootLayout = ({ children, }: Readonly<{ children: React.ReactNode -}>) { +}>) => { + const locale = cookies().get('Next-Locale')?.value || 'en' + return ( - + - - - {children} - - - + {children} ) } + +export default RootLayout diff --git a/app/not-found.tsx b/app/not-found.tsx index bdb5ac6..44d8c8a 100644 --- a/app/not-found.tsx +++ b/app/not-found.tsx @@ -15,7 +15,7 @@ export default function NotFound() { 404 - Page Not Found

- The page you re looking for doesn t exist or has been moved. + {`The page you re looking for doesn't exist or has been moved.`}

+ + + {locales.map((locale) => ( + changeLocale(locale.value as typeof currentLocale)} + disabled={locale.value === currentLocale} + > + {locale.name} + {locale.value === currentLocale ? ( + + + + ) : null} + + ))} + + + ) +} diff --git a/components/magicui/dock.tsx b/components/magicui/dock.tsx index cf4c37e..d5d68f5 100755 --- a/components/magicui/dock.tsx +++ b/components/magicui/dock.tsx @@ -17,7 +17,7 @@ const DEFAULT_MAGNIFICATION = 60 const DEFAULT_DISTANCE = 140 const dockVariants = cva( - 'mx-auto w-max h-full p-2 flex items-end rounded-full border' + 'mx-auto w-max h-full p-2 flex items-end rounded-xl border' ) const Dock = React.forwardRef( @@ -105,7 +105,7 @@ const DockIcon = ({ ref={ref} style={{ width }} className={cn( - 'flex aspect-square cursor-pointer items-center justify-center rounded-full', + 'flex aspect-square cursor-pointer items-center justify-center rounded-xl', className )} {...props} diff --git a/components/navbar.tsx b/components/navbar.tsx index 17690f2..ebef724 100755 --- a/components/navbar.tsx +++ b/components/navbar.tsx @@ -13,6 +13,7 @@ import { import { DATA } from '@/data/resume' import { cn } from '@/lib/utils' +import { LocalToggle } from './locale-toggle' export default function Navbar() { return ( @@ -65,6 +66,17 @@ export default function Navbar() { ))} + + + + + + +

Locale

+
+
+
+ diff --git a/components/ui/dropdown-menu.tsx b/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..242b07a --- /dev/null +++ b/components/ui/dropdown-menu.tsx @@ -0,0 +1,205 @@ +"use client" + +import * as React from "react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { + CheckIcon, + ChevronRightIcon, + DotFilledIcon, +} from "@radix-ui/react-icons" + +import { cn } from "@/lib/utils" + +const DropdownMenu = DropdownMenuPrimitive.Root + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger + +const DropdownMenuGroup = DropdownMenuPrimitive.Group + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal + +const DropdownMenuSub = DropdownMenuPrimitive.Sub + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +DropdownMenuShortcut.displayName = "DropdownMenuShortcut" + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +} diff --git a/data/resume.tsx b/data/resume.tsx index d8fe720..713731e 100755 --- a/data/resume.tsx +++ b/data/resume.tsx @@ -9,8 +9,16 @@ export const DATA = { url: 'https://andostronaut.com', location: 'Antananarivo, MG', locationLink: 'https://www.google.com/maps/place/antananarivo', - description: - "I'm a software developer, open source enthusiast, and community builder. My experience includes leading workshops on AI, showcasing my versatility and commitment to the tech field.", + description: 'description', + keywords: [ + 'Open Source', + 'Developer', + 'Open-Sourcer', + 'OSS', + 'Contributor', + 'Software Developer', + 'Software Engineer', + ], navbar: [ { href: '/', icon: HomeIcon, label: 'Home' }, { href: '#', icon: CodeIcon, label: 'Projects' }, @@ -136,4 +144,4 @@ export const DATA = { video: '', }, ], -} as const +} diff --git a/locales/en.ts b/locales/en.ts new file mode 100644 index 0000000..c6631a6 --- /dev/null +++ b/locales/en.ts @@ -0,0 +1,17 @@ +export default { + global: { + description: + "I'm a software developer, open source enthusiast, and community builder. My experience includes leading workshops on AI, showcasing my versatility and commitment to the tech field.", + projects: { + intro: 'Explore my latest creations and open-source projects.', + about: + "I've participated in the conception and development of a range of projects, including libraries, npm packages, and complex web applications. Here are a few of my favorites.", + }, + contact: { + get_in_touch: 'Get in Touch', + want: 'Want to connect? Send me a', + direct_message: 'direct message on X', + back: ", and I'll make sure to get back to you.", + }, + }, +} as const diff --git a/locales/fr.ts b/locales/fr.ts new file mode 100644 index 0000000..4d392f4 --- /dev/null +++ b/locales/fr.ts @@ -0,0 +1,17 @@ +export default { + global: { + description: + "Je suis un développeur de logiciels, un passionné de l'open source et un créateur de communauté. Mon expérience comprend l'animation d'ateliers sur l'IA, démontrant ainsi ma polyvalence et mon engagement dans le domaine de la technologie.", + projects: { + intro: 'Explorez mes dernières créations et projets open source.', + about: + "J'ai participé à la conception et au développement d'une gamme de projets, y compris des bibliothèques, des packages npm et des applications web complexes. Voici quelques-uns de mes favoris.", + }, + contact: { + get_in_touch: 'Prenez contact', + want: 'Envie de me contacter ? Envoyez-moi un', + direct_message: 'message direct sur X', + back: ', et je veillerai à vous répondre.', + }, + }, +} as const diff --git a/locales/lib/client.ts b/locales/lib/client.ts new file mode 100644 index 0000000..918322e --- /dev/null +++ b/locales/lib/client.ts @@ -0,0 +1,14 @@ +'use client' + +import { createI18nClient } from 'next-international/client' + +export const { + useI18n, + useScopedI18n, + I18nProviderClient, + useCurrentLocale, + useChangeLocale, +} = createI18nClient({ + en: () => import('../en'), + fr: () => import('../fr'), +}) diff --git a/locales/lib/server.ts b/locales/lib/server.ts new file mode 100644 index 0000000..c3dc87d --- /dev/null +++ b/locales/lib/server.ts @@ -0,0 +1,7 @@ +import { createI18nServer } from 'next-international/server' + +export const { getI18n, getScopedI18n, getStaticParams, getCurrentLocale } = + createI18nServer({ + en: () => import('../en'), + fr: () => import('../fr'), + }) diff --git a/middleware.ts b/middleware.ts new file mode 100644 index 0000000..9dfa2bf --- /dev/null +++ b/middleware.ts @@ -0,0 +1,15 @@ +import { createI18nMiddleware } from 'next-international/middleware' +import { type NextRequest } from 'next/server' + +const I18nMiddleware = createI18nMiddleware({ + locales: ['en', 'fr'], + defaultLocale: 'en', +}) + +export function middleware(request: NextRequest) { + return I18nMiddleware(request) +} + +export const config = { + matcher: ['/((?!api|static|.*\\..*|_next|favicon.ico).*)'], +} diff --git a/package.json b/package.json index 94ddf0f..3abc56d 100755 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@radix-ui/react-avatar": "^1.0.4", + "@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-slot": "^1.0.2", @@ -21,6 +22,7 @@ "gray-matter": "^4.0.3", "lucide-react": "^0.395.0", "next": "14.2.4", + "next-international": "^1.2.4", "next-themes": "^0.3.0", "react": "^18", "react-dom": "^18",