feat(i18n): Hebrew locale + full RTL support

This commit is contained in:
Shuki Vaknin
2026-07-08 00:07:34 +02:00
committed by Linus Rath
parent 8904d724bb
commit 22418c17cf
113 changed files with 3958 additions and 494 deletions
+2 -2
View File
@@ -229,7 +229,7 @@ export function ContactActivity({ contact }: ContactActivityProps) {
key={email.id}
type="button"
onClick={() => handleOpenEmail(email)}
className="w-full text-left flex items-start gap-3 px-2 py-2 rounded-md hover:bg-muted/60 transition-colors touch-manipulation"
className="w-full text-start flex items-start gap-3 px-2 py-2 rounded-md hover:bg-muted/60 transition-colors touch-manipulation"
>
<Avatar name={sender.name} email={sender.address} size="sm" />
<div className="flex-1 min-w-0">
@@ -277,7 +277,7 @@ export function ContactActivity({ contact }: ContactActivityProps) {
key={event.id}
type="button"
onClick={() => handleOpenEvent(event)}
className="w-full text-left flex items-baseline gap-3 px-2 py-2 rounded-md hover:bg-muted/60 transition-colors touch-manipulation"
className="w-full text-start flex items-baseline gap-3 px-2 py-2 rounded-md hover:bg-muted/60 transition-colors touch-manipulation"
>
<span className="text-xs text-muted-foreground tabular-nums w-20 flex-shrink-0">
{formatEventTime(event)}
+4 -4
View File
@@ -201,7 +201,7 @@ export function ContactDetail({ contact, onEdit, onDelete, onAddToGroup, onDupli
onClick={onCompose}
className="touch-manipulation"
>
<Send className="w-4 h-4 mr-1" />
<Send className="w-4 h-4 me-1" />
{t("detail.compose_email")}
</Button>
)}
@@ -210,12 +210,12 @@ export function ContactDetail({ contact, onEdit, onDelete, onAddToGroup, onDupli
href={`tel:${phone}`}
className="inline-flex items-center justify-center rounded-md font-medium h-9 px-3 text-sm border border-input bg-background hover:bg-accent hover:text-accent-foreground transition-colors touch-manipulation"
>
<Phone className="w-4 h-4 mr-1" />
<Phone className="w-4 h-4 me-1" />
{t("context_menu.call")}
</a>
)}
<Button variant="outline" size="sm" onClick={onEdit} className="touch-manipulation">
<Pencil className="w-4 h-4 mr-1" />
<Pencil className="w-4 h-4 me-1" />
{t("form.edit_title")}
</Button>
<MoreActionsMenu items={moreItems} label={t("detail.more_actions")} />
@@ -587,7 +587,7 @@ function MoreActionsMenu({ items, label }: { items: MoreItem[]; label: string })
setOpen(false);
}}
className={cn(
"w-full flex items-center gap-2 px-3 py-1.5 text-sm text-left hover:bg-muted focus:bg-muted focus:outline-none transition-colors",
"w-full flex items-center gap-2 px-3 py-1.5 text-sm text-start hover:bg-muted focus:bg-muted focus:outline-none transition-colors",
item.destructive && "text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-950 focus:bg-red-50 dark:focus:bg-red-950",
)}
>
+10 -10
View File
@@ -72,7 +72,7 @@ function FormSection({ icon: Icon, title, children, collapsible, defaultOpen = t
<button
type="button"
className={cn(
"flex items-center gap-2 w-full text-left",
"flex items-center gap-2 w-full text-start",
collapsible ? "cursor-pointer" : "cursor-default"
)}
onClick={() => collapsible && setOpen(!open)}
@@ -736,12 +736,12 @@ export function ContactForm({ contact, addressBooks, allKeywords, defaultAddress
)}
</div>
{emailErrors[i] && (
<p className="text-xs text-red-600 dark:text-red-400 mt-1 ml-1">{emailErrors[i]}</p>
<p className="text-xs text-red-600 dark:text-red-400 mt-1 ms-1">{emailErrors[i]}</p>
)}
</div>
))}
<Button type="button" variant="ghost" size="sm" onClick={() => setEmails([...emails, { address: "", context: "" }])} className="text-xs">
<Plus className="w-3 h-3 mr-1" />
<Plus className="w-3 h-3 me-1" />
{t("add_email")}
</Button>
</div>
@@ -799,7 +799,7 @@ export function ContactForm({ contact, addressBooks, allKeywords, defaultAddress
</div>
))}
<Button type="button" variant="ghost" size="sm" onClick={() => setPhones([...phones, { number: "", context: "", feature: "" }])} className="text-xs">
<Plus className="w-3 h-3 mr-1" />
<Plus className="w-3 h-3 me-1" />
{t("add_phone")}
</Button>
</div>
@@ -855,7 +855,7 @@ export function ContactForm({ contact, addressBooks, allKeywords, defaultAddress
</div>
))}
<Button type="button" variant="ghost" size="sm" onClick={() => setAddresses([...addresses, { street: "", locality: "", region: "", postcode: "", country: "", context: "" }])} className="text-xs">
<Plus className="w-3 h-3 mr-1" />
<Plus className="w-3 h-3 me-1" />
{t("add_address")}
</Button>
</div>
@@ -884,7 +884,7 @@ export function ContactForm({ contact, addressBooks, allKeywords, defaultAddress
</div>
))}
<Button type="button" variant="ghost" size="sm" onClick={() => setOnlineServices([...onlineServices, { uri: "", service: "", label: "" }])} className="text-xs">
<Plus className="w-3 h-3 mr-1" />
<Plus className="w-3 h-3 me-1" />
{t("add_online_service")}
</Button>
</div>
@@ -916,7 +916,7 @@ export function ContactForm({ contact, addressBooks, allKeywords, defaultAddress
</div>
))}
<Button type="button" variant="ghost" size="sm" onClick={() => setAnniversaries([...anniversaries, { date: "", kind: "birth" }])} className="text-xs">
<Plus className="w-3 h-3 mr-1" />
<Plus className="w-3 h-3 me-1" />
{t("add_anniversary")}
</Button>
</div>
@@ -957,7 +957,7 @@ export function ContactForm({ contact, addressBooks, allKeywords, defaultAddress
</div>
))}
<Button type="button" variant="ghost" size="sm" onClick={() => setPersonalInfoEntries([...personalInfoEntries, { value: "", kind: "hobby", level: "" }])} className="text-xs">
<Plus className="w-3 h-3 mr-1" />
<Plus className="w-3 h-3 me-1" />
{t("add_personal_info")}
</Button>
</div>
@@ -1145,7 +1145,7 @@ function CategoryComboBox({
<button
key={kw}
type="button"
className="w-full flex items-center gap-2 px-3 py-1.5 text-sm hover:bg-accent transition-colors text-left"
className="w-full flex items-center gap-2 px-3 py-1.5 text-sm hover:bg-accent transition-colors text-start"
onClick={() => { addKeyword(kw); inputRef.current?.focus(); }}
>
<Tag className="w-3.5 h-3.5 text-muted-foreground flex-shrink-0" />
@@ -1155,7 +1155,7 @@ function CategoryComboBox({
{canAddNew && (
<button
type="button"
className="w-full flex items-center gap-2 px-3 py-1.5 text-sm hover:bg-accent transition-colors text-left text-primary"
className="w-full flex items-center gap-2 px-3 py-1.5 text-sm hover:bg-accent transition-colors text-start text-primary"
onClick={() => { addKeyword(inputValue); inputRef.current?.focus(); }}
>
<Plus className="w-3.5 h-3.5 flex-shrink-0" />
+2 -2
View File
@@ -53,7 +53,7 @@ export function ContactGroupDetail({
</div>
<div className="flex gap-2">
<Button variant="outline" size="sm" onClick={onEdit} className="touch-manipulation">
<Pencil className="w-4 h-4 mr-1" />
<Pencil className="w-4 h-4 me-1" />
{t("form.edit_title")}
</Button>
<Button
@@ -106,7 +106,7 @@ export function ContactGroupDetail({
className="flex items-center gap-3 px-3 py-2.5 rounded-md hover:bg-muted group transition-colors"
>
<button
className="flex items-center gap-3 flex-1 min-w-0 text-left"
className="flex items-center gap-3 flex-1 min-w-0 text-start"
onClick={() => onSelectMember(member.id)}
>
<Avatar name={mName} email={mEmail} size="sm" />
+2 -2
View File
@@ -114,7 +114,7 @@ export function ContactGroupForm({
placeholder={t("groups.search_members")}
value={memberSearch}
onChange={(e) => setMemberSearch(e.target.value)}
className="pl-9"
className="ps-9"
/>
</div>
@@ -134,7 +134,7 @@ export function ContactGroupForm({
type="button"
onClick={() => toggleMember(contact.id)}
className={cn(
"w-full flex items-center gap-3 px-3 py-2.5 text-left transition-colors",
"w-full flex items-center gap-3 px-3 py-2.5 text-start transition-colors",
"hover:bg-muted",
isSelected && "bg-primary/5"
)}
+2 -2
View File
@@ -45,7 +45,7 @@ export function ContactGroupList({
<div className={cn("flex flex-col", className)}>
<div className="px-4 py-2 border-b border-border">
<Button size="sm" variant="outline" onClick={onCreateGroup} className="w-full">
<Plus className="w-4 h-4 mr-1" />
<Plus className="w-4 h-4 me-1" />
{t("groups.create")}
</Button>
</div>
@@ -69,7 +69,7 @@ export function ContactGroupList({
key={group.id}
onClick={() => onSelectGroup(group.id)}
className={cn(
"w-full flex items-center px-4 text-left transition-colors",
"w-full flex items-center px-4 text-start transition-colors",
"hover:bg-muted",
group.id === selectedGroupId && "bg-accent text-accent-foreground"
)}
@@ -187,7 +187,7 @@ export function ContactImportDialog({
type="button"
onClick={() => toggleSelect(idx)}
className={cn(
"w-full flex items-center gap-3 px-3 py-2.5 text-left transition-colors hover:bg-muted",
"w-full flex items-center gap-3 px-3 py-2.5 text-start transition-colors hover:bg-muted",
isSelected && "bg-primary/5"
)}
>
+7 -7
View File
@@ -310,7 +310,7 @@ export function ContactList({
placeholder={t("search_placeholder")}
value={searchQuery}
onChange={(e) => onSearchChange(e.target.value)}
className={cn("pl-9 h-9", searchQuery && "pr-8")}
className={cn("ps-9 h-9", searchQuery && "pe-8")}
/>
{searchQuery && (
<button
@@ -359,7 +359,7 @@ export function ContactList({
onClick={() => setFilters(EMPTY_FILTERS)}
className="h-7 px-2 text-xs"
>
<RotateCcw className="w-3 h-3 mr-1" />
<RotateCcw className="w-3 h-3 me-1" />
{t("filters.clear")}
</Button>
<Button
@@ -479,11 +479,11 @@ export function ContactList({
</span>
<div className="flex-1" />
<Button variant="ghost" size="sm" onClick={onBulkAddToGroup} className="h-7 text-xs">
<Users className="w-3.5 h-3.5 mr-1" />
<Users className="w-3.5 h-3.5 me-1" />
{t("bulk.add_to_group")}
</Button>
<Button variant="ghost" size="sm" onClick={onBulkExport} className="h-7 text-xs">
<Download className="w-3.5 h-3.5 mr-1" />
<Download className="w-3.5 h-3.5 me-1" />
{t("bulk.export")}
</Button>
<Button
@@ -492,7 +492,7 @@ export function ContactList({
onClick={onBulkDelete}
className="h-7 text-xs text-red-600 dark:text-red-400 hover:text-red-700 dark:hover:text-red-300"
>
<Trash2 className="w-3.5 h-3.5 mr-1" />
<Trash2 className="w-3.5 h-3.5 me-1" />
{t("bulk.delete")}
</Button>
<Button variant="ghost" size="icon" onClick={onClearSelection} className="h-7 w-7">
@@ -526,7 +526,7 @@ export function ContactList({
)}
{activeFilters > 0 && (
<Button variant="outline" size="sm" onClick={() => setFilters(EMPTY_FILTERS)}>
<RotateCcw className="w-3.5 h-3.5 mr-1" />
<RotateCcw className="w-3.5 h-3.5 me-1" />
{t("filters.clear")}
</Button>
)}
@@ -538,7 +538,7 @@ export function ContactList({
<p className="text-sm font-medium text-foreground">{t("empty_state_title")}</p>
<p className="text-xs text-muted-foreground mt-1">{t("empty_state_subtitle")}</p>
<Button size="sm" className="mt-3" onClick={onCreateNew}>
<UserPlus className="w-4 h-4 mr-1.5" />
<UserPlus className="w-4 h-4 me-1.5" />
{t("create_new")}
</Button>
</>
+20 -20
View File
@@ -287,14 +287,14 @@ export function ContactsSidebar({
className="absolute right-0 top-full mt-1 w-44 rounded-md border border-border bg-background text-foreground shadow-md z-50 py-1"
>
<button
className="w-full flex items-center gap-2 px-3 py-1.5 text-sm hover:bg-accent transition-colors text-left"
className="w-full flex items-center gap-2 px-3 py-1.5 text-sm hover:bg-accent transition-colors text-start"
onClick={() => { setShowMenu(false); onCreateContact(); }}
>
<UserPlus className="w-4 h-4" />
{t("create_new")}
</button>
<button
className="w-full flex items-center gap-2 px-3 py-1.5 text-sm hover:bg-accent transition-colors text-left"
className="w-full flex items-center gap-2 px-3 py-1.5 text-sm hover:bg-accent transition-colors text-start"
onClick={() => { setShowMenu(false); onCreateGroup(); }}
>
<UsersRound className="w-4 h-4" />
@@ -302,7 +302,7 @@ export function ContactsSidebar({
</button>
{onCreateAddressBook && (
<button
className="w-full flex items-center gap-2 px-3 py-1.5 text-sm hover:bg-accent transition-colors text-left"
className="w-full flex items-center gap-2 px-3 py-1.5 text-sm hover:bg-accent transition-colors text-start"
onClick={() => { setShowMenu(false); onCreateAddressBook(); }}
>
<BookPlus className="w-4 h-4" />
@@ -311,7 +311,7 @@ export function ContactsSidebar({
)}
{onImport && (
<button
className="w-full flex items-center gap-2 px-3 py-1.5 text-sm hover:bg-accent transition-colors text-left"
className="w-full flex items-center gap-2 px-3 py-1.5 text-sm hover:bg-accent transition-colors text-start"
onClick={() => { setShowMenu(false); onImport(); }}
>
<Upload className="w-4 h-4" />
@@ -338,7 +338,7 @@ export function ContactsSidebar({
>
<BookUser className="w-4 h-4 flex-shrink-0" />
<span className="truncate">{t("tabs.all")}</span>
<span className="ml-auto text-xs text-muted-foreground tabular-nums">
<span className="ms-auto text-xs text-muted-foreground tabular-nums">
{individuals.length}
</span>
</button>
@@ -355,7 +355,7 @@ export function ContactsSidebar({
<div className="flex items-center px-3 py-1 group">
<button
onClick={() => toggleSection(sectionKey)}
className="flex items-center gap-1 flex-1 min-w-0 text-left"
className="flex items-center gap-1 flex-1 min-w-0 text-start"
>
{expanded ? (
<ChevronDown className="w-3 h-3 text-muted-foreground" />
@@ -369,7 +369,7 @@ export function ContactsSidebar({
</button>
</div>
{expanded && (
<div className="pl-2">
<div className="ps-2">
{owned.length > 0 && (
<div className="mt-1">
<div className="px-3 py-0.5 text-[10px] font-medium text-muted-foreground/80 uppercase tracking-wider">
@@ -418,7 +418,7 @@ export function ContactsSidebar({
<div className="flex items-center px-3 py-1 group">
<button
onClick={() => toggleSection("addressBooks")}
className="flex items-center gap-1 flex-1 text-left"
className="flex items-center gap-1 flex-1 text-start"
>
{collapsed.addressBooks ? (
<ChevronRight className="w-3 h-3 text-muted-foreground" />
@@ -461,7 +461,7 @@ export function ContactsSidebar({
<div className="mt-2">
<button
onClick={() => toggleSection("groups")}
className="flex items-center gap-1 px-3 py-1 w-full text-left group"
className="flex items-center gap-1 px-3 py-1 w-full text-start group"
>
{collapsed.groups ? (
<ChevronRight className="w-3 h-3 text-muted-foreground" />
@@ -483,7 +483,7 @@ export function ContactsSidebar({
onClick={() => onSelectCategory({ groupId: group.id })}
onContextMenu={(e) => openGroupContextMenu(e, group)}
className={cn(
"w-full flex items-center gap-2 pl-5 pr-3 text-sm transition-colors",
"w-full flex items-center gap-2 ps-5 pe-3 text-sm transition-colors",
isActive
? "bg-accent text-accent-foreground font-medium"
: "text-foreground/80 hover:bg-muted"
@@ -492,7 +492,7 @@ export function ContactsSidebar({
>
<Users className="w-4 h-4 flex-shrink-0" />
<span className="truncate">{getContactDisplayName(group)}</span>
<span className="ml-auto text-xs text-muted-foreground tabular-nums">
<span className="ms-auto text-xs text-muted-foreground tabular-nums">
{memberCount}
</span>
</button>
@@ -505,7 +505,7 @@ export function ContactsSidebar({
<div className="mt-2">
<button
onClick={() => toggleSection("categories")}
className="flex items-center gap-1 px-3 py-1 w-full text-left group"
className="flex items-center gap-1 px-3 py-1 w-full text-start group"
>
{collapsed.categories ? (
<ChevronRight className="w-3 h-3 text-muted-foreground" />
@@ -523,7 +523,7 @@ export function ContactsSidebar({
<button
onClick={() => onSelectCategory("uncategorized")}
className={cn(
"w-full flex items-center gap-2 pl-5 pr-3 text-sm transition-colors",
"w-full flex items-center gap-2 ps-5 pe-3 text-sm transition-colors",
activeCategory === "uncategorized"
? "bg-accent text-accent-foreground font-medium"
: "text-foreground/80 hover:bg-muted"
@@ -532,7 +532,7 @@ export function ContactsSidebar({
>
<Tag className="w-3.5 h-3.5 flex-shrink-0 opacity-50" />
<span className="truncate italic">{t("no_category")}</span>
<span className="ml-auto text-xs text-muted-foreground tabular-nums">
<span className="ms-auto text-xs text-muted-foreground tabular-nums">
{uncategorizedCount}
</span>
</button>
@@ -561,7 +561,7 @@ export function ContactsSidebar({
<div className="flex items-center px-3 py-1 group">
<button
onClick={() => toggleSection(`shared-${group.accountId}`)}
className="flex items-center gap-1 flex-1 min-w-0 text-left"
className="flex items-center gap-1 flex-1 min-w-0 text-start"
>
{collapsed[`shared-${group.accountId}`] ? (
<ChevronRight className="w-3 h-3 text-muted-foreground" />
@@ -779,7 +779,7 @@ function CategoryItem({
onDragLeave={handleDragLeave}
onDrop={handleDrop}
className={cn(
"w-full flex items-center gap-2 pl-5 pr-3 text-sm transition-colors",
"w-full flex items-center gap-2 ps-5 pe-3 text-sm transition-colors",
isActive
? "bg-accent text-accent-foreground font-medium"
: "text-foreground/80 hover:bg-muted",
@@ -789,7 +789,7 @@ function CategoryItem({
>
<Tag className="w-3.5 h-3.5 flex-shrink-0" />
<span className="truncate">{keyword}</span>
<span className="ml-auto text-xs text-muted-foreground tabular-nums">
<span className="ms-auto text-xs text-muted-foreground tabular-nums">
{count}
</span>
</button>
@@ -847,7 +847,7 @@ function AddressBookItem({
onDragLeave={handleDragLeave}
onDrop={handleDrop}
className={cn(
"w-full flex items-center gap-2 pl-5 pr-3 text-sm transition-colors",
"w-full flex items-center gap-2 ps-5 pe-3 text-sm transition-colors",
isActive
? "bg-accent text-accent-foreground font-medium"
: "text-foreground/80 hover:bg-muted",
@@ -858,11 +858,11 @@ function AddressBookItem({
<Book className="w-4 h-4 flex-shrink-0" />
<span className="truncate">{book.name}</span>
{!book.isShared && Object.keys(book.shareWith || {}).length > 0 && (
<Users className="w-3 h-3 text-muted-foreground flex-shrink-0 ml-auto" />
<Users className="w-3 h-3 text-muted-foreground flex-shrink-0 ms-auto" />
)}
<span className={cn(
"text-xs text-muted-foreground tabular-nums",
!(!book.isShared && Object.keys(book.shareWith || {}).length > 0) && "ml-auto"
!(!book.isShared && Object.keys(book.shareWith || {}).length > 0) && "ms-auto"
)}>
{contactCount}
</span>