add i18n
This commit is contained in:
parent
7024c76f3a
commit
8d24d3c36e
124
messages/en.json
Normal file
124
messages/en.json
Normal file
@ -0,0 +1,124 @@
|
||||
{
|
||||
"navigation": {
|
||||
"home": "Home",
|
||||
"studio": "Studio",
|
||||
"profile": "Profile",
|
||||
"signIn": "Sign In",
|
||||
"signUp": "Sign Up",
|
||||
"signOut": "Sign Out"
|
||||
},
|
||||
"common": {
|
||||
"loading": "Loading...",
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
"delete": "Delete",
|
||||
"edit": "Edit",
|
||||
"create": "Create",
|
||||
"search": "Search",
|
||||
"filter": "Filter",
|
||||
"language": "Language",
|
||||
"theme": "Theme",
|
||||
"light": "Light",
|
||||
"dark": "Dark",
|
||||
"system": "System"
|
||||
},
|
||||
"auth": {
|
||||
"signIn": "Sign In",
|
||||
"signUp": "Sign Up",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"username": "Username",
|
||||
"confirmPassword": "Confirm Password",
|
||||
"forgotPassword": "Forgot Password?",
|
||||
"noAccount": "Don't have an account?",
|
||||
"hasAccount": "Already have an account?",
|
||||
"signInWithGoogle": "Sign in with Google",
|
||||
"signUpWithGoogle": "Sign up with Google",
|
||||
"signInTitle": "Welcome Back",
|
||||
"signInSubtitle": "Sign in to your Prmbr account",
|
||||
"signUpTitle": "Create Account",
|
||||
"signUpSubtitle": "Join Prmbr - AI Prompt Studio"
|
||||
},
|
||||
"profile": {
|
||||
"title": "Profile Settings",
|
||||
"personalInfo": "Personal Information",
|
||||
"accountSettings": "Account Settings",
|
||||
"preferences": "Preferences",
|
||||
"username": "Username",
|
||||
"email": "Email",
|
||||
"bio": "Bio",
|
||||
"avatar": "Avatar",
|
||||
"language": "Language",
|
||||
"changePassword": "Change Password",
|
||||
"currentPassword": "Current Password",
|
||||
"newPassword": "New Password",
|
||||
"confirmNewPassword": "Confirm New Password"
|
||||
},
|
||||
"studio": {
|
||||
"title": "AI Prompt Studio",
|
||||
"myPrompts": "My Prompts",
|
||||
"createPrompt": "Create Prompt",
|
||||
"promptName": "Prompt Name",
|
||||
"promptContent": "Prompt Content",
|
||||
"promptAlbum": "Album",
|
||||
"promptTag": "Tags",
|
||||
"version": "Version",
|
||||
"testRun": "Test Run",
|
||||
"versions": "Versions",
|
||||
"createVersion": "Create Version",
|
||||
"runTest": "Run Test",
|
||||
"noPrompts": "No prompts yet",
|
||||
"createFirstPrompt": "Create your first prompt to get started"
|
||||
},
|
||||
"home": {
|
||||
"hero": {
|
||||
"title": "Prmbr - AI Prompt Studio",
|
||||
"subtitle": "Build, manage, and optimize your AI prompts with version control and testing capabilities",
|
||||
"getStarted": "Get Started",
|
||||
"learnMore": "Learn More"
|
||||
},
|
||||
"features": {
|
||||
"title": "Features",
|
||||
"promptManager": {
|
||||
"title": "Prompt Management",
|
||||
"description": "Organize your prompts with albums, tags, and powerful search capabilities"
|
||||
},
|
||||
"versionControl": {
|
||||
"title": "Version Control",
|
||||
"description": "Track changes and manage different versions of your prompts"
|
||||
},
|
||||
"testing": {
|
||||
"title": "Testing & Validation",
|
||||
"description": "Run tests on your prompts to ensure optimal performance"
|
||||
}
|
||||
}
|
||||
},
|
||||
"pricing": {
|
||||
"title": "Pricing Plans",
|
||||
"free": {
|
||||
"title": "Free",
|
||||
"price": "$0",
|
||||
"features": [
|
||||
"20 Prompt Limit",
|
||||
"3 Versions per Prompt",
|
||||
"$5 AI Credit Monthly"
|
||||
]
|
||||
},
|
||||
"pro": {
|
||||
"title": "Pro",
|
||||
"price": "$19.9",
|
||||
"features": [
|
||||
"500 Prompt Limit",
|
||||
"10 Versions per Prompt",
|
||||
"$20 AI Credit Monthly"
|
||||
]
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"generic": "Something went wrong. Please try again.",
|
||||
"network": "Network error. Please check your connection.",
|
||||
"unauthorized": "You are not authorized to perform this action.",
|
||||
"notFound": "The requested resource was not found.",
|
||||
"validationError": "Please check your input and try again."
|
||||
}
|
||||
}
|
124
messages/zh.json
Normal file
124
messages/zh.json
Normal file
@ -0,0 +1,124 @@
|
||||
{
|
||||
"navigation": {
|
||||
"home": "首页",
|
||||
"studio": "工作室",
|
||||
"profile": "个人资料",
|
||||
"signIn": "登录",
|
||||
"signUp": "注册",
|
||||
"signOut": "退出登录"
|
||||
},
|
||||
"common": {
|
||||
"loading": "加载中...",
|
||||
"save": "保存",
|
||||
"cancel": "取消",
|
||||
"delete": "删除",
|
||||
"edit": "编辑",
|
||||
"create": "创建",
|
||||
"search": "搜索",
|
||||
"filter": "筛选",
|
||||
"language": "语言",
|
||||
"theme": "主题",
|
||||
"light": "浅色",
|
||||
"dark": "深色",
|
||||
"system": "跟随系统"
|
||||
},
|
||||
"auth": {
|
||||
"signIn": "登录",
|
||||
"signUp": "注册",
|
||||
"email": "邮箱",
|
||||
"password": "密码",
|
||||
"username": "用户名",
|
||||
"confirmPassword": "确认密码",
|
||||
"forgotPassword": "忘记密码?",
|
||||
"noAccount": "还没有账户?",
|
||||
"hasAccount": "已有账户?",
|
||||
"signInWithGoogle": "使用 Google 登录",
|
||||
"signUpWithGoogle": "使用 Google 注册",
|
||||
"signInTitle": "欢迎回来",
|
||||
"signInSubtitle": "登录您的 Prmbr 账户",
|
||||
"signUpTitle": "创建账户",
|
||||
"signUpSubtitle": "加入 Prmbr - AI 提示词工作室"
|
||||
},
|
||||
"profile": {
|
||||
"title": "个人设置",
|
||||
"personalInfo": "个人信息",
|
||||
"accountSettings": "账户设置",
|
||||
"preferences": "偏好设置",
|
||||
"username": "用户名",
|
||||
"email": "邮箱",
|
||||
"bio": "个人简介",
|
||||
"avatar": "头像",
|
||||
"language": "语言",
|
||||
"changePassword": "修改密码",
|
||||
"currentPassword": "当前密码",
|
||||
"newPassword": "新密码",
|
||||
"confirmNewPassword": "确认新密码"
|
||||
},
|
||||
"studio": {
|
||||
"title": "AI 提示词工作室",
|
||||
"myPrompts": "我的提示词",
|
||||
"createPrompt": "创建提示词",
|
||||
"promptName": "提示词名称",
|
||||
"promptContent": "提示词内容",
|
||||
"promptAlbum": "专辑",
|
||||
"promptTag": "标签",
|
||||
"version": "版本",
|
||||
"testRun": "测试运行",
|
||||
"versions": "版本管理",
|
||||
"createVersion": "创建版本",
|
||||
"runTest": "运行测试",
|
||||
"noPrompts": "暂无提示词",
|
||||
"createFirstPrompt": "创建您的第一个提示词开始使用"
|
||||
},
|
||||
"home": {
|
||||
"hero": {
|
||||
"title": "Prmbr - AI 提示词工作室",
|
||||
"subtitle": "构建、管理和优化您的 AI 提示词,支持版本控制和测试功能",
|
||||
"getStarted": "立即开始",
|
||||
"learnMore": "了解更多"
|
||||
},
|
||||
"features": {
|
||||
"title": "功能特性",
|
||||
"promptManager": {
|
||||
"title": "提示词管理",
|
||||
"description": "使用专辑、标签和强大的搜索功能组织您的提示词"
|
||||
},
|
||||
"versionControl": {
|
||||
"title": "版本控制",
|
||||
"description": "跟踪变更并管理提示词的不同版本"
|
||||
},
|
||||
"testing": {
|
||||
"title": "测试与验证",
|
||||
"description": "对您的提示词进行测试以确保最佳性能"
|
||||
}
|
||||
}
|
||||
},
|
||||
"pricing": {
|
||||
"title": "价格方案",
|
||||
"free": {
|
||||
"title": "免费版",
|
||||
"price": "免费",
|
||||
"features": [
|
||||
"20 个提示词限制",
|
||||
"每个提示词 3 个版本",
|
||||
"每月 5 美元 AI 积分"
|
||||
]
|
||||
},
|
||||
"pro": {
|
||||
"title": "专业版",
|
||||
"price": "$19.9",
|
||||
"features": [
|
||||
"500 个提示词限制",
|
||||
"每个提示词 10 个版本",
|
||||
"每月 20 美元 AI 积分"
|
||||
]
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"generic": "出现错误,请重试。",
|
||||
"network": "网络错误,请检查您的网络连接。",
|
||||
"unauthorized": "您无权执行此操作。",
|
||||
"notFound": "未找到请求的资源。",
|
||||
"validationError": "请检查您的输入并重试。"
|
||||
}
|
||||
}
|
@ -1,5 +1,31 @@
|
||||
import { createServerClient } from '@supabase/ssr'
|
||||
import { NextResponse, type NextRequest } from 'next/server'
|
||||
import { locales, defaultLocale } from './src/i18n/config'
|
||||
|
||||
function getLocaleFromHeaders(acceptLanguage: string | null): string {
|
||||
if (!acceptLanguage) return defaultLocale;
|
||||
|
||||
const languages = acceptLanguage
|
||||
.split(',')
|
||||
.map(lang => lang.split(';')[0].trim().toLowerCase());
|
||||
|
||||
// Check for exact matches first
|
||||
for (const lang of languages) {
|
||||
if (locales.includes(lang as any)) {
|
||||
return lang;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for language prefix matches (e.g., 'zh-CN' -> 'zh')
|
||||
for (const lang of languages) {
|
||||
const prefix = lang.split('-')[0];
|
||||
if (locales.includes(prefix as any)) {
|
||||
return prefix;
|
||||
}
|
||||
}
|
||||
|
||||
return defaultLocale;
|
||||
}
|
||||
|
||||
export async function middleware(request: NextRequest) {
|
||||
let response = NextResponse.next({
|
||||
@ -8,6 +34,20 @@ export async function middleware(request: NextRequest) {
|
||||
},
|
||||
})
|
||||
|
||||
// Handle locale detection and cookie setting
|
||||
const currentLocale = request.cookies.get('locale')?.value;
|
||||
const acceptLanguage = request.headers.get('accept-language');
|
||||
|
||||
if (!currentLocale) {
|
||||
const detectedLocale = getLocaleFromHeaders(acceptLanguage);
|
||||
response.cookies.set('locale', detectedLocale, {
|
||||
maxAge: 60 * 60 * 24 * 365, // 1 year
|
||||
httpOnly: false,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
});
|
||||
}
|
||||
|
||||
const supabase = createServerClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||
|
@ -1,4 +1,7 @@
|
||||
import type { NextConfig } from "next";
|
||||
import createNextIntlPlugin from 'next-intl/plugin';
|
||||
|
||||
const withNextIntl = createNextIntlPlugin('./src/i18n/config.ts');
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
images: {
|
||||
@ -31,4 +34,4 @@ const nextConfig: NextConfig = {
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
export default withNextIntl(nextConfig);
|
||||
|
135
package-lock.json
generated
135
package-lock.json
generated
@ -16,6 +16,7 @@
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.532.0",
|
||||
"next": "15.4.4",
|
||||
"next-intl": "^4.3.4",
|
||||
"prisma": "^6.12.0",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
@ -234,6 +235,66 @@
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@formatjs/ecma402-abstract": {
|
||||
"version": "2.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.4.tgz",
|
||||
"integrity": "sha512-qrycXDeaORzIqNhBOx0btnhpD1c+/qFIHAN9znofuMJX6QBwtbrmlpWfD4oiUUD2vJUOIYFA/gYtg2KAMGG7sA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@formatjs/fast-memoize": "2.2.7",
|
||||
"@formatjs/intl-localematcher": "0.6.1",
|
||||
"decimal.js": "^10.4.3",
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@formatjs/ecma402-abstract/node_modules/@formatjs/intl-localematcher": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.1.tgz",
|
||||
"integrity": "sha512-ePEgLgVCqi2BBFnTMWPfIghu6FkbZnnBVhO2sSxvLfrdFw7wCHAHiDoM2h4NRgjbaY7+B7HgOLZGkK187pZTZg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@formatjs/fast-memoize": {
|
||||
"version": "2.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz",
|
||||
"integrity": "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@formatjs/icu-messageformat-parser": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.2.tgz",
|
||||
"integrity": "sha512-AfiMi5NOSo2TQImsYAg8UYddsNJ/vUEv/HaNqiFjnI3ZFfWihUtD5QtuX6kHl8+H+d3qvnE/3HZrfzgdWpsLNA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@formatjs/ecma402-abstract": "2.3.4",
|
||||
"@formatjs/icu-skeleton-parser": "1.8.14",
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@formatjs/icu-skeleton-parser": {
|
||||
"version": "1.8.14",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.14.tgz",
|
||||
"integrity": "sha512-i4q4V4qslThK4Ig8SxyD76cp3+QJ3sAqr7f6q9VVfeGtxG9OhiAk3y9XF6Q41OymsKzsGQ6OQQoJNY4/lI8TcQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@formatjs/ecma402-abstract": "2.3.4",
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@formatjs/intl-localematcher": {
|
||||
"version": "0.5.10",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.10.tgz",
|
||||
"integrity": "sha512-af3qATX+m4Rnd9+wHcjJ4w2ijq+rAVP3CCinJQvFv1kgSu1W6jypUmvleJxcewdxmutM8dmIRZFxO/IQBZmP2Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "2"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanfs/core": {
|
||||
"version": "0.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||
@ -1074,6 +1135,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@schummar/icu-type-parser": {
|
||||
"version": "1.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@schummar/icu-type-parser/-/icu-type-parser-1.21.5.tgz",
|
||||
"integrity": "sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@stitches/core": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@stitches/core/-/core-1.2.8.tgz",
|
||||
@ -2744,6 +2811,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/decimal.js": {
|
||||
"version": "10.6.0",
|
||||
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
|
||||
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/deep-is": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
||||
@ -3930,6 +4003,18 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/intl-messageformat": {
|
||||
"version": "10.7.16",
|
||||
"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.16.tgz",
|
||||
"integrity": "sha512-UmdmHUmp5CIKKjSoE10la5yfU+AYJAaiYLsodbjL4lji83JNvgOQUjGaGhGrpFCb0Uh7sl7qfP1IyILa8Z40ug==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@formatjs/ecma402-abstract": "2.3.4",
|
||||
"@formatjs/fast-memoize": "2.2.7",
|
||||
"@formatjs/icu-messageformat-parser": "2.11.2",
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-array-buffer": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
||||
@ -4945,6 +5030,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/negotiator": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
|
||||
"integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/next": {
|
||||
"version": "15.4.4",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-15.4.4.tgz",
|
||||
@ -4997,6 +5091,33 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/next-intl": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.3.4.tgz",
|
||||
"integrity": "sha512-VWLIDlGbnL/o4LnveJTJD1NOYN8lh3ZAGTWw2krhfgg53as3VsS4jzUVnArJdqvwtlpU/2BIDbWTZ7V4o1jFEw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/amannn"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@formatjs/intl-localematcher": "^0.5.4",
|
||||
"negotiator": "^1.0.0",
|
||||
"use-intl": "^4.3.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0",
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/next/node_modules/postcss": {
|
||||
"version": "8.4.31",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
||||
@ -6343,6 +6464,20 @@
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/use-intl": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.3.4.tgz",
|
||||
"integrity": "sha512-sHfiU0QeJ1rirNWRxvCyvlSh9+NczcOzRnPyMeo2rtHXhVnBsvMRjE+UG4eh3lRhCxrvcqei/I0lBxsc59on1w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@formatjs/fast-memoize": "^2.2.0",
|
||||
"@schummar/icu-type-parser": "1.21.5",
|
||||
"intl-messageformat": "^10.5.14"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
|
@ -17,6 +17,7 @@
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.532.0",
|
||||
"next": "15.4.4",
|
||||
"next-intl": "^4.3.4",
|
||||
"prisma": "^6.12.0",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
|
@ -1,5 +1,7 @@
|
||||
import type { Metadata, Viewport } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import { NextIntlClientProvider } from 'next-intl';
|
||||
import { getMessages, getLocale } from 'next-intl/server';
|
||||
import { ThemeProvider } from "@/contexts/ThemeContext";
|
||||
import "./globals.css";
|
||||
|
||||
@ -54,13 +56,16 @@ export const viewport: Viewport = {
|
||||
],
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const locale = await getLocale();
|
||||
const messages = await getMessages();
|
||||
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<html lang={locale} suppressHydrationWarning>
|
||||
<head>
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<script
|
||||
@ -84,7 +89,9 @@ export default function RootLayout({
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased min-h-screen`}
|
||||
>
|
||||
<ThemeProvider defaultTheme="system" storageKey="prmbr-theme">
|
||||
{children}
|
||||
<NextIntlClientProvider messages={messages}>
|
||||
{children}
|
||||
</NextIntlClientProvider>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,10 +1,14 @@
|
||||
'use client'
|
||||
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { Header } from '@/components/layout/Header'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Zap, Target, Layers, BarChart3, Check } from 'lucide-react'
|
||||
|
||||
export default function Home() {
|
||||
const t = useTranslations('home')
|
||||
const tPricing = useTranslations('pricing')
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<Header />
|
||||
@ -14,18 +18,17 @@ export default function Home() {
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pt-20 pb-24">
|
||||
<div className="text-center">
|
||||
<h1 className="text-4xl md:text-6xl font-bold text-foreground mb-6">
|
||||
AI Prompt Studio
|
||||
{t('hero.title')}
|
||||
</h1>
|
||||
<p className="text-xl text-muted-foreground mb-8 max-w-3xl mx-auto">
|
||||
Build, test, and manage your AI prompts with precision.
|
||||
Version control, collaboration tools, and analytics in one professional platform.
|
||||
{t('hero.subtitle')}
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Button size="lg" onClick={() => window.location.href = '/studio'}>
|
||||
Start Building
|
||||
{t('hero.getStarted')}
|
||||
</Button>
|
||||
<Button variant="outline" size="lg">
|
||||
View Showcase
|
||||
{t('hero.learnMore')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@ -37,7 +40,7 @@ export default function Home() {
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-3xl font-bold text-foreground mb-4">
|
||||
Everything you need to master prompts
|
||||
{t('features.title')}
|
||||
</h2>
|
||||
<p className="text-xl text-muted-foreground">
|
||||
Professional tools for prompt engineering and management
|
||||
@ -48,30 +51,30 @@ export default function Home() {
|
||||
<div className="bg-card p-6 rounded-lg shadow-sm border">
|
||||
<Target className="h-12 w-12 text-primary mb-4" />
|
||||
<h3 className="text-lg font-semibold text-card-foreground mb-2">
|
||||
Prompt Builder
|
||||
{t('features.promptManager.title')}
|
||||
</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Intuitive interface for crafting and refining AI prompts with real-time preview.
|
||||
{t('features.promptManager.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-card p-6 rounded-lg shadow-sm border">
|
||||
<Layers className="h-12 w-12 text-primary mb-4" />
|
||||
<h3 className="text-lg font-semibold text-card-foreground mb-2">
|
||||
Version Control
|
||||
{t('features.versionControl.title')}
|
||||
</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Track changes, compare versions, and rollback to previous iterations seamlessly.
|
||||
{t('features.versionControl.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-card p-6 rounded-lg shadow-sm border">
|
||||
<BarChart3 className="h-12 w-12 text-primary mb-4" />
|
||||
<h3 className="text-lg font-semibold text-card-foreground mb-2">
|
||||
Test & Analytics
|
||||
{t('features.testing.title')}
|
||||
</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Run tests, analyze performance, and optimize your prompts with detailed metrics.
|
||||
{t('features.testing.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -93,7 +96,7 @@ export default function Home() {
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-3xl font-bold text-foreground mb-4">
|
||||
Simple, transparent pricing
|
||||
{tPricing('title')}
|
||||
</h2>
|
||||
<p className="text-xl text-muted-foreground">
|
||||
Choose the plan that fits your needs
|
||||
@ -103,22 +106,22 @@ export default function Home() {
|
||||
<div className="grid md:grid-cols-2 gap-8 max-w-4xl mx-auto">
|
||||
<div className="bg-card p-8 rounded-lg shadow-sm border">
|
||||
<div className="text-center mb-6">
|
||||
<h3 className="text-2xl font-bold text-card-foreground mb-2">Free</h3>
|
||||
<div className="text-4xl font-bold text-card-foreground mb-2">$0</div>
|
||||
<h3 className="text-2xl font-bold text-card-foreground mb-2">{tPricing('free.title')}</h3>
|
||||
<div className="text-4xl font-bold text-card-foreground mb-2">{tPricing('free.price')}</div>
|
||||
<p className="text-muted-foreground">Perfect for getting started</p>
|
||||
</div>
|
||||
<ul className="space-y-3 mb-8">
|
||||
<li className="flex items-center">
|
||||
<Check className="h-5 w-5 text-green-500 mr-3" />
|
||||
<span className="text-card-foreground">20 prompts</span>
|
||||
<span className="text-card-foreground">20 Prompt Limit</span>
|
||||
</li>
|
||||
<li className="flex items-center">
|
||||
<Check className="h-5 w-5 text-green-500 mr-3" />
|
||||
<span className="text-card-foreground">3 versions per prompt</span>
|
||||
<span className="text-card-foreground">3 Versions per Prompt</span>
|
||||
</li>
|
||||
<li className="flex items-center">
|
||||
<Check className="h-5 w-5 text-green-500 mr-3" />
|
||||
<span className="text-card-foreground">$5 AI credits monthly</span>
|
||||
<span className="text-card-foreground">$5 AI Credit Monthly</span>
|
||||
</li>
|
||||
</ul>
|
||||
<Button className="w-full" onClick={() => window.location.href = '/signup'}>
|
||||
@ -131,26 +134,26 @@ export default function Home() {
|
||||
Popular
|
||||
</div>
|
||||
<div className="text-center mb-6">
|
||||
<h3 className="text-2xl font-bold mb-2">Pro</h3>
|
||||
<div className="text-4xl font-bold mb-2">$19.9</div>
|
||||
<h3 className="text-2xl font-bold mb-2">{tPricing('pro.title')}</h3>
|
||||
<div className="text-4xl font-bold mb-2">{tPricing('pro.price')}</div>
|
||||
<p className="text-primary-foreground/80">per month</p>
|
||||
</div>
|
||||
<ul className="space-y-3 mb-8">
|
||||
<li className="flex items-center">
|
||||
<Check className="h-5 w-5 text-primary-foreground/80 mr-3" />
|
||||
<span>500 prompts</span>
|
||||
<span>500 Prompt Limit</span>
|
||||
</li>
|
||||
<li className="flex items-center">
|
||||
<Check className="h-5 w-5 text-primary-foreground/80 mr-3" />
|
||||
<span>10 versions per prompt</span>
|
||||
<span>10 Versions per Prompt</span>
|
||||
</li>
|
||||
<li className="flex items-center">
|
||||
<Check className="h-5 w-5 text-primary-foreground/80 mr-3" />
|
||||
<span>$20 AI credits monthly</span>
|
||||
<span>$20 AI Credit Monthly</span>
|
||||
</li>
|
||||
<li className="flex items-center">
|
||||
<Check className="h-5 w-5 text-primary-foreground/80 mr-3" />
|
||||
<span>Priority support</span>
|
||||
<span>Priority Support</span>
|
||||
</li>
|
||||
</ul>
|
||||
<Button variant="outline" className="w-full bg-primary-foreground text-primary hover:bg-primary-foreground/90" onClick={() => window.location.href = '/signup'}>
|
||||
|
@ -13,7 +13,6 @@ import {
|
||||
Save,
|
||||
Copy,
|
||||
Settings,
|
||||
FileText,
|
||||
Folder,
|
||||
Plus,
|
||||
Search,
|
||||
@ -75,7 +74,7 @@ export default function StudioPage() {
|
||||
|
||||
// Mock response
|
||||
setTestResult(`Test result for: "${promptContent.substring(0, 50)}${promptContent.length > 50 ? '...' : ''}"\n\nThis is a simulated response from the AI model. In a real implementation, this would be the actual output from your chosen AI provider (OpenAI, Anthropic, etc.).\n\nResponse quality: Good\nToken usage: 150 tokens\nLatency: 1.2s`)
|
||||
} catch (error) {
|
||||
} catch {
|
||||
setTestResult('Error: Failed to run prompt. Please try again.')
|
||||
} finally {
|
||||
setIsRunning(false)
|
||||
@ -308,7 +307,7 @@ Please write a professional email that..."
|
||||
<div>
|
||||
<Zap className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
|
||||
<p className="text-muted-foreground">
|
||||
Click "Run Test" to see your prompt results
|
||||
Click "Run Test" to see your prompt results
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,13 +1,17 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { ThemeToggle, MobileThemeToggle } from '@/components/ui/theme-toggle'
|
||||
import { LanguageToggle, MobileLanguageToggle } from '@/components/ui/language-toggle'
|
||||
import { Menu, X, Zap } from 'lucide-react'
|
||||
|
||||
export function Header() {
|
||||
const { user, signOut } = useAuth()
|
||||
const t = useTranslations('navigation')
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
||||
|
||||
return (
|
||||
@ -24,38 +28,39 @@ export function Header() {
|
||||
{/* Desktop Navigation */}
|
||||
<div className="hidden md:block">
|
||||
<div className="ml-10 flex items-baseline space-x-4">
|
||||
<a href="#features" className="text-muted-foreground hover:text-foreground px-3 py-2 text-sm font-medium transition-colors">
|
||||
Features
|
||||
</a>
|
||||
<Link href="/" className="text-muted-foreground hover:text-foreground px-3 py-2 text-sm font-medium transition-colors">
|
||||
{t('home')}
|
||||
</Link>
|
||||
<Link href="/studio" className="text-muted-foreground hover:text-foreground px-3 py-2 text-sm font-medium transition-colors">
|
||||
{t('studio')}
|
||||
</Link>
|
||||
<a href="#pricing" className="text-muted-foreground hover:text-foreground px-3 py-2 text-sm font-medium transition-colors">
|
||||
Pricing
|
||||
</a>
|
||||
<a href="#showcase" className="text-muted-foreground hover:text-foreground px-3 py-2 text-sm font-medium transition-colors">
|
||||
Showcase
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Desktop Auth */}
|
||||
<div className="hidden md:block">
|
||||
<div className="flex items-center space-x-2">
|
||||
<LanguageToggle variant="dropdown" showLabel={false} />
|
||||
<ThemeToggle variant="dropdown" showLabel={false} />
|
||||
{user ? (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button variant="ghost" size="sm" onClick={() => window.location.href = '/profile'}>
|
||||
Profile
|
||||
{t('profile')}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={signOut}>
|
||||
Sign Out
|
||||
{t('signOut')}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button variant="ghost" onClick={() => window.location.href = '/signin'}>
|
||||
Sign In
|
||||
{t('signIn')}
|
||||
</Button>
|
||||
<Button onClick={() => window.location.href = '/signup'}>
|
||||
Sign Up
|
||||
{t('signUp')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
@ -64,6 +69,7 @@ export function Header() {
|
||||
|
||||
{/* Mobile controls */}
|
||||
<div className="md:hidden flex items-center space-x-2">
|
||||
<LanguageToggle variant="button" />
|
||||
<ThemeToggle variant="button" />
|
||||
<button
|
||||
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||
@ -79,18 +85,23 @@ export function Header() {
|
||||
{mobileMenuOpen && (
|
||||
<div className="md:hidden">
|
||||
<div className="px-2 pt-2 pb-3 space-y-1 border-t">
|
||||
<a href="#features" className="block text-muted-foreground hover:text-foreground px-3 py-2 text-base font-medium transition-colors rounded-md hover:bg-accent">
|
||||
Features
|
||||
</a>
|
||||
<Link href="/" className="block text-muted-foreground hover:text-foreground px-3 py-2 text-base font-medium transition-colors rounded-md hover:bg-accent">
|
||||
{t('home')}
|
||||
</Link>
|
||||
<Link href="/studio" className="block text-muted-foreground hover:text-foreground px-3 py-2 text-base font-medium transition-colors rounded-md hover:bg-accent">
|
||||
{t('studio')}
|
||||
</Link>
|
||||
<a href="#pricing" className="block text-muted-foreground hover:text-foreground px-3 py-2 text-base font-medium transition-colors rounded-md hover:bg-accent">
|
||||
Pricing
|
||||
</a>
|
||||
<a href="#showcase" className="block text-muted-foreground hover:text-foreground px-3 py-2 text-base font-medium transition-colors rounded-md hover:bg-accent">
|
||||
Showcase
|
||||
</a>
|
||||
|
||||
{/* Mobile Language Toggle */}
|
||||
<div className="pt-4 pb-2 border-t">
|
||||
<MobileLanguageToggle />
|
||||
</div>
|
||||
|
||||
{/* Mobile Theme Toggle */}
|
||||
<div className="pt-4 pb-2 border-t">
|
||||
<div className="pt-2 pb-2 border-t">
|
||||
<MobileThemeToggle />
|
||||
</div>
|
||||
|
||||
@ -98,19 +109,19 @@ export function Header() {
|
||||
{user ? (
|
||||
<div className="space-y-2">
|
||||
<Button variant="ghost" className="w-full justify-start" onClick={() => window.location.href = '/profile'}>
|
||||
Profile
|
||||
{t('profile')}
|
||||
</Button>
|
||||
<Button variant="outline" className="w-full" onClick={signOut}>
|
||||
Sign Out
|
||||
{t('signOut')}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<Button variant="ghost" className="w-full" onClick={() => window.location.href = '/signin'}>
|
||||
Sign In
|
||||
{t('signIn')}
|
||||
</Button>
|
||||
<Button className="w-full" onClick={() => window.location.href = '/signup'}>
|
||||
Sign Up
|
||||
{t('signUp')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
153
src/components/ui/language-toggle.tsx
Normal file
153
src/components/ui/language-toggle.tsx
Normal file
@ -0,0 +1,153 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Languages, Check } from 'lucide-react'
|
||||
|
||||
interface LanguageToggleProps {
|
||||
variant?: 'dropdown' | 'button'
|
||||
showLabel?: boolean
|
||||
}
|
||||
|
||||
export function LanguageToggle({ variant = 'dropdown', showLabel = true }: LanguageToggleProps) {
|
||||
const t = useTranslations('common')
|
||||
const [currentLocale, setCurrentLocale] = useState<string>('en')
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false)
|
||||
|
||||
const languages = [
|
||||
{ code: 'en', name: 'English', nativeName: 'English' },
|
||||
{ code: 'zh', name: 'Chinese', nativeName: '中文' }
|
||||
]
|
||||
|
||||
useEffect(() => {
|
||||
// Get current locale from cookie
|
||||
const localeCookie = document.cookie
|
||||
.split('; ')
|
||||
.find(row => row.startsWith('locale='))
|
||||
?.split('=')[1];
|
||||
|
||||
if (localeCookie) {
|
||||
setCurrentLocale(localeCookie)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const changeLanguage = (locale: string) => {
|
||||
// Set cookie
|
||||
document.cookie = `locale=${locale}; path=/; max-age=${60 * 60 * 24 * 365}; samesite=lax`
|
||||
|
||||
// Reload page to apply new locale
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
if (variant === 'button') {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const nextLocale = currentLocale === 'en' ? 'zh' : 'en'
|
||||
changeLanguage(nextLocale)
|
||||
}}
|
||||
className="h-9 w-9 px-0"
|
||||
>
|
||||
<Languages className="h-4 w-4" />
|
||||
<span className="sr-only">{t('language')}</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setDropdownOpen(!dropdownOpen)}
|
||||
className="h-9 gap-2"
|
||||
>
|
||||
<Languages className="h-4 w-4" />
|
||||
{showLabel && <span className="hidden sm:inline">{t('language')}</span>}
|
||||
</Button>
|
||||
|
||||
{dropdownOpen && (
|
||||
<>
|
||||
<div
|
||||
className="fixed inset-0 z-40"
|
||||
onClick={() => setDropdownOpen(false)}
|
||||
/>
|
||||
<div className="absolute right-0 top-full z-50 mt-2 w-48 rounded-md border bg-popover p-1 text-popover-foreground shadow-md">
|
||||
{languages.map((language) => (
|
||||
<button
|
||||
key={language.code}
|
||||
onClick={() => {
|
||||
changeLanguage(language.code)
|
||||
setDropdownOpen(false)
|
||||
}}
|
||||
className="relative flex w-full items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground"
|
||||
>
|
||||
<span className="flex-1 text-left">
|
||||
{language.nativeName}
|
||||
</span>
|
||||
{currentLocale === language.code && (
|
||||
<Check className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function MobileLanguageToggle() {
|
||||
const t = useTranslations('common')
|
||||
const [currentLocale, setCurrentLocale] = useState<string>('en')
|
||||
|
||||
const languages = [
|
||||
{ code: 'en', name: 'English', nativeName: 'English' },
|
||||
{ code: 'zh', name: 'Chinese', nativeName: '中文' }
|
||||
]
|
||||
|
||||
useEffect(() => {
|
||||
// Get current locale from cookie
|
||||
const localeCookie = document.cookie
|
||||
.split('; ')
|
||||
.find(row => row.startsWith('locale='))
|
||||
?.split('=')[1];
|
||||
|
||||
if (localeCookie) {
|
||||
setCurrentLocale(localeCookie)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const changeLanguage = (locale: string) => {
|
||||
// Set cookie
|
||||
document.cookie = `locale=${locale}; path=/; max-age=${60 * 60 * 24 * 365}; samesite=lax`
|
||||
|
||||
// Reload page to apply new locale
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 px-3 py-2">
|
||||
<Languages className="h-4 w-4" />
|
||||
<span className="text-sm font-medium">{t('language')}</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{languages.map((language) => (
|
||||
<Button
|
||||
key={language.code}
|
||||
variant={currentLocale === language.code ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => changeLanguage(language.code)}
|
||||
className="justify-start"
|
||||
>
|
||||
{language.nativeName}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
66
src/i18n/config.ts
Normal file
66
src/i18n/config.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import { getRequestConfig } from 'next-intl/server';
|
||||
import { headers } from 'next/headers';
|
||||
|
||||
export const locales = ['en', 'zh'] as const;
|
||||
export const defaultLocale = 'en' as const;
|
||||
|
||||
export type Locale = (typeof locales)[number];
|
||||
|
||||
async function getLocaleFromHeaders(): Promise<Locale> {
|
||||
const headersList = await headers();
|
||||
const acceptLanguage = headersList.get('accept-language');
|
||||
|
||||
if (acceptLanguage) {
|
||||
// Parse accept-language header
|
||||
const languages = acceptLanguage
|
||||
.split(',')
|
||||
.map(lang => lang.split(';')[0].trim().toLowerCase());
|
||||
|
||||
// Check for exact matches first
|
||||
for (const lang of languages) {
|
||||
if (locales.includes(lang as Locale)) {
|
||||
return lang as Locale;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for language prefix matches (e.g., 'zh-CN' -> 'zh')
|
||||
for (const lang of languages) {
|
||||
const prefix = lang.split('-')[0];
|
||||
if (locales.includes(prefix as Locale)) {
|
||||
return prefix as Locale;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return defaultLocale;
|
||||
}
|
||||
|
||||
export default getRequestConfig(async () => {
|
||||
// Get locale from user preference (cookie) or browser headers
|
||||
const headersList = await headers();
|
||||
const cookieHeader = headersList.get('cookie');
|
||||
|
||||
let locale: Locale = defaultLocale;
|
||||
|
||||
// Parse locale from cookies
|
||||
if (cookieHeader) {
|
||||
const cookies = cookieHeader.split(';');
|
||||
const localeCookie = cookies.find(cookie => cookie.trim().startsWith('locale='));
|
||||
if (localeCookie) {
|
||||
const localeValue = localeCookie.split('=')[1];
|
||||
if (locales.includes(localeValue as Locale)) {
|
||||
locale = localeValue as Locale;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no locale found in cookies, detect from headers
|
||||
if (locale === defaultLocale) {
|
||||
locale = await getLocaleFromHeaders();
|
||||
}
|
||||
|
||||
return {
|
||||
locale,
|
||||
messages: (await import(`../../messages/${locale}.json`)).default
|
||||
};
|
||||
});
|
Loading…
Reference in New Issue
Block a user