diff --git a/apps/web/app/password/[linkId]/form.tsx b/apps/web/app/password/[linkId]/form.tsx
index 6e20aa43500..318dcc67350 100644
--- a/apps/web/app/password/[linkId]/form.tsx
+++ b/apps/web/app/password/[linkId]/form.tsx
@@ -6,16 +6,18 @@ import { useParams } from "next/navigation";
import { useActionState } from "react";
import { useFormStatus } from "react-dom";
import { verifyPassword } from "./action";
+import { getTranslations, Language } from "./translations";
const initialState = {
error: null,
};
-export default function PasswordForm() {
+export default function PasswordForm({ language }: { language: Language }) {
const { linkId } = useParams() as {
linkId: string;
};
const [state, formAction] = useActionState(verifyPassword, initialState);
+ const t = getTranslations(language);
const { isMobile } = useMediaQuery();
@@ -27,7 +29,7 @@ export default function PasswordForm() {
>
@@ -54,17 +56,17 @@ export default function PasswordForm() {
{state.error && (
- Incorrect password
+ {t.incorrectPassword}
)}
-
+
);
}
-const FormButton = () => {
+const FormButton = ({ text }: { text: string }) => {
const { pending } = useFormStatus();
- return ;
+ return ;
};
diff --git a/apps/web/app/password/[linkId]/page.tsx b/apps/web/app/password/[linkId]/page.tsx
index 2c375d01655..2c37b4df668 100644
--- a/apps/web/app/password/[linkId]/page.tsx
+++ b/apps/web/app/password/[linkId]/page.tsx
@@ -3,10 +3,11 @@ import { NewBackground } from "@/ui/shared/new-background";
import { prismaEdge } from "@dub/prisma/edge";
import { BlurImage, Wordmark } from "@dub/ui";
import { constructMetadata, createHref, isDubDomain } from "@dub/utils";
-import { cookies } from "next/headers";
+import { cookies, headers } from "next/headers";
import Link from "next/link";
import { notFound, redirect } from "next/navigation";
import PasswordForm from "./form";
+import { getLanguage, getTranslations } from "./translations";
export const dynamic = "force-dynamic";
export const runtime = "edge";
@@ -59,6 +60,13 @@ export default async function PasswordProtectedLinkPage(props: {
params: Promise<{ linkId: string }>;
}) {
const params = await props.params;
+
+ // Detect language from Accept-Language header
+ const headersList = await headers();
+ const acceptLanguage = headersList.get("accept-language");
+ const language = getLanguage(acceptLanguage);
+ const t = getTranslations(language);
+
const link = await prismaEdge.link.findUnique({
where: {
id: params.linkId,
@@ -110,12 +118,12 @@ export default async function PasswordProtectedLinkPage(props: {
)}
- Password required
+ {t.passwordRequired}
- {description}
+ {t.description}
-
+
- What is Dub?
+ {t.whatIsDub}
>
diff --git a/apps/web/app/password/[linkId]/translations.ts b/apps/web/app/password/[linkId]/translations.ts
new file mode 100644
index 00000000000..a62cb8646e2
--- /dev/null
+++ b/apps/web/app/password/[linkId]/translations.ts
@@ -0,0 +1,74 @@
+export const translations = {
+ en: {
+ passwordRequired: "Password required",
+ description:
+ "This link is password protected. Enter the password to view it.",
+ whatIsDub: "What is Dub?",
+ passwordLabel: "Password",
+ incorrectPassword: "Incorrect password",
+ viewPage: "View page",
+ },
+ zh: {
+ passwordRequired: "需要密码",
+ description: "此链接受密码保护。请输入密码以查看。",
+ whatIsDub: "什么是 Dub?",
+ passwordLabel: "密码",
+ incorrectPassword: "密码错误",
+ viewPage: "查看页面",
+ },
+ es: {
+ passwordRequired: "Contraseña requerida",
+ description:
+ "Este enlace está protegido con contraseña. Ingresa la contraseña para verlo.",
+ whatIsDub: "¿Qué es Dub?",
+ passwordLabel: "Contraseña",
+ incorrectPassword: "Contraseña incorrecta",
+ viewPage: "Ver página",
+ },
+ fr: {
+ passwordRequired: "Mot de passe requis",
+ description:
+ "Ce lien est protégé par un mot de passe. Entrez le mot de passe pour y accéder.",
+ whatIsDub: "Qu'est-ce que Dub ?",
+ passwordLabel: "Mot de passe",
+ incorrectPassword: "Mot de passe incorrect",
+ viewPage: "Voir la page",
+ },
+ tr: {
+ passwordRequired: "Şifre gerekli",
+ description:
+ "Bu bağlantı şifre korumalıdır. Görüntülemek için şifreyi girin.",
+ whatIsDub: "Dub nedir?",
+ passwordLabel: "Şifre",
+ incorrectPassword: "Yanlış şifre",
+ viewPage: "Sayfayı görüntüle",
+ },
+} as const;
+
+export type Language = keyof typeof translations;
+
+export function getLanguage(acceptLanguage?: string | null): Language {
+ if (!acceptLanguage) return "en";
+
+ const languages = acceptLanguage
+ .toLowerCase()
+ .split(",")
+ .map((lang) => {
+ const [code] = lang.trim().split(";");
+ return code.split("-")[0]; // Extract base language code (e.g., "en" from "en-US")
+ });
+
+ // Check for supported languages in order of preference
+ for (const lang of languages) {
+ if (lang in translations) {
+ return lang as Language;
+ }
+ }
+
+ // Default to English if no match found
+ return "en";
+}
+
+export function getTranslations(language: Language) {
+ return translations[language];
+}