Jiseoup/showmycodePublic
EN|KO
  • Code
  • Commits
  • Pull Requests
← Back to list

i18n: localize unauthorized page and default to English fallback

JiseoupJiseoup · Jun 12, 2026d9a08ee

Files changed5+114 -62

Changed files

+114 -62 · 5

@@ -1,66 +1,23 @@
-"use client";
+import { headers } from "next/headers";
+import { getDictionary, defaultLocale, hasLocale } from "@/lib/i18n.server";
+import UnauthorizedForm from "@/components/UnauthorizedForm";
-import { useRef, useState } from "react";
-
-// Standalone page — outside [lang] layout, so no i18n context available.
-export default function UnauthorizedPage() {
- const inputRef = useRef<HTMLInputElement>(null);
- const [error, setError] = useState(false);
- const [submitting, setSubmitting] = useState(false);
-
- const handleSubmit = async (e: React.SyntheticEvent) => {
- e.preventDefault();
- const token = inputRef.current?.value.trim();
- if (!token || submitting) return;
-
- setSubmitting(true);
- setError(false);
- try {
- const res = await fetch("/api/auth", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ token }),
- });
- if (res.ok) {
- // Full page navigation so the freshly-set cookie is sent on the next request.
- window.location.assign("/");
- return;
- }
- setError(true);
- } catch {
- setError(true);
- } finally {
- setSubmitting(false);
- }
- };
+// Standalone page — outside the [lang] layout, so there is no locale param.
+// Resolve the locale from Accept-Language, mirroring proxy.ts.
+export default async function UnauthorizedPage() {
+ const acceptLang = (await headers()).get("accept-language") ?? "";
+ const detected = acceptLang.split(",")[0].split("-")[0].toLowerCase();
+ const locale = hasLocale(detected) ? detected : defaultLocale;
+ const dict = await getDictionary(locale);
+ const t = dict.unauthorized;
return (
- <div className="flex min-h-screen flex-col items-center justify-center gap-4">
- <p className="text-base font-semibold">Access restricted</p>
- <p className="text-muted-foreground text-sm">Enter your access token to continue.</p>
- <form onSubmit={handleSubmit} className="flex flex-col items-center gap-2">
- <div className="flex items-center gap-2">
- <input
- ref={inputRef}
- type="password"
- placeholder="Access token"
- autoFocus
- onChange={() => setError(false)}
- className="border-border bg-background text-foreground placeholder:text-muted-foreground focus:ring-border w-56 rounded border px-3 py-1.5 text-sm focus:ring-1 focus:outline-none"
- />
- <button
- type="submit"
- disabled={submitting}
- className="border-border bg-muted hover:bg-accent cursor-pointer rounded border px-3 py-1.5 text-sm transition-colors disabled:cursor-not-allowed disabled:opacity-50"
- >
- Enter
- </button>
- </div>
- {/* Reserve space so the layout doesn't shift when the message toggles. */}
- <p aria-live="polite" className={`text-sm text-red-500 ${error ? "visible" : "invisible"}`}>
- Invalid token. Please try again.
- </p>
- </form>
- </div>
+ <UnauthorizedForm
+ title={t.title}
+ description={t.description}
+ placeholder={t.placeholder}
+ submit={t.submit}
+ invalid={t.invalid}
+ />
);
}
@@ -0,0 +1,81 @@
+"use client";
+
+import { useRef, useState } from "react";
+
+// Strings are passed from the server component, which resolves the locale
+// from the Accept-Language header (this page lives outside the [lang] layout).
+type Props = {
+ title: string;
+ description: string;
+ placeholder: string;
+ submit: string;
+ invalid: string;
+};
+
+export default function UnauthorizedForm({
+ title,
+ description,
+ placeholder,
+ submit,
+ invalid,
+}: Props) {
+ const inputRef = useRef<HTMLInputElement>(null);
+ const [error, setError] = useState(false);
+ const [submitting, setSubmitting] = useState(false);
+
+ const handleSubmit = async (e: React.SyntheticEvent) => {
+ e.preventDefault();
+ const token = inputRef.current?.value.trim();
+ if (!token || submitting) return;
+
+ setSubmitting(true);
+ setError(false);
+ try {
+ const res = await fetch("/api/auth", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ token }),
+ });
+ if (res.ok) {
+ // Full page navigation so the freshly-set cookie is sent on the next request.
+ window.location.assign("/");
+ return;
+ }
+ setError(true);
+ } catch {
+ setError(true);
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+ return (
+ <div className="flex min-h-screen flex-col items-center justify-center gap-4">
+ <p className="text-base font-semibold">{title}</p>
+ <p className="text-muted-foreground text-sm">{description}</p>
+ <form onSubmit={handleSubmit} className="flex flex-col items-center gap-2">
+ <div className="flex items-center gap-2">
+ <input
+ ref={inputRef}
+ type="password"
+ placeholder={placeholder}
+ autoFocus
+ onChange={() => setError(false)}
+ className="border-border bg-background text-foreground placeholder:text-muted-foreground focus:ring-border w-56 rounded border px-3 py-1.5 text-sm focus:ring-1 focus:outline-none"
+ />
+ <button
+ type="submit"
+ disabled={submitting}
+ className="border-border bg-muted hover:bg-accent cursor-pointer rounded border px-3 py-1.5 text-sm transition-colors disabled:cursor-not-allowed disabled:opacity-50"
+ >
+ {submit}
+ </button>
+ </div>
+ {/* Reserve space so the layout doesn't shift when the message toggles. */}
+ <p aria-live="polite" className={`text-sm text-red-500 ${error ? "visible" : "invisible"}`}>
+ {invalid}
+ </p>
+ </form>
+ </div>
+ );
+}
@@ -1,5 +1,5 @@
export const locales = ["ko", "en"] as const;
export type Locale = (typeof locales)[number];
-export const defaultLocale: Locale = "ko";
+export const defaultLocale: Locale = "en";
export const hasLocale = (locale: string): locale is Locale =>
(locales as readonly string[]).includes(locale);
@@ -72,5 +72,12 @@
"error": {
"title": "Failed to load data.",
"retry": "Try again"
+ },
+ "unauthorized": {
+ "title": "Access restricted",
+ "description": "Enter your access token to continue.",
+ "placeholder": "Access token",
+ "submit": "Enter",
+ "invalid": "Invalid token. Please try again."
}
}
@@ -72,5 +72,12 @@
"error": {
"title": "데이터를 불러오지 못했습니다.",
"retry": "다시 시도"
+ },
+ "unauthorized": {
+ "title": "접근이 제한되었습니다",
+ "description": "계속하려면 액세스 토큰을 입력하세요.",
+ "placeholder": "액세스 토큰",
+ "submit": "입력",
+ "invalid": "유효하지 않은 토큰입니다. 다시 시도하세요."
}
}