mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-31 00:40:46 +02:00
The four user-visible strings exposed by packages/ui rendered untranslated on every page that used them: - file-upload-button.tsx — "Attach file" aria-label/title - sidebar.tsx — "Toggle Sidebar" sr-only label/aria-label/title - pagination.tsx — "Go to previous/next page" aria-labels - CodeBlock.tsx — "plain text" language fallback + "Copy code" aria-label/tooltip Root cause: the package had no i18n hookup at all because the package boundary rule forbids importing @multica/core. Replicating the pattern five times would have been the same hack five times. Hooking up react-i18next directly is the structurally clean fix — i18next is a generic library, not business logic, and the upstream I18nextProvider already exposes the instance via context. To let packages/ui typecheck the selector form standalone (i.e. without the views resource-types augmentation in scope), the augmentation is split: views declares everything except the `ui` namespace on a new global `I18nResources` interface, and packages/ui contributes the `ui` slice via declaration merging in packages/ui/types/i18next.ts. Views' resources-types side-effect-imports that file so both packages see the merged shape during downstream typechecks. Scope intentionally excludes: - packages/ui/components/common/error-boundary.tsx — keeping its fallback in English so a render-time crash never depends on i18n being healthy. - apps/desktop/src/renderer/src/components/update-notification.tsx — ships with the next desktop release, not via this PR. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai>
63 lines
1.6 KiB
TypeScript
63 lines
1.6 KiB
TypeScript
"use client";
|
|
|
|
import { useRef } from "react";
|
|
import { Paperclip } from "lucide-react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { cn } from "@multica/ui/lib/utils";
|
|
|
|
interface FileUploadButtonProps {
|
|
/** Called with the selected File — caller handles upload. */
|
|
onSelect: (file: File) => void;
|
|
disabled?: boolean;
|
|
className?: string;
|
|
size?: "sm" | "default";
|
|
}
|
|
|
|
function FileUploadButton({
|
|
onSelect,
|
|
disabled,
|
|
className,
|
|
size = "default",
|
|
}: FileUploadButtonProps) {
|
|
const { t } = useTranslation("ui");
|
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
const attachLabel = t(($) => $.attach_file);
|
|
|
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = e.target.files?.[0];
|
|
if (!file) return;
|
|
e.target.value = "";
|
|
onSelect(file);
|
|
};
|
|
|
|
const iconSize = size === "sm" ? "h-3.5 w-3.5" : "h-4 w-4";
|
|
const btnSize = size === "sm" ? "h-6 w-6" : "h-7 w-7";
|
|
|
|
return (
|
|
<>
|
|
<button
|
|
type="button"
|
|
onClick={() => inputRef.current?.click()}
|
|
disabled={disabled}
|
|
aria-label={attachLabel}
|
|
title={attachLabel}
|
|
className={cn(
|
|
"inline-flex items-center justify-center rounded-full text-muted-foreground hover:bg-accent hover:text-foreground transition-colors disabled:opacity-50 disabled:pointer-events-none",
|
|
btnSize,
|
|
className,
|
|
)}
|
|
>
|
|
<Paperclip className={iconSize} />
|
|
</button>
|
|
<input
|
|
ref={inputRef}
|
|
type="file"
|
|
className="hidden"
|
|
onChange={handleChange}
|
|
/>
|
|
</>
|
|
);
|
|
}
|
|
|
|
export { FileUploadButton, type FileUploadButtonProps };
|