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
@@ -123,7 +123,7 @@ export function ContextMenuItem({
role="menuitem"
disabled={disabled}
className={cn(
"w-full px-3 py-1.5 text-sm text-left flex items-center gap-2",
"w-full px-3 py-1.5 text-sm text-start flex items-center gap-2",
"transition-colors duration-150",
"focus:outline-none focus:bg-muted",
disabled && "opacity-50 cursor-not-allowed",
@@ -139,7 +139,7 @@ export function ContextMenuItem({
{Icon && <Icon className="w-4 h-4 flex-shrink-0" />}
<span className="flex-1">{label}</span>
{shortcut && (
<span className="text-xs text-muted-foreground ml-auto">{shortcut}</span>
<span className="text-xs text-muted-foreground ms-auto">{shortcut}</span>
)}
</button>
);
+17 -1
View File
@@ -245,6 +245,21 @@ export function FlagIR(props: FlagProps) {
);
}
/** Israel white field, two blue stripes, Star of David */
export function FlagIL(props: FlagProps) {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 220 160" width={W} height={H} className={flagClass} {...props}>
<rect width="220" height="160" fill="#fff" />
<rect width="220" height="20" y="25" fill="#0038b8" />
<rect width="220" height="20" y="115" fill="#0038b8" />
<g fill="none" stroke="#0038b8" strokeWidth="5.5">
<path d="M110 58 L91 91 L129 91 Z" />
<path d="M110 102 L91 69 L129 69 Z" />
</g>
</svg>
);
}
const skShield =
"m269.993 459.98-3.906-1.867c-25.267-12.173-56.294-30.4-81.294-58.133" +
"-25-27.733-43.8-65.307-43.8-114.24 0-93.6 4.52-136.68 4.52-136.68" +
@@ -280,7 +295,7 @@ export function FlagSK(props: FlagProps) {
);
}
/** Map locale codes to flag components */
export const flagComponents: Record<string, (props: FlagProps) => ReactElement> = {
cs: FlagCS,
sk: FlagSK,
@@ -303,4 +318,5 @@ export const flagComponents: Record<string, (props: FlagProps) => ReactElement>
uk: FlagUA,
zh: FlagCN,
fa: FlagIR,
he: FlagIL,
};
+10 -9
View File
@@ -1,13 +1,13 @@
"use client";
import { useState, useRef, useEffect } from "react";
import { useLocale } from 'next-intl';
import { useLocaleStore } from '@/stores/locale-store';
import { ChevronDown } from 'lucide-react';
import { cn } from '@/lib/utils';
import { flagComponents } from './flag-icons';
const languages = [
{ value: 'auto', label: 'Auto' },
{ value: 'cs', label: 'Česky' },
{ value: 'sk', label: 'Slovenčina' },
{ value: 'da', label: 'Dansk' },
@@ -16,6 +16,7 @@ const languages = [
{ value: 'fa', label: 'فارسی' },
{ value: 'es', label: 'Español' },
{ value: 'fr', label: 'Français' },
{ value: 'he', label: 'עברית' },
{ value: 'it', label: 'Italiano' },
{ value: 'hu', label: 'Magyar' },
{ value: 'lv', label: 'Latviešu' },
@@ -38,13 +39,13 @@ function FlagIcon({ locale }: { locale: string }) {
}
export function LanguageSwitcher({ className }: { className?: string }) {
const currentLocale = useLocale();
const setLocale = useLocaleStore((state) => state.setLocale);
const choice = useLocaleStore((state) => state.locale) || 'auto';
const [open, setOpen] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
const listRef = useRef<HTMLUListElement>(null);
const current = languages.find((l) => l.value === currentLocale) ?? languages[0];
const current = languages.find((l) => l.value === choice) ?? languages[0];
// Close on outside click
useEffect(() => {
@@ -78,7 +79,7 @@ export function LanguageSwitcher({ className }: { className?: string }) {
aria-expanded={open}
>
<FlagIcon locale={current.value} />
<span className="flex-1 text-left">{current.label}</span>
<span className="flex-1 text-start">{current.label}</span>
<ChevronDown className={cn("h-3.5 w-3.5 text-muted-foreground transition-transform duration-150", open && "rotate-180")} />
</button>
@@ -86,22 +87,22 @@ export function LanguageSwitcher({ className }: { className?: string }) {
<ul
ref={listRef}
role="listbox"
aria-activedescendant={`lang-${currentLocale}`}
className="absolute z-50 mt-1 w-full max-h-60 overflow-auto rounded-md border border-border bg-background shadow-lg py-1"
aria-activedescendant={`lang-${choice}`}
className="absolute start-0 z-50 mt-1 min-w-full w-max max-w-[min(16rem,80vw)] max-h-60 overflow-auto rounded-md border border-border bg-background shadow-lg py-1"
>
{languages.map((lang) => (
<li
key={lang.value}
id={`lang-${lang.value}`}
role="option"
aria-selected={lang.value === currentLocale}
aria-selected={lang.value === choice}
onClick={() => {
setLocale(lang.value);
setOpen(false);
}}
className={cn(
"flex items-center gap-2 px-3 py-1.5 text-sm cursor-pointer transition-colors duration-100",
lang.value === currentLocale
"flex items-center gap-2 px-3 py-1.5 text-sm cursor-pointer whitespace-nowrap transition-colors duration-100",
lang.value === choice
? "bg-accent text-accent-foreground font-medium"
: "text-foreground hover:bg-accent/50"
)}
+1 -1
View File
@@ -101,7 +101,7 @@ export function ToastItem({ toast, onClose }: ToastProps) {
{/* Left accent bar */}
<div className={cn("absolute left-0 top-0 bottom-0 w-1", progressBarStyles[toast.type])} />
<div className="flex items-start gap-3 p-3.5 pl-4.5 flex-1 min-w-0">
<div className="flex items-start gap-3 p-3.5 ps-4.5 flex-1 min-w-0">
{/* Icon */}
{toast.icon !== undefined ? (
toast.icon
+2 -2
View File
@@ -111,7 +111,7 @@ export function WelcomeBanner() {
onClick={() => { dismiss(); startTour(); }}
className="text-xs h-7"
>
<PlayCircle className="w-3.5 h-3.5 mr-1" />
<PlayCircle className="w-3.5 h-3.5 me-1" />
{t("start_tour")}
</Button>
<Button
@@ -120,7 +120,7 @@ export function WelcomeBanner() {
onClick={() => { dismiss(); router.push('/settings'); }}
className="text-xs h-7"
>
<Settings className="w-3.5 h-3.5 mr-1" />
<Settings className="w-3.5 h-3.5 me-1" />
{t("settings")}
</Button>
<Button