diff --git a/src/components/ui/CopyButton.tsx b/src/components/ui/CopyButton.tsx index e7325e3..65ba083 100644 --- a/src/components/ui/CopyButton.tsx +++ b/src/components/ui/CopyButton.tsx @@ -1,6 +1,7 @@ import { useState } from 'react'; import { Check, Copy } from 'lucide-react'; import { Button } from './Button'; +import { useUi } from '@/i18n/shared'; interface CopyButtonProps { value: string; @@ -8,7 +9,8 @@ interface CopyButtonProps { disabled?: boolean; } -export function CopyButton({ value, label = 'Copy', disabled }: CopyButtonProps) { +export function CopyButton({ value, label, disabled }: CopyButtonProps) { + const ui = useUi(); const [copied, setCopied] = useState(false); const handleCopy = async () => { @@ -25,7 +27,7 @@ export function CopyButton({ value, label = 'Copy', disabled }: CopyButtonProps) return ( ); } diff --git a/src/components/ui/CopyImageButton.tsx b/src/components/ui/CopyImageButton.tsx index b3af9f5..0d22696 100644 --- a/src/components/ui/CopyImageButton.tsx +++ b/src/components/ui/CopyImageButton.tsx @@ -1,6 +1,7 @@ import { useEffect, useState } from 'react'; import { Check, Copy } from 'lucide-react'; import { clipboardService } from '@/services/clipboard.service'; +import { useUi } from '@/i18n/shared'; import { Button } from './Button'; interface CopyImageButtonProps { @@ -11,6 +12,7 @@ interface CopyImageButtonProps { /** Copies an image to the clipboard, with brief "Copied" / error feedback. */ export function CopyImageButton({ blob, disabled }: CopyImageButtonProps) { + const ui = useUi(); const [state, setState] = useState<'idle' | 'copied' | 'error'>('idle'); // Only decide support after mount so SSR and the first client render match // (clipboardService.supported is false on the server, true in the browser). @@ -35,7 +37,7 @@ export function CopyImageButton({ blob, disabled }: CopyImageButtonProps) { return ( ); } diff --git a/src/components/ui/DownloadTextButton.tsx b/src/components/ui/DownloadTextButton.tsx index 99a3e79..3ce1dd0 100644 --- a/src/components/ui/DownloadTextButton.tsx +++ b/src/components/ui/DownloadTextButton.tsx @@ -1,5 +1,6 @@ import { Download } from 'lucide-react'; import { downloadService } from '@/services/download'; +import { useUi } from '@/i18n/shared'; import { Button } from './Button'; interface DownloadTextButtonProps { @@ -10,12 +11,13 @@ interface DownloadTextButtonProps { } /** Download a string as a file (client-side, via the shared download service). */ -export function DownloadTextButton({ text, filename, mime = 'text/plain;charset=utf-8', label = 'Download' }: DownloadTextButtonProps) { +export function DownloadTextButton({ text, filename, mime = 'text/plain;charset=utf-8', label }: DownloadTextButtonProps) { + const ui = useUi(); const onClick = () => downloadService.download(new Blob([text], { type: mime }), filename); return ( ); } diff --git a/src/components/ui/Dropzone.tsx b/src/components/ui/Dropzone.tsx index 8fe4de7..406980a 100644 --- a/src/components/ui/Dropzone.tsx +++ b/src/components/ui/Dropzone.tsx @@ -1,4 +1,5 @@ import { useCallback, useState } from 'react'; +import { useUi } from '@/i18n/shared'; export interface DropzoneProps { onDrop: (files: File[]) => void | Promise; @@ -8,6 +9,7 @@ export interface DropzoneProps { } export function Dropzone({ onDrop, accept, multiple = true, children }: DropzoneProps) { + const ui = useUi(); const [isDragging, setIsDragging] = useState(false); const handleDragOver = useCallback((e: React.DragEvent) => { @@ -41,7 +43,7 @@ export function Dropzone({ onDrop, accept, multiple = true, children }: Dropzone className={`block cursor-pointer border-[3px] border-dashed p-8 text-center transition-all ${isDragging ? 'border-accent bg-accent/10 shadow-brutal' : 'border-border hover:shadow-brutal'}`} > - {children ||

Drop files here or click to browse

} + {children ||

{ui.dropzone}

} ); } diff --git a/src/components/ui/EditInAnnotatorButton.tsx b/src/components/ui/EditInAnnotatorButton.tsx index e3d53c0..fd5424a 100644 --- a/src/components/ui/EditInAnnotatorButton.tsx +++ b/src/components/ui/EditInAnnotatorButton.tsx @@ -1,5 +1,6 @@ import { PenTool } from 'lucide-react'; import { sendImageToAnnotator } from '@/services/handoff'; +import { useUi } from '@/i18n/shared'; import { Button } from './Button'; interface EditInAnnotatorButtonProps { @@ -11,6 +12,7 @@ interface EditInAnnotatorButtonProps { /** Opens the Image Annotator pre-loaded with this image (via IndexedDB handoff). */ export function EditInAnnotatorButton({ blob, filename = 'image.png', disabled }: EditInAnnotatorButtonProps) { + const ui = useUi(); const handleClick = async () => { if (!blob) return; const resolved = typeof blob === 'function' ? await blob() : blob; @@ -20,7 +22,7 @@ export function EditInAnnotatorButton({ blob, filename = 'image.png', disabled } return ( ); } diff --git a/src/components/ui/ImageResult.tsx b/src/components/ui/ImageResult.tsx index 08823a8..3affaae 100644 --- a/src/components/ui/ImageResult.tsx +++ b/src/components/ui/ImageResult.tsx @@ -1,5 +1,6 @@ import { useEffect, useState } from 'react'; import { ResultActions } from './ResultActions'; +import { useUi } from '@/i18n/shared'; import { formatBytes } from '@/tools/image/canvas.lib'; interface ImageResultProps { @@ -11,6 +12,7 @@ interface ImageResultProps { /** Shows an image result: preview, output size (and % change), download. */ export function ImageResult({ blob, filename, originalSize }: ImageResultProps) { + const ui = useUi(); const [url, setUrl] = useState(''); useEffect(() => { @@ -25,7 +27,7 @@ export function ImageResult({ blob, filename, originalSize }: ImageResultProps) return (
- Result + {ui.result} {formatBytes(blob.size)} {reduction !== null && ( - {reduction >= 0 ? `−${reduction}% smaller` : `+${-reduction}% larger`} + {reduction >= 0 ? `−${reduction}% ${ui.smaller}` : `+${-reduction}% ${ui.larger}`} )}
{url && ( Result preview )} diff --git a/src/components/ui/LoadFileButton.tsx b/src/components/ui/LoadFileButton.tsx index 82c7e66..7927099 100644 --- a/src/components/ui/LoadFileButton.tsx +++ b/src/components/ui/LoadFileButton.tsx @@ -1,5 +1,6 @@ import { useRef } from 'react'; import { FileUp } from 'lucide-react'; +import { useUi } from '@/i18n/shared'; import { Button } from './Button'; interface LoadFileButtonProps { @@ -11,7 +12,8 @@ interface LoadFileButtonProps { } /** A small button that reads a text file from disk and hands back its contents. */ -export function LoadFileButton({ onLoad, accept, label = 'Load file' }: LoadFileButtonProps) { +export function LoadFileButton({ onLoad, accept, label }: LoadFileButtonProps) { + const ui = useUi(); const ref = useRef(null); const handle = async (e: React.ChangeEvent) => { @@ -25,7 +27,7 @@ export function LoadFileButton({ onLoad, accept, label = 'Load file' }: LoadFile <> diff --git a/src/components/ui/ResultActions.tsx b/src/components/ui/ResultActions.tsx index c04b94e..62c4e2a 100644 --- a/src/components/ui/ResultActions.tsx +++ b/src/components/ui/ResultActions.tsx @@ -1,5 +1,6 @@ import { Download } from 'lucide-react'; import { downloadService } from '@/services/download'; +import { useUi } from '@/i18n/shared'; import { Button } from './Button'; import { CopyImageButton } from './CopyImageButton'; import { EditInAnnotatorButton } from './EditInAnnotatorButton'; @@ -11,6 +12,7 @@ export interface ResultActionsProps { } export function ResultActions({ blob, filename, disabled }: ResultActionsProps) { + const ui = useUi(); const handleDownload = async () => { if (!blob) return; await downloadService.download(blob, filename); @@ -22,7 +24,7 @@ export function ResultActions({ blob, filename, disabled }: ResultActionsProps)
{isImage && } {isImage && } diff --git a/src/i18n/shared.ts b/src/i18n/shared.ts new file mode 100644 index 0000000..febcddf --- /dev/null +++ b/src/i18n/shared.ts @@ -0,0 +1,39 @@ +import { useEffect, useState } from 'react'; +import type { Lang } from './config'; + +/** + * Client-side current-locale hook for shared components that aren't passed a `lang` + * prop. Reads the URL (/id/… → 'id') and re-checks on every view-transition + * navigation. SSR returns 'en'; it settles to the real locale on hydration. + */ +export function useLang(): Lang { + const [lang, setLang] = useState('en'); + useEffect(() => { + const detect = () => setLang(/^\/id(\/|$)/.test(location.pathname) ? 'id' : 'en'); + detect(); + document.addEventListener('astro:page-load', detect); + return () => document.removeEventListener('astro:page-load', detect); + }, []); + return lang; +} + +/** Common strings shared across UI components (Copy, Download, Dropzone, …). */ +const SHARED = { + en: { + copy: 'Copy', copied: 'Copied', copyImage: 'Copy image', copyFailed: 'Copy failed', + download: 'Download', loadFile: 'Load file', editAnnotator: 'Edit in Annotator', + dropzone: 'Drop files here or click to browse', result: 'Result', resultAlt: 'Result preview', + smaller: 'smaller', larger: 'larger', + }, + id: { + copy: 'Salin', copied: 'Tersalin', copyImage: 'Salin gambar', copyFailed: 'Gagal menyalin', + download: 'Unduh', loadFile: 'Muat file', editAnnotator: 'Edit di Anotator', + dropzone: 'Letakkan file di sini atau klik untuk menelusuri', result: 'Hasil', resultAlt: 'Pratinjau hasil', + smaller: 'lebih kecil', larger: 'lebih besar', + }, +} satisfies Record>; + +/** Shared UI strings for the current locale. */ +export function useUi() { + return SHARED[useLang()]; +}