app/admin/dashboard.tsx | 7 ++++-- app/layout.tsx | 7 +++++- app/not-found.tsx | 4 +++- app/page.tsx | 6 +++-- app/s/[subdomain]/page.tsx | 4 +++- app/subdomain-form.tsx | 11 ++++++---- components/ui/dialog.tsx | 4 +++- components/ui/emoji-picker.tsx | 4 +++- i18n/request.ts | 11 ++++++++++ messages/en.json | 26 ++++++++++++++++++++++ messages/ja.json | 26 ++++++++++++++++++++++ messages/translation-todo.json | 50 ++++++++++++++++++++++++++++++++++++++++++ next.config.ts | 5 ++++- package.json | 3 ++- 14 files changed, 153 insertions(+), 15 deletions(-) diff --git a/app/admin/dashboard.tsx b/app/admin/dashboard.tsx index 047b49f..2692c97 100644 --- a/app/admin/dashboard.tsx +++ b/app/admin/dashboard.tsx @@ -1,4 +1,5 @@ 'use client'; +import { useTranslations } from 'next-intl'; import { useActionState } from 'react'; import { Button } from '@/components/ui/button'; @@ -20,11 +21,12 @@ type DeleteState = { }; function DashboardHeader() { + const t = useTranslations(); // TODO: You can add authentication here with your preferred auth provider return (
-

Subdomain Management

+

{t('dashboard.subdomain_management')}

void; isPending: boolean; }) { + const t = useTranslations(); if (tenants.length === 0) { return ( -

No subdomains have been created yet.

+

{t('dashboard.no_subdomains_have_been_created_yet')}

); diff --git a/app/layout.tsx b/app/layout.tsx index 0aa9de8..390e7fa 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,3 +1,5 @@ +import { NextIntlClientProvider } from 'next-intl'; +import { getMessages } from 'next-intl/server'; import type { Metadata } from 'next'; import { Geist } from 'next/font/google'; import { SpeedInsights } from '@vercel/speed-insights/next'; @@ -13,16 +15,19 @@ export const metadata: Metadata = { description: 'Next.js template for building a multi-tenant SaaS.' }; -export default function RootLayout({ +export default async function RootLayout({ children }: Readonly<{ children: React.ReactNode; }>) { + const messages = await getMessages(); return ( + {children} + ); diff --git a/app/not-found.tsx b/app/not-found.tsx index 3bdb1ba..83febdb 100644 --- a/app/not-found.tsx +++ b/app/not-found.tsx @@ -1,4 +1,5 @@ 'use client'; +import { useTranslations } from 'next-intl'; import Link from 'next/link'; import { useEffect, useState } from 'react'; @@ -6,6 +7,7 @@ import { usePathname } from 'next/navigation'; import { rootDomain, protocol } from '@/lib/utils'; export default function NotFound() { + const t = useTranslations(); const [subdomain, setSubdomain] = useState(null); const pathname = usePathname(); @@ -40,7 +42,7 @@ export default function NotFound() { )}

- This subdomain hasn't been created yet. + {t('not_found.this_subdomain_hasnt_been_created_yet')}

@@ -10,7 +12,7 @@ export default async function HomePage() { href="/admin" className="text-sm text-gray-500 hover:text-gray-700 transition-colors" > - Admin + {t('page.admin')}
@@ -20,7 +22,7 @@ export default async function HomePage() { {rootDomain}

- Create your own subdomain with a custom emoji + {t('page.create_your_own_subdomain_with_a_custom')}

diff --git a/app/s/[subdomain]/page.tsx b/app/s/[subdomain]/page.tsx index be83a29..b271349 100644 --- a/app/s/[subdomain]/page.tsx +++ b/app/s/[subdomain]/page.tsx @@ -1,3 +1,4 @@ +import { getTranslations } from 'next-intl/server'; import Link from 'next/link'; import type { Metadata } from 'next'; import { notFound } from 'next/navigation'; @@ -29,6 +30,7 @@ export default async function SubdomainPage({ }: { params: Promise<{ subdomain: string }>; }) { + const t = await getTranslations(); const { subdomain } = await params; const subdomainData = await getSubdomainData(subdomain); @@ -54,7 +56,7 @@ export default async function SubdomainPage({ Welcome to {subdomain}.{rootDomain}

- This is your custom subdomain page + {t('page.this_is_your_custom_subdomain_page')}

diff --git a/app/subdomain-form.tsx b/app/subdomain-form.tsx index e26371a..4983920 100644 --- a/app/subdomain-form.tsx +++ b/app/subdomain-form.tsx @@ -1,4 +1,5 @@ 'use client'; +import { useTranslations } from 'next-intl'; import type React from 'react'; @@ -31,15 +32,16 @@ type CreateState = { }; function SubdomainInput({ defaultValue }: { defaultValue?: string }) { + const t = useTranslations(); return (
- +
void; defaultValue?: string; }) { + const t = useTranslations(); const [isPickerOpen, setIsPickerOpen] = useState(false); const handleEmojiSelect = ({ emoji }: { emoji: string }) => { @@ -71,7 +74,7 @@ function IconPicker({ return (
- +
@@ -117,7 +120,7 @@ function IconPicker({

- Select an emoji to represent your subdomain + {t('subdomain_form.select_an_emoji_to_represent_your_subdom')}

diff --git a/components/ui/dialog.tsx b/components/ui/dialog.tsx index 7d7a9d3..90c5969 100644 --- a/components/ui/dialog.tsx +++ b/components/ui/dialog.tsx @@ -1,4 +1,5 @@ "use client" +import { useTranslations } from 'next-intl'; import * as React from "react" import * as DialogPrimitive from "@radix-ui/react-dialog" @@ -51,6 +52,7 @@ function DialogContent({ children, ...props }: React.ComponentProps) { + const t = useTranslations(); return ( @@ -65,7 +67,7 @@ function DialogContent({ {children} - Close + {t('dialog.close')} diff --git a/components/ui/emoji-picker.tsx b/components/ui/emoji-picker.tsx index 56042d2..6518c5e 100644 --- a/components/ui/emoji-picker.tsx +++ b/components/ui/emoji-picker.tsx @@ -1,4 +1,5 @@ "use client"; +import { useTranslations } from 'next-intl'; import { type EmojiPickerListCategoryHeaderProps, @@ -92,6 +93,7 @@ function EmojiPickerContent({ className, ...props }: React.ComponentProps) { + const t = useTranslations(); return ( - No emoji found. + {t('emoji_picker.no_emoji_found')} { + const locale = 'en'; + return { + locale, + messages: (await import(`../messages/${locale}.json`)).default, + }; +}); diff --git a/messages/en.json b/messages/en.json new file mode 100644 index 0000000..b3d48ae --- /dev/null +++ b/messages/en.json @@ -0,0 +1,26 @@ +{ + "dashboard": { + "no_subdomains_have_been_created_yet": "No subdomains have been created yet.", + "subdomain_management": "Subdomain Management" + }, + "dialog": { + "close": "Close" + }, + "emoji_picker": { + "no_emoji_found": "No emoji found." + }, + "not_found": { + "this_subdomain_hasnt_been_created_yet": "This subdomain hasn't been created yet." + }, + "page": { + "admin": "Admin", + "create_your_own_subdomain_with_a_custom": "Create your own subdomain with a custom emoji", + "this_is_your_custom_subdomain_page": "This is your custom subdomain page" + }, + "subdomain_form": { + "icon": "Icon", + "select_an_emoji_to_represent_your_subdom": "Select an emoji to represent your subdomain", + "subdomain": "Subdomain", + "your_subdomain": "your-subdomain" + } +} diff --git a/messages/ja.json b/messages/ja.json new file mode 100644 index 0000000..b3d48ae --- /dev/null +++ b/messages/ja.json @@ -0,0 +1,26 @@ +{ + "dashboard": { + "no_subdomains_have_been_created_yet": "No subdomains have been created yet.", + "subdomain_management": "Subdomain Management" + }, + "dialog": { + "close": "Close" + }, + "emoji_picker": { + "no_emoji_found": "No emoji found." + }, + "not_found": { + "this_subdomain_hasnt_been_created_yet": "This subdomain hasn't been created yet." + }, + "page": { + "admin": "Admin", + "create_your_own_subdomain_with_a_custom": "Create your own subdomain with a custom emoji", + "this_is_your_custom_subdomain_page": "This is your custom subdomain page" + }, + "subdomain_form": { + "icon": "Icon", + "select_an_emoji_to_represent_your_subdom": "Select an emoji to represent your subdomain", + "subdomain": "Subdomain", + "your_subdomain": "your-subdomain" + } +} diff --git a/messages/translation-todo.json b/messages/translation-todo.json new file mode 100644 index 0000000..744411f --- /dev/null +++ b/messages/translation-todo.json @@ -0,0 +1,50 @@ +[ + { + "key": "dashboard.no_subdomains_have_been_created_yet", + "en": "No subdomains have been created yet." + }, + { + "key": "dashboard.subdomain_management", + "en": "Subdomain Management" + }, + { + "key": "dialog.close", + "en": "Close" + }, + { + "key": "emoji_picker.no_emoji_found", + "en": "No emoji found." + }, + { + "key": "not_found.this_subdomain_hasnt_been_created_yet", + "en": "This subdomain hasn't been created yet." + }, + { + "key": "page.admin", + "en": "Admin" + }, + { + "key": "page.create_your_own_subdomain_with_a_custom", + "en": "Create your own subdomain with a custom emoji" + }, + { + "key": "page.this_is_your_custom_subdomain_page", + "en": "This is your custom subdomain page" + }, + { + "key": "subdomain_form.icon", + "en": "Icon" + }, + { + "key": "subdomain_form.select_an_emoji_to_represent_your_subdom", + "en": "Select an emoji to represent your subdomain" + }, + { + "key": "subdomain_form.subdomain", + "en": "Subdomain" + }, + { + "key": "subdomain_form.your_subdomain", + "en": "your-subdomain" + } +] diff --git a/next.config.ts b/next.config.ts index 9b765b9..43a313b 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,3 +1,6 @@ +import createNextIntlPlugin from 'next-intl/plugin'; +const withNextIntl = createNextIntlPlugin(); + import type { NextConfig } from "next"; const nextConfig: NextConfig = { @@ -21,4 +24,4 @@ const nextConfig: NextConfig = { // }, }; -export default nextConfig; +export default withNextIntl(nextConfig); diff --git a/package.json b/package.json index e2b0897..d2b0ea1 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,8 @@ "next": "^15.3.6", "react": "^19.1.0", "react-dom": "^19.1.0", - "tailwind-merge": "^3.3.0" + "tailwind-merge": "^3.3.0", + "next-intl": "^4.0.0" }, "devDependencies": { "@tailwindcss/postcss": "^4.1.6",