mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 03:20:46 +02:00
fix(i18n): wrap hard-coded English strings in t() across UI (#6566)
## Summary
Audit + bulk fix of hard-coded English UI strings - `aria-label`,
`title`, `placeholder`, `label`, and raw JSX literals that bypassed i18n
entirely. Each literal now goes through `t("key", "English Default")`
from `react-i18next`, and every new key has a corresponding entry in
`en-GB/translation.toml` so translators can pick it up.
## What this fixes
Strings were rendered untranslated in every non-EN locale because they
never went through `t()` at all (not just "value not translated yet").
Affects screen-reader labels, tooltips, form placeholders, empty/loading
states, plan card content, and the entire workflow ParticipantView.
## Coverage (~143 keys / 50 files)
- **Viewer chrome** - search bar (close, clear, prev/next, "of N"
results), link/signature/redaction actions, viewer error state, zoom
labels
- **Page editor** - undo/redo/rotate/delete toolbar tooltips, empty
state, bulk selection operator chip tooltips
- **Shared primitives** - Tooltip close, InfoBanner dismiss, TextInput
clear, Toast dismiss/toggle, UpdateModal close, EditableSecretField
edit, DropdownListWithFooter search, FileCard/FileDropdownMenu actions,
EmptyFilesState + AddFileCard upload
- **Tools** - Image upload + hint, ColorControl eyedropper, sign Use
Signature, CompressSettings, OCR loading, PageLayout
margin/border/row/col placeholders, FormFill switch + save + re-scan
- **Proprietary admin** - OverviewHeader signed-in line + logout,
AdminPremiumSection moved-features list (via `<Trans>`),
AdminPlanSection no-data alert, AdminAdvancedSection temp-dir
placeholders, AdminEndpointsSection multiselect placeholders,
AdminMailSection + AdminDatabaseSection password placeholders
- **Onboarding** - MFASetupSlide QR loading + auth code label,
SecurityCheckSlide role select + options
- **ParticipantView** - entire sign-document UI (~30 strings: loading,
error, badges, headings, cert-type Select, all input labels and
placeholders, action buttons, completion + expired alerts) - file
previously imported `useTranslation` but only used `t()` for cert
validation
- **planConstants.ts refactor** - replaced `PLAN_FEATURES` /
`PLAN_HIGHLIGHTS` const exports with `usePlanFeatures()` /
`usePlanHighlights()` hooks. Service layer (`licenseService.getPlans`)
updated to accept feature/highlight maps so it stays hook-free. Callers
(`usePlans`, `CheckoutContext`) resolve the hooks at the React boundary
- **Previously catalogued offenders** - `FileSidebarFileItem`
open/close-viewer aria-labels, `quickAccessBar/ActiveToolButton` "Back
to all tools" tooltip + aria, `AppConfigModal` close button
## Notes
- One small refactor in `usePageSelectionTips.ts` was needed to resolve
a TOML key-shape conflict: the existing scalar keys
`bulkSelection.operators.{and,not,comma}` needed to become tables to
hold the new `.title` subkeys for OperatorsSection's chip tooltips. The
existing descriptions moved to `[bulkSelection.operators.descriptions]`
and the three i18n key paths in usePageSelectionTips were updated to
match.
- Viewer sidebar close buttons
(Bookmark/Layer/Thumbnail/Attachment/Comments) were on the audit list
but are NOT on main - they're added by the unmerged PR #6552
(feat/viewer-sidebar-ux). Those particular strings will need wrapping
when that PR lands.
- TOML hook (`toml-sort-fix`) ran and re-sorted the translation file.
## Test plan
- [ ] `task frontend:typecheck` passes (core + proprietary + desktop
variants)
- [ ] `task frontend:lint` passes
- [ ] Switching language to Deutsch / Русский: previously-English
`aria-label`s + tooltips + placeholders + plan card bullets now render
translated (when the locale has values) or fall back to the English
default (when it doesn't)
- [ ] Plan page bullet points in EN render unchanged
- [ ] Sign-document flow (ParticipantView) renders unchanged in EN
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
||||
Group,
|
||||
} from "@mantine/core";
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import ColorizeIcon from "@mui/icons-material/Colorize";
|
||||
|
||||
// safari and firefox do not support the eye dropper API, only edge, chrome and opera do.
|
||||
@@ -33,6 +34,7 @@ export function ColorControl({
|
||||
label,
|
||||
disabled = false,
|
||||
}: ColorControlProps) {
|
||||
const { t } = useTranslation();
|
||||
const [opened, setOpened] = useState(false);
|
||||
// Buffer the colour locally so the picker stays responsive during drag.
|
||||
// Only propagate to the parent (which triggers expensive annotation updates)
|
||||
@@ -111,7 +113,9 @@ export function ColorControl({
|
||||
/>
|
||||
{supportsEyeDropper && (
|
||||
<Group justify="flex-end">
|
||||
<Tooltip label="Pick colour from screen">
|
||||
<Tooltip
|
||||
label={t("color.eyeDropper.tooltip", "Pick colour from screen")}
|
||||
>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState } from "react";
|
||||
import { Stack } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { BaseAnnotationTool } from "@app/components/annotation/shared/BaseAnnotationTool";
|
||||
import { ImageUploader } from "@app/components/annotation/shared/ImageUploader";
|
||||
|
||||
@@ -12,6 +13,7 @@ export const ImageTool: React.FC<ImageToolProps> = ({
|
||||
onImageChange,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [, setImageData] = useState<string | null>(null);
|
||||
|
||||
const handleImageUpload = async (file: File | null) => {
|
||||
@@ -57,9 +59,12 @@ export const ImageTool: React.FC<ImageToolProps> = ({
|
||||
<ImageUploader
|
||||
onImageChange={handleImageUpload}
|
||||
disabled={disabled}
|
||||
label="Upload Image"
|
||||
placeholder="Select image file"
|
||||
hint="Upload a PNG, JPG, SVG, or other image file to place on the PDF. SVG files will be converted to PNG for compatibility."
|
||||
label={t("image.upload.label", "Upload Image")}
|
||||
placeholder={t("image.upload.placeholder", "Select image file")}
|
||||
hint={t(
|
||||
"image.upload.hint",
|
||||
"Upload a PNG, JPG, SVG, or other image file to place on the PDF. SVG files will be converted to PNG for compatibility.",
|
||||
)}
|
||||
/>
|
||||
</Stack>
|
||||
</BaseAnnotationTool>
|
||||
|
||||
@@ -134,7 +134,7 @@ const AddFileCard = ({
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
aria-label="Upload"
|
||||
aria-label={t("addFileCard.upload", "Upload")}
|
||||
style={{
|
||||
backgroundColor: "var(--landing-button-bg)",
|
||||
color: "var(--landing-button-color)",
|
||||
|
||||
@@ -77,7 +77,7 @@ const EmptyFilesState: React.FC = () => {
|
||||
onMouseLeave={() => setIsUploadHover(false)}
|
||||
>
|
||||
<Button
|
||||
aria-label="Upload"
|
||||
aria-label={t("emptyFilesState.upload", "Upload")}
|
||||
style={{
|
||||
backgroundColor: "var(--bg-file-manager)",
|
||||
color: "var(--landing-button-color)",
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
TextInput,
|
||||
} from "@mantine/core";
|
||||
import { QRCodeSVG } from "qrcode.react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { SlideConfig } from "@app/types/types";
|
||||
import { UNIFIED_CIRCLE_CONFIG } from "@app/components/onboarding/slides/unifiedBackgroundConfig";
|
||||
import { accountService } from "@app/services/accountService";
|
||||
@@ -31,6 +32,7 @@ interface MFASetupSlideProps {
|
||||
}
|
||||
|
||||
function MFASetupContent({ onMfaSetupComplete }: MFASetupSlideProps) {
|
||||
const { t } = useTranslation();
|
||||
const [mfaSetupData, setMfaSetupData] = useState<MfaSetupResponse | null>(
|
||||
null,
|
||||
);
|
||||
@@ -179,7 +181,9 @@ function MFASetupContent({ onMfaSetupComplete }: MFASetupSlideProps) {
|
||||
{mfaLoading && !isReady && (
|
||||
<Group gap="sm">
|
||||
<Loader size="sm" />
|
||||
<Text size="sm">Generating your QR code…</Text>
|
||||
<Text size="sm">
|
||||
{t("onboarding.mfa.qrCodeLoading", "Generating your QR code…")}
|
||||
</Text>
|
||||
</Group>
|
||||
)}
|
||||
|
||||
@@ -189,7 +193,10 @@ function MFASetupContent({ onMfaSetupComplete }: MFASetupSlideProps) {
|
||||
<Stack gap="sm">
|
||||
<TextInput
|
||||
id="mfa-setup-code"
|
||||
label="Authentication code"
|
||||
label={t(
|
||||
"onboarding.mfa.authenticationCode",
|
||||
"Authentication code",
|
||||
)}
|
||||
placeholder="123456"
|
||||
value={mfaSetupCode}
|
||||
onChange={(event) =>
|
||||
|
||||
@@ -37,11 +37,20 @@ export default function SecurityCheckSlide({
|
||||
</div>
|
||||
|
||||
<Select
|
||||
placeholder="Confirm your role"
|
||||
placeholder={i18n.t(
|
||||
"onboarding.securityCheck.rolePlaceholder",
|
||||
"Confirm your role",
|
||||
)}
|
||||
value={selectedRole}
|
||||
data={[
|
||||
{ value: "admin", label: "Admin" },
|
||||
{ value: "user", label: "User" },
|
||||
{
|
||||
value: "admin",
|
||||
label: i18n.t("onboarding.securityCheck.roleAdmin", "Admin"),
|
||||
},
|
||||
{
|
||||
value: "user",
|
||||
label: i18n.t("onboarding.securityCheck.roleUser", "User"),
|
||||
},
|
||||
]}
|
||||
onChange={(value) =>
|
||||
onRoleSelect((value as "admin" | "user") ?? null)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useCallback, useRef, useEffect, useMemo } from "react";
|
||||
import { Text, Center, Box, LoadingOverlay, Stack } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useFileState, useFileActions } from "@app/contexts/FileContext";
|
||||
import {
|
||||
useNavigationGuard,
|
||||
@@ -33,6 +34,7 @@ export interface PageEditorProps {
|
||||
}
|
||||
|
||||
const PageEditor = ({ onFunctionsReady }: PageEditorProps) => {
|
||||
const { t } = useTranslation();
|
||||
// Use split contexts to prevent re-renders
|
||||
const { state, selectors } = useFileState();
|
||||
const { actions } = useFileActions();
|
||||
@@ -688,9 +690,14 @@ const PageEditor = ({ onFunctionsReady }: PageEditorProps) => {
|
||||
<Text size="lg" c="dimmed">
|
||||
📄
|
||||
</Text>
|
||||
<Text c="dimmed">No PDF files loaded</Text>
|
||||
<Text c="dimmed">
|
||||
{t("pageEditor.emptyState.title", "No PDF files loaded")}
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
Add files to start editing pages
|
||||
{t(
|
||||
"pageEditor.emptyState.body",
|
||||
"Add files to start editing pages",
|
||||
)}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
|
||||
@@ -6,6 +6,7 @@ import RotateLeftIcon from "@mui/icons-material/RotateLeft";
|
||||
import RotateRightIcon from "@mui/icons-material/RotateRight";
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import InsertPageBreakIcon from "@mui/icons-material/InsertPageBreak";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface PageEditorControlsProps {
|
||||
// Close/Reset functions
|
||||
@@ -51,6 +52,7 @@ const PageEditorControls = ({
|
||||
displayDocument,
|
||||
splitPositions,
|
||||
}: PageEditorControlsProps) => {
|
||||
const { t } = useTranslation();
|
||||
// Calculate split tooltip text using smart toggle logic
|
||||
const getSplitTooltip = () => {
|
||||
if (!splitPositions || !displayDocument || selectedPageIds.length === 0) {
|
||||
@@ -131,7 +133,7 @@ const PageEditorControls = ({
|
||||
}}
|
||||
>
|
||||
{/* Undo/Redo */}
|
||||
<Tooltip label="Undo">
|
||||
<Tooltip label={t("pageEditor.toolbar.undo", "Undo")}>
|
||||
<ActionIcon
|
||||
onClick={onUndo}
|
||||
disabled={!canUndo}
|
||||
@@ -145,7 +147,7 @@ const PageEditorControls = ({
|
||||
<UndoIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Redo">
|
||||
<Tooltip label={t("pageEditor.toolbar.redo", "Redo")}>
|
||||
<ActionIcon
|
||||
onClick={onRedo}
|
||||
disabled={!canRedo}
|
||||
@@ -170,7 +172,9 @@ const PageEditorControls = ({
|
||||
/>
|
||||
|
||||
{/* Page Operations */}
|
||||
<Tooltip label="Rotate Selected Left">
|
||||
<Tooltip
|
||||
label={t("pageEditor.toolbar.rotateLeft", "Rotate Selected Left")}
|
||||
>
|
||||
<ActionIcon
|
||||
onClick={() => onRotate("left")}
|
||||
disabled={selectedPageIds.length === 0}
|
||||
@@ -187,7 +191,9 @@ const PageEditorControls = ({
|
||||
<RotateLeftIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Rotate Selected Right">
|
||||
<Tooltip
|
||||
label={t("pageEditor.toolbar.rotateRight", "Rotate Selected Right")}
|
||||
>
|
||||
<ActionIcon
|
||||
onClick={() => onRotate("right")}
|
||||
disabled={selectedPageIds.length === 0}
|
||||
@@ -204,7 +210,7 @@ const PageEditorControls = ({
|
||||
<RotateRightIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Delete Selected">
|
||||
<Tooltip label={t("pageEditor.toolbar.delete", "Delete Selected")}>
|
||||
<ActionIcon
|
||||
onClick={onDelete}
|
||||
disabled={selectedPageIds.length === 0}
|
||||
|
||||
+20
-5
@@ -26,7 +26,10 @@ const OperatorsSection = ({
|
||||
className={classes.operatorChip}
|
||||
onClick={() => onInsertOperator("and")}
|
||||
disabled={!csvInput.trim()}
|
||||
title="Combine selections (both conditions must be true)"
|
||||
title={t(
|
||||
"bulkSelection.operators.and.title",
|
||||
"Combine selections (both conditions must be true)",
|
||||
)}
|
||||
>
|
||||
<Text size="xs" fw={500}>
|
||||
and
|
||||
@@ -38,7 +41,10 @@ const OperatorsSection = ({
|
||||
className={classes.operatorChip}
|
||||
onClick={() => onInsertOperator("or")}
|
||||
disabled={!csvInput.trim()}
|
||||
title="Add to selection (either condition can be true)"
|
||||
title={t(
|
||||
"bulkSelection.operators.or.title",
|
||||
"Add to selection (either condition can be true)",
|
||||
)}
|
||||
>
|
||||
<Text size="xs" fw={500}>
|
||||
or
|
||||
@@ -50,7 +56,10 @@ const OperatorsSection = ({
|
||||
className={classes.operatorChip}
|
||||
onClick={() => onInsertOperator("not")}
|
||||
disabled={!csvInput.trim()}
|
||||
title="Exclude from selection"
|
||||
title={t(
|
||||
"bulkSelection.operators.not.title",
|
||||
"Exclude from selection",
|
||||
)}
|
||||
>
|
||||
<Text size="xs" fw={500}>
|
||||
not
|
||||
@@ -64,7 +73,10 @@ const OperatorsSection = ({
|
||||
variant="outline"
|
||||
className={classes.operatorChip}
|
||||
onClick={() => onInsertOperator("even")}
|
||||
title="Select all even-numbered pages (2, 4, 6, 8...)"
|
||||
title={t(
|
||||
"bulkSelection.operators.even.title",
|
||||
"Select all even-numbered pages (2, 4, 6, 8...)",
|
||||
)}
|
||||
>
|
||||
<Text size="xs" fw={500}>
|
||||
even
|
||||
@@ -75,7 +87,10 @@ const OperatorsSection = ({
|
||||
variant="outline"
|
||||
className={classes.operatorChip}
|
||||
onClick={() => onInsertOperator("odd")}
|
||||
title="Select all odd-numbered pages (1, 3, 5, 7...)"
|
||||
title={t(
|
||||
"bulkSelection.operators.odd.title",
|
||||
"Select all odd-numbered pages (1, 3, 5, 7...)",
|
||||
)}
|
||||
>
|
||||
<Text size="xs" fw={500}>
|
||||
odd
|
||||
|
||||
+3
-1
@@ -45,7 +45,9 @@ const PageSelectionInput = ({
|
||||
height="1rem"
|
||||
style={{ color: "var(--text-instruction)" }}
|
||||
/>
|
||||
<Text>Page Selection</Text>
|
||||
<Text>
|
||||
{t("bulkSelection.pageSelection.title", "Page Selection")}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
{typeof advancedOpened === "boolean" && (
|
||||
|
||||
@@ -7,6 +7,7 @@ import React, {
|
||||
} from "react";
|
||||
import { Badge, Modal, Text, ActionIcon, Tooltip, Group } from "@mantine/core";
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import LocalIcon from "@app/components/shared/LocalIcon";
|
||||
import { useConfigNavSections } from "@app/components/shared/config/configNavSections";
|
||||
import { NavKey, VALID_NAV_KEYS } from "@app/components/shared/config/types";
|
||||
@@ -33,6 +34,7 @@ const AppConfigModalInner: React.FC<AppConfigModalProps> = ({
|
||||
opened,
|
||||
onClose,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [active, setActive] = useState<NavKey>("general");
|
||||
const isMobile = useIsMobile();
|
||||
const navigate = useNavigate();
|
||||
@@ -332,7 +334,7 @@ const AppConfigModalInner: React.FC<AppConfigModalProps> = ({
|
||||
ref={closeButtonRef}
|
||||
variant="subtle"
|
||||
onClick={handleClose}
|
||||
aria-label="Close"
|
||||
aria-label={t("settings.close", "Close")}
|
||||
data-autofocus
|
||||
>
|
||||
<LocalIcon icon="close-rounded" width={18} height={18} />
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
Group,
|
||||
TextInput,
|
||||
} from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import UnfoldMoreIcon from "@mui/icons-material/UnfoldMore";
|
||||
import SearchIcon from "@mui/icons-material/Search";
|
||||
import { Z_INDEX_AUTOMATE_DROPDOWN } from "@app/styles/zIndex";
|
||||
@@ -71,6 +72,7 @@ const DropdownListWithFooter: React.FC<DropdownListWithFooterProps> = ({
|
||||
withinPortal = true,
|
||||
zIndex = Z_INDEX_AUTOMATE_DROPDOWN,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
|
||||
const isMultiValue = Array.isArray(value);
|
||||
@@ -184,7 +186,7 @@ const DropdownListWithFooter: React.FC<DropdownListWithFooterProps> = ({
|
||||
}}
|
||||
>
|
||||
<TextInput
|
||||
placeholder="Search..."
|
||||
placeholder={t("dropdownList.searchPlaceholder", "Search...")}
|
||||
value={searchTerm}
|
||||
onChange={handleSearchChange}
|
||||
leftSection={<SearchIcon style={{ fontSize: "1rem" }} />}
|
||||
|
||||
@@ -98,8 +98,11 @@ export default function EditableSecretField({
|
||||
variant="light"
|
||||
onClick={handleEdit}
|
||||
disabled={disabled}
|
||||
title="Edit"
|
||||
aria-label="Edit secret value"
|
||||
title={t("editableSecretField.edit", "Edit")}
|
||||
aria-label={t(
|
||||
"editableSecretField.editSecretValue",
|
||||
"Edit secret value",
|
||||
)}
|
||||
>
|
||||
<LocalIcon icon="edit" width="1rem" height="1rem" />
|
||||
</ActionIcon>
|
||||
|
||||
@@ -111,7 +111,7 @@ const FileCard = ({
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{onView && (
|
||||
<Tooltip label="View in Viewer">
|
||||
<Tooltip label={t("fileCard.viewInViewer", "View in Viewer")}>
|
||||
<ActionIcon
|
||||
size="sm"
|
||||
variant="subtle"
|
||||
@@ -126,7 +126,9 @@ const FileCard = ({
|
||||
</Tooltip>
|
||||
)}
|
||||
{onEdit && (
|
||||
<Tooltip label="Open in File Editor">
|
||||
<Tooltip
|
||||
label={t("fileCard.openInFileEditor", "Open in File Editor")}
|
||||
>
|
||||
<ActionIcon
|
||||
size="sm"
|
||||
variant="subtle"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
import { Menu, Loader, Group, Text, ActionIcon, Tooltip } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile";
|
||||
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
@@ -28,6 +29,7 @@ export const FileDropdownMenu: React.FC<FileDropdownMenuProps> = ({
|
||||
switchingTo,
|
||||
viewOptionStyle,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Menu trigger="click" position="bottom" width="30rem">
|
||||
<Menu.Target>
|
||||
@@ -95,7 +97,10 @@ export const FileDropdownMenu: React.FC<FileDropdownMenuProps> = ({
|
||||
</Text>
|
||||
)}
|
||||
{onFileRemove && (
|
||||
<Tooltip label="Close file" withArrow>
|
||||
<Tooltip
|
||||
label={t("fileDropdownMenu.closeFile", "Close file")}
|
||||
withArrow
|
||||
>
|
||||
<ActionIcon
|
||||
component="div"
|
||||
size="xs"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useCallback, useRef } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import VisibilityOutlinedIcon from "@mui/icons-material/VisibilityOutlined";
|
||||
import VisibilityOffOutlinedIcon from "@mui/icons-material/VisibilityOffOutlined";
|
||||
import type { FileId } from "@app/types/file";
|
||||
@@ -141,6 +142,7 @@ export function FileItem({
|
||||
onClick,
|
||||
onEyeClick,
|
||||
}: FileItemProps) {
|
||||
const { t } = useTranslation();
|
||||
const ext = getFileExtension(name);
|
||||
const dateLabel = lastModified ? formatFileDate(lastModified) : "";
|
||||
const typeLabel = ext ? ext.toUpperCase() : "File";
|
||||
@@ -215,7 +217,11 @@ export function FileItem({
|
||||
}}
|
||||
tabIndex={-1}
|
||||
type="button"
|
||||
aria-label={isViewedInViewer ? "Close viewer" : "Open in viewer"}
|
||||
aria-label={
|
||||
isViewedInViewer
|
||||
? t("fileSidebar.fileItem.closeViewer", "Close viewer")
|
||||
: t("fileSidebar.fileItem.openInViewer", "Open in viewer")
|
||||
}
|
||||
>
|
||||
<VisibilityOutlinedIcon
|
||||
className="file-sidebar-eye-open"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { ReactNode } from "react";
|
||||
import { Paper, Group, Text, Button, ActionIcon, Stack } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import LocalIcon from "@app/components/shared/LocalIcon";
|
||||
|
||||
type InfoBannerTone = "info" | "warning";
|
||||
@@ -85,6 +86,7 @@ export const InfoBanner: React.FC<InfoBannerProps> = ({
|
||||
closeIconColor,
|
||||
compact = false,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
if (!show) {
|
||||
return null;
|
||||
}
|
||||
@@ -192,7 +194,7 @@ export const InfoBanner: React.FC<InfoBannerProps> = ({
|
||||
color={closeIconColor ? undefined : "gray"}
|
||||
size="sm"
|
||||
onClick={handleDismiss}
|
||||
aria-label="Dismiss"
|
||||
aria-label={t("infoBanner.dismiss", "Dismiss")}
|
||||
style={closeIconColor ? { color: closeIconColor } : undefined}
|
||||
>
|
||||
<LocalIcon
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { forwardRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import LocalIcon from "@app/components/shared/LocalIcon";
|
||||
import styles from "@app/components/shared/textInput/TextInput.module.css";
|
||||
|
||||
@@ -63,6 +64,7 @@ export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const { t } = useTranslation();
|
||||
const handleClear = () => {
|
||||
if (onClear) {
|
||||
onClear();
|
||||
@@ -112,7 +114,7 @@ export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
|
||||
type="button"
|
||||
className={styles.clearButton}
|
||||
onClick={handleClear}
|
||||
aria-label="Clear input"
|
||||
aria-label={t("textInput.clear", "Clear input")}
|
||||
>
|
||||
<LocalIcon icon="close-rounded" width="1.25rem" height="1.25rem" />
|
||||
</button>
|
||||
|
||||
@@ -6,6 +6,7 @@ import React, {
|
||||
useCallback,
|
||||
} from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import LocalIcon from "@app/components/shared/LocalIcon";
|
||||
import { addEventListenerWithCleanup } from "@app/utils/genericUtils";
|
||||
import { useTooltipPosition } from "@app/hooks/useTooltipPosition";
|
||||
@@ -68,6 +69,7 @@ export const Tooltip: React.FC<TooltipProps> = ({
|
||||
manualCloseOnly = false,
|
||||
showCloseButton = false,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [internalOpen, setInternalOpen] = useState(false);
|
||||
const [isPinned, setIsPinned] = useState(false);
|
||||
const { tooltipLogo } = useLogoAssets();
|
||||
@@ -401,8 +403,8 @@ export const Tooltip: React.FC<TooltipProps> = ({
|
||||
setIsPinned(false);
|
||||
setOpen(false);
|
||||
}}
|
||||
title="Close tooltip"
|
||||
aria-label="Close tooltip"
|
||||
title={t("tooltip.close", "Close tooltip")}
|
||||
aria-label={t("tooltip.close", "Close tooltip")}
|
||||
>
|
||||
<LocalIcon icon="close-rounded" width="1.25rem" height="1.25rem" />
|
||||
</button>
|
||||
|
||||
@@ -259,7 +259,7 @@ const UpdateModal: React.FC<UpdateModalProps> = ({
|
||||
onClick={onClose}
|
||||
size="lg"
|
||||
variant="subtle"
|
||||
aria-label="Close update modal"
|
||||
aria-label={t("update.closeModal", "Close update modal")}
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { ActionIcon, Slider } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useViewer } from "@app/contexts/ViewerContext";
|
||||
import { useNavigationState } from "@app/contexts/NavigationContext";
|
||||
import ZoomInIcon from "@mui/icons-material/ZoomIn";
|
||||
@@ -9,6 +10,7 @@ import ZoomOutIcon from "@mui/icons-material/ZoomOut";
|
||||
* Compact zoom controls rendered inline in the WorkbenchBar when the current workbench is "viewer".
|
||||
*/
|
||||
export function ViewerInlineControls() {
|
||||
const { t } = useTranslation();
|
||||
const { workbench } = useNavigationState();
|
||||
const viewer = useViewer();
|
||||
|
||||
@@ -39,7 +41,7 @@ export function ViewerInlineControls() {
|
||||
radius="md"
|
||||
className="workbench-bar-action-icon"
|
||||
onClick={() => viewer.zoomActions.zoomOut()}
|
||||
aria-label="Zoom out"
|
||||
aria-label={t("viewer.zoomOut", "Zoom out")}
|
||||
>
|
||||
<ZoomOutIcon sx={{ fontSize: "1rem" }} />
|
||||
</ActionIcon>
|
||||
@@ -68,7 +70,7 @@ export function ViewerInlineControls() {
|
||||
radius="md"
|
||||
className="workbench-bar-action-icon"
|
||||
onClick={() => viewer.zoomActions.zoomIn()}
|
||||
aria-label="Zoom in"
|
||||
aria-label={t("viewer.zoomIn", "Zoom in")}
|
||||
>
|
||||
<ZoomInIcon sx={{ fontSize: "1rem" }} />
|
||||
</ActionIcon>
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { ActionIcon, Divider } from "@mantine/core";
|
||||
import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useToolWorkflow } from "@app/contexts/ToolWorkflowContext";
|
||||
import {
|
||||
useNavigationState,
|
||||
@@ -37,6 +38,7 @@ const ActiveToolButton: React.FC<ActiveToolButtonProps> = ({
|
||||
setActiveButton,
|
||||
tooltipPosition = "right",
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { selectedTool, selectedToolKey, leftPanelView, handleBackToTools } =
|
||||
useToolWorkflow();
|
||||
const { hasUnsavedChanges } = useNavigationState();
|
||||
@@ -158,7 +160,11 @@ const ActiveToolButton: React.FC<ActiveToolButtonProps> = ({
|
||||
<div className="current-tool-content">
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<Tooltip
|
||||
content={isBackHover ? "Back to all tools" : indicatorTool.name}
|
||||
content={
|
||||
isBackHover
|
||||
? t("quickAccess.backToAllTools", "Back to all tools")
|
||||
: indicatorTool.name
|
||||
}
|
||||
position={tooltipPosition}
|
||||
arrow
|
||||
maxWidth={140}
|
||||
@@ -183,7 +189,9 @@ const ActiveToolButton: React.FC<ActiveToolButtonProps> = ({
|
||||
onMouseEnter={() => setIsBackHover(true)}
|
||||
onMouseLeave={() => setIsBackHover(false)}
|
||||
aria-label={
|
||||
isBackHover ? "Back to all tools" : indicatorTool.name
|
||||
isBackHover
|
||||
? t("quickAccess.backToAllTools", "Back to all tools")
|
||||
: indicatorTool.name
|
||||
}
|
||||
style={{
|
||||
backgroundColor: isBackHover
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useToast } from "@app/components/toast/ToastContext";
|
||||
import { ToastInstance, ToastLocation } from "@app/components/toast/types";
|
||||
import { LocalIcon } from "@app/components/shared/LocalIcon";
|
||||
@@ -38,6 +39,7 @@ function getDefaultIconName(t: ToastInstance): string {
|
||||
}
|
||||
|
||||
export default function ToastRenderer() {
|
||||
const { t: translate } = useTranslation();
|
||||
const { toasts, dismiss } = useToast();
|
||||
|
||||
const grouped = toasts.reduce<Record<ToastLocation, ToastInstance[]>>(
|
||||
@@ -88,7 +90,10 @@ export default function ToastRenderer() {
|
||||
<div className="toast-controls">
|
||||
{t.expandable && (
|
||||
<button
|
||||
aria-label="Toggle details"
|
||||
aria-label={translate(
|
||||
"toast.toggleDetails",
|
||||
"Toggle details",
|
||||
)}
|
||||
onClick={() => {
|
||||
const evt = new CustomEvent("toast:toggle", {
|
||||
detail: { id: t.id },
|
||||
@@ -101,7 +106,7 @@ export default function ToastRenderer() {
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
aria-label="Dismiss"
|
||||
aria-label={translate("toast.dismiss", "Dismiss")}
|
||||
onClick={() => dismiss(t.id)}
|
||||
className="toast-button"
|
||||
>
|
||||
|
||||
@@ -99,11 +99,14 @@ const CompressSettings = ({
|
||||
{parameters.compressionMethod === "filesize" && (
|
||||
<Stack gap="sm">
|
||||
<Text size="sm" fw={500}>
|
||||
Desired File Size
|
||||
{t("compress.settings.desiredSize", "Desired File Size")}
|
||||
</Text>
|
||||
<div style={{ display: "flex", gap: "8px", alignItems: "flex-end" }}>
|
||||
<NumberInput
|
||||
placeholder="Enter size"
|
||||
placeholder={t(
|
||||
"compress.settings.desiredSizePlaceholder",
|
||||
"Enter size",
|
||||
)}
|
||||
value={parameters.fileSizeValue}
|
||||
onChange={(value) =>
|
||||
onParameterChange("fileSizeValue", value?.toString() || "")
|
||||
|
||||
@@ -139,7 +139,9 @@ const LanguagePicker: React.FC<LanguagePickerProps> = ({
|
||||
return (
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||
<Loader size="xs" />
|
||||
<Text size="sm">Loading available languages...</Text>
|
||||
<Text size="sm">
|
||||
{t("ocr.languagePicker.loading", "Loading available languages...")}
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+21
-6
@@ -75,7 +75,7 @@ export default function PageLayoutMarginsBordersSettings({
|
||||
<Stack gap="sm">
|
||||
<NumberInput
|
||||
label={t("pageLayout.top", "Top Margin")}
|
||||
placeholder="Enter top margin"
|
||||
placeholder={t("pageLayout.margins.topPlaceholder", "Enter top margin")}
|
||||
value={parameters.topMargin}
|
||||
onChange={(v) => onParameterChange("topMargin", Number(v))}
|
||||
min={0}
|
||||
@@ -85,7 +85,10 @@ export default function PageLayoutMarginsBordersSettings({
|
||||
/>
|
||||
<NumberInput
|
||||
label={t("pageLayout.bottom", "Bottom Margin")}
|
||||
placeholder="Enter bottom margin"
|
||||
placeholder={t(
|
||||
"pageLayout.margins.bottomPlaceholder",
|
||||
"Enter bottom margin",
|
||||
)}
|
||||
value={parameters.bottomMargin}
|
||||
onChange={(v) => onParameterChange("bottomMargin", Number(v))}
|
||||
min={0}
|
||||
@@ -95,7 +98,10 @@ export default function PageLayoutMarginsBordersSettings({
|
||||
/>
|
||||
<NumberInput
|
||||
label={t("pageLayout.left", "Left Margin")}
|
||||
placeholder="Enter left margin"
|
||||
placeholder={t(
|
||||
"pageLayout.margins.leftPlaceholder",
|
||||
"Enter left margin",
|
||||
)}
|
||||
value={parameters.leftMargin}
|
||||
onChange={(v) => onParameterChange("leftMargin", Number(v))}
|
||||
min={0}
|
||||
@@ -104,7 +110,10 @@ export default function PageLayoutMarginsBordersSettings({
|
||||
/>
|
||||
<NumberInput
|
||||
label={t("pageLayout.right", "Right Margin")}
|
||||
placeholder="Enter right margin"
|
||||
placeholder={t(
|
||||
"pageLayout.margins.rightPlaceholder",
|
||||
"Enter right margin",
|
||||
)}
|
||||
value={parameters.rightMargin}
|
||||
onChange={(v) => onParameterChange("rightMargin", Number(v))}
|
||||
min={0}
|
||||
@@ -114,7 +123,10 @@ export default function PageLayoutMarginsBordersSettings({
|
||||
/>
|
||||
<NumberInput
|
||||
label={t("pageLayout.innerMargin", "Inner Margin")}
|
||||
placeholder="Enter inner margin"
|
||||
placeholder={t(
|
||||
"pageLayout.margins.innerPlaceholder",
|
||||
"Enter inner margin",
|
||||
)}
|
||||
value={parameters.innerMargin}
|
||||
onChange={(v) => onParameterChange("innerMargin", Number(v))}
|
||||
min={0}
|
||||
@@ -137,7 +149,10 @@ export default function PageLayoutMarginsBordersSettings({
|
||||
{parameters.addBorder && (
|
||||
<NumberInput
|
||||
label={t("pageLayout.borderWidth", "Border Thickness")}
|
||||
placeholder="Enter border thickness"
|
||||
placeholder={t(
|
||||
"pageLayout.margins.borderThicknessPlaceholder",
|
||||
"Enter border thickness",
|
||||
)}
|
||||
value={parameters.borderWidth}
|
||||
onChange={(v) => onParameterChange("borderWidth", Number(v))}
|
||||
min={1}
|
||||
|
||||
@@ -80,7 +80,7 @@ export default function PageLayoutSettings({
|
||||
<>
|
||||
<NumberInput
|
||||
label={t("pageLayout.rows", "Rows")}
|
||||
placeholder="Enter rows"
|
||||
placeholder={t("pageLayout.rowsPlaceholder", "Enter rows")}
|
||||
value={parameters.rows}
|
||||
onChange={(v) => onParameterChange("rows", Number(v))}
|
||||
min={1}
|
||||
@@ -90,7 +90,7 @@ export default function PageLayoutSettings({
|
||||
|
||||
<NumberInput
|
||||
label={t("pageLayout.cols", "Columns")}
|
||||
placeholder="Enter columns"
|
||||
placeholder={t("pageLayout.colsPlaceholder", "Enter columns")}
|
||||
value={parameters.cols}
|
||||
onChange={(v) => onParameterChange("cols", Number(v))}
|
||||
min={1}
|
||||
|
||||
@@ -353,7 +353,7 @@ export const SavedSignaturesSection = ({
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="blue"
|
||||
aria-label="Use signature"
|
||||
aria-label={t("sign.saved.use", "Use signature")}
|
||||
onClick={() => onUseSignature(activePersonalSignature)}
|
||||
disabled={disabled}
|
||||
>
|
||||
@@ -480,7 +480,7 @@ export const SavedSignaturesSection = ({
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="blue"
|
||||
aria-label="Use signature"
|
||||
aria-label={t("sign.saved.use", "Use signature")}
|
||||
onClick={() => onUseSignature(activeSharedSignature)}
|
||||
disabled={disabled}
|
||||
>
|
||||
@@ -618,7 +618,7 @@ export const SavedSignaturesSection = ({
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="blue"
|
||||
aria-label="Use signature"
|
||||
aria-label={t("sign.saved.use", "Use signature")}
|
||||
onClick={() =>
|
||||
onUseSignature(activeLocalStorageSignature)
|
||||
}
|
||||
|
||||
@@ -32,16 +32,16 @@ export const usePageSelectionTips = (): TooltipContent => {
|
||||
),
|
||||
bullets: [
|
||||
t(
|
||||
"bulkSelection.operators.and",
|
||||
'AND: & or "and" — require both conditions (e.g., 1-50 & even)',
|
||||
"bulkSelection.operators.descriptions.and",
|
||||
'AND: & or "and" - require both conditions (e.g., 1-50 & even)',
|
||||
),
|
||||
t(
|
||||
"bulkSelection.operators.comma",
|
||||
"Comma: , or | — combine selections (e.g., 1-10, 20)",
|
||||
"bulkSelection.operators.descriptions.comma",
|
||||
"Comma: , or | - combine selections (e.g., 1-10, 20)",
|
||||
),
|
||||
t(
|
||||
"bulkSelection.operators.not",
|
||||
'NOT: ! or "not" — exclude pages (e.g., 3n & not 30)',
|
||||
"bulkSelection.operators.descriptions.not",
|
||||
'NOT: ! or "not" - exclude pages (e.g., 3n & not 30)',
|
||||
),
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1176,7 +1176,12 @@ const EmbedPdfViewerContent = ({
|
||||
|
||||
{!effectiveFile ? (
|
||||
<Center style={{ flex: 1 }}>
|
||||
<Text c="red">Error: No file provided to viewer</Text>
|
||||
<Text c="red">
|
||||
{t(
|
||||
"viewer.error.noFileProvided",
|
||||
"Error: No file provided to viewer",
|
||||
)}
|
||||
</Text>
|
||||
</Center>
|
||||
) : isCurrentFileEncrypted ? (
|
||||
<Center style={{ flex: 1 }}>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useCallback, useState, useMemo, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useDocumentState } from "@embedpdf/core/react";
|
||||
import { useScroll } from "@embedpdf/plugin-scroll/react";
|
||||
import { useAnnotation } from "@embedpdf/plugin-annotation/react";
|
||||
@@ -143,6 +144,7 @@ const LinkToolbar: React.FC<LinkToolbarProps> = React.memo(
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const centerX =
|
||||
(annotationLink.rect.origin.x + annotationLink.rect.size.width / 2) *
|
||||
scale;
|
||||
@@ -170,8 +172,8 @@ const LinkToolbar: React.FC<LinkToolbarProps> = React.memo(
|
||||
e.stopPropagation();
|
||||
onDelete(annotationLink);
|
||||
}}
|
||||
aria-label="Delete link"
|
||||
title="Delete link"
|
||||
aria-label={t("viewer.link.delete", "Delete link")}
|
||||
title={t("viewer.link.delete", "Delete link")}
|
||||
>
|
||||
<TrashIcon />
|
||||
</button>
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
forwardRef,
|
||||
useRef,
|
||||
} from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { createPluginRegistration } from "@embedpdf/core";
|
||||
import type { PluginRegistry } from "@embedpdf/core";
|
||||
import { EmbedPDF } from "@embedpdf/core/react";
|
||||
@@ -157,6 +158,7 @@ export const LocalEmbedPDFWithAnnotations = forwardRef<
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const { t } = useTranslation();
|
||||
const [pdfUrl, setPdfUrl] = useState<string | null>(null);
|
||||
const annotationApiRef = useRef<any>(null);
|
||||
const zoomApiRef = useRef<any>(null);
|
||||
@@ -653,7 +655,10 @@ export const LocalEmbedPDFWithAnnotations = forwardRef<
|
||||
),
|
||||
);
|
||||
}}
|
||||
aria-label="Delete signature"
|
||||
aria-label={t(
|
||||
"viewer.signature.delete",
|
||||
"Delete signature",
|
||||
)}
|
||||
>
|
||||
<CloseIcon
|
||||
style={{ fontSize: "0.8rem" }}
|
||||
|
||||
@@ -139,7 +139,7 @@ function RedactionSelectionMenuInner({
|
||||
}}
|
||||
>
|
||||
<Group gap="sm" wrap="nowrap" justify="center">
|
||||
<Tooltip label="Remove this mark">
|
||||
<Tooltip label={t("viewer.redaction.removeMark", "Remove this mark")}>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
|
||||
@@ -203,7 +203,7 @@ export function SearchInterface({ visible, onClose }: SearchInterfaceProps) {
|
||||
variant="subtle"
|
||||
size="sm"
|
||||
onClick={handleCloseClick}
|
||||
aria-label="Close search"
|
||||
aria-label={t("viewer.search.close", "Close search")}
|
||||
>
|
||||
<LocalIcon icon="close" width="1rem" height="1rem" />
|
||||
</ActionIcon>
|
||||
@@ -227,7 +227,7 @@ export function SearchInterface({ visible, onClose }: SearchInterfaceProps) {
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
onClick={handleClearSearch}
|
||||
aria-label="Clear search"
|
||||
aria-label={t("viewer.search.clear", "Clear search")}
|
||||
>
|
||||
<LocalIcon icon="close" width="0.875rem" height="0.875rem" />
|
||||
</ActionIcon>
|
||||
@@ -267,7 +267,9 @@ export function SearchInterface({ visible, onClose }: SearchInterfaceProps) {
|
||||
disabled={!resultInfo || resultInfo.totalResults === 0}
|
||||
/>
|
||||
<Text size="sm" c="dimmed">
|
||||
of {resultInfo?.totalResults || 0}
|
||||
{t("viewer.search.resultsOf", "of {{total}}", {
|
||||
total: resultInfo?.totalResults || 0,
|
||||
})}
|
||||
</Text>
|
||||
</Group>
|
||||
|
||||
@@ -277,7 +279,7 @@ export function SearchInterface({ visible, onClose }: SearchInterfaceProps) {
|
||||
size="sm"
|
||||
onClick={handlePrevious}
|
||||
disabled={!resultInfo || resultInfo.currentIndex <= 1}
|
||||
aria-label="Previous result"
|
||||
aria-label={t("viewer.search.previous", "Previous result")}
|
||||
>
|
||||
<LocalIcon icon="keyboard-arrow-up" width="1rem" height="1rem" />
|
||||
</ActionIcon>
|
||||
@@ -288,7 +290,7 @@ export function SearchInterface({ visible, onClose }: SearchInterfaceProps) {
|
||||
disabled={
|
||||
!resultInfo || resultInfo.currentIndex >= resultInfo.totalResults
|
||||
}
|
||||
aria-label="Next result"
|
||||
aria-label={t("viewer.search.next", "Next result")}
|
||||
>
|
||||
<LocalIcon icon="keyboard-arrow-down" width="1rem" height="1rem" />
|
||||
</ActionIcon>
|
||||
|
||||
@@ -34,7 +34,7 @@ interface FormFieldSidebarProps {
|
||||
}
|
||||
|
||||
export function FormFieldSidebar({ visible, onToggle }: FormFieldSidebarProps) {
|
||||
useTranslation();
|
||||
const { t } = useTranslation();
|
||||
const { state, setValue, setActiveField } = useFormFill();
|
||||
const { fields, activeFieldName, loading } = state;
|
||||
const activeFieldRef = useRef<HTMLDivElement>(null);
|
||||
@@ -119,7 +119,7 @@ export function FormFieldSidebar({ visible, onToggle }: FormFieldSidebarProps) {
|
||||
variant="subtle"
|
||||
size="sm"
|
||||
onClick={onToggle}
|
||||
aria-label="Close sidebar"
|
||||
aria-label={t("formFill.sidebar.close", "Close sidebar")}
|
||||
>
|
||||
<CloseIcon sx={{ fontSize: 16 }} />
|
||||
</ActionIcon>
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
Tooltip,
|
||||
ActionIcon,
|
||||
} from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
useFormFill,
|
||||
useAllFormValues,
|
||||
@@ -120,6 +121,7 @@ const _MODE_TABS: ModeTabDef[] = [
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const FormFill = (_props: BaseToolProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { selectedTool } = useNavigation();
|
||||
const { selectors, state: fileState } = useFileState();
|
||||
|
||||
@@ -488,7 +490,10 @@ const FormFill = (_props: BaseToolProps) => {
|
||||
|
||||
{/* Flatten toggle */}
|
||||
<Switch
|
||||
label="Flatten after filling"
|
||||
label={t(
|
||||
"formFill.flattenAfterFilling",
|
||||
"Flatten after filling",
|
||||
)}
|
||||
checked={flatten}
|
||||
onChange={(e) => setFlatten(e.currentTarget.checked)}
|
||||
size="xs"
|
||||
@@ -507,15 +512,22 @@ const FormFill = (_props: BaseToolProps) => {
|
||||
loading={saving}
|
||||
disabled={!formState.isDirty && !flattenChanged}
|
||||
>
|
||||
Save
|
||||
{t("formFill.save", "Save")}
|
||||
</Button>
|
||||
|
||||
<Tooltip label="Re-scan fields" withArrow position="bottom">
|
||||
<Tooltip
|
||||
label={t("formFill.rescanFields", "Re-scan fields")}
|
||||
withArrow
|
||||
position="bottom"
|
||||
>
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
size="md"
|
||||
onClick={handleRefresh}
|
||||
aria-label="Re-scan form fields"
|
||||
aria-label={t(
|
||||
"formFill.rescanFormFields",
|
||||
"Re-scan form fields",
|
||||
)}
|
||||
>
|
||||
<RefreshIcon sx={{ fontSize: 16 }} />
|
||||
</ActionIcon>
|
||||
|
||||
Reference in New Issue
Block a user