Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion src/contactLink.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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('<html lang="ar" dir="rtl">')
expect(page).toContain(`<title>${title}</title>`)
expect(page).toContain(`<h1>${title}</h1>`)
expect(page).toContain('<meta property="og:locale" content="ar_AR">')
expect(page).toContain('>احصل على WitnessWork</a>')
expect(page).toContain('>فتح التطبيق (مثبّت بالفعل)</a>')
expect(ogUrl(page)).toBe(CANONICAL_URL)
expect(page.slice(0, page.indexOf('</head>'))).not.toContain(PAYLOAD)
}
expect(await html('/c?lang=en-us')).toContain('<html lang="en-us">')
})

it('translates instructions, buttons, descriptions and image labels', async () => {
const response = await request('/c', { headers: { 'Accept-Language': 'es-MX' } })
const page = await response.text()
Expand Down Expand Up @@ -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'],
Expand All @@ -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(`<html lang="${locale}">`)
expect(await response.text()).toContain(`<html lang="${locale}"`)
})

it('normalizes explicit language and falls back safely for invalid values', async () => {
Expand Down
5 changes: 4 additions & 1 deletion src/contactLink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { HTTP_STATUS } from './config'
import { apiError } from './errors'
import { nameTransactionAfterRoute } from './sentry'
import {
contactLinkDirection,
contactLinkTranslations,
resolveContactLinkLocale,
type ContactLinkLocale,
Expand Down Expand Up @@ -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 `<!doctype html>
<html lang="${locale}">
<html lang="${locale}"${dir}>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
Expand Down
18 changes: 17 additions & 1 deletion src/contactLinkLocales.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, ContactLinkMessages>

export type ContactLinkLocale = keyof typeof contactLinkTranslations
const DEFAULT_LOCALE: ContactLinkLocale = 'en-us'

const RIGHT_TO_LEFT: ReadonlySet<ContactLinkLocale> = 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)) {
Expand All @@ -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. */
Expand Down
Loading