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