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:
@@ -804,6 +804,221 @@ export const en = {
|
||||
'instance.general.reset': 'Reset',
|
||||
'instance.general.saved': 'Settings saved',
|
||||
'instance.general.saveFailed': 'Failed to save',
|
||||
// System messages — timeline line and sidebar preview
|
||||
'system.unknown': 'Unknown',
|
||||
'system.someone': 'Someone',
|
||||
'system.aMember': 'A member',
|
||||
'system.memberAdded': '{actor} added {target} to the group',
|
||||
'system.memberLeft': '{target} left the group',
|
||||
'system.memberRemoved': '{actor} removed {target} from the group',
|
||||
'system.ownerChanged': '{owner} is now the group owner',
|
||||
'system.nameChanged': '{actor} renamed the group to "{name}"',
|
||||
'system.nameCleared': '{actor} cleared the group name',
|
||||
'system.iconChanged': '{actor} updated the group icon',
|
||||
'system.preview.inviteTo': 'Sent invite to {space}',
|
||||
'system.preview.invite': 'Sent a space invite',
|
||||
'system.preview.memberAdded': '{actor} added {target}',
|
||||
'system.preview.memberRemoved': '{actor} removed {target}',
|
||||
'system.preview.nameChanged': '{actor} renamed the group',
|
||||
'system.preview.generic': 'System message',
|
||||
// Login, register and the invite landing page
|
||||
'auth.username': 'Username',
|
||||
'auth.password': 'Password',
|
||||
'auth.username.required': 'Username is required',
|
||||
'auth.password.required': 'Password is required',
|
||||
'auth.login.title': 'Welcome back!',
|
||||
'auth.login.subtitle': "We're so excited to see you again!",
|
||||
'auth.login.submit': 'Log In',
|
||||
'auth.login.loggingIn': 'Logging in...',
|
||||
'auth.login.failed': 'Login failed',
|
||||
'auth.login.rateLimited': 'Too many login attempts',
|
||||
'auth.retryIn': 'Try again in {seconds}s',
|
||||
'auth.needAccount': 'Need an account?',
|
||||
'auth.register': 'Register',
|
||||
'auth.haveAccount': 'Already have an account?',
|
||||
'auth.register.title': 'Create an account',
|
||||
'auth.register.confirmPassword': 'Confirm Password',
|
||||
'auth.register.username.length': 'Username must be between 3 and 32 characters',
|
||||
'auth.register.username.charset': 'Username can only contain lowercase letters, numbers, and underscores',
|
||||
'auth.register.username.checking': 'Checking availability...',
|
||||
'auth.register.username.available': 'Username is available',
|
||||
'auth.register.username.taken': 'Username is already taken',
|
||||
'auth.register.password.length': 'Password must be at least 6 characters',
|
||||
'auth.register.password.mismatch': 'Passwords do not match',
|
||||
'auth.register.failed': 'Registration failed',
|
||||
'auth.register.rateLimited': 'Too many attempts',
|
||||
'auth.register.continue': 'Continue',
|
||||
'auth.register.invite': 'Invite code or link',
|
||||
'auth.register.invite.checking': 'Checking...',
|
||||
'auth.register.invite.validating': 'Validating invite...',
|
||||
'auth.register.invite.expired': 'This invite link has expired. Ask the admin for a new one.',
|
||||
'auth.register.invite.exhausted': 'This invite has reached its usage limit. Ask the admin to extend it.',
|
||||
'auth.register.invite.invalid': 'Invalid invite code.',
|
||||
'auth.register.invite.invalidLink': 'Invalid invite link — please request a new one',
|
||||
'auth.register.profile.title': 'Make it yours',
|
||||
'auth.register.profile.subtitle': 'Personalize your profile, or skip for now',
|
||||
'auth.register.displayName': 'Display Name',
|
||||
'auth.register.displayName.placeholder': 'Display name',
|
||||
'auth.register.avatarColor': 'Avatar Color',
|
||||
'auth.register.submit': 'Get Started',
|
||||
'auth.register.creating': 'Creating account...',
|
||||
'auth.register.back': 'Back',
|
||||
'auth.register.skip': 'Skip for now',
|
||||
'auth.register.cropAvatar': 'Crop Avatar',
|
||||
'join.page.loading': 'Loading invite...',
|
||||
'join.page.invalid.title': 'Invalid Invite',
|
||||
'join.page.invalid.body': 'This invite link is invalid or has expired.',
|
||||
'join.page.invitedTo': "You've been invited to join",
|
||||
'join.page.noCode': 'No invite code provided',
|
||||
'join.page.invalidCode': 'Invalid invite code',
|
||||
'join.page.loadFailed': 'Failed to load invite',
|
||||
'join.page.joinFailed': 'Failed to join space',
|
||||
'join.page.connectFailed': 'Failed to connect',
|
||||
'join.page.loginFailed': 'Failed to log in',
|
||||
'join.page.instance.placeholder': 'e.g. my-instance.com',
|
||||
'join.page.alreadyMember': "You're already in {space}!",
|
||||
'join.page.goTo': 'Go to {space}',
|
||||
'join.page.redirecting': 'Redirecting you now...',
|
||||
// Voice — controls, connection info and the mobile join sheet
|
||||
'voice.title': 'Voice & Video',
|
||||
'voice.dmCall': 'DM Call',
|
||||
'voice.channel': 'Voice Channel',
|
||||
'voice.connected': 'Voice Connected',
|
||||
'voice.connecting': 'Connecting...',
|
||||
'voice.failed': 'Connection Failed',
|
||||
'voice.goToCall': 'Go to call',
|
||||
'voice.disconnect': 'Disconnect',
|
||||
'voice.camera.on': 'Turn On Camera',
|
||||
'voice.camera.off': 'Turn Off Camera',
|
||||
'voice.share.start': 'Share Screen',
|
||||
'voice.share.stop': 'Stop Sharing',
|
||||
'voice.soundboard': 'Soundboard',
|
||||
'voice.videoQuality': 'Video Quality',
|
||||
'voice.noise.enable': 'Enable AI Noise Suppression',
|
||||
'voice.noise.disable': 'Disable AI Noise Suppression',
|
||||
'voice.info.title': 'Connection Info',
|
||||
'voice.info.notConnected': 'Not connected',
|
||||
'voice.info.gathering': 'Gathering stats...',
|
||||
'voice.info.network': 'Network',
|
||||
'voice.info.ping': 'Ping',
|
||||
'voice.info.packetLoss': 'Packet Loss',
|
||||
'voice.info.jitter': 'Jitter',
|
||||
'voice.info.server': 'Server',
|
||||
'voice.info.protocol': 'Protocol',
|
||||
'voice.info.audio': 'Audio',
|
||||
'voice.info.video': 'Video',
|
||||
'voice.info.microphone': 'Microphone',
|
||||
'voice.info.camera': 'Camera',
|
||||
'voice.info.screen': 'Screen',
|
||||
'voice.info.screenAudio': 'Screen Audio',
|
||||
'voice.info.unknown': 'Unknown',
|
||||
'voice.info.remote': 'Remote',
|
||||
'voice.join.empty': 'No one is in this channel yet.',
|
||||
'voice.join.beFirst': 'Be the first to join!',
|
||||
'voice.join.mute': 'Mute microphone',
|
||||
'voice.join.unmute': 'Unmute microphone',
|
||||
'voice.join.switch': 'Switch Channel',
|
||||
'voice.join.join': 'Join Voice',
|
||||
'voice.join.close': 'Close',
|
||||
'voice.join.selectedCamera': 'Selected camera',
|
||||
'voice.join.enablePreview': 'Enable camera preview',
|
||||
'voice.join.checkingCamera': 'Checking camera…',
|
||||
'voice.join.tapToPreview': 'Tap to preview camera',
|
||||
'voice.join.permissionDenied': 'Camera permission denied.',
|
||||
'voice.settings.volume': 'Volume',
|
||||
'voice.settings.sfxVolume': 'Sound Effects Volume',
|
||||
'voice.settings.messageSound': 'Play sound for every message',
|
||||
'voice.settings.messageSound.description': 'Off (default): only DMs and messages that mention you. On: every channel.',
|
||||
'voice.settings.processing': 'Voice Processing',
|
||||
'voice.settings.noise': 'AI Noise Suppression',
|
||||
'voice.settings.noise.description': 'ML-based noise removal (RNNoise) — filters keyboard, fans, and background noise',
|
||||
'voice.settings.echo': 'Echo Cancellation',
|
||||
'voice.settings.echo.description': 'Cancels echo from your speakers feeding back into the mic. Always on for voice channels and calls.',
|
||||
'voice.settings.agc': 'Auto Gain Control',
|
||||
'voice.settings.agc.description': 'Auto-adjusts mic volume — can cause voice ducking during streams',
|
||||
// Settings navigation (space, user and instance)
|
||||
'nav.general': 'General',
|
||||
'nav.management': 'Management',
|
||||
'nav.overview': 'Overview',
|
||||
'nav.discovery': 'Discovery',
|
||||
'nav.members': 'Members',
|
||||
'nav.roles': 'Roles',
|
||||
'nav.bans': 'Bans',
|
||||
'nav.userSettings': 'User Settings',
|
||||
'nav.appSettings': 'App Settings',
|
||||
'nav.administration': 'Administration',
|
||||
'nav.instance': 'Instance',
|
||||
'nav.registration': 'Registration',
|
||||
'nav.federation': 'Federation',
|
||||
'nav.streaming': 'Streaming',
|
||||
'nav.storage': 'Storage',
|
||||
'nav.users': 'Users',
|
||||
'nav.back': 'Back to space settings menu',
|
||||
'space.discovery.title': 'Discovery',
|
||||
'space.discovery.visibility': 'Visibility',
|
||||
'space.discovery.visibility.description': 'Control who can discover and join this space.',
|
||||
'space.discovery.description': 'Description',
|
||||
'space.discovery.description.hint': 'A short summary shown on the Explore page.',
|
||||
'space.discovery.description.placeholder': 'A short description for the Explore page...',
|
||||
'space.requests.loading': 'Loading...',
|
||||
'space.requests.empty': 'No pending join requests',
|
||||
'space.requests.accept': 'Accept',
|
||||
'space.requests.decline': 'Decline',
|
||||
'space.requests.failed': 'Action failed',
|
||||
|
||||
// Mobile screens
|
||||
'mobile.messages': 'Messages',
|
||||
'mobile.noConversations': 'No conversations yet.',
|
||||
'mobile.loadingChannels': 'Loading channels',
|
||||
'mobile.noChannels': 'No channels yet.',
|
||||
'mobile.groupInfo': 'Group Info',
|
||||
'mobile.info': 'Info',
|
||||
'mobile.cancelEdit': 'Cancel edit',
|
||||
'mobile.removeGroupIcon': 'Remove group icon',
|
||||
'mobile.federatedGroup': 'Federated group',
|
||||
'mobile.invitePeople': 'Invite People',
|
||||
'mobile.inviteCopied': 'Invite link copied to clipboard',
|
||||
'mobile.inviteFailed': 'Failed to generate invite',
|
||||
'mobile.createFolder': 'Create Folder',
|
||||
'mobile.folderCreated': 'Folder created',
|
||||
'mobile.moveToFolder': 'Move to Folder',
|
||||
'mobile.movedToFolder': 'Moved to folder',
|
||||
'mobile.removeFromFolder': 'Remove from Folder',
|
||||
'mobile.removedFromFolder': 'Removed from folder',
|
||||
'mobile.folderUngrouped': 'Folder ungrouped',
|
||||
'mobile.unnamedFolder': 'Unnamed Folder',
|
||||
'mobile.leaveSpace': 'Leave Space',
|
||||
'mobile.leave': 'Leave',
|
||||
'mobile.leftSpace': 'Left space',
|
||||
'mobile.leaveSpaceFailed': 'Failed to leave space',
|
||||
'mobile.muteUser': 'Mute User',
|
||||
'mobile.channelSettings': 'Channel Settings',
|
||||
'mobile.deleteChannel': 'Delete Channel',
|
||||
'mobile.channelDeleted': 'Channel deleted',
|
||||
'mobile.categorySettings': 'Category Settings',
|
||||
'mobile.deleteCategory': 'Delete Category',
|
||||
'mobile.categoryDeleted': 'Category deleted',
|
||||
'mobile.delete': 'Delete',
|
||||
'mobile.createSpace': 'Create Space',
|
||||
'mobile.joinSpace': 'Join Space',
|
||||
'mobile.exploreSpaces': 'Explore Spaces',
|
||||
'mobile.leaveSpace.confirm': "Are you sure you want to leave this space? You'll need a new invite to rejoin.",
|
||||
'mobile.deleteChannel.confirm': 'Are you sure? This will permanently delete the channel and all its messages.',
|
||||
'mobile.deleteCategory.confirm': 'Are you sure? Channels in this category will be moved to uncategorized.',
|
||||
'mobile.deleteChannelFailed': 'Failed to delete channel',
|
||||
'mobile.voice.title': 'Voice Call',
|
||||
'mobile.voice.collapse': 'Collapse call',
|
||||
'mobile.voice.members': 'View members',
|
||||
'mobile.voice.unmute': 'Unmute',
|
||||
'mobile.voice.deafen': 'Deafen',
|
||||
'mobile.voice.undeafen': 'Undeafen',
|
||||
'mobile.voice.cameraOn': 'Turn camera on',
|
||||
'mobile.voice.cameraOff': 'Turn camera off',
|
||||
'mobile.voice.switchCamera': 'Switch camera',
|
||||
'mobile.voice.selectCamera': 'Select camera',
|
||||
'mobile.voice.shareScreen': 'Share screen',
|
||||
'mobile.voice.stopSharing': 'Stop sharing screen',
|
||||
'mobile.voice.disconnect': 'Disconnect from call',
|
||||
} as const;
|
||||
|
||||
export type TranslationKey = keyof typeof en;
|
||||
|
||||
Reference in New Issue
Block a user