feat(desktop): add Arabic (ar) locale with RTL support

Arabic is the desktop app's first right-to-left locale. The i18n provider
now sets `document.dir`/`lang` from the active locale so Tailwind logical
utilities flip automatically, and `ar` is registered in the catalog,
language options, and alias table. The catalog is a partial `defineLocale`
so keys added to English later fall back cleanly.

Co-authored-by: 3ssiri <assiri@gmail.com>
Co-authored-by: Da7-Tech <286182457+Da7-Tech@users.noreply.github.com>
This commit is contained in:
Brooklyn Nicholson 2026-07-24 12:09:57 -05:00
parent 9f384783e7
commit 5b6990e7a0
7 changed files with 2616 additions and 3 deletions

2561
apps/desktop/src/i18n/ar.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,4 @@
import { ar } from './ar'
import { en } from './en'
import { ja } from './ja'
import type { Locale, Translations } from './types'
@ -8,5 +9,6 @@ export const TRANSLATIONS: Record<Locale, Translations> = {
en,
zh,
'zh-hant': zhHant,
ja
ja,
ar
}

View File

@ -209,6 +209,24 @@ describe('I18nProvider', () => {
expect(screen.getByTestId('locale').textContent).toBe('ja')
})
it('applies RTL direction for Arabic and restores LTR on switch back', async () => {
render(
<I18nProvider configClient={null} initialLocale="ar">
<LanguageProbe target="en" />
</I18nProvider>
)
expect(screen.getByTestId('locale').textContent).toBe('ar')
expect(document.documentElement.dir).toBe('rtl')
expect(document.documentElement.lang).toBe('ar')
fireEvent.click(screen.getByRole('button', { name: 'switch' }))
await waitFor(() => expect(screen.getByTestId('locale').textContent).toBe('en'))
expect(document.documentElement.dir).toBe('ltr')
expect(document.documentElement.lang).toBe('en')
})
it('rolls back the visible locale when saving fails', async () => {
const configClient: I18nConfigClient = {
getConfig: vi.fn().mockResolvedValue({ display: { language: 'en' } }),

View File

@ -55,6 +55,17 @@ function toError(error: unknown): Error {
return error instanceof Error ? error : new Error(String(error))
}
const RTL_LOCALES = new Set<Locale>(['ar'])
function applyDocumentLocale(locale: Locale) {
if (typeof document === 'undefined') {
return
}
document.documentElement.lang = locale
document.documentElement.dir = RTL_LOCALES.has(locale) ? 'rtl' : 'ltr'
}
export interface I18nContextValue {
configLoadError: Error | null
isLoadingConfig: boolean
@ -92,6 +103,7 @@ export function I18nProvider({ children, configClient = defaultConfigClient, ini
useEffect(() => {
localeRef.current = locale
setRuntimeI18nLocale(locale)
applyDocumentLocale(locale)
}, [locale])
useEffect(() => {

View File

@ -15,6 +15,9 @@ describe('desktop i18n languages', () => {
expect(normalizeLocale('zh_HK')).toBe('zh-hant')
expect(normalizeLocale('ja')).toBe('ja')
expect(normalizeLocale('ja-JP')).toBe('ja')
expect(normalizeLocale('ar')).toBe('ar')
expect(normalizeLocale('AR-SA')).toBe('ar')
expect(normalizeLocale(' ar_eg ')).toBe('ar')
})
it('falls back to English for empty or unsupported values', () => {
@ -32,6 +35,7 @@ describe('desktop i18n languages', () => {
expect(isLocale('zh')).toBe(true)
expect(isLocale('zh-hant')).toBe(true)
expect(isLocale('ja')).toBe(true)
expect(isLocale('ar')).toBe(true)
})
it('returns the persisted config value for supported locales', () => {
@ -39,5 +43,6 @@ describe('desktop i18n languages', () => {
expect(localeConfigValue('zh')).toBe('zh')
expect(localeConfigValue('zh-hant')).toBe('zh-hant')
expect(localeConfigValue('ja')).toBe('ja')
expect(localeConfigValue('ar')).toBe('ar')
})
})

View File

@ -28,6 +28,12 @@ export const LOCALE_OPTIONS = [
name: '日本語',
englishName: 'Japanese',
configValue: 'ja'
},
{
id: 'ar',
name: 'العربية',
englishName: 'Arabic',
configValue: 'ar'
}
] as const satisfies readonly { configValue: string; englishName: string; id: Locale; name: string }[]
@ -64,7 +70,16 @@ const LOCALE_ALIASES: Record<string, Locale> = {
zh_hant_hk: 'zh-hant',
ja: 'ja',
'ja-jp': 'ja',
ja_jp: 'ja'
ja_jp: 'ja',
ar: 'ar',
'ar-sa': 'ar',
ar_sa: 'ar',
'ar-ae': 'ar',
ar_ae: 'ar',
'ar-eg': 'ar',
ar_eg: 'ar',
arabic: 'ar',
العربية: 'ar'
}
export function isLocale(value: unknown): value is Locale {

View File

@ -5,7 +5,7 @@
// partial locales should use `defineLocale()` so missing desktop-only strings
// fall back to English while new keys remain type-checked.
export type Locale = 'en' | 'zh' | 'zh-hant' | 'ja'
export type Locale = 'en' | 'zh' | 'zh-hant' | 'ja' | 'ar'
export type ToolTitleKey =
| 'browser_click'