Skip to content

Add language detection for password-protected link page#3515

Open
ctafsiras wants to merge 1 commit intodubinc:mainfrom
ctafsiras:feat/password-language-detection-360
Open

Add language detection for password-protected link page#3515
ctafsiras wants to merge 1 commit intodubinc:mainfrom
ctafsiras:feat/password-language-detection-360

Conversation

@ctafsiras
Copy link
Copy Markdown

@ctafsiras ctafsiras commented Feb 27, 2026

Summary

Adds automatic language detection to the password-protected link page (/password/[linkId]) based on the browser's Accept-Language header. This addresses the feature request in #360 where users with non-English-speaking audiences see a fully English password prompt.

Supported languages: English, Chinese, Spanish, French, Turkish — matching the same set already used in the deeplink preview page.

Changes

  • apps/web/app/password/[linkId]/translations.ts (new) — Translation strings for 5 languages with getLanguage() and getTranslations() utilities, following the same pattern as the deeplink translations
  • apps/web/app/password/[linkId]/page.tsx — Reads Accept-Language header server-side, resolves the best matching language, and passes it to the form component. All 3 user-facing strings are now translated (heading, description, footer link)
  • apps/web/app/password/[linkId]/form.tsx — Accepts language prop and translates the input label, error message, and submit button text

Design decisions

  • OG metadata stays English-only — Crawlers/social bots rarely send Accept-Language headers, so translating metatags would have no practical benefit
  • No i18n library added — Uses the same hand-rolled translation pattern already established in the deeplink page to keep the PR minimal
  • Edge runtime compatibleheaders() from next/headers works on edge runtime (the page uses runtime = "edge")

Closes #360

Summary by CodeRabbit

  • New Features

    • Multi-language support for password-protected links (English, Chinese, Spanish, French, Turkish).
    • Automatic language detection from browser headers and localized UI text for prompts, descriptions, labels, error messages, and buttons.
  • Bug Fixes / UX

    • Consistent translated strings applied across the password entry page and form for clearer user experience.

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Feb 27, 2026

@ctafsiras is attempting to deploy a commit to the Dub Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 27, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between edd5324 and a801a03.

📒 Files selected for processing (3)
  • apps/web/app/password/[linkId]/form.tsx
  • apps/web/app/password/[linkId]/page.tsx
  • apps/web/app/password/[linkId]/translations.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/app/password/[linkId]/translations.ts

📝 Walkthrough

Walkthrough

Adds internationalization to the password-protected link flow: a new translations module with language detection from Accept-Language, page-level language selection, and PasswordForm updated to accept a language prop and render localized strings.

Changes

Cohort / File(s) Summary
Translations module
apps/web/app/password/[linkId]/translations.ts
New file exporting translations (en, zh, es, fr, tr), Language type, getLanguage(acceptLanguage?), and getTranslations(language).
Password page
apps/web/app/password/[linkId]/page.tsx
Adds headers() usage to read Accept-Language, computes language via getLanguage, loads t = getTranslations(language), replaces hardcoded strings with t.*, and passes language prop to PasswordForm.
Password form
apps/web/app/password/[linkId]/form.tsx
Signature changed to accept { language }, initializes t = getTranslations(language), replaces hardcoded labels/messages with localized keys, and updates FormButton to accept/display passed text.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant Browser
participant NextServer as Next Server (page.tsx)
participant Translations as translations.ts
participant PasswordForm as Form (form.tsx)

Browser->>NextServer: Request with Accept-Language header
NextServer->>Translations: getLanguage(headers["accept-language"])
Translations-->>NextServer: language
NextServer->>Translations: getTranslations(language)
Translations-->>NextServer: t (localized strings)
NextServer->>PasswordForm: render(language)
PasswordForm->>Translations: getTranslations(language)
Translations-->>PasswordForm: t
PasswordForm-->>Browser: HTML with localized labels and messages

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • #3362: Adds the same translation utilities and updates components to accept a Language prop and use getTranslations, showing a direct code-level overlap.

Poem

🐰 I hopped through headers, sniffed the tongue,
Five voices found, the page now sung.
Keys and prompts in each tongue play,
A tiny hop, a kinder way. 🗝️✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely describes the primary change: adding language detection to the password-protected link page.
Linked Issues check ✅ Passed The PR fully implements the core requirement from issue #360: automatic language detection and localization of the Password modal UI for multiple languages (English, Chinese, Spanish, French, Turkish).
Out of Scope Changes check ✅ Passed All changes directly support language detection and translation functionality; no unrelated modifications are present in the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/app/password/`[linkId]/translations.ts:
- Around line 53-66: The getLanguage logic currently ignores q-weights and uses
"in" to check translations, causing incorrect picks; update getLanguage to parse
acceptLanguage into language;q pairs (extract base code from each entry), sort
entries by descending q (default q=1), then iterate sorted entries and use an
own-key check (e.g., Object.prototype.hasOwnProperty.call(translations, lang))
to find the first supported language, returning it as Language; if none match,
fall back to the existing default. Ensure you reference the variables/functions
getLanguage, acceptLanguage, and translations when making the change.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 54e397a and edd5324.

📒 Files selected for processing (3)
  • apps/web/app/password/[linkId]/form.tsx
  • apps/web/app/password/[linkId]/page.tsx
  • apps/web/app/password/[linkId]/translations.ts

Comment on lines +53 to +66
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;
}
}
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

getLanguage does not compute the actual best match from Accept-Language.

Current logic uses list order only and ignores q weights, so headers like fr;q=0.8,en;q=0.9 can resolve incorrectly. Also, prefer own-key matching over in for user-derived keys.

💡 Suggested fix
 export function getLanguage(acceptLanguage?: string | null): Language {
   if (!acceptLanguage) return "en";

-  const languages = acceptLanguage
+  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")
-    });
+    .map((entry, index) => {
+      const [rawCode, ...params] = entry.trim().split(";");
+      const baseCode = rawCode.split("-")[0];
+      const qParam = params.find((p) => p.trim().startsWith("q="));
+      const q = qParam ? Number.parseFloat(qParam.split("=")[1]) : 1;
+      return {
+        baseCode,
+        q: Number.isFinite(q) ? q : 0,
+        index,
+      };
+    })
+    .sort((a, b) => b.q - a.q || a.index - b.index);

   // Check for supported languages in order of preference
-  for (const lang of languages) {
-    if (lang in translations) {
-      return lang as Language;
+  for (const { baseCode } of languages) {
+    if (Object.hasOwn(translations, baseCode)) {
+      return baseCode as Language;
     }
   }

   // Default to English if no match found
   return "en";
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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;
}
}
const languages = acceptLanguage
.toLowerCase()
.split(",")
.map((entry, index) => {
const [rawCode, ...params] = entry.trim().split(";");
const baseCode = rawCode.split("-")[0];
const qParam = params.find((p) => p.trim().startsWith("q="));
const q = qParam ? Number.parseFloat(qParam.split("=")[1]) : 1;
return {
baseCode,
q: Number.isFinite(q) ? q : 0,
index,
};
})
.sort((a, b) => b.q - a.q || a.index - b.index);
// Check for supported languages in order of preference
for (const { baseCode } of languages) {
if (Object.hasOwn(translations, baseCode)) {
return baseCode as Language;
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/app/password/`[linkId]/translations.ts around lines 53 - 66, The
getLanguage logic currently ignores q-weights and uses "in" to check
translations, causing incorrect picks; update getLanguage to parse
acceptLanguage into language;q pairs (extract base code from each entry), sort
entries by descending q (default q=1), then iterate sorted entries and use an
own-key check (e.g., Object.prototype.hasOwnProperty.call(translations, lang))
to find the first supported language, returning it as Language; if none match,
fall back to the existing default. Ensure you reference the variables/functions
getLanguage, acceptLanguage, and translations when making the change.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the thorough review! This is a valid observation about q-weight parsing.

However, this getLanguage() implementation intentionally mirrors the existing pattern in the deeplink page (apps/web/app/app.dub.co/(deeplink)/deeplink/[domain]/[[...key]]/translations.ts), which was merged by the maintainer in PR #3362. Keeping both implementations consistent felt more appropriate than diverging.

In practice, most browsers send Accept-Language headers with the preferred language listed first (the order already reflects priority), so the current approach works correctly for the vast majority of real-world cases.

Happy to improve q-weight handling across both locations in a follow-up PR if the maintainers would like that.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the existing pattern in the deeplink translations file
cat "apps/web/app/app.dub.co/(deeplink)/deeplink/[domain]/[[...key]]/translations.ts" 2>/dev/null | grep -A 20 "function getLanguage"

Length of output: 567


@ctafsiras, thank you for the clarification! I've verified that the implementation indeed matches the existing pattern in the deeplink page, and your reasoning about maintaining consistency makes sense—especially since that approach was already reviewed and merged by the maintainers.

You're right that browser behavior typically handles this well in practice, as the preferred language usually appears first in the Accept-Language header.

If you'd like to track the potential q-weight improvement for both locations, feel free to open an issue for future consideration. Either way, maintaining consistency with the existing codebase is a reasonable choice here. 👍


✏️ Learnings added
Learnt from: ctafsiras
Repo: dubinc/dub PR: 3515
File: apps/web/app/password/[linkId]/translations.ts:53-66
Timestamp: 2026-02-27T20:55:21.761Z
Learning: In the Dub codebase, when implementing language detection from Accept-Language headers, the existing pattern (seen in apps/web/app/app.dub.co/(deeplink)/deeplink/[domain]/[[...key]]/translations.ts) intentionally does not parse q-weights and instead relies on the order of languages in the header. This simple approach works for most browser implementations. Maintaining consistency with this established pattern is preferred over introducing divergent implementations.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: devkiran
Repo: dubinc/dub PR: 3346
File: apps/web/lib/api/bounties/approve-bounty-submission.ts:165-165
Timestamp: 2026-01-13T12:06:42.476Z
Learning: Use 'noreply' as a sentinel for the replyTo email field: if replyTo === 'noreply', omit the replyTo property by spreading an empty object. Call sites can use replyTo: someEmail || 'noreply' to conditionally set replyTo without null/undefined. This pattern is broadly applicable across the TypeScript codebase; ensure this behavior is documented and that readability is preserved, using explicit conditional logic if the intent may be unclear.

Learnt from: devkiran
Repo: dubinc/dub PR: 3358
File: apps/web/app/(ee)/api/cron/partners/deactivate/route.ts:7-7
Timestamp: 2026-02-03T14:14:47.579Z
Learning: In all TypeScript files under apps/web, import zod using a namespace import: import * as z from 'zod/v4' and avoid using named import: import { z } from 'zod'. This ensures consistent API access and version specificity. During reviews, look for and replace any occurrences of import { z } from 'zod' with import * as z from 'zod/v4', and verify all zod usage continues to compile.

@ctafsiras ctafsiras force-pushed the feat/password-language-detection-360 branch from edd5324 to a801a03 Compare February 27, 2026 21:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request: Language selection for the 'Password' modal.

1 participant