feat(i18n): translate the five systems users actually see
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
Ordered by what the group hits, not by string count.
- Chat system messages ("X added Y to the group") and their sidebar
preview twin. These are content, not chrome, so they sat in English in
the middle of conversations.
- Login, register and the invite landing page — the first screen anyone
invited to the instance sees.
- Voice — controls, connection info, the mobile join sheet, settings tab.
- Mobile screens.
- Settings navigation across space, user and instance, plus Discovery and
join requests.
Three call sites build strings outside a component and so read the locale
directly rather than through the hook: the sidebar preview formatter, the
connection-info track labels, and MobileShell's screen map — which holds
plain functions that are *called*, not rendered, where a hook would run
under whichever component invoked the map. Each says so at the definition;
all three re-render often enough that a language switch shows through
promptly.
Adds a test for a mistake made three times in this series: a file with an
exported component plus a private sub-component, where only the exported one
got the hook. tsc catches it, but only after a full build and without naming
the component. The test names it, and also flags the inverse — a hook placed
below an early return, which tsc accepts and React breaks at runtime (this
project shipped that once, as error #310). Verified by mutation: removing a
hook makes it fail and print the component.
Dictionary is at 856 keys, en and pt-BR at exact parity.
Verified: web build (tsc + vite), 492 web tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import type { DmChannel, MessageWithUser, SpaceInviteSystemPayload, User } from '@backspace/shared';
|
||||
import { SpaceInviteCard } from './SpaceInviteCard';
|
||||
import { useT } from '../../i18n';
|
||||
|
||||
interface SystemMessageProps {
|
||||
message: MessageWithUser;
|
||||
@@ -13,15 +14,20 @@ interface SystemMessageProps {
|
||||
dm?: Pick<DmChannel, 'members'> | null;
|
||||
}
|
||||
|
||||
function resolveActorName(message: MessageWithUser, dm?: Pick<DmChannel, 'members'> | null): string {
|
||||
function resolveActorName(
|
||||
message: MessageWithUser,
|
||||
dm: Pick<DmChannel, 'members'> | null | undefined,
|
||||
unknown: string,
|
||||
someone: string,
|
||||
): string {
|
||||
if (dm) {
|
||||
const fromRoster = dm.members.find(m => m.id === message.userId) as User | undefined;
|
||||
if (fromRoster) {
|
||||
return fromRoster.displayName ?? fromRoster.username ?? 'Unknown';
|
||||
return fromRoster.displayName ?? fromRoster.username ?? unknown;
|
||||
}
|
||||
return 'Unknown';
|
||||
return unknown;
|
||||
}
|
||||
return message.user?.displayName ?? message.user?.username ?? 'Someone';
|
||||
return message.user?.displayName ?? message.user?.username ?? someone;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -32,10 +38,11 @@ function resolveActorName(message: MessageWithUser, dm?: Pick<DmChannel, 'member
|
||||
* Exported so unit tests can render it directly without mounting MessageList.
|
||||
*/
|
||||
export function SystemMessage({ message, dm }: SystemMessageProps) {
|
||||
const t = useT();
|
||||
let data: Record<string, unknown> = {};
|
||||
try { data = JSON.parse(message.content ?? '{}'); } catch { /* fall through to default branch */ }
|
||||
|
||||
const actorName = resolveActorName(message, dm);
|
||||
const actorName = resolveActorName(message, dm, t('system.unknown'), t('system.someone'));
|
||||
|
||||
if (data.event === 'space_invite') {
|
||||
return (
|
||||
@@ -51,32 +58,32 @@ export function SystemMessage({ message, dm }: SystemMessageProps) {
|
||||
switch (data.event) {
|
||||
case 'member_added':
|
||||
icon = '→'; // →
|
||||
text = `${actorName} added ${data.targetDisplayName} to the group`;
|
||||
text = t('system.memberAdded', { actor: actorName, target: String(data.targetDisplayName) });
|
||||
break;
|
||||
case 'member_removed':
|
||||
if (data.reason === 'leave') {
|
||||
icon = '←'; // ←
|
||||
text = `${data.targetDisplayName} left the group`;
|
||||
text = t('system.memberLeft', { target: String(data.targetDisplayName) });
|
||||
} else {
|
||||
icon = '←';
|
||||
text = `${actorName} removed ${data.targetDisplayName} from the group`;
|
||||
text = t('system.memberRemoved', { actor: actorName, target: String(data.targetDisplayName) });
|
||||
}
|
||||
break;
|
||||
case 'owner_changed':
|
||||
icon = '♛'; // ♛
|
||||
text = `${data.newOwnerDisplayName} is now the group owner`;
|
||||
text = t('system.ownerChanged', { owner: String(data.newOwnerDisplayName) });
|
||||
break;
|
||||
case 'name_changed':
|
||||
icon = '✎'; // ✎
|
||||
// newName === null is a meaningful "cleared" state — distinct from a
|
||||
// missing field — so we test for a non-empty string explicitly.
|
||||
text = typeof data.newName === 'string' && data.newName.length > 0
|
||||
? `${actorName} renamed the group to "${data.newName}"`
|
||||
: `${actorName} cleared the group name`;
|
||||
? t('system.nameChanged', { actor: actorName, name: data.newName })
|
||||
: t('system.nameCleared', { actor: actorName });
|
||||
break;
|
||||
case 'icon_changed':
|
||||
icon = '\u{1F5BC}'; // 🖼
|
||||
text = `${actorName} updated the group icon`;
|
||||
text = t('system.iconChanged', { actor: actorName });
|
||||
break;
|
||||
default:
|
||||
text = message.content ?? '';
|
||||
|
||||
Reference in New Issue
Block a user