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 { createServerClient } from '@supabase/ssr'
|
||||||
import { NextResponse, type NextRequest } from 'next/server'
|
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) {
|
export async function middleware(request: NextRequest) {
|
||||||
let response = NextResponse.next({
|
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(
|
const supabase = createServerClient(
|
||||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
import createNextIntlPlugin from 'next-intl/plugin';
|
||||||
|
|
||||||
|
const withNextIntl = createNextIntlPlugin('./src/i18n/config.ts');
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
images: {
|
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",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^0.532.0",
|
"lucide-react": "^0.532.0",
|
||||||
"next": "15.4.4",
|
"next": "15.4.4",
|
||||||
|
"next-intl": "^4.3.4",
|
||||||
"prisma": "^6.12.0",
|
"prisma": "^6.12.0",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
@ -234,6 +235,66 @@
|
|||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"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": {
|
"node_modules/@humanfs/core": {
|
||||||
"version": "0.19.1",
|
"version": "0.19.1",
|
||||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||||
@ -1074,6 +1135,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@stitches/core": {
|
||||||
"version": "1.2.8",
|
"version": "1.2.8",
|
||||||
"resolved": "https://registry.npmjs.org/@stitches/core/-/core-1.2.8.tgz",
|
"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": {
|
"node_modules/deep-is": {
|
||||||
"version": "0.1.4",
|
"version": "0.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
||||||
@ -3930,6 +4003,18 @@
|
|||||||
"node": ">= 0.4"
|
"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": {
|
"node_modules/is-array-buffer": {
|
||||||
"version": "3.0.5",
|
"version": "3.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
||||||
@ -4945,6 +5030,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/next": {
|
||||||
"version": "15.4.4",
|
"version": "15.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/next/-/next-15.4.4.tgz",
|
"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": {
|
"node_modules/next/node_modules/postcss": {
|
||||||
"version": "8.4.31",
|
"version": "8.4.31",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
||||||
@ -6343,6 +6464,20 @@
|
|||||||
"punycode": "^2.1.0"
|
"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": {
|
"node_modules/webidl-conversions": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^0.532.0",
|
"lucide-react": "^0.532.0",
|
||||||
"next": "15.4.4",
|
"next": "15.4.4",
|
||||||
|
"next-intl": "^4.3.4",
|
||||||
"prisma": "^6.12.0",
|
"prisma": "^6.12.0",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import type { Metadata, Viewport } from "next";
|
import type { Metadata, Viewport } from "next";
|
||||||
import { Geist, Geist_Mono } from "next/font/google";
|
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 { ThemeProvider } from "@/contexts/ThemeContext";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
|
||||||
@ -54,13 +56,16 @@ export const viewport: Viewport = {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default async function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
|
const locale = await getLocale();
|
||||||
|
const messages = await getMessages();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang="en" suppressHydrationWarning>
|
<html lang={locale} suppressHydrationWarning>
|
||||||
<head>
|
<head>
|
||||||
<meta name="theme-color" content="#ffffff" />
|
<meta name="theme-color" content="#ffffff" />
|
||||||
<script
|
<script
|
||||||
@ -84,7 +89,9 @@ export default function RootLayout({
|
|||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased min-h-screen`}
|
className={`${geistSans.variable} ${geistMono.variable} antialiased min-h-screen`}
|
||||||
>
|
>
|
||||||
<ThemeProvider defaultTheme="system" storageKey="prmbr-theme">
|
<ThemeProvider defaultTheme="system" storageKey="prmbr-theme">
|
||||||
{children}
|
<NextIntlClientProvider messages={messages}>
|
||||||
|
{children}
|
||||||
|
</NextIntlClientProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
|
import { useTranslations } from 'next-intl'
|
||||||
import { Header } from '@/components/layout/Header'
|
import { Header } from '@/components/layout/Header'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Zap, Target, Layers, BarChart3, Check } from 'lucide-react'
|
import { Zap, Target, Layers, BarChart3, Check } from 'lucide-react'
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
|
const t = useTranslations('home')
|
||||||
|
const tPricing = useTranslations('pricing')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen">
|
<div className="min-h-screen">
|
||||||
<Header />
|
<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="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pt-20 pb-24">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h1 className="text-4xl md:text-6xl font-bold text-foreground mb-6">
|
<h1 className="text-4xl md:text-6xl font-bold text-foreground mb-6">
|
||||||
AI Prompt Studio
|
{t('hero.title')}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-xl text-muted-foreground mb-8 max-w-3xl mx-auto">
|
<p className="text-xl text-muted-foreground mb-8 max-w-3xl mx-auto">
|
||||||
Build, test, and manage your AI prompts with precision.
|
{t('hero.subtitle')}
|
||||||
Version control, collaboration tools, and analytics in one professional platform.
|
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||||
<Button size="lg" onClick={() => window.location.href = '/studio'}>
|
<Button size="lg" onClick={() => window.location.href = '/studio'}>
|
||||||
Start Building
|
{t('hero.getStarted')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" size="lg">
|
<Button variant="outline" size="lg">
|
||||||
View Showcase
|
{t('hero.learnMore')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</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="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div className="text-center mb-16">
|
<div className="text-center mb-16">
|
||||||
<h2 className="text-3xl font-bold text-foreground mb-4">
|
<h2 className="text-3xl font-bold text-foreground mb-4">
|
||||||
Everything you need to master prompts
|
{t('features.title')}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-xl text-muted-foreground">
|
<p className="text-xl text-muted-foreground">
|
||||||
Professional tools for prompt engineering and management
|
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">
|
<div className="bg-card p-6 rounded-lg shadow-sm border">
|
||||||
<Target className="h-12 w-12 text-primary mb-4" />
|
<Target className="h-12 w-12 text-primary mb-4" />
|
||||||
<h3 className="text-lg font-semibold text-card-foreground mb-2">
|
<h3 className="text-lg font-semibold text-card-foreground mb-2">
|
||||||
Prompt Builder
|
{t('features.promptManager.title')}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
Intuitive interface for crafting and refining AI prompts with real-time preview.
|
{t('features.promptManager.description')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-card p-6 rounded-lg shadow-sm border">
|
<div className="bg-card p-6 rounded-lg shadow-sm border">
|
||||||
<Layers className="h-12 w-12 text-primary mb-4" />
|
<Layers className="h-12 w-12 text-primary mb-4" />
|
||||||
<h3 className="text-lg font-semibold text-card-foreground mb-2">
|
<h3 className="text-lg font-semibold text-card-foreground mb-2">
|
||||||
Version Control
|
{t('features.versionControl.title')}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
Track changes, compare versions, and rollback to previous iterations seamlessly.
|
{t('features.versionControl.description')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-card p-6 rounded-lg shadow-sm border">
|
<div className="bg-card p-6 rounded-lg shadow-sm border">
|
||||||
<BarChart3 className="h-12 w-12 text-primary mb-4" />
|
<BarChart3 className="h-12 w-12 text-primary mb-4" />
|
||||||
<h3 className="text-lg font-semibold text-card-foreground mb-2">
|
<h3 className="text-lg font-semibold text-card-foreground mb-2">
|
||||||
Test & Analytics
|
{t('features.testing.title')}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
Run tests, analyze performance, and optimize your prompts with detailed metrics.
|
{t('features.testing.description')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div className="text-center mb-16">
|
<div className="text-center mb-16">
|
||||||
<h2 className="text-3xl font-bold text-foreground mb-4">
|
<h2 className="text-3xl font-bold text-foreground mb-4">
|
||||||
Simple, transparent pricing
|
{tPricing('title')}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-xl text-muted-foreground">
|
<p className="text-xl text-muted-foreground">
|
||||||
Choose the plan that fits your needs
|
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="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="bg-card p-8 rounded-lg shadow-sm border">
|
||||||
<div className="text-center mb-6">
|
<div className="text-center mb-6">
|
||||||
<h3 className="text-2xl font-bold text-card-foreground mb-2">Free</h3>
|
<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">$0</div>
|
<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>
|
<p className="text-muted-foreground">Perfect for getting started</p>
|
||||||
</div>
|
</div>
|
||||||
<ul className="space-y-3 mb-8">
|
<ul className="space-y-3 mb-8">
|
||||||
<li className="flex items-center">
|
<li className="flex items-center">
|
||||||
<Check className="h-5 w-5 text-green-500 mr-3" />
|
<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>
|
||||||
<li className="flex items-center">
|
<li className="flex items-center">
|
||||||
<Check className="h-5 w-5 text-green-500 mr-3" />
|
<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>
|
||||||
<li className="flex items-center">
|
<li className="flex items-center">
|
||||||
<Check className="h-5 w-5 text-green-500 mr-3" />
|
<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>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<Button className="w-full" onClick={() => window.location.href = '/signup'}>
|
<Button className="w-full" onClick={() => window.location.href = '/signup'}>
|
||||||
@ -131,26 +134,26 @@ export default function Home() {
|
|||||||
Popular
|
Popular
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center mb-6">
|
<div className="text-center mb-6">
|
||||||
<h3 className="text-2xl font-bold mb-2">Pro</h3>
|
<h3 className="text-2xl font-bold mb-2">{tPricing('pro.title')}</h3>
|
||||||
<div className="text-4xl font-bold mb-2">$19.9</div>
|
<div className="text-4xl font-bold mb-2">{tPricing('pro.price')}</div>
|
||||||
<p className="text-primary-foreground/80">per month</p>
|
<p className="text-primary-foreground/80">per month</p>
|
||||||
</div>
|
</div>
|
||||||
<ul className="space-y-3 mb-8">
|
<ul className="space-y-3 mb-8">
|
||||||
<li className="flex items-center">
|
<li className="flex items-center">
|
||||||
<Check className="h-5 w-5 text-primary-foreground/80 mr-3" />
|
<Check className="h-5 w-5 text-primary-foreground/80 mr-3" />
|
||||||
<span>500 prompts</span>
|
<span>500 Prompt Limit</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-center">
|
<li className="flex items-center">
|
||||||
<Check className="h-5 w-5 text-primary-foreground/80 mr-3" />
|
<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>
|
||||||
<li className="flex items-center">
|
<li className="flex items-center">
|
||||||
<Check className="h-5 w-5 text-primary-foreground/80 mr-3" />
|
<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>
|
||||||
<li className="flex items-center">
|
<li className="flex items-center">
|
||||||
<Check className="h-5 w-5 text-primary-foreground/80 mr-3" />
|
<Check className="h-5 w-5 text-primary-foreground/80 mr-3" />
|
||||||
<span>Priority support</span>
|
<span>Priority Support</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<Button variant="outline" className="w-full bg-primary-foreground text-primary hover:bg-primary-foreground/90" onClick={() => window.location.href = '/signup'}>
|
<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,
|
Save,
|
||||||
Copy,
|
Copy,
|
||||||
Settings,
|
Settings,
|
||||||
FileText,
|
|
||||||
Folder,
|
Folder,
|
||||||
Plus,
|
Plus,
|
||||||
Search,
|
Search,
|
||||||
@ -75,7 +74,7 @@ export default function StudioPage() {
|
|||||||
|
|
||||||
// Mock response
|
// 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`)
|
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.')
|
setTestResult('Error: Failed to run prompt. Please try again.')
|
||||||
} finally {
|
} finally {
|
||||||
setIsRunning(false)
|
setIsRunning(false)
|
||||||
@ -308,7 +307,7 @@ Please write a professional email that..."
|
|||||||
<div>
|
<div>
|
||||||
<Zap className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
|
<Zap className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
Click "Run Test" to see your prompt results
|
Click "Run Test" to see your prompt results
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,13 +1,17 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { useTranslations } from 'next-intl'
|
||||||
import { useAuth } from '@/hooks/useAuth'
|
import { useAuth } from '@/hooks/useAuth'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { ThemeToggle, MobileThemeToggle } from '@/components/ui/theme-toggle'
|
import { ThemeToggle, MobileThemeToggle } from '@/components/ui/theme-toggle'
|
||||||
|
import { LanguageToggle, MobileLanguageToggle } from '@/components/ui/language-toggle'
|
||||||
import { Menu, X, Zap } from 'lucide-react'
|
import { Menu, X, Zap } from 'lucide-react'
|
||||||
|
|
||||||
export function Header() {
|
export function Header() {
|
||||||
const { user, signOut } = useAuth()
|
const { user, signOut } = useAuth()
|
||||||
|
const t = useTranslations('navigation')
|
||||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -24,38 +28,39 @@ export function Header() {
|
|||||||
{/* Desktop Navigation */}
|
{/* Desktop Navigation */}
|
||||||
<div className="hidden md:block">
|
<div className="hidden md:block">
|
||||||
<div className="ml-10 flex items-baseline space-x-4">
|
<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">
|
<Link href="/" className="text-muted-foreground hover:text-foreground px-3 py-2 text-sm font-medium transition-colors">
|
||||||
Features
|
{t('home')}
|
||||||
</a>
|
</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">
|
<a href="#pricing" className="text-muted-foreground hover:text-foreground px-3 py-2 text-sm font-medium transition-colors">
|
||||||
Pricing
|
Pricing
|
||||||
</a>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Desktop Auth */}
|
{/* Desktop Auth */}
|
||||||
<div className="hidden md:block">
|
<div className="hidden md:block">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
|
<LanguageToggle variant="dropdown" showLabel={false} />
|
||||||
<ThemeToggle variant="dropdown" showLabel={false} />
|
<ThemeToggle variant="dropdown" showLabel={false} />
|
||||||
{user ? (
|
{user ? (
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Button variant="ghost" size="sm" onClick={() => window.location.href = '/profile'}>
|
<Button variant="ghost" size="sm" onClick={() => window.location.href = '/profile'}>
|
||||||
Profile
|
{t('profile')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" onClick={signOut}>
|
<Button variant="outline" onClick={signOut}>
|
||||||
Sign Out
|
{t('signOut')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Button variant="ghost" onClick={() => window.location.href = '/signin'}>
|
<Button variant="ghost" onClick={() => window.location.href = '/signin'}>
|
||||||
Sign In
|
{t('signIn')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={() => window.location.href = '/signup'}>
|
<Button onClick={() => window.location.href = '/signup'}>
|
||||||
Sign Up
|
{t('signUp')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -64,6 +69,7 @@ export function Header() {
|
|||||||
|
|
||||||
{/* Mobile controls */}
|
{/* Mobile controls */}
|
||||||
<div className="md:hidden flex items-center space-x-2">
|
<div className="md:hidden flex items-center space-x-2">
|
||||||
|
<LanguageToggle variant="button" />
|
||||||
<ThemeToggle variant="button" />
|
<ThemeToggle variant="button" />
|
||||||
<button
|
<button
|
||||||
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||||
@ -79,18 +85,23 @@ export function Header() {
|
|||||||
{mobileMenuOpen && (
|
{mobileMenuOpen && (
|
||||||
<div className="md:hidden">
|
<div className="md:hidden">
|
||||||
<div className="px-2 pt-2 pb-3 space-y-1 border-t">
|
<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">
|
<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">
|
||||||
Features
|
{t('home')}
|
||||||
</a>
|
</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">
|
<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
|
Pricing
|
||||||
</a>
|
</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
|
{/* Mobile Language Toggle */}
|
||||||
</a>
|
<div className="pt-4 pb-2 border-t">
|
||||||
|
<MobileLanguageToggle />
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Mobile Theme Toggle */}
|
{/* Mobile Theme Toggle */}
|
||||||
<div className="pt-4 pb-2 border-t">
|
<div className="pt-2 pb-2 border-t">
|
||||||
<MobileThemeToggle />
|
<MobileThemeToggle />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -98,19 +109,19 @@ export function Header() {
|
|||||||
{user ? (
|
{user ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Button variant="ghost" className="w-full justify-start" onClick={() => window.location.href = '/profile'}>
|
<Button variant="ghost" className="w-full justify-start" onClick={() => window.location.href = '/profile'}>
|
||||||
Profile
|
{t('profile')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" className="w-full" onClick={signOut}>
|
<Button variant="outline" className="w-full" onClick={signOut}>
|
||||||
Sign Out
|
{t('signOut')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Button variant="ghost" className="w-full" onClick={() => window.location.href = '/signin'}>
|
<Button variant="ghost" className="w-full" onClick={() => window.location.href = '/signin'}>
|
||||||
Sign In
|
{t('signIn')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button className="w-full" onClick={() => window.location.href = '/signup'}>
|
<Button className="w-full" onClick={() => window.location.href = '/signup'}>
|
||||||
Sign Up
|
{t('signUp')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</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