diff --git a/app/(auth)/forgot-password/page.tsx b/app/(auth)/forgot-password/page.tsx index 0093433..3636e46 100644 --- a/app/(auth)/forgot-password/page.tsx +++ b/app/(auth)/forgot-password/page.tsx @@ -1,26 +1,5 @@ -import type { Metadata } from 'next' -import { SITE_NAME } from '@/lib/consts' -import { ForgotPasswordForm } from '@/components/forgot-password-form' -import { Route } from 'next' +import { redirect } from 'next/navigation' -export const metadata: Metadata = { - title: `Forgot Password · ${SITE_NAME}`, -} - -export default async function Page({ - searchParams, -}: { - searchParams: Promise<{ - email?: string - from?: Route - }> -}) { - const { email, from = '/' } = await searchParams - return ( -
-
- -
-
- ) +export default function ForgotPasswordRedirect() { + redirect('/en/forgot-password') } diff --git a/app/(auth)/login/page.tsx b/app/(auth)/login/page.tsx index 44f2530..8c839c2 100644 --- a/app/(auth)/login/page.tsx +++ b/app/(auth)/login/page.tsx @@ -1,27 +1,5 @@ -import { LoginForm } from '@/components/login-form' -import type { Metadata, Route } from 'next' -import { SITE_NAME } from '@/lib/consts' -import { AnimatedBooksBackground } from '@/components/ui/animated-books-background' +import { redirect } from 'next/navigation' -export const metadata: Metadata = { - title: `Login · ${SITE_NAME}`, -} - -export default async function Page({ - searchParams, -}: { - searchParams: Promise<{ - email?: string - from?: Route - }> -}) { - const { email, from = '/' } = await searchParams - return ( -
- -
- -
-
- ) +export default function LoginRedirect() { + redirect('/en/login') } diff --git a/app/(auth)/signup/page.tsx b/app/(auth)/signup/page.tsx index c4fe389..c2a30a0 100644 --- a/app/(auth)/signup/page.tsx +++ b/app/(auth)/signup/page.tsx @@ -1,19 +1,5 @@ -import { SignUpForm } from '@/components/signup-form' -import type { Metadata } from 'next' -import { SITE_NAME } from '@/lib/consts' -import { AnimatedBooksBackground } from '@/components/ui/animated-books-background' +import { redirect } from 'next/navigation' -export const metadata: Metadata = { - title: `Signup · ${SITE_NAME}`, -} - -export default async function Page() { - return ( -
- -
- -
-
- ) +export default function SignupRedirect() { + redirect('/en/signup') } diff --git a/app/[locale]/about/page.tsx b/app/[locale]/about/page.tsx new file mode 100644 index 0000000..281dfc5 --- /dev/null +++ b/app/[locale]/about/page.tsx @@ -0,0 +1,69 @@ +import type { Metadata } from 'next' +import Link from 'next/link' +import { ArrowLeft } from 'lucide-react' +import { notFound } from 'next/navigation' +import { Button } from '@/components/ui/button' +import { + getDictionary, + getMetadataForPage, + isLocale, + localizePath, + type Locale, +} from '@/lib/i18n' + +type Props = { + params: Promise<{ + locale: string + }> +} + +async function getPageLocale(params: Props['params']): Promise { + const { locale } = await params + + if (!isLocale(locale)) { + notFound() + } + + return locale +} + +export async function generateMetadata({ + params, +}: Props): Promise { + const locale = await getPageLocale(params) + const dictionary = await getDictionary(locale) + + return getMetadataForPage(locale, '/about', dictionary.meta.about) +} + +export default async function AboutPage({ params }: Props) { + const locale = await getPageLocale(params) + const dictionary = await getDictionary(locale) + + return ( +
+
+
+ + +
+

+ {dictionary.about.title} +

+
+
+ + {dictionary.about.paragraphs.map((paragraph) => ( +

+ {paragraph} +

+ ))} +
+
+ ) +} diff --git a/app/[locale]/forgot-password/page.tsx b/app/[locale]/forgot-password/page.tsx new file mode 100644 index 0000000..d75fa41 --- /dev/null +++ b/app/[locale]/forgot-password/page.tsx @@ -0,0 +1,62 @@ +import type { Metadata, Route } from 'next' +import { notFound } from 'next/navigation' +import { ForgotPasswordForm } from '@/components/forgot-password-form' +import { + getDictionary, + getMetadataForPage, + isLocale, + type Locale, +} from '@/lib/i18n' + +type Props = { + params: Promise<{ locale: string }> + searchParams: Promise<{ + email?: string + from?: Route + }> +} + +async function getPageLocale(params: Props['params']): Promise { + const { locale } = await params + + if (!isLocale(locale)) { + notFound() + } + + return locale +} + +export async function generateMetadata({ + params, +}: Props): Promise { + const locale = await getPageLocale(params) + const dictionary = await getDictionary(locale) + + return getMetadataForPage( + locale, + '/forgot-password', + dictionary.meta.forgotPassword + ) +} + +export default async function ForgotPasswordPage({ + params, + searchParams, +}: Props) { + const locale = await getPageLocale(params) + const dictionary = await getDictionary(locale) + const { email, from = '/' } = await searchParams + + return ( +
+
+ +
+
+ ) +} diff --git a/app/[locale]/layout.tsx b/app/[locale]/layout.tsx new file mode 100644 index 0000000..6c3c803 --- /dev/null +++ b/app/[locale]/layout.tsx @@ -0,0 +1,25 @@ +import { notFound } from 'next/navigation' +import { isLocale, locales } from '@/lib/i18n' + +type Props = { + children: React.ReactNode + params: Promise<{ + locale: string + }> +} + +export const dynamicParams = false + +export function generateStaticParams() { + return locales.map((locale) => ({ locale })) +} + +export default async function LocaleLayout({ children, params }: Props) { + const { locale } = await params + + if (!isLocale(locale)) { + notFound() + } + + return children +} diff --git a/app/[locale]/login/page.tsx b/app/[locale]/login/page.tsx new file mode 100644 index 0000000..51fd71b --- /dev/null +++ b/app/[locale]/login/page.tsx @@ -0,0 +1,57 @@ +import type { Metadata, Route } from 'next' +import { notFound } from 'next/navigation' +import { AnimatedBooksBackground } from '@/components/ui/animated-books-background' +import { LoginForm } from '@/components/login-form' +import { + getDictionary, + getMetadataForPage, + isLocale, + type Locale, +} from '@/lib/i18n' + +type Props = { + params: Promise<{ locale: string }> + searchParams: Promise<{ + email?: string + from?: Route + }> +} + +async function getPageLocale(params: Props['params']): Promise { + const { locale } = await params + + if (!isLocale(locale)) { + notFound() + } + + return locale +} + +export async function generateMetadata({ + params, +}: Props): Promise { + const locale = await getPageLocale(params) + const dictionary = await getDictionary(locale) + + return getMetadataForPage(locale, '/login', dictionary.meta.login) +} + +export default async function LoginPage({ params, searchParams }: Props) { + const locale = await getPageLocale(params) + const dictionary = await getDictionary(locale) + const { email, from = '/' } = await searchParams + + return ( +
+ +
+ +
+
+ ) +} diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx new file mode 100644 index 0000000..7649938 --- /dev/null +++ b/app/[locale]/page.tsx @@ -0,0 +1,119 @@ +import Link from 'next/link' +import type { Metadata } from 'next' +import { + Library, + Book, + BellIcon, + BookUser, + Ticket, + Settings, + BookCopy, +} from 'lucide-react' +import { notFound } from 'next/navigation' +import { Button } from '@/components/ui/button' +import { IsLoggedIn } from '@/lib/firebase/firebase' +import Landing from '@/components/landing' +import { logoutAction } from '@/lib/actions/logout' +import { ModeToggle } from '@/components/button-toggle-theme' +import { + getDictionary, + getMetadataForPage, + isLocale, + type Locale, +} from '@/lib/i18n' + +const menuItems = [ + { title: 'Libraries', icon: Library, href: '/libraries' }, + { title: 'Notifications', icon: BellIcon, href: '/notifications' }, + { title: 'Books', icon: Book, href: '/books' }, + { + title: 'My Subscriptions', + icon: Ticket, + href: '/subscriptions', + }, + { title: 'My Borrows', icon: BookUser, href: '/borrows' }, + { title: 'Collections', icon: BookCopy, href: '/collections' }, +] as const + +type Props = { + params: Promise<{ + locale: string + }> +} + +async function getPageLocale(params: Props['params']): Promise { + const { locale } = await params + + if (!isLocale(locale)) { + notFound() + } + + return locale +} + +export async function generateMetadata({ + params, +}: Props): Promise { + const locale = await getPageLocale(params) + const dictionary = await getDictionary(locale) + + return getMetadataForPage(locale, '/', dictionary.meta.home) +} + +export default async function LocalizedHomePage({ params }: Props) { + const locale = await getPageLocale(params) + const dictionary = await getDictionary(locale) + const claim = await IsLoggedIn() + + if (!claim || !claim.librarease) { + return + } + + return ( +
+
+
+

Librarease

+
+ + +
+
+
+ {menuItems.map((item) => { + const Icon = item.icon + + return ( + + ) + })} + + {claim.librarease.admin_libs.concat(claim.librarease.staff_libs) + .length > 0 ? ( + + ) : null} +
+
+
+ ) +} diff --git a/app/privacy/page.tsx b/app/[locale]/privacy/page.tsx similarity index 60% rename from app/privacy/page.tsx rename to app/[locale]/privacy/page.tsx index 262f3ca..f807370 100644 --- a/app/privacy/page.tsx +++ b/app/[locale]/privacy/page.tsx @@ -1,22 +1,56 @@ -import { getPrivacyDoc } from '@/lib/api/docs' +import type { Metadata } from 'next' +import Link from 'next/link' import { ArrowLeft } from 'lucide-react' +import { notFound } from 'next/navigation' import { Button } from '@/components/ui/button' -import Link from 'next/link' +import { + getDictionary, + getMetadataForPage, + isLocale, + localizePath, + type Locale, +} from '@/lib/i18n' + +type Props = { + params: Promise<{ + locale: string + }> +} + +async function getPageLocale(params: Props['params']): Promise { + const { locale } = await params + + if (!isLocale(locale)) { + notFound() + } -export default async function PrivacyPage() { - const doc = await getPrivacyDoc() + return locale +} + +export async function generateMetadata({ + params, +}: Props): Promise { + const locale = await getPageLocale(params) + const dictionary = await getDictionary(locale) + + return getMetadataForPage(locale, '/privacy', dictionary.meta.privacy) +} + +export default async function PrivacyPage({ params }: Props) { + const locale = await getPageLocale(params) + const dictionary = await getDictionary(locale) return ( -
+
- {/* Header */}
+
diff --git a/app/[locale]/signup/page.tsx b/app/[locale]/signup/page.tsx new file mode 100644 index 0000000..7377da8 --- /dev/null +++ b/app/[locale]/signup/page.tsx @@ -0,0 +1,47 @@ +import type { Metadata } from 'next' +import { notFound } from 'next/navigation' +import { AnimatedBooksBackground } from '@/components/ui/animated-books-background' +import { SignUpForm } from '@/components/signup-form' +import { + getDictionary, + getMetadataForPage, + isLocale, + type Locale, +} from '@/lib/i18n' + +type Props = { + params: Promise<{ locale: string }> +} + +async function getPageLocale(params: Props['params']): Promise { + const { locale } = await params + + if (!isLocale(locale)) { + notFound() + } + + return locale +} + +export async function generateMetadata({ + params, +}: Props): Promise { + const locale = await getPageLocale(params) + const dictionary = await getDictionary(locale) + + return getMetadataForPage(locale, '/signup', dictionary.meta.signup) +} + +export default async function SignupPage({ params }: Props) { + const locale = await getPageLocale(params) + const dictionary = await getDictionary(locale) + + return ( +
+ +
+ +
+
+ ) +} diff --git a/app/terms/page.tsx b/app/[locale]/terms/page.tsx similarity index 60% rename from app/terms/page.tsx rename to app/[locale]/terms/page.tsx index f9d1e2e..6de34ec 100644 --- a/app/terms/page.tsx +++ b/app/[locale]/terms/page.tsx @@ -1,20 +1,53 @@ -import { getTermsDoc } from '@/lib/api/docs' +import type { Metadata } from 'next' +import Link from 'next/link' import { ArrowLeft } from 'lucide-react' +import { notFound } from 'next/navigation' import { Button } from '@/components/ui/button' -import Link from 'next/link' +import { + getDictionary, + getMetadataForPage, + isLocale, + localizePath, + type Locale, +} from '@/lib/i18n' + +type Props = { + params: Promise<{ + locale: string + }> +} + +async function getPageLocale(params: Props['params']): Promise { + const { locale } = await params + + if (!isLocale(locale)) { + notFound() + } + + return locale +} + +export async function generateMetadata({ + params, +}: Props): Promise { + const locale = await getPageLocale(params) + const dictionary = await getDictionary(locale) + + return getMetadataForPage(locale, '/terms', dictionary.meta.terms) +} -export default async function TermsPage() { - const doc = await getTermsDoc() +export default async function TermsPage({ params }: Props) { + const locale = await getPageLocale(params) + const dictionary = await getDictionary(locale) return ( -
+
- {/* Header */}
@@ -31,7 +64,7 @@ export default async function TermsPage() { prose-li:mb-1 prose-li:leading-relaxed prose-blockquote:border-l-emerald-500 prose-blockquote:bg-emerald-50 prose-blockquote:p-4 prose-blockquote:rounded-r-lg prose-code:bg-slate-100 prose-code:px-2 prose-code:py-1 prose-code:rounded prose-code:text-foreground" - dangerouslySetInnerHTML={{ __html: doc }} + dangerouslySetInnerHTML={{ __html: dictionary.terms.html }} />
diff --git a/app/globals.css b/app/globals.css index b07d506..b722f10 100644 --- a/app/globals.css +++ b/app/globals.css @@ -70,7 +70,11 @@ @layer utilities { body { - font-family: Arial, Helvetica, sans-serif; + font-family: + var(--font-geist-sans), + Arial, + Helvetica, + sans-serif; } } @@ -138,6 +142,34 @@ body { @apply bg-background text-foreground; } + + html[lang='my'] body { + font-family: + 'Pyidaungsu', + var(--font-geist-sans), + sans-serif; + line-height: 1.8; + letter-spacing: 0; + } + + html[lang='my'] p, + html[lang='my'] li, + html[lang='my'] blockquote, + html[lang='my'] label, + html[lang='my'] input, + html[lang='my'] textarea, + html[lang='my'] button { + line-height: 1.9; + } + + html[lang='my'] h1, + html[lang='my'] h2, + html[lang='my'] h3, + html[lang='my'] h4, + html[lang='my'] h5, + html[lang='my'] h6 { + line-height: 1.45; + } } @layer base { diff --git a/app/layout.tsx b/app/layout.tsx index abb5e9e..796bb96 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -7,11 +7,13 @@ // https://github.com/librarease/librarease-web import type { Metadata, Viewport } from 'next' +import { headers } from 'next/headers' import { Geist, Geist_Mono } from 'next/font/google' import './globals.css' import { Toaster } from '@/components/ui/sonner' import { SITE_DESCRIPTION, SITE_NAME } from '@/lib/consts' import { ThemeProvider } from '@/components/theme-provider' +import { defaultLocale, isLocale, LOCALE_HEADER } from '@/lib/i18n' const geistSans = Geist({ variable: '--font-geist-sans', @@ -60,13 +62,18 @@ export const metadata: Metadata = { category: 'BusinessApplication', } -export default function RootLayout({ +export default async function RootLayout({ children, }: Readonly<{ children: React.ReactNode }>) { + const headerList = await headers() + const requestLocale = headerList.get(LOCALE_HEADER) + const locale = + requestLocale && isLocale(requestLocale) ? requestLocale : defaultLocale + return ( - + - } - - return ( -
-
-
-

Librarease

-
- - -
-
-
- {menuItems.map((item) => { - const Icon = item.icon - return ( - - ) - })} - - {claim.librarease.admin_libs.concat(claim.librarease.staff_libs) - .length > 0 ? ( - - ) : null} -
-
-
- ) +export default function HomeRedirect() { + redirect('/en') } diff --git a/components/forgot-password-form.tsx b/components/forgot-password-form.tsx index f34b143..e9b91c5 100644 --- a/components/forgot-password-form.tsx +++ b/components/forgot-password-form.tsx @@ -15,11 +15,21 @@ import { useActionState, useEffect } from 'react' import { loginAction } from '@/lib/actions/login' import Link from 'next/link' import { Route } from 'next' +import { localizePath, type Locale } from '@/lib/i18n' + +type ForgotPasswordCopy = Awaited< + ReturnType +>['auth']['forgotPassword'] export function ForgotPasswordForm({ className, ...props -}: React.ComponentPropsWithoutRef<'div'> & { email?: string; from: Route }) { +}: React.ComponentPropsWithoutRef<'div'> & { + email?: string + from: Route + locale: Locale + copy: ForgotPasswordCopy +}) { const initialState = { error: '', email: props.email ?? '', @@ -44,10 +54,8 @@ export function ForgotPasswordForm({
- Forgot Password - - Enter your email to reset your password - + {props.copy.title} + {props.copy.description} {state.error && (
{state.error}
)} @@ -56,32 +64,35 @@ export function ForgotPasswordForm({
- +
- Remember your password? + {props.copy.backToLogin}
- Don't have an account?{' '} - - Sign up + {props.copy.signupPrompt}{' '} + + {props.copy.signupLink}
diff --git a/components/landing.tsx b/components/landing.tsx index 9966057..8f8756c 100644 --- a/components/landing.tsx +++ b/components/landing.tsx @@ -11,96 +11,46 @@ import { Star, } from 'lucide-react' import { Button } from '@/components/ui/button' +import { localizePath, type LandingCopy, type Locale } from '@/lib/i18n' import { ModeToggle } from './button-toggle-theme' -const forLibraries = [ - { - icon: BookOpen, - title: 'Organize your collection', - description: - 'Add books with cover art and details. Import your existing catalog in bulk with a CSV file.', - }, - { - icon: Users, - title: 'Manage memberships', - description: - 'Create plans with custom loan limits, borrow periods, and fine rates. Members subscribe and start borrowing.', - }, - { - icon: BarChart3, - title: "See what's happening", - description: - 'A clear picture of borrowing activity, popular titles, and revenue — updated in real time.', - }, - { - icon: ShieldCheck, - title: 'Built for your whole team', - description: - 'Give staff access to handle day-to-day borrowing and returns, while admins keep full oversight.', - }, -] +type LandingPageProps = { + locale: Locale + copy: LandingCopy +} -const forReaders = [ - { - icon: Search, - title: 'Browse available books', - description: - "Explore your library's collection and see what's available before you visit.", - }, - { - icon: BookMarked, - title: 'Track your borrows', - description: - "Know exactly what you have out, when it's due, and your full borrowing history.", - }, - { - icon: Bell, - title: 'Stay on top of due dates', - description: - "Get notified before a book is due so you never pay a fine you didn't expect.", - }, - { - icon: Star, - title: 'Leave a review', - description: - "Share your thoughts on books you've read. Help other members find their next favourite.", - }, -] +const libraryIcons = [BookOpen, Users, BarChart3, ShieldCheck] +const readerIcons = [Search, BookMarked, Bell, Star] -const stats = [ - { value: '50+', label: 'Libraries' }, - { value: '10,000+', label: 'Active Members' }, - { value: '200,000+', label: 'Books' }, - { value: '99.9%', label: 'Uptime' }, -] +export default function LandingPage({ locale, copy }: LandingPageProps) { + const currentYear = new Date().getFullYear() -export default function LandingPage() { return (
{/* Nav */}
- Librarease + {copy.brand}
@@ -128,27 +80,28 @@ export default function LandingPage() {
-

- Your library, +

+ {copy.hero.title}
- connected. + {copy.hero.accent}

- Experience a modern library ecosystem built for smooth operations - and a better borrowing experience. Smart management for librarians - and effortless access for readers. + {copy.hero.description}

@@ -160,7 +113,7 @@ export default function LandingPage() {
- {stats.map((stat) => ( + {copy.stats.map((stat) => (

{stat.value} @@ -179,15 +132,15 @@ export default function LandingPage() {

- For Libraries + {copy.forLibraries.eyebrow}

- Everything you need to run a modern library. + {copy.forLibraries.title}

- {forLibraries.map((item) => { - const Icon = item.icon + {copy.forLibraries.items.map((item, index) => { + const Icon = libraryIcons[index] return (

- For Readers + {copy.forReaders.eyebrow}

- Borrow books and keep track of your reading. + {copy.forReaders.title}

- {forReaders.map((item) => { - const Icon = item.icon + {copy.forReaders.items.map((item, index) => { + const Icon = readerIcons[index] return (
-

Just here to read?

+

+ {copy.cta.readerTitle} +

- Browse books across all libraries on Librarease. No account - needed to explore. + {copy.cta.readerDescription}

@@ -272,14 +226,18 @@ export default function LandingPage() { className="h-6 w-6 text-muted-foreground mb-6" strokeWidth={1.5} /> -

Running a library?

+

+ {copy.cta.libraryTitle} +

- Set up your library on Librarease in minutes. Manage your - collection, members, and borrowing all in one place. + {copy.cta.libraryDescription}

@@ -292,14 +250,14 @@ export default function LandingPage() {
- Librarease + {copy.brand}
- Explore Books + {copy.footer.exploreBooks} - Terms of Service + {copy.footer.terms} - Privacy Policy + {copy.footer.privacy} - Sign in + {copy.footer.signIn}
- © {new Date().getFullYear()} Librarease + + {copy.footer.copyright.replace('{year}', `${currentYear}`)} +
diff --git a/components/login-form.tsx b/components/login-form.tsx index 0107c1b..cb0e27a 100644 --- a/components/login-form.tsx +++ b/components/login-form.tsx @@ -17,11 +17,21 @@ import Link from 'next/link' import { Checkbox } from './ui/checkbox' import { Route } from 'next' import { Spinner } from './ui/spinner' +import { localizePath, type Locale } from '@/lib/i18n' + +type LoginCopy = Awaited< + ReturnType +>['auth']['login'] export function LoginForm({ className, ...props -}: React.ComponentPropsWithoutRef<'div'> & { email?: string; from: Route }) { +}: React.ComponentPropsWithoutRef<'div'> & { + email?: string + from: Route + locale: Locale + copy: LoginCopy +}) { const initialState = { error: '', email: props.email ?? '', @@ -49,10 +59,8 @@ export function LoginForm({
- Login - - Enter your email below to login to your account - + {props.copy.title} + {props.copy.description} {state.error && (
{state.error}
)} @@ -61,24 +69,24 @@ export function LoginForm({
- +
- + @@ -91,27 +99,30 @@ export function LoginForm({ onCheckedChange={toggleShowPassword} /> - Forgot your password? + {props.copy.forgotPassword}
{/* */}
- Don't have an account?{' '} - - Sign up + {props.copy.signupPrompt}{' '} + + {props.copy.signupLink}
diff --git a/components/signup-form.tsx b/components/signup-form.tsx index 041b40b..df2d80a 100644 --- a/components/signup-form.tsx +++ b/components/signup-form.tsx @@ -14,6 +14,7 @@ import { Label } from '@/components/ui/label' import { useActionState } from 'react' import { registerAction } from '@/lib/actions/register' import Link from 'next/link' +import { localizePath, type Locale } from '@/lib/i18n' const initialState = { error: '', @@ -22,71 +23,80 @@ const initialState = { password: '', } +type SignupCopy = Awaited< + ReturnType +>['auth']['signup'] + export function SignUpForm({ className, ...props -}: React.ComponentPropsWithoutRef<'div'>) { +}: React.ComponentPropsWithoutRef<'div'> & { + locale: Locale + copy: SignupCopy +}) { const [state, action, isPending] = useActionState( registerAction, initialState, - '/login' + localizePath(props.locale, '/login') ) return (
- Sign Up - - Enter your information below to create your account - + {props.copy.title} + {props.copy.description} {state.error && (
{state.error}
)}
+
- +
- +
- +
- Already have an account?{' '} - - Login + {props.copy.loginPrompt}{' '} + + {props.copy.loginLink}
diff --git a/lib/actions/register.ts b/lib/actions/register.ts index 23ca00b..ef535af 100644 --- a/lib/actions/register.ts +++ b/lib/actions/register.ts @@ -8,6 +8,7 @@ // } from 'firebase/auth' import { redirect, RedirectType } from 'next/navigation' import { registerUser } from '../api/auth' +import { defaultLocale, isLocale } from '@/lib/i18n' // const auth = getAuth(app) @@ -25,6 +26,11 @@ export async function registerAction( const name = formData.get('name') as string const email = formData.get('email') as string const password = formData.get('password') as string + const localeValue = formData.get('locale') + const locale = + typeof localeValue === 'string' && isLocale(localeValue) + ? localeValue + : defaultLocale try { await registerUser({ name, email, password }) @@ -38,5 +44,5 @@ export async function registerAction( } } - redirect(`/login?email=${email}`, RedirectType.replace) + redirect(`/${locale}/login?email=${email}`, RedirectType.replace) } diff --git a/lib/i18n.ts b/lib/i18n.ts new file mode 100644 index 0000000..5292ea2 --- /dev/null +++ b/lib/i18n.ts @@ -0,0 +1,634 @@ +import type { Metadata } from 'next' +import type { Route } from 'next' +import { SITE_NAME } from '@/lib/consts' + +export const locales = ['en', 'my'] as const +export type Locale = (typeof locales)[number] + +export const defaultLocale: Locale = 'en' +export const LOCALE_HEADER = 'x-librarease-locale' + +const localizedPaths = [ + '/', + '/about', + '/terms', + '/privacy', + '/login', + '/signup', + '/forgot-password', +] as const +export type LocalizedPath = (typeof localizedPaths)[number] + +export function isLocale(value: string): value is Locale { + return locales.includes(value as Locale) +} + +export function isLocalizedPublicPath( + value: string, +): value is LocalizedPath { + return localizedPaths.includes(value as LocalizedPath) +} + +export function localizePath( + locale: Locale, + path: LocalizedPath, +): Route { + if (path === '/') { + return `/${locale}` as Route + } + + return `/${locale}${path}` as Route +} + +export function getLocalizedUrl( + locale: Locale, + path: LocalizedPath, +): string { + const baseUrl = process.env.NEXT_PUBLIC_APP_URL + + if (!baseUrl) { + return localizePath(locale, path) + } + + return new URL(localizePath(locale, path), baseUrl).toString() +} + +export function getLocaleAlternates( + locale: Locale, + path: LocalizedPath, +) { + return { + canonical: getLocalizedUrl(locale, path), + languages: { + en: getLocalizedUrl('en', path), + my: getLocalizedUrl('my', path), + }, + } +} + +type MetadataCopy = { + title: string + description: string +} + +type LandingCard = { + title: string + description: string +} + +type LandingStat = { + value: string + label: string +} + +type LandingDictionary = { + brand: string + nav: { + forLibraries: string + forReaders: string + exploreBooks: string + signIn: string + getStarted: string + } + hero: { + title: string + accent: string + description: string + primaryCta: string + secondaryCta: string + } + stats: LandingStat[] + forLibraries: { + eyebrow: string + title: string + items: LandingCard[] + } + forReaders: { + eyebrow: string + title: string + items: LandingCard[] + } + cta: { + readerTitle: string + readerDescription: string + readerButton: string + libraryTitle: string + libraryDescription: string + libraryButton: string + } + footer: { + exploreBooks: string + terms: string + privacy: string + signIn: string + copyright: string + } +} + +type AboutDictionary = { + backToHome: string + title: string + paragraphs: string[] +} + +type LegalDictionary = { + backToHome: string + html: string +} + +type Dictionary = { + meta: { + home: MetadataCopy + about: MetadataCopy + terms: MetadataCopy + privacy: MetadataCopy + login: MetadataCopy + signup: MetadataCopy + forgotPassword: MetadataCopy + } + landing: LandingDictionary + about: AboutDictionary + terms: LegalDictionary + privacy: LegalDictionary + auth: { + login: { + title: string + description: string + emailLabel: string + emailPlaceholder: string + passwordLabel: string + passwordPlaceholder: string + showPassword: string + forgotPassword: string + submit: string + signupPrompt: string + signupLink: string + } + signup: { + title: string + description: string + nameLabel: string + namePlaceholder: string + emailLabel: string + emailPlaceholder: string + passwordLabel: string + passwordPlaceholder: string + submit: string + loginPrompt: string + loginLink: string + } + forgotPassword: { + title: string + description: string + emailLabel: string + emailPlaceholder: string + backToLogin: string + submit: string + signupPrompt: string + signupLink: string + } + } +} + +export type LandingCopy = LandingDictionary + +const dictionaries: Record = { + en: { + meta: { + home: { + title: SITE_NAME, + description: + 'Modern library management for librarians and readers with a smoother borrowing experience.', + }, + about: { + title: `About · ${SITE_NAME}`, + description: + 'Learn what LibrarEase offers for library teams and readers.', + }, + terms: { + title: `Terms · ${SITE_NAME}`, + description: 'Terms and conditions for using LibrarEase.', + }, + privacy: { + title: `Privacy · ${SITE_NAME}`, + description: 'Privacy information for LibrarEase users and libraries.', + }, + login: { + title: `Login · ${SITE_NAME}`, + description: 'Log in to your LibrarEase account.', + }, + signup: { + title: `Signup · ${SITE_NAME}`, + description: 'Create your LibrarEase account.', + }, + forgotPassword: { + title: `Forgot Password · ${SITE_NAME}`, + description: 'Reset access to your LibrarEase account.', + }, + }, + landing: { + brand: 'Librarease', + nav: { + forLibraries: 'For Libraries', + forReaders: 'For Readers', + exploreBooks: 'Explore Books', + signIn: 'Sign in', + getStarted: 'Get started', + }, + hero: { + title: 'Your library,', + accent: 'connected.', + description: + 'Experience a modern library ecosystem built for smooth operations and a better borrowing experience. Smart management for librarians and effortless access for readers.', + primaryCta: 'Get started free', + secondaryCta: 'Browse books', + }, + stats: [ + { value: '50+', label: 'Libraries' }, + { value: '10,000+', label: 'Active Members' }, + { value: '200,000+', label: 'Books' }, + { value: '99.9%', label: 'Uptime' }, + ], + forLibraries: { + eyebrow: 'For Libraries', + title: 'Everything you need to run a modern library.', + items: [ + { + title: 'Organize your collection', + description: + 'Add books with cover art and details. Import your existing catalog in bulk with a CSV file.', + }, + { + title: 'Manage memberships', + description: + 'Create plans with custom loan limits, borrow periods, and fine rates. Members subscribe and start borrowing.', + }, + { + title: "See what's happening", + description: + 'A clear picture of borrowing activity, popular titles, and revenue updated in real time.', + }, + { + title: 'Built for your whole team', + description: + 'Give staff access to handle day-to-day borrowing and returns, while admins keep full oversight.', + }, + ], + }, + forReaders: { + eyebrow: 'For Readers', + title: 'Borrow books and keep track of your reading.', + items: [ + { + title: 'Browse available books', + description: + "Explore your library's collection and see what's available before you visit.", + }, + { + title: 'Track your borrows', + description: + "Know exactly what you have out, when it's due, and your full borrowing history.", + }, + { + title: 'Stay on top of due dates', + description: + "Get notified before a book is due so you never pay a fine you didn't expect.", + }, + { + title: 'Leave a review', + description: + "Share your thoughts on books you've read and help other members find their next favourite.", + }, + ], + }, + cta: { + readerTitle: 'Just here to read?', + readerDescription: + 'Browse books across all libraries on Librarease. No account needed to explore.', + readerButton: 'Explore books', + libraryTitle: 'Running a library?', + libraryDescription: + 'Set up your library on Librarease in minutes. Manage your collection, members, and borrowing all in one place.', + libraryButton: 'Get started free', + }, + footer: { + exploreBooks: 'Explore Books', + terms: 'Terms of Service', + privacy: 'Privacy Policy', + signIn: 'Sign in', + copyright: '© {year} Librarease', + }, + }, + about: { + backToHome: 'Back to Home', + title: 'About LibrarEase', + paragraphs: [ + 'LibrarEase is a modern library management platform built to make libraries easier to run and more enjoyable to use. It gives libraries of all sizes one place to manage books, memberships, subscriptions, and borrowing activities.', + 'For librarians, LibrarEase provides practical tools to organize collections, track borrowing and return history, and manage memberships with less manual work. Teams can assign admin and staff roles, send notifications, and keep day-to-day operations moving smoothly.', + 'For readers and members, LibrarEase makes borrowing simple and accessible. Users can explore collections, subscribe through library memberships, receive timely notifications, and keep track of their activity in one place.', + 'Fast, reliable, and easy to use, LibrarEase brings together the essentials for a better library experience for both library teams and readers.', + ], + }, + terms: { + backToHome: 'Back to Home', + html: ` +

Terms of Service

+

These terms govern your access to and use of LibrarEase. By using the service, you agree to use it in a lawful manner and to follow these terms.

+

Accounts and Access

+

You are responsible for the accuracy of the information associated with your account and for keeping your access credentials secure.

+

Library Data

+

Libraries remain responsible for the books, member records, and operational data they manage through LibrarEase. You should only upload data you are allowed to manage.

+

Acceptable Use

+

You may not misuse the service, attempt to disrupt availability, or access data that you do not have permission to view.

+

Service Changes

+

We may update, improve, or discontinue features from time to time. When material changes are made, we will aim to communicate them clearly.

+

Contact

+

If you have questions about these terms, contact the LibrarEase team through the support channels provided by your library or deployment owner.

+ `, + }, + privacy: { + backToHome: 'Back to Home', + html: ` +

Privacy Policy

+

This policy explains how LibrarEase handles information used to provide library services and the borrowing experience.

+

Information We Process

+

LibrarEase may process account details, membership information, borrowing history, notifications, and library catalog data in order to operate the service.

+

How Information Is Used

+

We use information to provide core product features such as catalog browsing, borrowing workflows, notifications, staff administration, and reporting.

+

Access and Sharing

+

Access to information should be limited to authorized users such as library admins, staff, and the relevant member account owner. Information should not be shared outside those purposes without a valid reason.

+

Retention

+

Libraries and deployment owners are responsible for deciding how long operational records should be retained in line with their policies and legal obligations.

+

Contact

+

If you have questions about privacy practices for a specific library deployment, contact the library or service owner responsible for that deployment.

+ `, + }, + auth: { + login: { + title: 'Login', + description: 'Enter your email below to log in to your account', + emailLabel: 'Email', + emailPlaceholder: 'e.g. mgmg@example.com', + passwordLabel: 'Password', + passwordPlaceholder: 'e.g. mypassword', + showPassword: 'Show Password', + forgotPassword: 'Forgot your password?', + submit: 'Login', + signupPrompt: "Don't have an account?", + signupLink: 'Sign up', + }, + signup: { + title: 'Sign Up', + description: 'Enter your information below to create your account', + nameLabel: 'Name', + namePlaceholder: 'e.g. Mg Mg', + emailLabel: 'Email', + emailPlaceholder: 'e.g. mgmg@example.com', + passwordLabel: 'Password', + passwordPlaceholder: 'Set a password', + submit: 'Sign Up', + loginPrompt: 'Already have an account?', + loginLink: 'Login', + }, + forgotPassword: { + title: 'Forgot Password', + description: 'Enter your email to reset your password', + emailLabel: 'Email', + emailPlaceholder: 'e.g. mgmg@example.com', + backToLogin: 'Remember your password?', + submit: 'Reset Password', + signupPrompt: "Don't have an account?", + signupLink: 'Sign up', + }, + }, + }, + my: { + meta: { + home: { + title: `${SITE_NAME} | မြန်မာ`, + description: + 'စာကြည့်တိုက်လုပ်ငန်းစဉ်များကို ပိုမိုစနစ်တကျ စီမံနိုင်ပြီး အသုံးပြုသူအတွေ့အကြုံကို မြှင့်တင်ပေးသော LibrarEase platform ။', + }, + about: { + title: `LibrarEase အကြောင်း · ${SITE_NAME}`, + description: + 'LibrarEase က စာကြည့်တိုက်အဖွဲ့များနှင့် အသုံးပြုသူများအတွက် ပံ့ပိုးပေးသည့် အဓိကစွမ်းဆောင်ရည်များကို လေ့လာပါ။', + }, + terms: { + title: `စည်းကမ်းချက်များ · ${SITE_NAME}`, + description: 'LibrarEase ဝန်ဆောင်မှုကို အသုံးပြုရာတွင် လိုက်နာရမည့် စည်းကမ်းချက်များ။', + }, + privacy: { + title: `ကိုယ်ရေးအချက်အလက် · ${SITE_NAME}`, + description: + 'LibrarEase တွင် ကိုယ်ရေးအချက်အလက်များကို မည်သို့ ကိုင်တွယ်အသုံးပြုသည်ကို ရှင်းပြထားသော အချက်အလက်များ။', + }, + login: { + title: `ဝင်ရန် · ${SITE_NAME}`, + description: 'သင့် LibrarEase အကောင့်သို့ ဝင်ရောက်ရန် အချက်အလက်များ ဖြည့်သွင်းပါ။', + }, + signup: { + title: `အကောင့်ဖွင့်ရန် · ${SITE_NAME}`, + description: 'LibrarEase အကောင့်အသစ်တစ်ခု ဖန်တီးရန် အချက်အလက်များ ဖြည့်သွင်းပါ။', + }, + forgotPassword: { + title: `စကားဝှက်မေ့သွားပါသလား · ${SITE_NAME}`, + description: 'သင့်အကောင့်ဝင်ရောက်ခွင့် ပြန်လည်ရယူရန် အီးမေးလ်လိပ်စာ ဖြည့်သွင်းပါ။', + }, + }, + landing: { + brand: 'Librarease', + nav: { + forLibraries: 'စာကြည့်တိုက်များအတွက်', + forReaders: 'စာဖတ်သူများအတွက်', + exploreBooks: 'စာအုပ်များကြည့်ရှုရန်', + signIn: 'ဝင်ရန်', + getStarted: 'စတင်အသုံးပြုပါ', + }, + hero: { + title: 'စာကြည့်တိုက်လုပ်ငန်းကို', + accent: 'ပိုမိုချောမွေ့စွာ လည်ပတ်ပါ။', + description: + 'LibrarEase သည် စာကြည့်တိုက်အုပ်ချုပ်မှု၊ အသင်းဝင်စီမံခန့်ခွဲမှုနှင့် စာအုပ်ငှားရမ်းမှုလုပ်ငန်းစဉ်များကို တစ်နေရာတည်းတွင် ထိရောက်စွာ စီမံနိုင်စေရန် တည်ဆောက်ထားသော ခေတ်မီ platform ဖြစ်သည်။ ဝန်ထမ်းများအတွက် လုပ်ငန်းစဉ်ပိုင်းကို လွယ်ကူစေသလို စာဖတ်သူများအတွက်လည်း အသုံးပြုရလွယ်ကူသော အတွေ့အကြုံကို ပေးစွမ်းပါသည်။', + primaryCta: 'အခမဲ့ စတင်ပါ', + secondaryCta: 'စာအုပ်များ ကြည့်ရှုပါ', + }, + stats: [ + { value: '50+', label: 'စာကြည့်တိုက်များ' }, + { value: '10,000+', label: 'အသုံးပြုသူအဖွဲ့ဝင်များ' }, + { value: '200,000+', label: 'စာအုပ်များ' }, + { value: '99.9%', label: 'စနစ်တည်ငြိမ်ချိန်' }, + ], + forLibraries: { + eyebrow: 'စာကြည့်တိုက်များအတွက်', + title: 'ခေတ်မီစာကြည့်တိုက်တစ်ခုကို စနစ်တကျ လည်ပတ်နိုင်ရန် လိုအပ်သမျှ အားလုံး။', + items: [ + { + title: 'စာအုပ်စုစည်းမှုကို စနစ်တကျ စီမံပါ', + description: + 'စာအုပ်အချက်အလက်များ၊ အုပ်အဖုံးပုံများနှင့် catalog data များကို စနစ်တကျ မှတ်တမ်းတင်နိုင်ပြီး CSV ဖြင့် အစုလိုက် တင်သွင်းနိုင်သည်။', + }, + { + title: 'အသင်းဝင်အစီအစဉ်များကို ထိရောက်စွာ စီမံပါ', + description: + 'ငှားရမ်းအရေအတွက်ကန့်သတ်ချက်၊ ပြန်အပ်ချိန်နှင့် ဒဏ်ကြေးနှုန်းများကို သတ်မှတ်ထားသော membership plan များကို လွယ်ကူစွာ ဖန်တီးနိုင်သည်။', + }, + { + title: 'လုပ်ငန်းအခြေအနေကို မြင်သာစွာ ခြေရာခံပါ', + description: + 'ငှားရမ်းမှုလှုပ်ရှားမှုများ၊ လူကြိုက်များသော စာအုပ်များနှင့် အဓိကစာရင်းအင်းများကို အချိန်နှင့်တပြေးညီ ကြည့်ရှုနိုင်သည်။', + }, + { + title: 'အဖွဲ့လိုက် အသုံးပြုရန် အဆင်ပြေသည်', + description: + 'နေ့စဉ်လုပ်ငန်းများအတွက် staff access ခွဲဝေပေးနိုင်ပြီး admin များက စနစ်တကျ ကြီးကြပ်စီမံနိုင်သည်။', + }, + ], + }, + forReaders: { + eyebrow: 'စာဖတ်သူများအတွက်', + title: 'စာအုပ်များကို အဆင်ပြေစွာ ရှာဖွေငှားရမ်းပြီး သင့်ဖတ်ရှုမှုကို စနစ်တကျ လိုက်ကြည့်ပါ။', + items: [ + { + title: 'ရရှိနိုင်သော စာအုပ်များကို လွယ်ကူစွာ ရှာဖွေပါ', + description: + 'စာကြည့်တိုက်သို့ မသွားမီ စုစည်းမှုအတွင်းရှိ စာအုပ်များနှင့် လက်ရှိရရှိနိုင်မှုအခြေအနေကို ကြိုတင်ကြည့်ရှုနိုင်သည်။', + }, + { + title: 'သင်ငှားထားသော စာအုပ်များကို စနစ်တကျ လိုက်ကြည့်ပါ', + description: + 'လက်ရှိငှားထားသောစာအုပ်များ၊ ပြန်အပ်ရမည့်နေ့စွဲများနှင့် ယခင်ငှားရမ်းမှတ်တမ်းများကို တစ်နေရာတည်းတွင် ကြည့်ရှုနိုင်သည်။', + }, + { + title: 'ပြန်အပ်ရက်များကို အချိန်မီ သိရှိနိုင်ပါစေ', + description: + 'ပြန်အပ်ရက်မတိုင်မီ သတိပေးချက်များ ရရှိနိုင်သဖြင့် မလိုအပ်သော နောက်ကျဒဏ်ကြေးများကို လျှော့ချနိုင်သည်။', + }, + { + title: 'ဖတ်ပြီးသော စာအုပ်များကို သုံးသပ်ချက်ပေးပါ', + description: + 'သင့်အမြင်နှင့် ဖတ်ရှုမှုအတွေ့အကြုံကို မျှဝေခြင်းဖြင့် အခြားစာဖတ်သူများအတွက် အသုံးဝင်သော အကြံပြုချက်များ ပေးနိုင်သည်။', + }, + ], + }, + cta: { + readerTitle: 'စာဖတ်ဖို့ ရှာဖွေနေပါသလား?', + readerDescription: + 'Librarease ပေါ်ရှိ စာကြည့်တိုက်များ၏ စာအုပ်စုစည်းမှုများကို အကောင့်မလိုဘဲ ကြည့်ရှုနိုင်သည်။', + readerButton: 'စာအုပ်များ ကြည့်ရှုပါ', + libraryTitle: 'သင်၏စာကြည့်တိုက်ကို စတင်စီမံလိုပါသလား?', + libraryDescription: + 'မိနစ်ပိုင်းအတွင်း LibrarEase ပေါ်တွင် သင့်စာကြည့်တိုက်ကို စတင်နိုင်ပြီး စာအုပ်စုစည်းမှု၊ အသင်းဝင်များနှင့် ငှားရမ်းမှုလုပ်ငန်းစဉ်များကို တစ်နေရာတည်းမှာ စီမံနိုင်သည်။', + libraryButton: 'အခမဲ့ စတင်ပါ', + }, + footer: { + exploreBooks: 'စာအုပ်များ ကြည့်ရှုပါ', + terms: 'အသုံးပြုမှု စည်းကမ်းချက်များ', + privacy: 'ကိုယ်ရေးအချက်အလက် မူဝါဒ', + signIn: 'ဝင်ရန်', + copyright: '© {year} Librarease', + }, + }, + about: { + backToHome: 'ပင်မသို့ ပြန်သွားရန်', + title: 'LibrarEase အကြောင်း', + paragraphs: [ + 'LibrarEase သည် စာကြည့်တိုက်လုပ်ငန်းစဉ်များကို ပိုမိုစနစ်တကျ၊ ပိုမိုထိရောက်စွာ စီမံနိုင်ရန် ရည်ရွယ်ဖန်တီးထားသော ခေတ်မီ စာကြည့်တိုက်စီမံခန့်ခွဲမှု platform တစ်ခုဖြစ်သည်။ စာအုပ်စုစည်းမှု၊ အသင်းဝင်စီမံခန့်ခွဲမှု၊ subscription အစီအစဉ်များနှင့် ငှားရမ်းမှုလုပ်ငန်းစဉ်များကို တစ်နေရာတည်းတွင် စုပေါင်းစီမံနိုင်သည်။', + 'စာကြည့်တိုက်အဖွဲ့များအတွက် LibrarEase သည် catalog စီမံခန့်ခွဲမှု၊ ငှား/ပြန်အပ်မှတ်တမ်းများကို လိုက်လံကြည့်ရှုခြင်း၊ အသင်းဝင်များကို စီမံခြင်းနှင့် အဖွဲ့လိုက် access ခွဲဝေပေးခြင်းတို့ကို ပိုမိုလွယ်ကူစေသည်။ နေ့စဉ်လုပ်ငန်းများကို ထိန်းချုပ်ရလွယ်ကူစေပြီး စီမံခန့်ခွဲမှုအရည်အသွေးကို မြှင့်တင်ပေးသည်။', + 'စာဖတ်သူများနှင့် အသင်းဝင်များအတွက်လည်း LibrarEase သည် စာအုပ်ရှာဖွေမှု၊ ငှားရမ်းမှုအခြေအနေ လိုက်ကြည့်မှုနှင့် သတိပေးချက်ရယူမှုတို့ကို အဆင်ပြေစွာ အသုံးပြုနိုင်စေသည်။ အသုံးပြုသူများအတွက် ရိုးရှင်းပြီး ယုံကြည်စိတ်ချရသော borrowing experience ကို ပေးစွမ်းရန် ဒီဇိုင်းထုတ်ထားသည်။', + 'မြန်ဆန်မှု၊ တည်ငြိမ်မှုနှင့် အသုံးပြုရလွယ်ကူမှုကို အခြေခံထားသော LibrarEase သည် စာကြည့်တိုက်အဖွဲ့များနှင့် စာဖတ်သူများအတွက် ပိုမိုကောင်းမွန်သော library experience ကို ဖန်တီးပေးသည့် platform တစ်ခုဖြစ်သည်။', + ], + }, + terms: { + backToHome: 'ပင်မသို့ ပြန်သွားရန်', + html: ` +

အသုံးပြုမှု စည်းကမ်းချက်များ

+

LibrarEase ဝန်ဆောင်မှုကို အသုံးပြုခြင်းနှင့် အသုံးပြုခွင့်ကို ဤစည်းကမ်းချက်များက ထိန်းချုပ်ပါသည်။ ဝန်ဆောင်မှုကို အသုံးပြုခြင်းအားဖြင့် ဤစည်းကမ်းချက်များကို လက်ခံသဘောတူသည်ဟု မှတ်ယူပါသည်။

+

အကောင့်နှင့် အသုံးပြုခွင့်

+

သင့်အကောင့်နှင့် ဆက်စပ်သော အချက်အလက်များ မှန်ကန်စေရန်နှင့် login အချက်အလက်များကို လုံခြုံစွာ ထိန်းသိမ်းရန် သင့်တွင် တာဝန်ရှိသည်။

+

စာကြည့်တိုက်ဒေတာ

+

LibrarEase ပေါ်တွင် စီမံထားသော စာအုပ်အချက်အလက်များ၊ အသင်းဝင်မှတ်တမ်းများနှင့် လုပ်ငန်းဆိုင်ရာဒေတာများအတွက် သက်ဆိုင်ရာ စာကြည့်တိုက် သို့မဟုတ် deployment owner ဘက်မှ တာဝန်ယူရမည်ဖြစ်သည်။ စီမံခန့်ခွဲခွင့်ရှိသော ဒေတာများသာ တင်သွင်းသင့်သည်။

+

သင့်လျော်သော အသုံးပြုမှု

+

ဝန်ဆောင်မှုကို အနှောင့်အယှက်ဖြစ်စေခြင်း၊ ခွင့်ပြုချက်မရှိသော ဒေတာများကို ဝင်ရောက်ကြည့်ရှုရန် ကြိုးစားခြင်း သို့မဟုတ် မသင့်လျော်သော ရည်ရွယ်ချက်များအတွက် အသုံးပြုခြင်း မပြုလုပ်ရပါ။

+

ဝန်ဆောင်မှု ပြောင်းလဲမှုများ

+

Feature များကို အချိန်အခါအလိုက် ပြင်ဆင်ခြင်း၊ တိုးတက်အောင် ဆောင်ရွက်ခြင်း သို့မဟုတ် ရပ်နားခြင်းများ ဖြစ်နိုင်သည်။ အရေးကြီးသော ပြောင်းလဲမှုများရှိပါက အသိပေးနိုင်ရန် ကြိုးစားပါမည်။

+

ဆက်သွယ်ရန်

+

ဤစည်းကမ်းချက်များနှင့် ပတ်သက်၍ မေးမြန်းလိုပါက သင့်စာကြည့်တိုက် သို့မဟုတ် deployment owner မှ ပံ့ပိုးပေးထားသော support channel များမှ ဆက်သွယ်နိုင်သည်။

+ `, + }, + privacy: { + backToHome: 'ပင်မသို့ ပြန်သွားရန်', + html: ` +

ကိုယ်ရေးအချက်အလက် မူဝါဒ

+

ဤမူဝါဒသည် LibrarEase က စာကြည့်တိုက်ဝန်ဆောင်မှုများနှင့် စာအုပ်ငှားရမ်းမှုအတွေ့အကြုံကို ပံ့ပိုးရန်အတွက် အချက်အလက်များကို မည်သို့ ကိုင်တွယ်အသုံးပြုသည်ကို ရှင်းပြပါသည်။

+

ကျွန်ုပ်တို့ ကိုင်တွယ်သော အချက်အလက်များ

+

ဝန်ဆောင်မှုကို လည်ပတ်ရန်အတွက် အကောင့်အသေးစိတ်၊ အသင်းဝင်အချက်အလက်၊ ငှားရမ်းမှတ်တမ်းများ၊ သတိပေးချက်ဆိုင်ရာ အချက်အလက်များနှင့် စာကြည့်တိုက် catalog data များကို LibrarEase က ကိုင်တွယ်နိုင်သည်။

+

အချက်အလက်အသုံးပြုပုံ

+

အချက်အလက်များကို catalog ကြည့်ရှုခြင်း၊ ငှားရမ်းမှုလုပ်ငန်းစဉ်များ၊ သတိပေးချက်ပို့ခြင်း၊ ဝန်ထမ်းစီမံခန့်ခွဲမှုနှင့် အစီရင်ခံစာများကဲ့သို့သော အဓိက feature များကို ပံ့ပိုးရန် အသုံးပြုသည်။

+

ဝင်ရောက်ခွင့်နှင့် မျှဝေမှု

+

အချက်အလက်ဝင်ရောက်ခွင့်ကို admin များ၊ staff များနှင့် သက်ဆိုင်ရာ member account ပိုင်ရှင်များကဲ့သို့ ခွင့်ပြုထားသော အသုံးပြုသူများထံသာ ကန့်သတ်ထားသင့်သည်။ လိုအပ်သော လုပ်ငန်းရည်ရွယ်ချက်မရှိဘဲ အပြင်ဘက်သို့ မျှဝေသင့်ခြင်း မရှိပါ။

+

သိုလှောင်ကာလ

+

လုပ်ငန်းဆိုင်ရာ မှတ်တမ်းများကို မည်မျှကြာ သိမ်းဆည်းမည်ကို စာကြည့်တိုက် သို့မဟုတ် deployment owner ဘက်မှ မူဝါဒများနှင့် ဥပဒေရေးရာ တာဝန်များအရ ဆုံးဖြတ်ရမည်ဖြစ်သည်။

+

ဆက်သွယ်ရန်

+

သီးသန့် library deployment တစ်ခုအတွက် ကိုယ်ရေးအချက်အလက်ဆိုင်ရာ မေးခွန်းများရှိပါက ထို deployment ကို စီမံသော library သို့မဟုတ် service owner ထံ ဆက်သွယ်ပါ။

+ `, + }, + auth: { + login: { + title: 'အကောင့်ဝင်ရန်', + description: + 'သင့် LibrarEase အကောင့်သို့ ဝင်ရောက်ရန် အီးမေးလ်နှင့် စကားဝှက်ကို ဖြည့်သွင်းပါ။', + emailLabel: 'အီးမေးလ်', + emailPlaceholder: 'ဥပမာ - mgmg@example.com', + passwordLabel: 'စကားဝှက်', + passwordPlaceholder: 'သင့်စကားဝှက်ကို ထည့်ပါ', + showPassword: 'စကားဝှက်ကို ပြပါ', + forgotPassword: 'စကားဝှက် မေ့သွားပါသလား?', + submit: 'ဝင်ရန်', + signupPrompt: 'အကောင့် မရှိသေးဘူးလား?', + signupLink: 'အကောင့်ဖွင့်ပါ', + }, + signup: { + title: 'အကောင့်ဖွင့်ရန်', + description: + 'LibrarEase ကို စတင်အသုံးပြုရန် လိုအပ်သော အချက်အလက်များကို အောက်တွင် ဖြည့်သွင်းပါ။', + nameLabel: 'အမည်', + namePlaceholder: 'ဥပမာ - မောင်မောင်', + emailLabel: 'အီးမေးလ်', + emailPlaceholder: 'ဥပမာ - mgmg@example.com', + passwordLabel: 'စကားဝှက်', + passwordPlaceholder: 'စကားဝှက်အသစ် သတ်မှတ်ပါ', + submit: 'အကောင့်ဖွင့်ပါ', + loginPrompt: 'အကောင့်ရှိပြီးသားလား?', + loginLink: 'ဝင်ရန်', + }, + forgotPassword: { + title: 'စကားဝှက် ပြန်လည်သတ်မှတ်ရန်', + description: + 'သင့်အကောင့်အတွက် စကားဝှက်ပြန်လည်သတ်မှတ်ရန် အီးမေးလ်လိပ်စာကို ဖြည့်သွင်းပါ။', + emailLabel: 'အီးမေးလ်', + emailPlaceholder: 'ဥပမာ - mgmg@example.com', + backToLogin: 'စကားဝှက်ကို မှတ်မိပါသလား?', + submit: 'စကားဝှက် ပြန်သတ်မှတ်ရန်', + signupPrompt: 'အကောင့် မရှိသေးဘူးလား?', + signupLink: 'အကောင့်ဖွင့်ပါ', + }, + }, + }, +} + +export async function getDictionary(locale: Locale): Promise { + return dictionaries[locale] +} + +export function getMetadataForPage( + locale: Locale, + path: LocalizedPath, + metadata: MetadataCopy, +): Metadata { + return { + title: metadata.title, + description: metadata.description, + alternates: getLocaleAlternates(locale, path), + } +} diff --git a/proxy.ts b/proxy.ts new file mode 100644 index 0000000..3d37689 --- /dev/null +++ b/proxy.ts @@ -0,0 +1,64 @@ +import type { NextRequest } from 'next/server' +import { NextResponse } from 'next/server' +import { + defaultLocale, + isLocale, + isLocalizedPublicPath, + LOCALE_HEADER, + localizePath, +} from '@/lib/i18n' + +function withLocaleHeader(request: NextRequest, locale: string) { + const requestHeaders = new Headers(request.headers) + requestHeaders.set(LOCALE_HEADER, locale) + return requestHeaders +} + +function redirectToLocale( + request: NextRequest, + pathname: + | '/' + | '/about' + | '/terms' + | '/privacy' + | '/login' + | '/signup' + | '/forgot-password', +) { + const url = request.nextUrl.clone() + url.pathname = localizePath(defaultLocale, pathname) + + return NextResponse.redirect(url) +} + +export function proxy(request: NextRequest) { + const { pathname } = request.nextUrl + const segments = pathname.split('/').filter(Boolean) + const firstSegment = segments[0] + + if (pathname === '/') { + return redirectToLocale(request, '/') + } + + if (isLocalizedPublicPath(pathname)) { + return redirectToLocale(request, pathname) + } + + if (firstSegment && isLocale(firstSegment)) { + return NextResponse.next({ + request: { + headers: withLocaleHeader(request, firstSegment), + }, + }) + } + + return NextResponse.next({ + request: { + headers: withLocaleHeader(request, defaultLocale), + }, + }) +} + +export const proxyConfig = { + matcher: ['/((?!api|_next|.*\\..*).*)'], +}