Files
infiplot-web/components/CustomForm.tsx
T
yuanzonghao 0a7076d5b9 fix(i18n): overhaul i18n with [locale] routing, SSR translations, and hreflang SEO
Rewrites the i18n system introduced in PR #94 to use Next.js App Router
[locale] dynamic segments with SSR-rendered translations and proper
middleware locale routing.

- Add middleware locale detection: / rewrites to /zh-CN/ internally,
  /en and /ja pass through, /zh-CN/... redirects to bare path
- Move all 7 pages under app/[locale]/ with SSR translation injection
- Fix server→client serialization: pre-evaluate function-valued
  translations (makeSerializable) to eliminate hydration flash
- Fix language switch key flash: use hard navigation with localStorage-
  only persistence, avoiding React state update before page reload
- Add <link rel="alternate" hreflang> tags for multilingual SEO
- Fix Supabase setAll overwriting locale rewrite response
- Trim locales from 22 to 3 (zh-CN/en/ja), delete 19 incomplete files
- LLM-translate 240 firstact game preset JSONs (en + ja, landscape +
  portrait) and story titles via gemini-3.5-flash
- Delete 11 one-off migration scripts and outdated i18n docs
- Add useLocalePath hook and navigation utilities

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-18 23:16:17 +08:00

99 lines
3.8 KiB
TypeScript

"use client";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { track } from "@/lib/analytics";
import { useI18n } from "@/lib/i18n/client";
import { useLocalePath } from "@/lib/i18n/hooks";
export function CustomForm() {
const router = useRouter();
const { t } = useI18n();
const lp = useLocalePath();
const [worldSetting, setWorldSetting] = useState("");
const [styleGuide, setStyleGuide] = useState("");
const [submitting, setSubmitting] = useState(false);
const canSubmit =
worldSetting.trim().length > 10 &&
styleGuide.trim().length > 5 &&
!submitting;
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!canSubmit) return;
setSubmitting(true);
sessionStorage.setItem(
"infiplot:custom",
JSON.stringify({ worldSetting, styleGuide }),
);
track("game_start", { source: "custom" });
router.push(lp("/play?custom=1"));
}
return (
<form onSubmit={handleSubmit} className="space-y-12 animate-fade-in">
<div>
<label className="flex items-baseline justify-between mb-4">
<span className="text-[10px] smallcaps text-clay-700 font-medium">
<span className="text-clay-400 mr-2 font-serif italic not-italic font-normal">
</span>
{t("customForm.world")}
</span>
<span className="text-[10px] text-clay-400 num">
{worldSetting.length}
</span>
</label>
<textarea
value={worldSetting}
onChange={(e) => setWorldSetting(e.target.value)}
rows={6}
placeholder={t("customForm.worldPlaceholder")}
className="w-full bg-transparent border-0 border-b border-clay-900/20 px-0 py-3 text-clay-900 font-serif text-lg leading-[1.7] focus:outline-none focus:border-clay-700 transition-colors resize-none placeholder:font-serif placeholder:italic placeholder:text-base placeholder:leading-[1.7]"
/>
</div>
<div>
<label className="flex items-baseline justify-between mb-4">
<span className="text-[10px] smallcaps text-clay-700 font-medium">
<span className="text-clay-400 mr-2 font-serif italic not-italic font-normal">
</span>
{t("customForm.style")}
</span>
<span className="text-[10px] text-clay-400 num">
{styleGuide.length}
</span>
</label>
<textarea
value={styleGuide}
onChange={(e) => setStyleGuide(e.target.value)}
rows={4}
placeholder={t("customForm.stylePlaceholder")}
className="w-full bg-transparent border-0 border-b border-clay-900/20 px-0 py-3 text-clay-900 font-serif text-lg leading-[1.7] focus:outline-none focus:border-clay-700 transition-colors resize-none placeholder:font-serif placeholder:italic placeholder:text-base placeholder:leading-[1.7]"
/>
</div>
<div className="pt-6 flex items-center justify-between">
<span className="text-[10px] smallcaps text-clay-500">
{submitting
? t("customForm.status.starting")
: canSubmit
? t("customForm.status.ready")
: t("customForm.status.needMore")}
</span>
<button
type="submit"
disabled={!canSubmit}
className="group flex items-center gap-3 text-[10px] smallcaps text-clay-900 disabled:text-clay-300 disabled:cursor-not-allowed enabled:hover:text-ember-500 transition-colors duration-300"
>
{t("customForm.start")}
<span className="w-10 h-px bg-current transition-all duration-300 group-enabled:group-hover:w-16" />
<i className="fa-solid fa-arrow-right text-[9px]" />
</button>
</div>
</form>
);
}