From 4ff7c608cfdf80bff52474c4752c378809c3c775 Mon Sep 17 00:00:00 2001 From: Levi Wilkerson Date: Wed, 14 Oct 2026 23:23:03 -0400 Subject: [PATCH] feat: add arabic to the shared contact page --- src/contactLink.test.ts | 26 +++++++++++++++++++++++++- src/contactLink.ts | 5 ++++- src/contactLinkLocales.ts | 18 +++++++++++++++++- 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/contactLink.test.ts b/src/contactLink.test.ts index 953a897..f3e1aa7 100644 --- a/src/contactLink.test.ts +++ b/src/contactLink.test.ts @@ -167,6 +167,27 @@ describe('contact link localization', () => { } }) + it('renders Arabic right to left on both routes', async () => { + const title = 'افتح جهة الاتصال هذه في WitnessWork' + for (const path of ['/c', `/c/${PAYLOAD}`]) { + const response = await request(path, { + headers: { 'Accept-Language': 'ar-SA,ar;q=0.9,en;q=0.5' }, + }) + expect(response.status).toBe(200) + expect(response.headers.get('content-language')).toBe('ar') + const page = await response.text() + expect(page).toContain('') + expect(page).toContain(`${title}`) + expect(page).toContain(`

${title}

`) + expect(page).toContain('') + expect(page).toContain('>احصل على WitnessWork') + expect(page).toContain('>فتح التطبيق (مثبّت بالفعل)') + expect(ogUrl(page)).toBe(CANONICAL_URL) + expect(page.slice(0, page.indexOf(''))).not.toContain(PAYLOAD) + } + expect(await html('/c?lang=en-us')).toContain('') + }) + it('translates instructions, buttons, descriptions and image labels', async () => { const response = await request('/c', { headers: { 'Accept-Language': 'es-MX' } }) const page = await response.text() @@ -199,6 +220,9 @@ describe('contact link localization', () => { ['zh-Hans-TW', 'zh-hans-cn'], ['zh-CN', 'zh-hans-cn'], ['zh', 'zh-hans-cn'], + ['ar', 'ar'], + ['ar-EG', 'ar'], + ['xx, AR-sa;q=0.6', 'ar'], ['*', 'en-us'], ['xx-XX', 'en-us'], ['constructor, __proto__', 'en-us'], @@ -207,7 +231,7 @@ describe('contact link localization', () => { headers: header ? { 'Accept-Language': header } : undefined, }) expect(response.headers.get('content-language')).toBe(locale) - expect(await response.text()).toContain(``) + expect(await response.text()).toContain(` { diff --git a/src/contactLink.ts b/src/contactLink.ts index 3163588..f8229a9 100644 --- a/src/contactLink.ts +++ b/src/contactLink.ts @@ -3,6 +3,7 @@ import { HTTP_STATUS } from './config' import { apiError } from './errors' import { nameTransactionAfterRoute } from './sentry' import { + contactLinkDirection, contactLinkTranslations, resolveContactLinkLocale, type ContactLinkLocale, @@ -180,10 +181,12 @@ function renderFallbackPage(locale: ContactLinkLocale, openAppLink: string): str const description = escapeHtml(messages.description) const imageAlt = escapeHtml(messages.imageAlt) // Open Graph uses language_TERRITORY rather than BCP 47 script subtags. + // Region-less Arabic becomes `ar_AR`, the code Open Graph uses for Arabic. const [language] = locale.split('-') const territory = locale.split('-').at(-1)!.toUpperCase() + const dir = contactLinkDirection(locale) === 'rtl' ? ' dir="rtl"' : '' return ` - + diff --git a/src/contactLinkLocales.ts b/src/contactLinkLocales.ts index 1336fbe..6d48978 100644 --- a/src/contactLinkLocales.ts +++ b/src/contactLinkLocales.ts @@ -153,11 +153,27 @@ export const contactLinkTranslations = { openApp: 'Fungura porogaramu (isanzwe iri ku gikoresho)', imageAlt: 'Agashushondanga ka porogaramu ya WitnessWork', }, + // One Arabic for every country, like the app (`ar` has no region). + 'ar': { + title: 'افتح جهة الاتصال هذه في WitnessWork', + description: 'تمت مشاركة جهة اتصال معك من WitnessWork — تطبيق إدارة وقت الخدمة وجهات الاتصال لشهود يهوه.', + instructions: 'ثبّت WitnessWork لاستيراد جهة الاتصال التي شوركت معك. إذا كان التطبيق مثبّتًا لديك، فمن المفترض أن يكون قد فُتح تلقائيًا.', + getApp: 'احصل على WitnessWork', + openApp: 'فتح التطبيق (مثبّت بالفعل)', + imageAlt: 'أيقونة تطبيق WitnessWork', + }, } satisfies Record export type ContactLinkLocale = keyof typeof contactLinkTranslations const DEFAULT_LOCALE: ContactLinkLocale = 'en-us' +const RIGHT_TO_LEFT: ReadonlySet = new Set(['ar']) + +/** The page's text direction in `locale`. */ +export function contactLinkDirection(locale: ContactLinkLocale): 'ltr' | 'rtl' { + return RIGHT_TO_LEFT.has(locale) ? 'rtl' : 'ltr' +} + function matchLocale(value: string): ContactLinkLocale | undefined { const locale = value.trim().toLowerCase() if (Object.hasOwn(contactLinkTranslations, locale)) { @@ -174,7 +190,7 @@ function matchLocale(value: string): ContactLinkLocale | undefined { : 'zh-hans-cn' } return (Object.keys(contactLinkTranslations) as ContactLinkLocale[]) - .find((key) => key.startsWith(`${language}-`)) + .find((key) => key === language || key.startsWith(`${language}-`)) } /** Explicit link language wins; otherwise use browser preferences, then English. */