i18n: localize hardcoded UI strings across 10 components

Wraps the hardcoded strings flagged in LOTUS_BUGS.md (Localization rows)
in t() via react-i18next, and adds the keys to public/locales/en.json
under the existing Organisms.* namespace. de.json intentionally left to
fall back to en for now (fallbackLng: 'en') rather than fabricate
translations.

Files: CreateRoomTypeSelector, ImageViewer, MsgTypeRenderers (MLocation),
Reply (ThreadIndicator), ImageContent, DeviceVerification (5 subcomponents),
UrlPreviewCard (DiscordCard), InviteUserPrompt, UploadBoard, PasswordStage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-26 17:43:36 -04:00
parent 7b94eeaa60
commit 1a5896ef84
11 changed files with 104 additions and 36 deletions
+40
View File
@@ -2,6 +2,46 @@
"Organisms": {
"RoomCommon": {
"changed_room_name": " changed room name"
},
"CreateRoom": {
"chat_room": "Chat Room",
"chat_room_desc": "Messages, photos, and videos.",
"voice_room": "Voice Room",
"voice_room_desc": "Live audio and video conversations."
},
"ImageViewer": {
"download": "Download"
},
"Message": {
"open_location": "Open Location",
"thread": "Thread"
},
"ImageContent": {
"view": "View",
"spoiler": "Spoiler",
"retry": "Retry"
},
"DeviceVerification": {
"close": "Close",
"accept": "Accept",
"they_match": "They Match",
"okay": "Okay"
},
"UrlPreview": {
"join_server": "Join Server"
},
"InviteUser": {
"invite": "Invite"
},
"UploadBoard": {
"files": "Files",
"send": "Send",
"upload_failed": "Upload Failed"
},
"PasswordStage": {
"account_password": "Account Password",
"password": "Password",
"invalid_password": "Invalid Password!"
}
}
}
+11 -5
View File
@@ -5,6 +5,7 @@ import {
Verifier,
} from 'matrix-js-sdk/lib/crypto-api';
import React, { CSSProperties, useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { VerificationMethod } from 'matrix-js-sdk/lib/types';
import {
Box,
@@ -51,11 +52,12 @@ function WaitingMessage({ message }: WaitingMessageProps) {
type VerificationUnexpectedProps = { message: string; onClose: () => void };
function VerificationUnexpected({ message, onClose }: VerificationUnexpectedProps) {
const { t } = useTranslation();
return (
<Box direction="Column" gap="400">
<Text>{message}</Text>
<Button variant="Secondary" fill="Soft" onClick={onClose}>
<Text size="B400">Close</Text>
<Text size="B400">{t('Organisms.DeviceVerification.close')}</Text>
</Button>
</Box>
);
@@ -74,6 +76,7 @@ type VerificationAcceptProps = {
onAccept: () => Promise<void>;
};
function VerificationAccept({ onAccept }: VerificationAcceptProps) {
const { t } = useTranslation();
const [acceptState, accept] = useAsyncCallback(onAccept);
const accepting = acceptState.status === AsyncStatus.Loading;
@@ -87,7 +90,7 @@ function VerificationAccept({ onAccept }: VerificationAcceptProps) {
before={accepting && <Spinner size="100" variant="Primary" fill="Solid" />}
disabled={accepting}
>
<Text size="B400">Accept</Text>
<Text size="B400">{t('Organisms.DeviceVerification.accept')}</Text>
</Button>
</Box>
);
@@ -118,6 +121,7 @@ function AutoVerificationStart({ onStart }: VerificationStartProps) {
}
function CompareEmoji({ sasData }: { sasData: ShowSasCallbacks }) {
const { t } = useTranslation();
const [confirmState, confirm] = useAsyncCallback(useCallback(() => sasData.confirm(), [sasData]));
const confirming =
@@ -157,7 +161,7 @@ function CompareEmoji({ sasData }: { sasData: ShowSasCallbacks }) {
disabled={confirming}
before={confirming && <Spinner size="100" variant="Primary" />}
>
<Text size="B400">They Match</Text>
<Text size="B400">{t('Organisms.DeviceVerification.they_match')}</Text>
</Button>
<Button
variant="Primary"
@@ -201,13 +205,14 @@ type VerificationDoneProps = {
onExit: () => void;
};
function VerificationDone({ onExit }: VerificationDoneProps) {
const { t } = useTranslation();
return (
<Box direction="Column" gap="400">
<div>
<Text>Your device is verified.</Text>
</div>
<Button variant="Primary" fill="Solid" onClick={onExit}>
<Text size="B400">Okay</Text>
<Text size="B400">{t('Organisms.DeviceVerification.okay')}</Text>
</Button>
</Box>
);
@@ -217,11 +222,12 @@ type VerificationCanceledProps = {
onClose: () => void;
};
function VerificationCanceled({ onClose }: VerificationCanceledProps) {
const { t } = useTranslation();
return (
<Box direction="Column" gap="400">
<Text>Verification has been canceled.</Text>
<Button variant="Secondary" fill="Soft" onClick={onClose}>
<Text size="B400">Close</Text>
<Text size="B400">{t('Organisms.DeviceVerification.close')}</Text>
</Button>
</Box>
);
@@ -1,4 +1,5 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Box, Text, Icon, Icons, config, IconSrc } from 'folds';
import { SequenceCard } from '../sequence-card';
import { SettingTile } from '../setting-tile';
@@ -17,6 +18,7 @@ export function CreateRoomTypeSelector({
disabled,
getIcon,
}: CreateRoomTypeSelectorProps) {
const { t } = useTranslation();
return (
<Box shrink="No" direction="Column" gap="100">
<SequenceCard
@@ -36,10 +38,10 @@ export function CreateRoomTypeSelector({
>
<Box gap="200" alignItems="Baseline">
<Text size="H6" style={{ flexShrink: 0 }}>
Chat Room
{t('Organisms.CreateRoom.chat_room')}
</Text>
<Text size="T300" priority="300" truncate>
- Messages, photos, and videos.
- {t('Organisms.CreateRoom.chat_room_desc')}
</Text>
</Box>
</SettingTile>
@@ -61,10 +63,10 @@ export function CreateRoomTypeSelector({
>
<Box gap="200" alignItems="Baseline">
<Text size="H6" style={{ flexShrink: 0 }}>
Voice Room
{t('Organisms.CreateRoom.voice_room')}
</Text>
<Text size="T300" priority="300" truncate>
- Live audio and video conversations.
- {t('Organisms.CreateRoom.voice_room_desc')}
</Text>
<BetaNoticeBadge />
</Box>
@@ -1,4 +1,5 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import FileSaver from 'file-saver';
import classNames from 'classnames';
import { Box, Chip, Header, Icon, IconButton, Icons, Text, as } from 'folds';
@@ -15,6 +16,7 @@ export type ImageViewerProps = {
export const ImageViewer = as<'div', ImageViewerProps>(
({ className, alt, src, requestClose, ...props }, ref) => {
const { t } = useTranslation();
const { zoom, zoomIn, zoomOut, setZoom } = useZoom(0.2);
const { pan, cursor, onMouseDown } = usePan(zoom !== 1);
@@ -69,7 +71,7 @@ export const ImageViewer = as<'div', ImageViewerProps>(
radii="300"
before={<Icon size="50" src={Icons.Download} />}
>
<Text size="B300">Download</Text>
<Text size="B300">{t('Organisms.ImageViewer.download')}</Text>
</Chip>
</Box>
</Header>
@@ -7,6 +7,7 @@ import React, {
useRef,
useState,
} from 'react';
import { useTranslation } from 'react-i18next';
import {
Overlay,
OverlayBackdrop,
@@ -66,6 +67,7 @@ type InviteUserProps = {
requestClose: () => void;
};
export function InviteUserPrompt({ room, requestClose }: InviteUserProps) {
const { t } = useTranslation();
const mx = useMatrixClient();
const modalStyle = useModalStyle(560);
const alive = useAlive();
@@ -194,7 +196,7 @@ export function InviteUserPrompt({ room, requestClose }: InviteUserProps) {
>
<Box grow="Yes">
<Text size="H4" truncate>
Invite
{t('Organisms.InviteUser.invite')}
</Text>
</Box>
<Box shrink="No" gap="100" alignItems="Center">
@@ -351,7 +353,7 @@ export function InviteUserPrompt({ room, requestClose }: InviteUserProps) {
disabled={!validUserId || inviting}
before={inviting && <Spinner size="200" variant="Primary" fill="Solid" />}
>
<Text size="B400">Invite</Text>
<Text size="B400">{t('Organisms.InviteUser.invite')}</Text>
</Button>
</Box>
</Box>
@@ -1,4 +1,5 @@
import React, { CSSProperties, ReactNode, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Box, Button, config, Icon, Icons, Text, color, toRem } from 'folds';
import { IContent } from 'matrix-js-sdk';
import { JUMBO_EMOJI_REG, URL_REG } from '../../utils/regex';
@@ -507,6 +508,7 @@ type MLocationProps = {
content: IContent;
};
export function MLocation({ content }: MLocationProps) {
const { t } = useTranslation();
const geoUri = content.geo_uri;
if (typeof geoUri !== 'string') return <BrokenContent />;
const location = parseGeoUri(geoUri);
@@ -549,7 +551,7 @@ export function MLocation({ content }: MLocationProps) {
radii="300"
before={<Icon src={Icons.External} size="50" />}
>
<Text size="B300">Open Location</Text>
<Text size="B300">{t('Organisms.Message.open_location')}</Text>
</Button>
</Box>
);
+17 -13
View File
@@ -1,6 +1,7 @@
import { Box, Icon, Icons, Text, as, color, toRem } from 'folds';
import { EventTimelineSet, Room } from 'matrix-js-sdk';
import React, { MouseEventHandler, ReactNode, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import classNames from 'classnames';
import { getMemberDisplayName, trimReplyFromBody } from '../../utils/room';
import { getMxIdLocalPart } from '../../utils/matrix';
@@ -37,19 +38,22 @@ export const ReplyLayout = as<'div', ReplyLayoutProps>(
),
);
export const ThreadIndicator = as<'div'>(({ ...props }, ref) => (
<Box
shrink="No"
className={css.ThreadIndicator}
alignItems="Center"
gap="100"
{...props}
ref={ref}
>
<Icon size="50" src={Icons.Thread} />
<Text size="L400">Thread</Text>
</Box>
));
export const ThreadIndicator = as<'div'>(({ ...props }, ref) => {
const { t } = useTranslation();
return (
<Box
shrink="No"
className={css.ThreadIndicator}
alignItems="Center"
gap="100"
{...props}
ref={ref}
>
<Icon size="50" src={Icons.Thread} />
<Text size="L400">{t('Organisms.Message.thread')}</Text>
</Box>
);
});
type ReplyProps = {
room: Room;
@@ -1,4 +1,5 @@
import React, { ReactNode, useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
Badge,
Box,
@@ -81,6 +82,7 @@ export const ImageContent = as<'div', ImageContentProps>(
const useAuthentication = useMediaAuthentication();
const blurHash = validBlurHash(info?.[MATRIX_BLUR_HASH_PROPERTY_NAME]);
const { t } = useTranslation();
const [load, setLoad] = useState(false);
const [error, setError] = useState(false);
const [viewer, setViewer] = useState(false);
@@ -168,7 +170,7 @@ export const ImageContent = as<'div', ImageContentProps>(
onClick={loadSrc}
before={<Icon size="Inherit" src={Icons.Photo} filled />}
>
<Text size="B300">View</Text>
<Text size="B300">{t('Organisms.ImageContent.view')}</Text>
</Button>
</Box>
)}
@@ -212,7 +214,7 @@ export const ImageContent = as<'div', ImageContentProps>(
}
}}
>
<Text size="B300">Spoiler</Text>
<Text size="B300">{t('Organisms.ImageContent.spoiler')}</Text>
</Chip>
)}
</TooltipProvider>
@@ -247,7 +249,7 @@ export const ImageContent = as<'div', ImageContentProps>(
onClick={handleRetry}
before={<Icon size="Inherit" src={Icons.Warning} filled />}
>
<Text size="B300">Retry</Text>
<Text size="B300">{t('Organisms.ImageContent.retry')}</Text>
</Button>
)}
</TooltipProvider>
@@ -1,5 +1,6 @@
import { Box, Button, color, config, Dialog, Header, Icon, IconButton, Icons, Text } from 'folds';
import React, { FormEventHandler } from 'react';
import { useTranslation } from 'react-i18next';
import { AuthType } from 'matrix-js-sdk';
import { StageComponentProps } from './types';
import { ErrorCode } from '../../cs-errorcode';
@@ -13,6 +14,7 @@ export function PasswordStage({
}: StageComponentProps & {
userId: string;
}) {
const { t } = useTranslation();
const { errorCode, error, session } = stageData;
const handleFormSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
@@ -44,7 +46,7 @@ export function PasswordStage({
>
<Box grow="Yes">
<Text as="h2" size="H4">
Account Password
{t('Organisms.PasswordStage.account_password')}
</Text>
</Box>
<IconButton size="300" onClick={onCancel} radii="300" aria-label="Cancel">
@@ -64,7 +66,7 @@ export function PasswordStage({
password.
</Text>
<Box direction="Column" gap="100">
<Text size="L400">Password</Text>
<Text size="L400">{t('Organisms.PasswordStage.password')}</Text>
<PasswordInput size="400" name="passwordInput" outlined autoFocus required />
{errorCode && (
<Box alignItems="Center" gap="100" style={{ color: color.Critical.Main }}>
@@ -72,7 +74,7 @@ export function PasswordStage({
<Text size="T200">
<b>
{errorCode === ErrorCode.M_FORBIDDEN
? 'Invalid Password!'
? t('Organisms.PasswordStage.invalid_password')
: `${errorCode}: ${error}`}
</b>
</Text>
@@ -1,4 +1,5 @@
import React, { MutableRefObject, ReactNode, useImperativeHandle, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { Badge, Box, Chip, Header, Icon, Icons, Spinner, Text, as, percent } from 'folds';
import classNames from 'classnames';
import { useAtomValue } from 'jotai';
@@ -43,6 +44,7 @@ export function UploadBoardHeader({
onSend,
imperativeHandlerRef,
}: UploadBoardHeaderProps) {
const { t } = useTranslation();
const sendingRef = useRef(false);
const uploads = useAtomValue(uploadFamilyObserverAtom);
@@ -88,7 +90,7 @@ export function UploadBoardHeader({
gap="100"
>
<Icon src={open ? Icons.ChevronTop : Icons.ChevronRight} size="50" />
<Text size="H6">Files</Text>
<Text size="H6">{t('Organisms.UploadBoard.files')}</Text>
</Box>
<Box className={css.UploadBoardHeaderContent} alignItems="Center" gap="100">
{isSuccess && (
@@ -100,12 +102,12 @@ export function UploadBoardHeader({
outlined
after={<Icon src={Icons.Send} size="50" filled />}
>
<Text size="B300">Send</Text>
<Text size="B300">{t('Organisms.UploadBoard.send')}</Text>
</Chip>
)}
{isError && !open && (
<Badge variant="Critical" fill="Solid" radii="300">
<Text size="L400">Upload Failed</Text>
<Text size="L400">{t('Organisms.UploadBoard.upload_failed')}</Text>
</Badge>
)}
{!isSuccess && !isError && !open && (
@@ -1,4 +1,5 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { IPreviewUrlResponse } from 'matrix-js-sdk';
import { Box, Icon, IconButton, Icons, Scroll, Spinner, Text, as, color, config } from 'folds';
import { ImageOverlay } from '../ImageOverlay';
@@ -1343,6 +1344,7 @@ function WikipediaCard({ url, prev }: { url: string; prev: IPreviewUrlResponse }
}
function DiscordCard({ url, prev }: { url: string; prev: IPreviewUrlResponse }) {
const { t } = useTranslation();
const title = prev['og:title'] ?? '';
const description = prev['og:description'] ?? '';
const iconUrl = (prev['og:image'] as string | undefined) ?? '';
@@ -1383,7 +1385,9 @@ function DiscordCard({ url, prev }: { url: string; prev: IPreviewUrlResponse })
priority="300"
>
<SiteBadge label="Discord" colorClass={previewCss.BadgeDiscord} />
<span style={{ marginLeft: '6px', opacity: 0.7, fontSize: '0.85em' }}>Join Server</span>
<span style={{ marginLeft: '6px', opacity: 0.7, fontSize: '0.85em' }}>
{t('Organisms.UrlPreview.join_server')}
</span>
</Text>
{title && (
<Text truncate priority="400">