Add i18n foundation (fa-IR/en-US) and localize landing + auth.

Introduce path-prefixed locale routing under app/[lang] with a middleware
that detects locale from cookie/Accept-Language (default fa-IR) and
redirects. Add fa-IR (source of truth) and en-US dictionaries, a server
getDictionary, a client I18nProvider/useT, locale-aware Link + router
helpers, and a language switcher. The root [lang] layout sets html
lang/dir and the per-locale font (Peyda for fa, Inter for en).

Landing sections and the login/register/auth shell now read all copy from
the dictionaries; dashboard localization follows in a later commit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
keyhan
2026-06-11 12:05:03 +03:30
parent 2b16846f67
commit 34993d417f
48 changed files with 785 additions and 200 deletions
+63
View File
@@ -0,0 +1,63 @@
'use client';
import { useCallback, useMemo } from 'react';
import {
useRouter as useNextRouter,
usePathname as useNextPathname,
} from 'next/navigation';
import { locales, type Locale } from './config';
import { useLocale } from './I18nProvider';
const localePrefixes = locales.map((l) => `/${l}`);
/** Prefix an internal href with the given locale. Leaves external/hash/already-prefixed hrefs untouched. */
export function localizeHref(href: string, locale: Locale): string {
if (!href.startsWith('/')) return href; // external, hash, or relative
// already locale-prefixed?
if (localePrefixes.some((p) => href === p || href.startsWith(p + '/'))) return href;
return href === '/' ? `/${locale}` : `/${locale}${href}`;
}
/** Strip a leading locale segment from a pathname, returning the locale-agnostic path. */
export function stripLocale(pathname: string): string {
for (const p of localePrefixes) {
if (pathname === p) return '/';
if (pathname.startsWith(p + '/')) return pathname.slice(p.length);
}
return pathname;
}
/** Replace (or add) the locale segment of a pathname. Used by the language switcher. */
export function swapLocale(pathname: string, locale: Locale): string {
return localizeHref(stripLocale(pathname), locale);
}
/** A next/navigation router whose push/replace/prefetch auto-prefix the active locale. */
export function useLocalizedRouter() {
const router = useNextRouter();
const locale = useLocale();
return useMemo(
() => ({
...router,
push: (href: string, opts?: Parameters<typeof router.push>[1]) =>
router.push(localizeHref(href, locale), opts),
replace: (href: string, opts?: Parameters<typeof router.replace>[1]) =>
router.replace(localizeHref(href, locale), opts),
prefetch: (href: string, opts?: Parameters<typeof router.prefetch>[1]) =>
router.prefetch(localizeHref(href, locale), opts),
}),
[router, locale],
);
}
/** The current pathname with the locale segment stripped (e.g. /dashboard/apps). */
export function usePathname(): string {
const pathname = useNextPathname();
return useMemo(() => stripLocale(pathname ?? '/'), [pathname]);
}
/** Returns a memoized localizer bound to the active locale. */
export function useLocalize() {
const locale = useLocale();
return useCallback((href: string) => localizeHref(href, locale), [locale]);
}