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:
parent
9f384783e7
commit
5b6990e7a0
File diff suppressed because it is too large
Load Diff
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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' } }),
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
Loading…
Reference in New Issue