Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ A modern, responsive web management console for RustFS distributed file system,

### Other Tools

- **[i18next](https://www.i18next.com/)** - Internationalization support (12 languages)
- **[i18next](https://www.i18next.com/)** - Internationalization support (13 languages)
- **[Recharts](https://recharts.org/)** - Chart visualization
- **[Sonner](https://sonner.emilkowal.ski/)** - Toast notifications
- **[date-fns](https://date-fns.org/)** / **[dayjs](https://day.js.org/)** - Date handling
Expand All @@ -65,7 +65,7 @@ console/
│ └── feedback/ # Global feedback APIs (toast, dialog)
├── types/ # TypeScript type definitions
├── i18n/ # Internationalization resource files
│ └── locales/ # Multi-language files (12 languages)
│ └── locales/ # Multi-language files (13 languages)
├── config/ # Configuration files
├── public/ # Static assets
└── tests/ # Test files (mirror source structure)
Expand Down Expand Up @@ -192,6 +192,7 @@ When tests are configured:

The project supports multiple languages. Currently supported languages:

- العربية (AR-MA)
- 中文(简体)(Chinese Simplified)
- English (US)
- Deutsch (DE)
Expand Down
4 changes: 2 additions & 2 deletions app/(dashboard)/status/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ export default function PerformancePage() {
<PageHeader
actions={
<Button variant="outline" onClick={refetch}>
<RiRefreshLine className="mr-2 size-4" aria-hidden />
<RiRefreshLine className="me-2 size-4" aria-hidden />
{t("Sync")}
</Button>
}
Expand All @@ -181,7 +181,7 @@ export default function PerformancePage() {
<PageHeader
actions={
<Button variant="outline" onClick={refetch} disabled={loading}>
<RiRefreshLine className="mr-2 size-4" aria-hidden />
<RiRefreshLine className="me-2 size-4" aria-hidden />
{t("Sync")}
</Button>
}
Expand Down
2 changes: 1 addition & 1 deletion components.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"prefix": ""
},
"iconLibrary": "remixicon",
"rtl": false,
"rtl": true,
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
Expand Down
14 changes: 10 additions & 4 deletions components/app-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import logoImage from "@/assets/logo.svg"
import type { NavItem } from "@/types/app-config"
import { usePermissions } from "@/hooks/use-permissions"
import { SidebarVersion } from "@/components/sidebars/version"
import { useDirection } from "@/components/ui/direction"

const APP_NAME = "RustFS"

Expand Down Expand Up @@ -60,6 +61,7 @@ export function AppSidebar() {
const pathname = usePathname()
const { state } = useSidebar()
const isCollapsed = state === "collapsed"
const dir = useDirection()
const brandInitial = APP_NAME.charAt(0).toUpperCase() ?? "R"

const { isAdmin, canAccessPath } = usePermissions()
Expand Down Expand Up @@ -118,7 +120,11 @@ export function AppSidebar() {
const getLabel = (item: NavItem) => t(item.label)

return (
<Sidebar collapsible="icon">
<Sidebar
collapsible="icon"
side={dir === "rtl" ? "right" : "left"}
className="**:data-[sidebar=menu-button]:text-start! **:data-[sidebar=menu-sub-button]:text-start!"
>
<SidebarHeader>
<Link href="/" className="flex items-center gap-3">
{isCollapsed ? (
Expand All @@ -134,7 +140,7 @@ export function AppSidebar() {
</SidebarHeader>

<SidebarContent>
<ScrollArea className="flex-1 pr-1">
<ScrollArea className="flex-1 pe-1">
<div className="flex flex-col gap-4">
{navGroups.map((group, groupIndex) => (
<SidebarGroup key={groupIndex} className="gap-4 py-0">
Expand All @@ -161,7 +167,7 @@ export function AppSidebar() {
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
<SidebarMenuSub className="border-l-0 border-s rtl:-translate-x-px">
{item.children!.map((child) => (
<SidebarMenuSubItem key={child.label}>
{isExternal(child) ? (
Expand All @@ -174,7 +180,7 @@ export function AppSidebar() {
>
<NavIcon name={child.icon} />
<span className="truncate">{getLabel(child)}</span>
<RiExternalLinkLine className="ml-auto size-3 text-muted-foreground" />
<RiExternalLinkLine className="ms-auto size-3 text-muted-foreground" />
</a>
</SidebarMenuSubButton>
) : (
Expand Down
2 changes: 1 addition & 1 deletion components/app-top-nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export function AppTopNav() {
return (
<header className="flex h-16 shrink-0 items-center justify-between gap-2 border-b bg-background transition-[height] ease-linear backdrop-blur group-has-data-[collapsible=icon]/sidebar-wrapper:h-12">
<div className="flex min-w-0 flex-1 items-center gap-3">
<SidebarTrigger className="-ml-1 shrink-0" />
<SidebarTrigger className="-ms-1 shrink-0" />
<TopNavBreadcrumb />
</div>
<div className="flex shrink-0 items-center gap-2">
Expand Down
6 changes: 3 additions & 3 deletions components/auth/heroes/hero-static.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ export function AuthHeroStatic() {
const locale = i18n.language?.split("-")[0] ?? "en"

return (
<div className="relative flex h-full w-full flex-col justify-center gap-8 overflow-hidden border-r bg-gray-50 p-16 dark:border-none dark:bg-black">
<div className="relative flex h-full w-full flex-col justify-center gap-8 overflow-hidden border-e bg-gray-50 p-16 dark:border-none dark:bg-black">
<div className="z-10 flex max-w-7xl flex-col">
<Image src={logoImage} alt="RustFS" width={112} height={24} className="max-w-28" />
<div className="my-6 px-0 text-4xl font-semibold text-primary">
<span className={locale !== "zh" ? "pr-1" : undefined}>{t("Rust-based")} </span>
<span className={locale !== "zh" ? "pe-1" : undefined}>{t("Rust-based")} </span>
<FlipWords
words={[
t("High Performance"),
Expand All @@ -37,7 +37,7 @@ export function AuthHeroStatic() {
className="z-10 inline-flex w-max items-center gap-2 rounded-full border border-blue-500 p-2 px-5 leading-none text-primary-500"
>
<span>{t("Visit website")}</span>
<RiArrowRightLongFill className="mr-2" />
<RiArrowRightLongFill className="me-2 rtl:-scale-x-100" />
</Link>
<div className="absolute inset-0 z-0 h-full">
<Image src={buildRoute("/backgrounds/ttten.svg")} alt="" fill className="object-cover opacity-45" />
Expand Down
2 changes: 1 addition & 1 deletion components/auth/login-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export function LoginForm({
<AuthHeroStatic />
</div>
<div className="relative flex w-full flex-col items-center justify-center bg-white dark:border-neutral-700 dark:bg-neutral-900 lg:w-1/2">
<div className="absolute right-4 top-4 z-10">
<div className="absolute end-4 top-4 z-10">
<Link
href="/config"
className="inline-flex h-8 w-8 items-center justify-center rounded-lg border border-gray-200 bg-white text-gray-600 shadow-sm transition-colors hover:bg-gray-50 hover:text-gray-800 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-400 dark:hover:bg-neutral-800 dark:hover:text-white"
Expand Down
6 changes: 3 additions & 3 deletions components/buckets/info.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,7 @@ export function BucketInfo({ bucketName }: BucketInfoProps) {
<ItemTitle>{t("Access Policy")}</ItemTitle>
<ItemActions>
<Button variant="outline" size="sm" className="shrink-0" onClick={openPolicyModal}>
<RiEdit2Line className="mr-2 size-4" />
<RiEdit2Line className="me-2 size-4" />
{t("Edit")}
</Button>
</ItemActions>
Expand All @@ -465,7 +465,7 @@ export function BucketInfo({ bucketName }: BucketInfoProps) {
</div>
<ItemActions>
<Button variant="outline" size="sm" className="shrink-0" onClick={openEncryptModal}>
<RiEdit2Line className="mr-2 size-4" />
<RiEdit2Line className="me-2 size-4" />
{t("Edit")}
</Button>
</ItemActions>
Expand All @@ -478,7 +478,7 @@ export function BucketInfo({ bucketName }: BucketInfoProps) {
<ItemTitle>{t("Tag")}</ItemTitle>
<ItemActions>
<Button variant="outline" size="sm" className="shrink-0" onClick={() => openTagModal()}>
<RiAddLine className="mr-2 size-4" />
<RiAddLine className="me-2 size-4" />
{t("Add")}
</Button>
</ItemActions>
Expand Down
1 change: 1 addition & 0 deletions components/language-switcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { LOCALE_CODES, type Locale } from "@/lib/i18n"

const languageConfig: Record<string, { text: string; icon: typeof RiTranslate2 }> = {
en: { text: "English", icon: RiTranslate2 },
ar: { text: "العربية", icon: RiTranslate2 },
zh: { text: "中文", icon: RiTranslate2 },
fr: { text: "Français", icon: RiTranslate2 },
tr: { text: "Türkçe", icon: RiTranslate2 },
Expand Down
4 changes: 2 additions & 2 deletions components/license/enterprise-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -246,11 +246,11 @@ export function LicenseEnterpriseSection() {

<div className="flex flex-wrap items-center gap-3">
<Button variant="default" onClick={updateLicense}>
<RiUploadFill className="mr-2 size-4" aria-hidden />
<RiUploadFill className="me-2 size-4" aria-hidden />
{t("Update License")}
</Button>
<Button variant="outline" onClick={contactSupport}>
<RiCustomerService2Line className="mr-2 size-4" aria-hidden />
<RiCustomerService2Line className="me-2 size-4" aria-hidden />
{t("Contact Support")}
</Button>
</div>
Expand Down
4 changes: 2 additions & 2 deletions components/object/list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -546,12 +546,12 @@ export function ObjectList({

<div className="flex justify-end gap-2">
<Button variant="outline" disabled={tokenHistory.length === 0} onClick={goToPreviousPage}>
<RiArrowLeftSLine className="mr-2 size-4" />
<RiArrowLeftSLine className="me-2 size-4 rtl:-scale-x-100" />
<span>{t("Previous Page")}</span>
</Button>
<Button variant="outline" disabled={!nextToken} onClick={goToNextPage}>
<span>{t("Next Page")}</span>
<RiArrowRightSLine className="ml-2 size-4" />
<RiArrowRightSLine className="ms-2 size-4 rtl:-scale-x-100" />
</Button>
</div>

Expand Down
4 changes: 2 additions & 2 deletions components/object/preview-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export function ObjectPreviewModal({ show, onShowChange, object }: ObjectPreview
<Dialog open={show} onOpenChange={onShowChange}>
<DialogContent className="sm:max-w-4xl max-h-[85vh] overflow-auto z-[1000]">
<DialogHeader>
<DialogTitle className="flex items-start justify-between mr-6">{t("Preview")}</DialogTitle>
<DialogTitle className="flex items-start justify-between me-6">{t("Preview")}</DialogTitle>
</DialogHeader>
<div className="min-h-[300px] rounded-md border p-4 flex flex-col">
{loading ? (
Expand All @@ -97,7 +97,7 @@ export function ObjectPreviewModal({ show, onShowChange, object }: ObjectPreview
{isText && (
<pre className="max-h-[70vh] relative overflow-auto whitespace-pre-wrap break-words">
{getFormattedContent()}
<div className="absolute right-0 top-0">
<div className="absolute end-0 top-0">
{isJson && (
<div className="flex justify-end">
<Button variant="outline" size="sm" onClick={() => setIsFormatted(!isFormatted)}>
Expand Down
29 changes: 26 additions & 3 deletions components/providers/i18n-provider.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,52 @@
"use client"

import { useEffect, useRef, useState } from "react"
import { initI18n } from "@/lib/i18n"
import { initI18n, RTL_LOCALES } from "@/lib/i18n"
import { DirectionProvider } from "@/components/ui/direction"
import i18n from "i18next"

type Dir = "ltr" | "rtl"

function getDir(lang: string): Dir {
return RTL_LOCALES.has(lang.split("-")[0]) ? "rtl" : "ltr"
}

export function I18nProvider({ children }: { children: React.ReactNode }) {
const [ready, setReady] = useState(false)
const [dir, setDir] = useState<Dir>("ltr")
const mountedRef = useRef(true)

const applyDir = (lang: string) => {
const d = getDir(lang)
document.documentElement.dir = d
setDir(d)
}

useEffect(() => {
mountedRef.current = true
initI18n()
.then(() => {
if (mountedRef.current) setReady(true)
if (mountedRef.current) {
applyDir(i18n.language ?? "en")
setReady(true)
}
})
.catch(() => {
if (mountedRef.current) setReady(true)
})

const handler = (lng: string) => applyDir(lng)
i18n.on("languageChanged", handler)

return () => {
mountedRef.current = false
i18n.off("languageChanged", handler)
}
}, [])

if (!ready) {
return <div className="min-h-screen bg-background" />
}

return <>{children}</>
return <DirectionProvider dir={dir}>{children}</DirectionProvider>
}
2 changes: 1 addition & 1 deletion components/site-replication/new-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ export function SiteReplicationNewForm({ open, onOpenChange, onSuccess }: SiteRe
type="button"
size="icon"
variant="ghost"
className="-mr-2 h-8 w-8"
className="-me-2 h-8 w-8"
aria-label={t("Delete")}
onClick={() => removeRemoteSite(index)}
>
Expand Down
2 changes: 1 addition & 1 deletion components/theme-switcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export function ThemeSwitcher() {
<DropdownMenuContent className="w-40" align="start">
{themeOptions.map(({ labelKey, key, Icon: OptionIcon }) => (
<DropdownMenuItem key={key} onSelect={() => setTheme(key)}>
<OptionIcon className="mr-2 h-4 w-4" />
<OptionIcon className="me-2 h-4 w-4" />
{t(labelKey)}
</DropdownMenuItem>
))}
Expand Down
6 changes: 3 additions & 3 deletions components/ui/input-group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
data-slot="input-group"
role="group"
className={cn(
"border-input dark:bg-input/30 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-disabled:bg-input/50 dark:has-disabled:bg-input/80 h-8 rounded-none border transition-colors has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:ring-1 has-[[data-slot][aria-invalid=true]]:ring-1 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5 [[data-slot=combobox-content]_&]:focus-within:border-inherit [[data-slot=combobox-content]_&]:focus-within:ring-0 group/input-group relative flex w-full min-w-0 items-center outline-none has-[>textarea]:h-auto",
"border-input dark:bg-input/30 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-disabled:bg-input/50 dark:has-disabled:bg-input/80 h-8 rounded-none border transition-colors has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:ring-1 has-[[data-slot][aria-invalid=true]]:ring-1 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pe-1.5 has-[>[data-align=inline-start]]:[&>input]:ps-1.5 [[data-slot=combobox-content]_&]:focus-within:border-inherit [[data-slot=combobox-content]_&]:focus-within:ring-0 group/input-group relative flex w-full min-w-0 items-center outline-none has-[>textarea]:h-auto",
className,
)}
{...props}
Expand All @@ -27,8 +27,8 @@ const inputGroupAddonVariants = cva(
{
variants: {
align: {
"inline-start": "pl-2 has-[>button]:ml-[-0.3rem] has-[>kbd]:ml-[-0.15rem] order-first",
"inline-end": "pr-2 has-[>button]:mr-[-0.3rem] has-[>kbd]:mr-[-0.15rem] order-last",
"inline-start": "ps-2 has-[>button]:ms-[-0.3rem] has-[>kbd]:ms-[-0.15rem] order-first",
"inline-end": "pe-2 has-[>button]:me-[-0.3rem] has-[>kbd]:me-[-0.15rem] order-last",
"block-start":
"px-2.5 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2 order-first w-full justify-start",
"block-end": "px-2.5 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2 order-last w-full justify-start",
Expand Down
3 changes: 2 additions & 1 deletion components/ui/switch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ function Switch({
<SwitchPrimitive.Root
data-slot="switch"
data-size={size}
dir="ltr"
className={cn(
"data-checked:bg-primary data-unchecked:bg-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 dark:data-unchecked:bg-input/80 shrink-0 rounded-full border border-transparent focus-visible:ring-1 aria-invalid:ring-1 data-[size=default]:h-[18.4px] data-[size=default]:w-[32px] data-[size=sm]:h-[14px] data-[size=sm]:w-[24px] peer group/switch relative inline-flex items-center transition-all outline-none after:absolute after:-inset-x-3 after:-inset-y-2 data-disabled:cursor-not-allowed data-disabled:opacity-50",
"data-checked:bg-primary data-unchecked:bg-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 dark:data-unchecked:bg-input/80 shrink-0 rounded-full border border-transparent focus-visible:ring-1 aria-invalid:ring-1 data-[size=default]:h-[18.4px] data-[size=default]:w-[32px] data-[size=sm]:h-[14px] data-[size=sm]:w-[24px] peer group/switch relative inline-flex items-center transition-all outline-none after:absolute after:-inset-x-3 after:-inset-y-2 data-disabled:cursor-not-allowed data-disabled:opacity-50 rtl:scale-x-[-1]",
className,
)}
{...props}
Expand Down
4 changes: 2 additions & 2 deletions components/ui/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ function TableHead({ className, ...props }: React.ComponentProps<"th">) {
<th
data-slot="table-head"
className={cn(
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0",
"text-foreground h-10 px-2 text-start align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pe-0",
className,
)}
{...props}
Expand All @@ -57,7 +57,7 @@ function TableCell({ className, ...props }: React.ComponentProps<"td">) {
return (
<td
data-slot="table-cell"
className={cn("p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0", className)}
className={cn("p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pe-0", className)}
{...props}
/>
)
Expand Down
2 changes: 1 addition & 1 deletion components/user-group/new-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export function UserGroupNewForm({ open, onOpenChange, onSuccess }: UserGroupNew
{t("Cancel")}
</Button>
<Button onClick={submitForm} disabled={submitting}>
{submitting && <Spinner className="mr-2 size-4" />}
{submitting && <Spinner className="me-2 size-4" />}
{t("Submit")}
</Button>
</DialogFooter>
Expand Down
Loading