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:
Anthony Stirling
2026-06-08 10:11:43 +00:00
committed by GitHub
parent 0dff192281
commit 1ef03c43b4
50 changed files with 884 additions and 250 deletions
@@ -39,13 +39,15 @@ export function OverviewHeader() {
</Text>
{user?.email && (
<Text size="xs" c="dimmed" mt="0.25rem">
Signed in as: {user.email}
{t("account.overview.signedInAs", "Signed in as: {{email}}", {
email: user.email,
})}
</Text>
)}
</div>
{user && (
<Button color="red" variant="filled" onClick={handleLogout}>
Log out
{t("account.overview.logOut", "Log out")}
</Button>
)}
</div>
@@ -809,7 +809,10 @@ export default function AdminAdvancedSection() {
},
})
}
placeholder="Default: java.io.tmpdir/stirling-pdf"
placeholder={t(
"admin.settings.advanced.tempFileManagement.baseTmpDir.placeholder",
"Default: java.io.tmpdir/stirling-pdf",
)}
disabled={!loginEnabled}
/>
</div>
@@ -834,7 +837,10 @@ export default function AdminAdvancedSection() {
},
})
}
placeholder="Default: baseTmpDir/libreoffice"
placeholder={t(
"admin.settings.advanced.tempFileManagement.libreofficeDir.placeholder",
"Default: baseTmpDir/libreoffice",
)}
disabled={!loginEnabled}
/>
</div>
@@ -859,7 +865,10 @@ export default function AdminAdvancedSection() {
},
})
}
placeholder="System temp directory path"
placeholder={t(
"admin.settings.advanced.tempFileManagement.systemTempDir.placeholder",
"System temp directory path",
)}
disabled={!loginEnabled}
/>
</div>
@@ -698,7 +698,10 @@ export default function AdminDatabaseSection() {
onChange={(value) =>
setSettings({ ...settings, password: value })
}
placeholder="Enter database password"
placeholder={t(
"admin.settings.database.password.placeholder",
"Enter database password",
)}
disabled={!loginEnabled}
/>
</div>
@@ -296,7 +296,10 @@ export default function AdminEndpointsSection() {
}))}
searchable
clearable
placeholder="Select endpoints to disable"
placeholder={t(
"admin.settings.endpoints.toRemove.placeholder",
"Select endpoints to disable",
)}
comboboxProps={{ zIndex: 1400 }}
disabled={!loginEnabled}
/>
@@ -330,7 +333,10 @@ export default function AdminEndpointsSection() {
}))}
searchable
clearable
placeholder="Select groups to disable"
placeholder={t(
"admin.settings.endpoints.groupsToRemove.placeholder",
"Select groups to disable",
)}
comboboxProps={{ zIndex: 1400 }}
disabled={!loginEnabled}
/>
@@ -286,7 +286,10 @@ export default function AdminMailSection() {
onChange={(value) =>
setSettings({ ...settings, password: value })
}
placeholder="Enter SMTP password"
placeholder={t(
"admin.settings.mail.password.placeholder",
"Enter SMTP password",
)}
/>
</div>
@@ -183,8 +183,14 @@ const AdminPlanSection: React.FC = () => {
if (!plans || plans.length === 0) {
return (
<Alert color="yellow" title="No data available">
Plans data is not available at the moment.
<Alert
color="yellow"
title={t("admin.settings.plan.noData.title", "No data available")}
>
{t(
"admin.settings.plan.noData.message",
"Plans data is not available at the moment.",
)}
</Alert>
);
}
@@ -1,5 +1,5 @@
import { useCallback, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { Trans, useTranslation } from "react-i18next";
import {
TextInput,
Switch,
@@ -132,17 +132,29 @@ export default function AdminPremiumSection() {
</List.Item>
<List.Item>
<Text size="sm" component="span">
<strong>Custom Metadata</strong> (PRO) - General
<Trans
i18nKey="admin.settings.premium.movedFeatures.customMetadata"
defaults="<0>Custom Metadata</0> (PRO) - General"
components={[<strong />]}
/>
</Text>
</List.Item>
<List.Item>
<Text size="sm" component="span">
<strong>Audit Logging</strong> (ENTERPRISE) - Security
<Trans
i18nKey="admin.settings.premium.movedFeatures.auditLogging"
defaults="<0>Audit Logging</0> (ENTERPRISE) - Security"
components={[<strong />]}
/>
</Text>
</List.Item>
<List.Item>
<Text size="sm" component="span">
<strong>Database Configuration</strong> (ENTERPRISE) - Database
<Trans
i18nKey="admin.settings.premium.movedFeatures.databaseConfiguration"
defaults="<0>Database Configuration</0> (ENTERPRISE) - Database"
components={[<strong />]}
/>
</Text>
</List.Item>
</List>
@@ -11,7 +11,10 @@ import {
import { useTranslation } from "react-i18next";
import { alert } from "@app/components/toast";
import { LicenseInfo, mapLicenseToTier } from "@app/services/licenseService";
import { PLAN_FEATURES, PLAN_HIGHLIGHTS } from "@app/constants/planConstants";
import {
usePlanFeatures,
usePlanHighlights,
} from "@app/constants/planConstants";
import FeatureComparisonTable from "@app/components/shared/config/configSections/plan/FeatureComparisonTable";
import StaticCheckoutModal from "@app/components/shared/config/configSections/plan/StaticCheckoutModal";
import LicenseKeySection from "@app/components/shared/config/configSections/plan/LicenseKeySection";
@@ -32,6 +35,8 @@ const StaticPlanSection: React.FC<StaticPlanSectionProps> = ({
currentLicenseInfo,
}) => {
const { t } = useTranslation();
const planFeatures = usePlanFeatures();
const planHighlights = usePlanHighlights();
const [showComparison, setShowComparison] = useState(false);
// Static checkout modal state
@@ -88,8 +93,8 @@ const StaticPlanSection: React.FC<StaticPlanSectionProps> = ({
price: 0,
currency: "£",
period: "",
highlights: PLAN_HIGHLIGHTS.FREE,
features: PLAN_FEATURES.FREE,
highlights: planHighlights.FREE,
features: planFeatures.FREE,
maxUsers: 5,
},
{
@@ -99,8 +104,8 @@ const StaticPlanSection: React.FC<StaticPlanSectionProps> = ({
currency: "",
period: "",
popular: false,
highlights: PLAN_HIGHLIGHTS.SERVER_MONTHLY,
features: PLAN_FEATURES.SERVER,
highlights: planHighlights.SERVER_MONTHLY,
features: planFeatures.SERVER,
maxUsers: "Unlimited users",
},
{
@@ -109,8 +114,8 @@ const StaticPlanSection: React.FC<StaticPlanSectionProps> = ({
price: 0,
currency: "",
period: "",
highlights: PLAN_HIGHLIGHTS.ENTERPRISE_MONTHLY,
features: PLAN_FEATURES.ENTERPRISE,
highlights: planHighlights.ENTERPRISE_MONTHLY,
features: planFeatures.ENTERPRISE,
maxUsers: "Custom",
},
];