Browse Source
feat: add internationalization and RTL support across UI components and profile API requests
master
feat: add internationalization and RTL support across UI components and profile API requests
master
16 changed files with 1259 additions and 392 deletions
-
343src/app/new-match/new-match-client.tsx
-
37src/app/new-match/profile/page.tsx
-
95src/app/questions-list/[slug]/question-detail-client.tsx
-
153src/app/questions-list/questions-list-client.tsx
-
28src/components/Componentes/call-result-sheet.tsx
-
32src/components/Componentes/dismiss-reason-sheet.tsx
-
11src/components/Componentes/female-consent-sheet.tsx
-
45src/components/Componentes/female-outcome-sheet.test.tsx
-
391src/components/Componentes/female-outcome-sheet.tsx
-
21src/components/Componentes/outcome-selection-sheet.tsx
-
14src/components/Componentes/slider-slide-four.tsx
-
17src/components/Componentes/subscription-required-sheet.tsx
-
209src/components/Componentes/token-switcher.tsx
-
30src/hooks/marriage/use-profile-main.ts
-
65src/lib/marriage-field-formatter.test.ts
-
160src/lib/marriage-field-formatter.ts
@ -4,6 +4,14 @@ import { useEffect, useState } from "react"; |
|||||
import { Ic } from "@/icons"; |
import { Ic } from "@/icons"; |
||||
import { getClientCookie, setClientCookie } from "@/lib/cookies"; |
import { getClientCookie, setClientCookie } from "@/lib/cookies"; |
||||
import { setCachedMarriageEntryPath } from "@/lib/entry-route-cache"; |
import { setCachedMarriageEntryPath } from "@/lib/entry-route-cache"; |
||||
|
import { |
||||
|
isLocale, |
||||
|
LANGUAGE_MAP, |
||||
|
type LanguageCode, |
||||
|
localeDirections, |
||||
|
locales, |
||||
|
localizePath, |
||||
|
} from "@/translations/config"; |
||||
|
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// ۱. توکنهای ثابت سیستم (System Preset Tokens)
|
// ۱. توکنهای ثابت سیستم (System Preset Tokens)
|
||||
@ -17,9 +25,51 @@ export const MALE_EMAIL = "[email protected]"; |
|||||
export const MALE_2_EMAIL = "[email protected]"; |
export const MALE_2_EMAIL = "[email protected]"; |
||||
|
|
||||
const TOKEN_COOKIE_NAME = "HABIB_TOKEN"; |
const TOKEN_COOKIE_NAME = "HABIB_TOKEN"; |
||||
|
const LANGUAGE_COOKIE_NAME = "HABIB_LANGUAGE"; |
||||
const DEV_CUSTOM_TOKENS_STORAGE_KEY = "HABIB_DEV_CUSTOM_TOKENS_V1"; |
const DEV_CUSTOM_TOKENS_STORAGE_KEY = "HABIB_DEV_CUSTOM_TOKENS_V1"; |
||||
const DEV_ACTIVE_NAME_STORAGE_KEY = "HABIB_DEV_ACTIVE_NAME_V1"; |
const DEV_ACTIVE_NAME_STORAGE_KEY = "HABIB_DEV_ACTIVE_NAME_V1"; |
||||
const DEV_ACTIVE_EMOJI_STORAGE_KEY = "HABIB_DEV_ACTIVE_EMOJI_V1"; |
const DEV_ACTIVE_EMOJI_STORAGE_KEY = "HABIB_DEV_ACTIVE_EMOJI_V1"; |
||||
|
const DEV_LOCALE_STORAGE_KEY = "HABIB_DEV_LOCALE_V1"; |
||||
|
const LANGUAGE_COOKIE_MAX_AGE = 60 * 60 * 24 * 365; |
||||
|
|
||||
|
export const LOCALE_FLAGS: Record<string, string> = { |
||||
|
fa: "🇮🇷", |
||||
|
en: "🇺🇸", |
||||
|
ar: "🇸🇦", |
||||
|
tr: "🇹🇷", |
||||
|
ur: "🇵🇰", |
||||
|
az: "🇦🇿", |
||||
|
bn: "🇧🇩", |
||||
|
zh: "🇨🇳", |
||||
|
da: "🇩🇰", |
||||
|
de: "🇩🇪", |
||||
|
es: "🇪🇸", |
||||
|
fr: "🇫🇷", |
||||
|
gu: "🇮🇳", |
||||
|
ha: "🇳🇬", |
||||
|
he: "🇮🇱", |
||||
|
hi: "🇮🇳", |
||||
|
id: "🇮🇩", |
||||
|
ks: "🇮🇳", |
||||
|
pt: "🇵🇹", |
||||
|
ru: "🇷🇺", |
||||
|
sw: "🇰🇪", |
||||
|
tg: "🇹🇯", |
||||
|
ul: "🇵🇰", |
||||
|
uz: "🇺🇿", |
||||
|
}; |
||||
|
|
||||
|
export const POPULAR_DEV_LOCALES: { |
||||
|
code: LanguageCode; |
||||
|
label: string; |
||||
|
flag: string; |
||||
|
}[] = [ |
||||
|
{ code: "fa", label: "فارسی", flag: "🇮🇷" }, |
||||
|
{ code: "en", label: "English", flag: "🇺🇸" }, |
||||
|
{ code: "ar", label: "العربية", flag: "🇸🇦" }, |
||||
|
{ code: "tr", label: "Türkçe", flag: "🇹🇷" }, |
||||
|
{ code: "ur", label: "اردو", flag: "🇵🇰" }, |
||||
|
]; |
||||
|
|
||||
type WindowWithHabibToken = Window & { |
type WindowWithHabibToken = Window & { |
||||
HABIB_TOKEN?: string; |
HABIB_TOKEN?: string; |
||||
@ -99,6 +149,7 @@ export function TokenSwitcher({ |
|||||
const [currentToken, setCurrentToken] = useState<string>(""); |
const [currentToken, setCurrentToken] = useState<string>(""); |
||||
const [activeStoredName, setActiveStoredName] = useState<string>(""); |
const [activeStoredName, setActiveStoredName] = useState<string>(""); |
||||
const [activeStoredEmoji, setActiveStoredEmoji] = useState<string>(""); |
const [activeStoredEmoji, setActiveStoredEmoji] = useState<string>(""); |
||||
|
const [activeLocale, setActiveLocale] = useState<string>("fa"); |
||||
const [savedCustomAccounts, setSavedCustomAccounts] = useState< |
const [savedCustomAccounts, setSavedCustomAccounts] = useState< |
||||
DevAccountItem[] |
DevAccountItem[] |
||||
>([]); |
>([]); |
||||
@ -119,6 +170,41 @@ export function TokenSwitcher({ |
|||||
const norm = normalizeToken(rawToken); |
const norm = normalizeToken(rawToken); |
||||
setCurrentToken(norm); |
setCurrentToken(norm); |
||||
|
|
||||
|
// Sync and detect dev locale for SSR
|
||||
|
const pathLocale = window.location.pathname.split("/")[1]; |
||||
|
const cookieLocale = |
||||
|
getClientCookie(LANGUAGE_COOKIE_NAME) ?? |
||||
|
getClientCookie("habib_language"); |
||||
|
let savedDevLocale: string | null = null; |
||||
|
try { |
||||
|
savedDevLocale = window.localStorage.getItem(DEV_LOCALE_STORAGE_KEY); |
||||
|
} catch {} |
||||
|
|
||||
|
const resolvedLocale = isLocale(pathLocale) |
||||
|
? pathLocale |
||||
|
: isLocale(cookieLocale) |
||||
|
? cookieLocale |
||||
|
: isLocale(savedDevLocale) |
||||
|
? savedDevLocale |
||||
|
: "fa"; |
||||
|
|
||||
|
setActiveLocale(resolvedLocale); |
||||
|
|
||||
|
// In dev mode, ensure cookie is actively set so SSR & proxy.ts have an authoritative locale
|
||||
|
const localeCookieOptions = { |
||||
|
maxAge: LANGUAGE_COOKIE_MAX_AGE, |
||||
|
path: "/", |
||||
|
}; |
||||
|
setClientCookie( |
||||
|
LANGUAGE_COOKIE_NAME, |
||||
|
resolvedLocale, |
||||
|
localeCookieOptions, |
||||
|
); |
||||
|
setClientCookie("habib_language", resolvedLocale, localeCookieOptions); |
||||
|
try { |
||||
|
window.localStorage.setItem(DEV_LOCALE_STORAGE_KEY, resolvedLocale); |
||||
|
} catch {} |
||||
|
|
||||
try { |
try { |
||||
const savedName = window.localStorage.getItem( |
const savedName = window.localStorage.getItem( |
||||
DEV_ACTIVE_NAME_STORAGE_KEY, |
DEV_ACTIVE_NAME_STORAGE_KEY, |
||||
@ -148,6 +234,38 @@ export function TokenSwitcher({ |
|||||
return null; |
return null; |
||||
} |
} |
||||
|
|
||||
|
const persistLocale = (localeCode: string) => { |
||||
|
if (!isLocale(localeCode)) return; |
||||
|
const options = { maxAge: LANGUAGE_COOKIE_MAX_AGE, path: "/" }; |
||||
|
setClientCookie(LANGUAGE_COOKIE_NAME, localeCode, options); |
||||
|
setClientCookie("habib_language", localeCode, options); |
||||
|
try { |
||||
|
window.localStorage.setItem(DEV_LOCALE_STORAGE_KEY, localeCode); |
||||
|
} catch (e) { |
||||
|
console.warn("Failed to persist dev locale:", e); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
const handleSelectLocale = (newLocale: string) => { |
||||
|
if (!isLocale(newLocale)) return; |
||||
|
persistLocale(newLocale); |
||||
|
setActiveLocale(newLocale); |
||||
|
|
||||
|
if (typeof document !== "undefined") { |
||||
|
document.documentElement.lang = newLocale; |
||||
|
document.documentElement.dir = localeDirections[newLocale] ?? "ltr"; |
||||
|
document.body.setAttribute("data-lang", newLocale); |
||||
|
} |
||||
|
|
||||
|
if (typeof window !== "undefined") { |
||||
|
const currentPath = window.location.pathname; |
||||
|
const localizedPath = localizePath(currentPath, newLocale); |
||||
|
window.location.replace( |
||||
|
`${localizedPath}${window.location.search}${window.location.hash}`, |
||||
|
); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
const persistCustomAccounts = (accounts: DevAccountItem[]) => { |
const persistCustomAccounts = (accounts: DevAccountItem[]) => { |
||||
setSavedCustomAccounts(accounts); |
setSavedCustomAccounts(accounts); |
||||
if (typeof window !== "undefined") { |
if (typeof window !== "undefined") { |
||||
@ -168,11 +286,16 @@ export function TokenSwitcher({ |
|||||
const savedTokens = window.localStorage.getItem( |
const savedTokens = window.localStorage.getItem( |
||||
DEV_CUSTOM_TOKENS_STORAGE_KEY, |
DEV_CUSTOM_TOKENS_STORAGE_KEY, |
||||
); |
); |
||||
|
const savedLocale = |
||||
|
window.localStorage.getItem(DEV_LOCALE_STORAGE_KEY) || activeLocale; |
||||
window.localStorage.clear(); |
window.localStorage.clear(); |
||||
window.sessionStorage.clear(); |
window.sessionStorage.clear(); |
||||
if (savedTokens) { |
if (savedTokens) { |
||||
window.localStorage.setItem(DEV_CUSTOM_TOKENS_STORAGE_KEY, savedTokens); |
window.localStorage.setItem(DEV_CUSTOM_TOKENS_STORAGE_KEY, savedTokens); |
||||
} |
} |
||||
|
if (savedLocale) { |
||||
|
window.localStorage.setItem(DEV_LOCALE_STORAGE_KEY, savedLocale); |
||||
|
} |
||||
if (keepName) { |
if (keepName) { |
||||
window.localStorage.setItem(DEV_ACTIVE_NAME_STORAGE_KEY, keepName); |
window.localStorage.setItem(DEV_ACTIVE_NAME_STORAGE_KEY, keepName); |
||||
} |
} |
||||
@ -221,6 +344,10 @@ export function TokenSwitcher({ |
|||||
if (typeof window !== "undefined") { |
if (typeof window !== "undefined") { |
||||
safeClearStorage(resolvedTitle, resolvedEmoji); |
safeClearStorage(resolvedTitle, resolvedEmoji); |
||||
|
|
||||
|
const targetLocale = |
||||
|
activeLocale && isLocale(activeLocale) ? activeLocale : "fa"; |
||||
|
persistLocale(targetLocale); |
||||
|
|
||||
setCachedMarriageEntryPath(null); |
setCachedMarriageEntryPath(null); |
||||
setClientCookie(TOKEN_COOKIE_NAME, cleanToken); |
setClientCookie(TOKEN_COOKIE_NAME, cleanToken); |
||||
setClientCookie("habib_token", cleanToken); |
setClientCookie("habib_token", cleanToken); |
||||
@ -235,7 +362,7 @@ export function TokenSwitcher({ |
|||||
setActiveStoredEmoji(resolvedEmoji); |
setActiveStoredEmoji(resolvedEmoji); |
||||
setIsOpen(false); |
setIsOpen(false); |
||||
|
|
||||
window.location.replace("/"); |
|
||||
|
window.location.replace(`/${targetLocale}`); |
||||
} |
} |
||||
}; |
}; |
||||
|
|
||||
@ -368,14 +495,14 @@ export function TokenSwitcher({ |
|||||
|
|
||||
return ( |
return ( |
||||
<> |
<> |
||||
{/* Floating Badge (وقتی بسته است فقط نام حساب و ایموجی را نمایش میدهد) */} |
|
||||
|
{/* Floating Badge (وقتی بسته است نام حساب، ایموجی و زبان فعال SSR را نمایش میدهد) */} |
||||
<button |
<button |
||||
type="button" |
type="button" |
||||
onClick={() => setIsOpen(true)} |
onClick={() => setIsOpen(true)} |
||||
aria-label="تغییر توکن کاربر (تست)" |
|
||||
title="تغییر توکن کاربر (محیط توسعه)" |
|
||||
|
aria-label="تغییر توکن کاربر و زبان (تست)" |
||||
|
title="تغییر توکن کاربر و زبان مریج (محیط توسعه)" |
||||
className={[ |
className={[ |
||||
"fixed top-4 left-4 z-[99999] inline-flex items-center gap-2 rounded-[15px] px-3.5 py-2 text-xs font-extrabold transition-all active:scale-95 shadow-md border border-slate-200/80 dark:border-slate-700/80 backdrop-blur-md max-w-[260px] cursor-pointer", |
|
||||
|
"fixed top-4 left-4 z-[99999] inline-flex items-center gap-2 rounded-[15px] px-3.5 py-2 text-xs font-extrabold transition-all active:scale-95 shadow-md border border-slate-200/80 dark:border-slate-700/80 backdrop-blur-md max-w-[320px] cursor-pointer", |
||||
variant === "transparent" |
variant === "transparent" |
||||
? "bg-black/40 text-white hover:bg-black/50" |
? "bg-black/40 text-white hover:bg-black/50" |
||||
: "bg-white/95 text-slate-800 hover:bg-white dark:bg-slate-900/95 dark:text-slate-100", |
: "bg-white/95 text-slate-800 hover:bg-white dark:bg-slate-900/95 dark:text-slate-100", |
||||
@ -386,6 +513,10 @@ export function TokenSwitcher({ |
|||||
> |
> |
||||
<Ic name="switchAccount" className="size-5 text-rose-500 shrink-0" /> |
<Ic name="switchAccount" className="size-5 text-rose-500 shrink-0" /> |
||||
<span className="truncate">{getFloatingButtonLabel()}</span> |
<span className="truncate">{getFloatingButtonLabel()}</span> |
||||
|
<span className="inline-flex items-center gap-1 rounded-md bg-rose-500/10 dark:bg-rose-500/20 px-1.5 py-0.5 text-[10px] font-bold text-rose-600 dark:text-rose-400 border border-rose-500/20 uppercase shrink-0"> |
||||
|
<span>{LOCALE_FLAGS[activeLocale] || "🌐"}</span> |
||||
|
<span>{activeLocale}</span> |
||||
|
</span> |
||||
</button> |
</button> |
||||
|
|
||||
{isOpen && ( |
{isOpen && ( |
||||
@ -413,10 +544,10 @@ export function TokenSwitcher({ |
|||||
</div> |
</div> |
||||
<div> |
<div> |
||||
<h3 className="font-extrabold text-slate-900 dark:text-white text-base leading-snug"> |
<h3 className="font-extrabold text-slate-900 dark:text-white text-base leading-snug"> |
||||
انتخاب حساب کاربری (تست) |
|
||||
|
پنل توسعه مریج (اکانت و زبان) |
||||
</h3> |
</h3> |
||||
<p className="text-xs text-slate-500 dark:text-slate-400"> |
<p className="text-xs text-slate-500 dark:text-slate-400"> |
||||
تغییر سریع پروفایل، نامگذاری و مدیریت توکنها |
|
||||
|
مدیریت توکن احراز هویت، زبان فعال SSR و تست پروفایلها |
||||
</p> |
</p> |
||||
</div> |
</div> |
||||
</div> |
</div> |
||||
@ -431,6 +562,70 @@ export function TokenSwitcher({ |
|||||
|
|
||||
{/* Scrollable Content */} |
{/* Scrollable Content */} |
||||
<div className="flex-1 overflow-y-auto px-5 py-4 space-y-4 min-h-0"> |
<div className="flex-1 overflow-y-auto px-5 py-4 space-y-4 min-h-0"> |
||||
|
{/* Dev Active Locale Selector Section */} |
||||
|
<div className="rounded-2xl border border-rose-200/80 dark:border-rose-900/50 bg-gradient-to-br from-rose-50/60 to-white dark:from-rose-950/20 dark:to-slate-900 p-3.5 space-y-2.5"> |
||||
|
<div className="flex items-center justify-between"> |
||||
|
<div className="flex items-center gap-2"> |
||||
|
<span className="text-base"> |
||||
|
{LOCALE_FLAGS[activeLocale] || "🌐"} |
||||
|
</span> |
||||
|
<span className="text-xs font-extrabold text-slate-900 dark:text-white"> |
||||
|
زبان فعال در حالت توسعه (SSR / Locale) |
||||
|
</span> |
||||
|
</div> |
||||
|
<span className="text-[10px] font-mono px-2 py-0.5 rounded-full bg-rose-500 text-white font-bold"> |
||||
|
{activeLocale.toUpperCase()} |
||||
|
</span> |
||||
|
</div> |
||||
|
|
||||
|
<p className="text-[11px] text-slate-500 dark:text-slate-400 leading-relaxed"> |
||||
|
تعیین زبان سراسری مریج برای SSR، ریدایرکتهای proxy.ts، |
||||
|
فراخوانیهای بکاند و دیکشنریها: |
||||
|
</p> |
||||
|
|
||||
|
{/* Quick Selection Pills */} |
||||
|
<div className="flex items-center gap-1.5 flex-wrap"> |
||||
|
{POPULAR_DEV_LOCALES.map((item) => { |
||||
|
const isSelected = activeLocale === item.code; |
||||
|
return ( |
||||
|
<button |
||||
|
key={item.code} |
||||
|
type="button" |
||||
|
onClick={() => handleSelectLocale(item.code)} |
||||
|
className={[ |
||||
|
"px-2.5 py-1.5 rounded-xl text-xs font-bold flex items-center gap-1.5 transition-all cursor-pointer", |
||||
|
isSelected |
||||
|
? "bg-rose-500 text-white shadow-xs scale-105" |
||||
|
: "bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-300 border border-slate-200 dark:border-slate-700 hover:bg-slate-100 dark:hover:bg-slate-750", |
||||
|
].join(" ")} |
||||
|
> |
||||
|
<span>{item.flag}</span> |
||||
|
<span>{item.label}</span> |
||||
|
</button> |
||||
|
); |
||||
|
})} |
||||
|
</div> |
||||
|
|
||||
|
{/* Dropdown for all 24 supported languages */} |
||||
|
<div className="flex items-center gap-2 pt-1 border-t border-rose-100 dark:border-slate-800"> |
||||
|
<span className="text-[11px] text-slate-500 dark:text-slate-400 shrink-0"> |
||||
|
سایر زبانها: |
||||
|
</span> |
||||
|
<select |
||||
|
value={activeLocale} |
||||
|
onChange={(e) => handleSelectLocale(e.target.value)} |
||||
|
className="flex-1 rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 px-2.5 py-1.5 text-xs text-slate-800 dark:text-slate-200 outline-none focus:border-rose-500 cursor-pointer font-medium" |
||||
|
> |
||||
|
{locales.map((code) => ( |
||||
|
<option key={code} value={code}> |
||||
|
{LOCALE_FLAGS[code] || "🌐"} {LANGUAGE_MAP[code]} ( |
||||
|
{code}) |
||||
|
</option> |
||||
|
))} |
||||
|
</select> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
{/* Predefined System Accounts */} |
{/* Predefined System Accounts */} |
||||
<div className="space-y-2"> |
<div className="space-y-2"> |
||||
<div className="text-[11px] font-bold text-slate-400 dark:text-slate-500 px-1"> |
<div className="text-[11px] font-bold text-slate-400 dark:text-slate-500 px-1"> |
||||
|
|||||
@ -0,0 +1,65 @@ |
|||||
|
import { describe, expect, it } from "vitest"; |
||||
|
import { formatFieldLabel, titleFromKey } from "./marriage-field-formatter"; |
||||
|
import { dictionaries } from "@/translations/dictionaries"; |
||||
|
|
||||
|
describe("marriage-field-formatter: formatFieldLabel", () => { |
||||
|
it("directly respects dynamic Arabic labels returned from backend DB without hardcoded JSON dependency", () => { |
||||
|
const field = { |
||||
|
key: "personal_identity.place_of_birth", |
||||
|
label: "مكان الميلاد (الدولة والمدينة)", |
||||
|
value: { city: "Mashhad", country: "Iran" }, |
||||
|
}; |
||||
|
|
||||
|
// When viewing with Arabic dictionary, must directly display backend's Arabic title
|
||||
|
const labelAr = formatFieldLabel(field, dictionaries.ar as Record<string, string>); |
||||
|
expect(labelAr).toBe("مكان الميلاد (الدولة والمدينة)"); |
||||
|
|
||||
|
// Must NOT be overridden with English "Place Of Birth"
|
||||
|
expect(labelAr).not.toBe("Place Of Birth"); |
||||
|
}); |
||||
|
|
||||
|
it("directly respects dynamic Arabic labels for place_of_upbringing", () => { |
||||
|
const field = { |
||||
|
key: "personal_identity.place_of_upbringing", |
||||
|
label: "مكان النشأة والترعرع (الدولة والمدينة)", |
||||
|
value: { city: "Mashhad", country: "Iran" }, |
||||
|
}; |
||||
|
|
||||
|
const labelAr = formatFieldLabel(field, dictionaries.ar as Record<string, string>); |
||||
|
expect(labelAr).toBe("مكان النشأة والترعرع (الدولة والمدينة)"); |
||||
|
expect(labelAr).not.toBe("Place Of Upbringing"); |
||||
|
}); |
||||
|
|
||||
|
it("directly respects dynamically updated DB question text", () => { |
||||
|
const field = { |
||||
|
key: "personal_identity.place_of_birth", |
||||
|
label: "في أي دولة ومدينة ولدت؟", |
||||
|
value: "...", |
||||
|
}; |
||||
|
|
||||
|
const labelAr = formatFieldLabel(field, dictionaries.ar as Record<string, string>); |
||||
|
expect(labelAr).toBe("في أي دولة ومدينة ولدت؟"); |
||||
|
}); |
||||
|
|
||||
|
it("converts Persian/Arabic labels to English title when dictionary is English", () => { |
||||
|
const field = { |
||||
|
key: "personal_identity.place_of_birth", |
||||
|
label: "مكان الميلاد (الدولة والمدينة)", |
||||
|
value: { city: "Mashhad", country: "Iran" }, |
||||
|
}; |
||||
|
|
||||
|
const labelEn = formatFieldLabel(field, dictionaries.en as Record<string, string>); |
||||
|
expect(labelEn).toBe("Place Of Birth"); |
||||
|
}); |
||||
|
|
||||
|
it("falls back to canonical / titleFromKey when backend label is missing", () => { |
||||
|
const field = { |
||||
|
key: "personal_identity.place_of_birth", |
||||
|
label: "", |
||||
|
value: "...", |
||||
|
}; |
||||
|
|
||||
|
const labelAr = formatFieldLabel(field, dictionaries.ar as Record<string, string>); |
||||
|
expect(labelAr).toBe("مكان الميلاد"); |
||||
|
}); |
||||
|
}); |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue