feat: custom JWT auth + i18n with Chinese default

Replace Clerk with self-contained JWT auth (bcryptjs + jose), remove all
external auth dependencies. Add full i18n system with Chinese as default
language and English toggle — React Context + JSON dictionaries, no
external i18n library.

Auth:
- Add passwordHash to User model, generate JWT secret
- Create /api/auth/register, login, logout, me endpoints
- Rewrite proxy.ts middleware for custom session validation
- Add AuthProvider + useAuth hook, sign-in/sign-up pages
- Wire auth into layout and all API routes

i18n:
- React Context I18nProvider with localStorage + cookie persistence
- dictionaries/zh.ts (default) and dictionaries/en.ts
- LanguageSwitcher component, server-side createServerT() utility
- CJK font stack via [lang="zh"] CSS selector
- HTML lang attribute synced on language change
- All pages, components, TabBar, StageBadge translated
- Engine fallback strings (reflection-analyzer, summary-generator)
- API routes pass locale from cookie to engines

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jackyu66git
2026-05-20 22:47:14 +08:00
co-authored by Claude Opus 4.6
parent 4e83ce95cd
commit 00db3f6c96
43 changed files with 1647 additions and 300 deletions
+28 -10
View File
@@ -1,6 +1,12 @@
import OpenAI from "openai";
import { prisma } from "@/lib/prisma";
import { keywordCheck, llmSafetyCheck, type SafetyResult } from "@/lib/safety";
import type { Lang } from "@/lib/i18n/types";
import { createServerT } from "@/lib/i18n/utils";
import zh from "../../../dictionaries/zh";
import en from "../../../dictionaries/en";
const DICTS = { zh, en } as const;
// ---------------------------------------------------------------------------
// Types
@@ -56,8 +62,11 @@ Voice rules:
export async function generateWeeklySummary(
userId: string,
openai: OpenAI
openai: OpenAI,
locale: Lang = "zh"
): Promise<GeneratedSummary> {
const t = createServerT(DICTS[locale]);
// Gather week data
const weekAgo = new Date();
weekAgo.setDate(weekAgo.getDate() - 7);
@@ -85,8 +94,7 @@ export async function generateWeeklySummary(
if (!userState || sessions.length === 0) {
return {
summary:
"No completed practice sessions this week. Every week is a fresh start — your practice is here when you're ready.",
summary: t("engine.summary.no_sessions"),
safety: { safe: true },
};
}
@@ -124,7 +132,7 @@ export async function generateWeeklySummary(
// Safety check
const keywordResult = keywordCheck(summary);
if (!keywordResult.safe) {
return { summary: "Your practice summary for this week.", safety: keywordResult };
return { summary: t("engine.summary.safety_fallback"), safety: keywordResult };
}
const llmSafety = await llmSafetyCheck(summary, openai as any);
@@ -133,25 +141,34 @@ export async function generateWeeklySummary(
const weekStart = new Date(weekAgo);
weekStart.setHours(0, 0, 0, 0);
const safetyFallback = t("engine.summary.safety_fallback");
await prisma.weeklySummary.upsert({
where: { userId },
create: {
userId,
summary: llmSafety.safe ? summary : "Your practice summary for this week.",
summary: llmSafety.safe ? summary : safetyFallback,
weekStart,
},
update: {
summary: llmSafety.safe ? summary : "Your practice summary for this week.",
summary: llmSafety.safe ? summary : safetyFallback,
weekStart,
},
});
return {
summary: llmSafety.safe ? summary : "Your practice summary for this week.",
summary: llmSafety.safe ? summary : safetyFallback,
safety: llmSafety,
};
} catch {
const fallback = `You completed ${sessions.length} practice session${sessions.length === 1 ? "" : "s"} this week. Showing up consistently is the foundation everything else is built on. Your current streak is ${userState.streakDays} day${userState.streakDays === 1 ? "" : "s"}. See you next week.`;
const count = sessions.length;
const streak = userState.streakDays;
const fallback = t("engine.summary.fallback_template", {
count,
plural: count === 1 ? "" : "s",
streak,
streak_plural: streak === 1 ? "" : "s",
});
return { summary: fallback, safety: { safe: true } };
}
}
@@ -161,7 +178,8 @@ export async function generateWeeklySummary(
// ---------------------------------------------------------------------------
export async function generateAllWeeklySummaries(
openai: OpenAI
openai: OpenAI,
locale: Lang = "zh"
): Promise<{ userId: string; success: boolean }[]> {
// Find users who had sessions in the last 7 days
const weekAgo = new Date();
@@ -180,7 +198,7 @@ export async function generateAllWeeklySummaries(
for (const { userId } of activeUsers) {
try {
await generateWeeklySummary(userId, openai);
await generateWeeklySummary(userId, openai, locale);
results.push({ userId, success: true });
} catch {
results.push({ userId, success: false });