From 07ce2d013979d29ba117ce6bf806faa27f0226bf Mon Sep 17 00:00:00 2001 From: zisisvakras Date: Wed, 12 Aug 2026 02:34:06 +0300 Subject: [PATCH] fix: scope separate translation hooks instead of relative namespace paths next-intl has no "../" relative-namespace escape. A key like t('../context_menu.not_spam') is not resolved relative to anything - it is concatenated literally into the lookup key and always misses, throwing MISSING_MESSAGE. email-list.tsx, about-data-settings.tsx, and account-settings.tsx all used this pattern to reach keys outside their own translation namespace. Fixed by calling useTranslations() again for each additional namespace needed, which is the supported way to reach sibling namespaces, instead of trying to walk out of the current one. The translations test already verifies that every key referenced in source resolves in the en locale, but its extractor skipped any key starting with a dot, which is exactly the shape of the broken calls, so this class of bug never failed CI. Since no legitimate key can start with a dot, the exclusion is removed. With it gone the test reports all thirteen call sites fixed here when run against the old code, and passes with the fix, so a reintroduced relative path fails CI with the offending file listed. --- components/email/email-list.tsx | 16 +++++++++------- components/settings/about-data-settings.tsx | 7 ++++--- components/settings/account-settings.tsx | 7 ++++--- lib/__tests__/translations.test.ts | 1 - 4 files changed, 17 insertions(+), 14 deletions(-) diff --git a/components/email/email-list.tsx b/components/email/email-list.tsx index 9251f78cb..419f8fe84 100644 --- a/components/email/email-list.tsx +++ b/components/email/email-list.tsx @@ -83,6 +83,8 @@ export function EmailList({ onRescheduleScheduled, }: EmailListProps) { const t = useTranslations('email_list'); + const tContextMenu = useTranslations('context_menu'); + const tSpam = useTranslations('email_viewer.spam'); const { client } = useAuthStore(); const { selectedEmailIds, @@ -211,10 +213,10 @@ export function EmailList({ const emailIds = Array.from(selectedEmailIds); await batchUndoSpam(client, emailIds); const { toast } = await import('sonner'); - toast.success(t('../email_viewer.spam.toast_not_spam_batch', { count: emailIds.length })); + toast.success(tSpam('toast_not_spam_batch', { count: emailIds.length })); } catch { const { toast } = await import('sonner'); - toast.error(t('../email_viewer.spam.error_not_spam')); + toast.error(tSpam('error_not_spam')); } finally { setTimeout(() => setIsProcessing(false), 500); } @@ -392,7 +394,7 @@ export function EmailList({ variant="ghost" size="sm" onClick={handleBatchUndoSpam} - title={t('../context_menu.not_spam')} + title={tContextMenu('not_spam')} disabled={isProcessing} className="text-emerald-600 dark:text-emerald-400 hover:bg-emerald-100/50 dark:hover:bg-emerald-950/30 transition-colors disabled:opacity-50" > @@ -629,11 +631,11 @@ export function EmailList({ await batchMarkAsSpam(client, emailIds); const { toast } = await import('sonner'); toast.success( - t('../email_viewer.spam.toast_batch', { count: emailIds.length }) + tSpam('toast_batch', { count: emailIds.length }) ); } catch { const { toast } = await import('sonner'); - toast.error(t('../email_viewer.spam.error')); + toast.error(tSpam('error')); } } }} @@ -644,11 +646,11 @@ export function EmailList({ await batchUndoSpam(client, emailIds); const { toast } = await import('sonner'); toast.success( - t('../email_viewer.spam.toast_not_spam_batch', { count: emailIds.length }) + tSpam('toast_not_spam_batch', { count: emailIds.length }) ); } catch { const { toast } = await import('sonner'); - toast.error(t('../email_viewer.spam.error_not_spam')); + toast.error(tSpam('error_not_spam')); } } }} diff --git a/components/settings/about-data-settings.tsx b/components/settings/about-data-settings.tsx index 0c0556b90..8f8c647b5 100644 --- a/components/settings/about-data-settings.tsx +++ b/components/settings/about-data-settings.tsx @@ -51,6 +51,7 @@ function VersionUpdateTag() { export function AboutDataSettings() { const t = useTranslations('settings.advanced'); const tCommon = useTranslations('common'); + const tSettings = useTranslations('settings'); const { settingsSyncDisabled, updateSetting, resetToDefaults, exportSettings, importSettings } = useSettingsStore(); const { settingsSyncEnabled } = useConfig(); @@ -99,9 +100,9 @@ export function AboutDataSettings() { const json = event.target?.result as string; const success = importSettings(json); if (success) { - alert(t('../../settings.import_success')); + alert(tSettings('import_success')); } else { - alert(t('../../settings.import_error')); + alert(tSettings('import_error')); } }; reader.readAsText(file); @@ -120,7 +121,7 @@ export function AboutDataSettings() { if (showResetConfirm) { resetToDefaults(); setShowResetConfirm(false); - alert(t('../../settings.save_success')); + alert(tSettings('save_success')); } else { setShowResetConfirm(true); setTimeout(() => setShowResetConfirm(false), 5000); diff --git a/components/settings/account-settings.tsx b/components/settings/account-settings.tsx index ae0ccbf84..4845191cc 100644 --- a/components/settings/account-settings.tsx +++ b/components/settings/account-settings.tsx @@ -31,6 +31,7 @@ function firstScopedTab(caps: SharedAccount['capabilities']): string | null { export function AccountSettings() { const t = useTranslations('settings.account'); + const tCommon = useTranslations('common'); const router = useRouter(); const { username, serverUrl, isDemoMode, primaryIdentity, authMode, client } = useAuthStore(); const activeAccountId = useAuthStore((s) => s.activeAccountId); @@ -117,12 +118,12 @@ export function AccountSettings() { {/* Display Name */} - {displayName || t('../../common.unknown')} + {displayName || tCommon('unknown')} {/* Email Address */} - {email || t('../../common.unknown')} + {email || tCommon('unknown')} {/* Username / Login (show when it differs from email) */} @@ -142,7 +143,7 @@ export function AccountSettings() { {/* Server */} - {serverUrl || t('../../common.unknown')} + {serverUrl || tCommon('unknown')} diff --git a/lib/__tests__/translations.test.ts b/lib/__tests__/translations.test.ts index a91aab8d0..5213b7f96 100644 --- a/lib/__tests__/translations.test.ts +++ b/lib/__tests__/translations.test.ts @@ -75,7 +75,6 @@ function extractUsedKeys(filePath: string): string[] { const callRegex = new RegExp(`\\b${varName}\\(\\s*["']([^"'{}]+)["']`, 'g'); while ((m = callRegex.exec(content)) !== null) { const key = m[1]; - if (key.startsWith('.')) continue; // Find the nearest preceding assignment for this variable const ns = varAssignments .filter((a) => a.index < m!.index)