i18n: translate the channel and member sidebars
OpenSSF Scorecard / Scorecard analysis (push) Waiting to run
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

Clears the two remaining rounds owed under the fork's per-update translation
rule.

Covers channels, direct messages, the context menus, the inline voice settings
and the member list's loading and grouping labels.

The member grouping needed care: one function returns either a fixed label or
a role's own name, and role names are user data that must never be translated.
It now marks only the fixed cases and the component resolves them at render,
leaving role names untouched. The function is not a component, so it cannot
call the hook itself.
This commit is contained in:
2026-09-01 17:03:48 -03:00
parent c04b8b47eb
commit 61c68761df
5 changed files with 83 additions and 25 deletions
@@ -1,4 +1,5 @@
import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
import { useT } from '../../i18n';
import { useNavigate, useLocation } from 'react-router-dom';
import type { Channel } from '@backspace/shared';
import { useSpaceStore, getChannelOrigin, getMyUserIdForOrigin } from '../../stores/spaceStore';
@@ -26,6 +27,7 @@ import { useAudioDevices } from '../../hooks/useAudioDevices';
import { DropdownItem } from '../modals/settingsPanels/_shared/SettingsPickerPrimitives';
export function ChannelSidebar() {
const tr = useT();
const spaces = useSpaceStore((s) => s.spaces);
const currentSpaceId = useSpaceStore((s) => s.currentSpaceId);
const loadingSpaceId = useSpaceStore((s) => s.loadingSpaceId);
@@ -318,7 +320,7 @@ export function ChannelSidebar() {
items.push({
key: 'create-channel',
type: 'action',
label: 'Create Channel',
label: tr('sidebar.createChannel'),
icon: (
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M2.5 12.5v-9l5-2v9l-5 2zm6-9v9l5-2v-9l-5 2z" opacity="0.5" />
@@ -330,7 +332,7 @@ export function ChannelSidebar() {
items.push({
key: 'create-category',
type: 'action',
label: 'Create Category',
label: tr('sidebar.createCategory'),
icon: (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z" />
@@ -343,7 +345,7 @@ export function ChannelSidebar() {
items.push({
key: 'invite',
type: 'action',
label: 'Invite People',
label: tr('sidebar.invitePeople'),
icon: (
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M21 3H24V5H21V8H19V5H16V3H19V0H21V3ZM10 12C12.21 12 14 10.21 14 8C14 5.79 12.21 4 10 4C7.79 4 6 5.79 6 8C6 10.21 7.79 12 10 12ZM10 13C6.69 13 1 14.66 1 18V20H19V18C19 14.66 13.31 13 10 13Z" />
@@ -355,7 +357,7 @@ export function ChannelSidebar() {
items.push({
key: 'settings',
type: 'action',
label: 'Space Settings',
label: tr('sidebar.spaceSettings'),
icon: (
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58a.49.49 0 00.12-.61l-1.92-3.32a.49.49 0 00-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54a.484.484 0 00-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96a.49.49 0 00-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.07.62-.07.94s.02.64.07.94l-2.03 1.58a.49.49 0 00-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6A3.6 3.6 0 1112 8.4a3.6 3.6 0 010 7.2z" />
@@ -373,7 +375,7 @@ export function ChannelSidebar() {
{
key: 'leave-group',
type: 'action',
label: 'Leave Group',
label: tr('sidebar.leaveGroup'),
danger: true,
icon: (
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
@@ -453,7 +455,7 @@ export function ChannelSidebar() {
<path d="M3 18a1 1 0 0 0 1 1h18a1 1 0 0 0 1-1v-1c0-2.76-5.37-4-8-4s-8 1.24-8 4v1Z" />
<path d="M3.5 13.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7Z" opacity=".5" />
</svg>
<span className="font-medium text-[16px]">Friends</span>
<span className="font-medium text-[16px]">{tr('sidebar.friends')}</span>
</div>
{/* Placeholder nav items */}
@@ -463,7 +465,7 @@ export function ChannelSidebar() {
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" className="flex-shrink-0">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z" />
</svg>
<span className="font-medium text-[16px]">Coming Soon</span>
<span className="font-medium text-[16px]">{tr('sidebar.comingSoon')}</span>
</div>
<div
className="flex items-center gap-3 px-2 h-[42px] rounded-[6px] mb-[2px] text-txt-tertiary cursor-default opacity-50"
@@ -471,11 +473,11 @@ export function ChannelSidebar() {
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" className="flex-shrink-0">
<path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-7 14c-3.31 0-6-2.69-6-6s2.69-6 6-6 6 2.69 6 6-2.69 6-6 6z" />
</svg>
<span className="font-medium text-[16px]">Coming Soon</span>
<span className="font-medium text-[16px]">{tr('sidebar.comingSoon')}</span>
</div>
<div className="mt-[18px] px-2 mb-1 flex items-center justify-between group">
<span className="text-[12px] font-bold text-txt-tertiary tracking-wider">Direct Messages</span>
<span className="text-[12px] font-bold text-txt-tertiary tracking-wider">{tr('sidebar.directMessages')}</span>
<button
onClick={() => openModal('newDm')}
className="text-txt-tertiary hover:text-txt-primary transition-colors"
@@ -584,7 +586,7 @@ export function ChannelSidebar() {
{/* Channels — dynamic category layout */}
<div ref={scrollContainerRef} className="flex-1 overflow-y-auto pt-3 px-2 no-scrollbar" style={{ paddingBottom: floatingPanelHeight + 24 }} onDrop={containerHandlers.onDrop} onDragOver={containerHandlers.onDragOver} onContextMenu={handleSidebarContextMenu}>
{showChannelSkeleton ? (
<div className="px-2 pt-3" role="status" aria-label="Loading channels">
<div className="px-2 pt-3" role="status" aria-label={tr('sidebar.loadingChannels')}>
{/* Category group 1 */}
<div className="skeleton skeleton-bar h-2 w-[45%] ml-2 mb-3" />
{Array.from({ length: 3 }, (_, i) => (
@@ -706,7 +708,7 @@ export function ChannelSidebar() {
{
key: 'category-settings',
type: 'action',
label: 'Category Settings',
label: tr('sidebar.categorySettings'),
icon: (
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58a.49.49 0 00.12-.61l-1.92-3.32a.49.49 0 00-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54a.484.484 0 00-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96a.49.49 0 00-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.07.62-.07.94s.02.64.07.94l-2.03 1.58a.49.49 0 00-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6A3.6 3.6 0 1115.6 12 3.611 3.611 0 0112 15.6z" />
@@ -717,7 +719,7 @@ export function ChannelSidebar() {
{
key: 'delete-category',
type: 'action',
label: 'Delete Category',
label: tr('sidebar.deleteCategory'),
danger: true,
icon: (
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
@@ -758,7 +760,7 @@ export function ChannelSidebar() {
/>
))}
{catChannels.length === 0 && (
<div className="px-2 py-2 text-[12px] text-txt-tertiary italic opacity-40">No channels</div>
<div className="px-2 py-2 text-[12px] text-txt-tertiary italic opacity-40">{tr('sidebar.noChannels')}</div>
)}
</div>
)}
@@ -777,7 +779,7 @@ export function ChannelSidebar() {
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor" className="flex-shrink-0 opacity-70">
<path d="M8 2a.5.5 0 01.5.5v5h5a.5.5 0 010 1h-5v5a.5.5 0 01-1 0v-5h-5a.5.5 0 010-1h5v-5A.5.5 0 018 2z" />
</svg>
<span className="text-[12px]">Create Channel</span>
<span className="text-[12px]">{tr('sidebar.createChannel')}</span>
</button>
)}
<button
@@ -787,7 +789,7 @@ export function ChannelSidebar() {
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor" className="flex-shrink-0 opacity-70">
<path d="M8 2a.5.5 0 01.5.5v5h5a.5.5 0 010 1h-5v5a.5.5 0 01-1 0v-5h-5a.5.5 0 010-1h5v-5A.5.5 0 018 2z" />
</svg>
<span className="text-[12px]">Create Category</span>
<span className="text-[12px]">{tr('sidebar.createCategory')}</span>
</button>
</div>
)}
@@ -845,6 +847,7 @@ function UserAreaPanel({
onDeafenToggle: () => void;
onSettingsClick: (tab?: string) => void;
}) {
const tr = useT();
const [accountMenuOpen, setAccountMenuOpen] = useState(false);
const [openPanel, setOpenPanel] = useState<'input' | 'output' | null>(null);
const inputDeviceId = useVoiceStore((s) => s.inputDeviceId);
@@ -959,7 +962,7 @@ function UserAreaPanel({
className="w-full px-4 py-3 flex items-center justify-between hover:bg-interactive-hover transition-colors"
>
<div className="min-w-0 flex-1">
<div className="text-[15px] font-semibold text-txt-primary text-left">Input Device</div>
<div className="text-[15px] font-semibold text-txt-primary text-left">{tr('sidebar.inputDevice')}</div>
<div className="text-[13px] text-txt-tertiary truncate text-left">{selectedInputLabel}</div>
</div>
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" className="text-txt-tertiary flex-shrink-0 ml-2">
@@ -1004,7 +1007,7 @@ function UserAreaPanel({
{/* Input Volume */}
<div className="px-4 py-3">
<div className="text-[15px] font-semibold text-txt-primary mb-2">Input Volume</div>
<div className="text-[15px] font-semibold text-txt-primary mb-2">{tr('sidebar.inputVolume')}</div>
<input
type="range"
min={0}
@@ -1039,7 +1042,7 @@ function UserAreaPanel({
onClick={() => onSettingsClick('voice')}
className="w-full px-4 py-3 flex items-center justify-between hover:bg-interactive-hover transition-colors"
>
<span className="text-[15px] font-semibold text-txt-primary">Voice Settings</span>
<span className="text-[15px] font-semibold text-txt-primary">{tr('sidebar.voiceSettings')}</span>
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" className="text-txt-tertiary">
<path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.07.62-.07.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z" />
</svg>
@@ -1057,7 +1060,7 @@ function UserAreaPanel({
className="w-full px-4 py-3 flex items-center justify-between hover:bg-interactive-hover transition-colors"
>
<div className="min-w-0 flex-1">
<div className="text-[15px] font-semibold text-txt-primary text-left">Output Device</div>
<div className="text-[15px] font-semibold text-txt-primary text-left">{tr('sidebar.outputDevice')}</div>
<div className="text-[13px] text-txt-tertiary truncate text-left">{selectedOutputLabel}</div>
</div>
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" className="text-txt-tertiary flex-shrink-0 ml-2">
@@ -1102,7 +1105,7 @@ function UserAreaPanel({
{/* Output Volume */}
<div className="px-4 py-3">
<div className="text-[15px] font-semibold text-txt-primary mb-2">Output Volume</div>
<div className="text-[15px] font-semibold text-txt-primary mb-2">{tr('sidebar.outputVolume')}</div>
<input
type="range"
min={0}
@@ -1125,7 +1128,7 @@ function UserAreaPanel({
onClick={() => onSettingsClick('voice')}
className="w-full px-4 py-3 flex items-center justify-between hover:bg-interactive-hover transition-colors"
>
<span className="text-[15px] font-semibold text-txt-primary">Voice Settings</span>
<span className="text-[15px] font-semibold text-txt-primary">{tr('sidebar.voiceSettings')}</span>
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" className="text-txt-tertiary">
<path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.07.62-.07.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z" />
</svg>