Локализует приложение на 10 языков + RTL для арабского и персидского

Добавляет zh-CN, fa, ar, tr, uk, be, vi, uz локали (81 ключ, идентичная
структура) к существующим en/ru; ToggleLanguge.tsx переделан из
бинарного ru/en тумблера в выпадающий список всех 10 языков.

TelegramWeb.tsx был полностью на захардкоженном русском несмотря на импорт
useTranslation — переведён на i18n-ключи. rtlLanguages.ts выносит список
RTL-языков в отдельный .ts-файл (именованный импорт из .js падал на
allowJs). App.tsx реактивно переключает document.dir/lang при смене языка.

Логические CSS-свойства (ps-/pe-/start-/end-/border-e) вместо физических
в badge/button/dialog/select и BurgerMenu — Flexbox иначе визуально
переворачивает детей под dir=rtl, ломая анимацию бургер-меню.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-08 22:52:20 +07:00
parent 5f6e06589c
commit da77c8d755
22 changed files with 832 additions and 66 deletions
+1 -1
View File
@@ -5,7 +5,7 @@ import { Slot } from "radix-ui"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
"group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
{
variants: {
variant: {
+4 -4
View File
@@ -22,10 +22,10 @@ const buttonVariants = cva(
},
size: {
default:
"h-9 gap-1.5 px-3 has-data-[icon=inline-end]:pr-2.5 has-data-[icon=inline-start]:pl-2.5",
xs: "h-6 gap-1 px-2.5 text-xs has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2 [&_svg:not([class*='size-'])]:size-3",
sm: "h-8 gap-1 px-3 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
lg: "h-10 gap-1.5 px-4 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
"h-9 gap-1.5 px-3 has-data-[icon=inline-end]:pe-2.5 has-data-[icon=inline-start]:ps-2.5",
xs: "h-6 gap-1 px-2.5 text-xs has-data-[icon=inline-end]:pe-2 has-data-[icon=inline-start]:ps-2 [&_svg:not([class*='size-'])]:size-3",
sm: "h-8 gap-1 px-3 has-data-[icon=inline-end]:pe-2 has-data-[icon=inline-start]:ps-2",
lg: "h-10 gap-1.5 px-4 has-data-[icon=inline-end]:pe-3 has-data-[icon=inline-start]:ps-3",
icon: "size-9",
"icon-xs": "size-6 [&_svg:not([class*='size-'])]:size-3",
"icon-sm": "size-8",
+6 -3
View File
@@ -1,5 +1,6 @@
import * as React from "react"
import { Dialog as DialogPrimitive } from "radix-ui"
import { useTranslation } from "react-i18next"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
@@ -53,6 +54,7 @@ function DialogContent({
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean
}) {
const { t } = useTranslation()
return (
<DialogPortal>
<DialogOverlay />
@@ -69,12 +71,12 @@ function DialogContent({
<DialogPrimitive.Close data-slot="dialog-close" asChild>
<Button
variant="ghost"
className="absolute top-4 right-4"
className="absolute top-4 end-4"
size="icon-sm"
>
<IconX
/>
<span className="sr-only">Close</span>
<span className="sr-only">{t("dialog_close")}</span>
</Button>
</DialogPrimitive.Close>
)}
@@ -101,6 +103,7 @@ function DialogFooter({
}: React.ComponentProps<"div"> & {
showCloseButton?: boolean
}) {
const { t } = useTranslation()
return (
<div
data-slot="dialog-footer"
@@ -113,7 +116,7 @@ function DialogFooter({
{children}
{showCloseButton && (
<DialogPrimitive.Close asChild>
<Button variant="outline">Close</Button>
<Button variant="outline">{t("dialog_close")}</Button>
</DialogPrimitive.Close>
)}
</div>
+2 -2
View File
@@ -110,12 +110,12 @@ function SelectItem({
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"relative flex w-full cursor-default items-center gap-2.5 rounded-xl py-2 pr-8 pl-3 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
"relative flex w-full cursor-default items-center gap-2.5 rounded-xl py-2 pe-8 ps-3 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className
)}
{...props}
>
<span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center">
<span className="pointer-events-none absolute end-2 flex size-4 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<IconCheck className="pointer-events-none" />
</SelectPrimitive.ItemIndicator>