Skip to content

Commit 41094f6

Browse files
Vonngclaude
andcommitted
feat(i18n): localize observability, admin tools, and shared components
IDP, KMS, logs, health report, speedtest, profiling, inspect, trace, and watch. The speedtest control bar moves from a rigid 3/4/4/1 grid to a wrapping flex row so the retest button no longer overflows its card. Shared chrome: credentials prompts, object manager, search boxes, date range/days selectors (localized presets and timestamps), progress and empty states. permissionTooltipHelper builds its message from localized templates while keeping English output byte-identical. The license page is fully translated, with legal names and the SILO acrostic retained. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 0298062 commit 41094f6

46 files changed

Lines changed: 913 additions & 593 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

web-app/src/common/SecureComponent/permissions.ts

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
// You should have received a copy of the GNU Affero General Public License
1515
// along with this program. If not, see <http://www.gnu.org/licenses/>.
1616

17+
import { getStoredLanguage, translate } from "../../i18n/lang";
18+
1719
export const IAM_ROLES = {
1820
BUCKET_OWNER: "BUCKET_OWNER", // upload/delete objects from the bucket
1921
BUCKET_VIEWER: "BUCKET_VIEWER", // only view objects on the bucket
@@ -467,20 +469,21 @@ export const IAM_PAGES_PERMISSIONS = {
467469
export const S3_ALL_RESOURCES = "arn:aws:s3:::*";
468470
export const CONSOLE_UI_RESOURCE = "console-ui";
469471

472+
// Plain function rather than a hook: it is called from render bodies all over
473+
// the console, so it reads the stored language directly. Callers re-render on a
474+
// language switch, which re-runs this.
470475
export const permissionTooltipHelper = (scopes: string[], name: string) => {
471-
let niceScopes = scopes.join(", ").toString();
472-
473-
return (
474-
"You require additional permissions in order to " +
475-
name +
476-
". Please ask your SILO administrator to grant you " +
477-
niceScopes +
478-
" permission" +
479-
(scopes.length > 1 ? "s" : "") +
480-
" in order to " +
481-
name +
482-
"."
483-
);
476+
const niceScopes = scopes.join(", ").toString();
477+
const lang = getStoredLanguage();
478+
const template =
479+
scopes.length > 1
480+
? "You require additional permissions in order to {name}. Please ask your SILO administrator to grant you {scopes} permissions in order to {name}."
481+
: "You require additional permissions in order to {name}. Please ask your SILO administrator to grant you {scopes} permission in order to {name}.";
482+
483+
return translate(lang, template)
484+
.split("{name}")
485+
.join(translate(lang, name))
486+
.replace("{scopes}", niceScopes);
484487
};
485488

486489
export const listUsersPermissions = [IAM_SCOPES.ADMIN_LIST_USERS];

web-app/src/common/utils.ts

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import { IBytesCalc } from "./types";
1818

1919
import get from "lodash/get";
20+
import { getStoredLanguage, translate } from "../i18n/lang";
2021

2122
export const units = [
2223
"B",
@@ -124,24 +125,29 @@ export const niceTimeFromSeconds = (seconds: number): string => {
124125
const parts = [];
125126

126127
if (days > 0) {
127-
parts.push(`${days} day${days !== 1 ? "s" : ""}`);
128+
parts.push(countWithUnit(days, days !== 1, "day", "days"));
128129
}
129130

130131
if (hours > 0) {
131-
parts.push(`${hours} hour${hours !== 1 ? "s" : ""}`);
132+
parts.push(countWithUnit(hours, hours !== 1, "hour", "hours"));
132133
}
133134

134135
if (minutes > 0) {
135-
parts.push(`${minutes} minute${minutes !== 1 ? "s" : ""}`);
136+
parts.push(countWithUnit(minutes, minutes !== 1, "minute", "minutes"));
136137
}
137138

138139
if (remainingSeconds > 0) {
139140
parts.push(
140-
`${remainingSeconds} second${remainingSeconds !== 1 ? "s" : ""}`,
141+
countWithUnit(
142+
remainingSeconds,
143+
remainingSeconds !== 1,
144+
"second",
145+
"seconds",
146+
),
141147
);
142148
}
143149

144-
return parts.join(" and ");
150+
return parts.join(translate(getStoredLanguage(), " and "));
145151
};
146152

147153
// seconds / minutes /hours / Days / Years calculator
@@ -151,6 +157,17 @@ export const niceDays = (secondsValue: string, timeVariant: string = "s") => {
151157
return niceDaysInt(seconds, timeVariant);
152158
};
153159

160+
// Joins a count with its unit word. The caller decides singular vs plural so the
161+
// English output keeps the exact wording each branch had before; other languages
162+
// take whichever form the dictionary carries.
163+
const countWithUnit = (
164+
value: number,
165+
plural: boolean,
166+
singular: string,
167+
pluralForm: string,
168+
) =>
169+
`${value} ${translate(getStoredLanguage(), plural ? pluralForm : singular)}`;
170+
154171
// niceDaysInt returns the string in the max unit found e.g. 92400 seconds -> 1 day
155172
export const niceDaysInt = (seconds: number, timeVariant: string = "s") => {
156173
switch (timeVariant) {
@@ -173,35 +190,40 @@ export const niceDaysInt = (seconds: number, timeVariant: string = "s") => {
173190

174191
if (days > 365) {
175192
const years = days / 365;
176-
return `${years} year${Math.floor(years) === 1 ? "" : "s"}`;
193+
return countWithUnit(years, Math.floor(years) !== 1, "year", "years");
177194
}
178195

179196
if (days > 30) {
180197
const months = Math.floor(days / 30);
181198
const diffDays = days - months * 30;
182199

183-
return `${months} month${Math.floor(months) === 1 ? "" : "s"} ${
184-
diffDays > 0 ? `${diffDays} day${diffDays > 1 ? "s" : ""}` : ""
200+
return `${countWithUnit(
201+
months,
202+
Math.floor(months) !== 1,
203+
"month",
204+
"months",
205+
)} ${
206+
diffDays > 0 ? countWithUnit(diffDays, diffDays > 1, "day", "days") : ""
185207
}`;
186208
}
187209

188210
if (days >= 7 && days <= 30) {
189211
const weeks = Math.floor(days / 7);
190212

191-
return `${Math.floor(weeks)} week${weeks === 1 ? "" : "s"}`;
213+
return countWithUnit(weeks, weeks !== 1, "week", "weeks");
192214
}
193215

194216
if (days >= 1 && days <= 6) {
195-
return `${days} day${days > 1 ? "s" : ""}`;
217+
return countWithUnit(days, days > 1, "day", "days");
196218
}
197219

198-
return `${hours >= 1 ? `${hours} hour${hours > 1 ? "s" : ""}` : ""} ${
220+
return `${hours >= 1 ? countWithUnit(hours, hours > 1, "hour", "hours") : ""} ${
199221
minutes >= 1 && hours === 0
200-
? `${minutes} minute${minutes > 1 ? "s" : ""}`
222+
? countWithUnit(minutes, minutes > 1, "minute", "minutes")
201223
: ""
202224
} ${
203225
seconds >= 1 && minutes === 0 && hours === 0
204-
? `${seconds} second${seconds > 1 ? "s" : ""}`
226+
? countWithUnit(seconds, seconds > 1, "second", "seconds")
205227
: ""
206228
}`;
207229
};

web-app/src/screens/Console/Common/ComponentsScreen.tsx

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,12 @@ import PageHeaderWrapper from "./PageHeaderWrapper/PageHeaderWrapper";
2121
import HelpMenu from "../HelpMenu";
2222
import { setHelpName } from "../../../systemSlice";
2323
import { useAppDispatch } from "../../../store";
24+
import { interpolate, useT } from "i18n";
2425

2526
const ComponentsScreen = () => {
2627
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
2728
const dispatch = useAppDispatch();
29+
const t = useT();
2830

2931
useEffect(() => {
3032
dispatch(setHelpName("components"));
@@ -38,10 +40,10 @@ const ComponentsScreen = () => {
3840
<PageLayout>
3941
<Grid container>
4042
<Grid item xs={12}>
41-
<SectionTitle>Confirm Dialogs</SectionTitle>
43+
<SectionTitle>{t("Confirm Dialogs")}</SectionTitle>
4244
</Grid>
4345
<Grid item xs={12}>
44-
<p>Used to confirm a non-idempotent action.</p>
46+
<p>{t("Used to confirm a non-idempotent action.")}</p>
4547
</Grid>
4648
<Grid item xs={12}>
4749
<Button
@@ -51,11 +53,11 @@ const ComponentsScreen = () => {
5153
onClick={() => {
5254
setDialogOpen(true);
5355
}}
54-
label={"Open Dialog"}
56+
label={t("Open Dialog")}
5557
/>
5658
<ConfirmDialog
57-
title={`Delete Bucket`}
58-
confirmText={"Delete"}
59+
title={t("Delete Bucket")}
60+
confirmText={t("Delete")}
5961
isOpen={dialogOpen}
6062
titleIcon={<ConfirmDeleteIcon />}
6163
isLoading={false}
@@ -67,8 +69,12 @@ const ComponentsScreen = () => {
6769
}}
6870
confirmationContent={
6971
<Fragment>
70-
Are you sure you want to delete bucket <b>bucket</b>
71-
? <br />A bucket can only be deleted if it's empty.
72+
{interpolate(
73+
t("Are you sure you want to delete bucket {bucket}?"),
74+
{ bucket: <b>bucket</b> },
75+
)}
76+
<br />
77+
{t("A bucket can only be deleted if it's empty.")}
7278
</Fragment>
7379
}
7480
/>

web-app/src/screens/Console/Common/CredentialsPrompt/CredentialItem.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { Button, CopyIcon, InputLabel, ReadBox, Box } from "mds";
1919
import CopyToClipboard from "react-copy-to-clipboard";
2020
import { setModalSnackMessage } from "../../../../systemSlice";
2121
import { useAppDispatch } from "../../../../store";
22+
import { useT } from "i18n";
2223

2324
interface ICredentialsItem {
2425
label?: string;
@@ -27,6 +28,7 @@ interface ICredentialsItem {
2728

2829
const CredentialItem = ({ label = "", value = "" }: ICredentialsItem) => {
2930
const dispatch = useAppDispatch();
31+
const t = useT();
3032

3133
return (
3234
<Box sx={{ marginTop: 12 }}>
@@ -38,7 +40,11 @@ const CredentialItem = ({ label = "", value = "" }: ICredentialsItem) => {
3840
id={"copy-path"}
3941
variant="regular"
4042
onClick={() => {
41-
dispatch(setModalSnackMessage(`${label} copied to clipboard`));
43+
dispatch(
44+
setModalSnackMessage(
45+
t("{label} copied to clipboard").replace("{label}", label),
46+
),
47+
);
4248
}}
4349
style={{
4450
marginRight: "5px",

web-app/src/screens/Console/Common/CredentialsPrompt/CredentialsPrompt.tsx

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import ModalWrapper from "../ModalWrapper/ModalWrapper";
3030
import CredentialItem from "./CredentialItem";
3131
import TooltipWrapper from "../TooltipWrapper/TooltipWrapper";
3232
import { modalStyleUtils } from "../FormComponents/common/styleLibrary";
33+
import { useT } from "i18n";
3334

3435
const WarningBlock = styled.div(({ theme }) => ({
3536
color: get(theme, "signalColors.danger", "#C51B3F"),
@@ -69,6 +70,8 @@ const CredentialsPrompt = ({
6970
closeModal,
7071
entity,
7172
}: ICredentialsPromptProps) => {
73+
const t = useT();
74+
7275
if (!newServiceAccount) {
7376
return null;
7477
}
@@ -146,12 +149,14 @@ const CredentialsPrompt = ({
146149
onClose={() => {
147150
closeModal();
148151
}}
149-
title={`New ${entity} Created`}
152+
title={t("New {entity} Created").replace("{entity}", t(entity))}
150153
titleIcon={<ServiceAccountCredentialsIcon />}
151154
>
152155
<Grid container>
153156
<Grid item xs={12}>
154-
A new {entity} has been created with the following details:
157+
{t(
158+
"A new {entity} has been created with the following details:",
159+
).replace("{entity}", t(entity))}
155160
{!idp && consoleCreds && (
156161
<Fragment>
157162
<Grid
@@ -169,18 +174,18 @@ const CredentialsPrompt = ({
169174
fontSize: ".9rem",
170175
}}
171176
>
172-
Console Credentials
177+
{t("Console Credentials")}
173178
</Box>
174179
{Array.isArray(consoleCreds) &&
175180
consoleCreds.map((credentialsPair, index) => {
176181
return (
177182
<Fragment>
178183
<CredentialItem
179-
label="Access Key"
184+
label={t("Access Key")}
180185
value={credentialsPair.accessKey}
181186
/>
182187
<CredentialItem
183-
label="Secret Key"
188+
label={t("Secret Key")}
184189
value={credentialsPair.secretKey}
185190
/>
186191
</Fragment>
@@ -189,11 +194,11 @@ const CredentialsPrompt = ({
189194
{!Array.isArray(consoleCreds) && (
190195
<Fragment>
191196
<CredentialItem
192-
label="Access Key"
197+
label={t("Access Key")}
193198
value={consoleCreds.accessKey}
194199
/>
195200
<CredentialItem
196-
label="Secret Key"
201+
label={t("Secret Key")}
197202
value={consoleCreds.secretKey}
198203
/>
199204
</Fragment>
@@ -204,25 +209,26 @@ const CredentialsPrompt = ({
204209
{(consoleCreds === null || consoleCreds === undefined) && (
205210
<>
206211
<CredentialItem
207-
label="Access Key"
212+
label={t("Access Key")}
208213
value={newServiceAccount.accessKey || ""}
209214
/>
210215
<CredentialItem
211-
label="Secret Key"
216+
label={t("Secret Key")}
212217
value={newServiceAccount.secretKey || ""}
213218
/>
214219
</>
215220
)}
216221
{idp ? (
217222
<WarningBlock>
218-
Please Login via the configured external identity provider.
223+
{t("Please Login via the configured external identity provider.")}
219224
</WarningBlock>
220225
) : (
221226
<WarningBlock>
222227
<WarnIcon />
223228
<span>
224-
Write these down, as this is the only time the secret will be
225-
displayed.
229+
{t(
230+
"Write these down, as this is the only time the secret will be displayed.",
231+
)}
226232
</span>
227233
</WarningBlock>
228234
)}
@@ -231,13 +237,13 @@ const CredentialsPrompt = ({
231237
{!idp && (
232238
<Fragment>
233239
<TooltipWrapper
234-
tooltip={
235-
"Download credentials in a JSON file formatted for import using mc alias import. This will only include the default login credentials."
236-
}
240+
tooltip={t(
241+
"Download credentials in a JSON file formatted for import using mc alias import. This will only include the default login credentials.",
242+
)}
237243
>
238244
<Button
239245
id={"download-button"}
240-
label={"Download for import"}
246+
label={t("Download for import")}
241247
onClick={downloadImport}
242248
icon={<DownloadIcon />}
243249
variant="callAction"
@@ -246,13 +252,13 @@ const CredentialsPrompt = ({
246252

247253
{Array.isArray(consoleCreds) && consoleCreds.length > 1 && (
248254
<TooltipWrapper
249-
tooltip={
250-
"Download all access credentials to a JSON file. NOTE: This file is not formatted for import using mc alias import. If you plan to import this alias from the file, please use the Download for Import button. "
251-
}
255+
tooltip={t(
256+
"Download all access credentials to a JSON file. NOTE: This file is not formatted for import using mc alias import. If you plan to import this alias from the file, please use the Download for Import button. ",
257+
)}
252258
>
253259
<Button
254260
id={"download-all-button"}
255-
label={"Download all access credentials"}
261+
label={t("Download all access credentials")}
256262
onClick={downloaddAllCredentials}
257263
icon={<DownloadIcon />}
258264
variant="callAction"

0 commit comments

Comments
 (0)