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
+2
View File
@@ -73,6 +73,8 @@ fixas por `t('chave')`. O que faltar cai para o inglês sozinho.
- Configurações → Conexões (nasceu bilíngue) - Configurações → Conexões (nasceu bilíngue)
- Seletor de GIF - Seletor de GIF
- Chat: composer e mensagens (placeholder, anexos, responder, reações) - Chat: composer e mensagens (placeholder, anexos, responder, reações)
- Barra lateral: servidores, canais e menus de contexto
- Lista de membros (grupos por cargo, carregamento)
- Registro de auditoria e Estatísticas (nasceram bilíngues) - Registro de auditoria e Estatísticas (nasceram bilíngues)
### Fila sugerida de tradução ### Fila sugerida de tradução
@@ -1,4 +1,5 @@
import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react'; import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
import { useT } from '../../i18n';
import { useNavigate, useLocation } from 'react-router-dom'; import { useNavigate, useLocation } from 'react-router-dom';
import type { Channel } from '@backspace/shared'; import type { Channel } from '@backspace/shared';
import { useSpaceStore, getChannelOrigin, getMyUserIdForOrigin } from '../../stores/spaceStore'; import { useSpaceStore, getChannelOrigin, getMyUserIdForOrigin } from '../../stores/spaceStore';
@@ -26,6 +27,7 @@ import { useAudioDevices } from '../../hooks/useAudioDevices';
import { DropdownItem } from '../modals/settingsPanels/_shared/SettingsPickerPrimitives'; import { DropdownItem } from '../modals/settingsPanels/_shared/SettingsPickerPrimitives';
export function ChannelSidebar() { export function ChannelSidebar() {
const tr = useT();
const spaces = useSpaceStore((s) => s.spaces); const spaces = useSpaceStore((s) => s.spaces);
const currentSpaceId = useSpaceStore((s) => s.currentSpaceId); const currentSpaceId = useSpaceStore((s) => s.currentSpaceId);
const loadingSpaceId = useSpaceStore((s) => s.loadingSpaceId); const loadingSpaceId = useSpaceStore((s) => s.loadingSpaceId);
@@ -318,7 +320,7 @@ export function ChannelSidebar() {
items.push({ items.push({
key: 'create-channel', key: 'create-channel',
type: 'action', type: 'action',
label: 'Create Channel', label: tr('sidebar.createChannel'),
icon: ( icon: (
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"> <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" /> <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({ items.push({
key: 'create-category', key: 'create-category',
type: 'action', type: 'action',
label: 'Create Category', label: tr('sidebar.createCategory'),
icon: ( icon: (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> <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" /> <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({ items.push({
key: 'invite', key: 'invite',
type: 'action', type: 'action',
label: 'Invite People', label: tr('sidebar.invitePeople'),
icon: ( icon: (
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"> <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" /> <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({ items.push({
key: 'settings', key: 'settings',
type: 'action', type: 'action',
label: 'Space Settings', label: tr('sidebar.spaceSettings'),
icon: ( icon: (
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"> <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" /> <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', key: 'leave-group',
type: 'action', type: 'action',
label: 'Leave Group', label: tr('sidebar.leaveGroup'),
danger: true, danger: true,
icon: ( icon: (
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"> <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 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" /> <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> </svg>
<span className="font-medium text-[16px]">Friends</span> <span className="font-medium text-[16px]">{tr('sidebar.friends')}</span>
</div> </div>
{/* Placeholder nav items */} {/* 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"> <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" /> <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> </svg>
<span className="font-medium text-[16px]">Coming Soon</span> <span className="font-medium text-[16px]">{tr('sidebar.comingSoon')}</span>
</div> </div>
<div <div
className="flex items-center gap-3 px-2 h-[42px] rounded-[6px] mb-[2px] text-txt-tertiary cursor-default opacity-50" 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"> <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" /> <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> </svg>
<span className="font-medium text-[16px]">Coming Soon</span> <span className="font-medium text-[16px]">{tr('sidebar.comingSoon')}</span>
</div> </div>
<div className="mt-[18px] px-2 mb-1 flex items-center justify-between group"> <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 <button
onClick={() => openModal('newDm')} onClick={() => openModal('newDm')}
className="text-txt-tertiary hover:text-txt-primary transition-colors" className="text-txt-tertiary hover:text-txt-primary transition-colors"
@@ -584,7 +586,7 @@ export function ChannelSidebar() {
{/* Channels — dynamic category layout */} {/* 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}> <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 ? ( {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 */} {/* Category group 1 */}
<div className="skeleton skeleton-bar h-2 w-[45%] ml-2 mb-3" /> <div className="skeleton skeleton-bar h-2 w-[45%] ml-2 mb-3" />
{Array.from({ length: 3 }, (_, i) => ( {Array.from({ length: 3 }, (_, i) => (
@@ -706,7 +708,7 @@ export function ChannelSidebar() {
{ {
key: 'category-settings', key: 'category-settings',
type: 'action', type: 'action',
label: 'Category Settings', label: tr('sidebar.categorySettings'),
icon: ( icon: (
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"> <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" /> <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', key: 'delete-category',
type: 'action', type: 'action',
label: 'Delete Category', label: tr('sidebar.deleteCategory'),
danger: true, danger: true,
icon: ( icon: (
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"> <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
@@ -758,7 +760,7 @@ export function ChannelSidebar() {
/> />
))} ))}
{catChannels.length === 0 && ( {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> </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"> <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" /> <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> </svg>
<span className="text-[12px]">Create Channel</span> <span className="text-[12px]">{tr('sidebar.createChannel')}</span>
</button> </button>
)} )}
<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"> <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" /> <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> </svg>
<span className="text-[12px]">Create Category</span> <span className="text-[12px]">{tr('sidebar.createCategory')}</span>
</button> </button>
</div> </div>
)} )}
@@ -845,6 +847,7 @@ function UserAreaPanel({
onDeafenToggle: () => void; onDeafenToggle: () => void;
onSettingsClick: (tab?: string) => void; onSettingsClick: (tab?: string) => void;
}) { }) {
const tr = useT();
const [accountMenuOpen, setAccountMenuOpen] = useState(false); const [accountMenuOpen, setAccountMenuOpen] = useState(false);
const [openPanel, setOpenPanel] = useState<'input' | 'output' | null>(null); const [openPanel, setOpenPanel] = useState<'input' | 'output' | null>(null);
const inputDeviceId = useVoiceStore((s) => s.inputDeviceId); 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" 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="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 className="text-[13px] text-txt-tertiary truncate text-left">{selectedInputLabel}</div>
</div> </div>
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" className="text-txt-tertiary flex-shrink-0 ml-2"> <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 */} {/* Input Volume */}
<div className="px-4 py-3"> <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 <input
type="range" type="range"
min={0} min={0}
@@ -1039,7 +1042,7 @@ function UserAreaPanel({
onClick={() => onSettingsClick('voice')} onClick={() => onSettingsClick('voice')}
className="w-full px-4 py-3 flex items-center justify-between hover:bg-interactive-hover transition-colors" 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"> <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" /> <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> </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" 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="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 className="text-[13px] text-txt-tertiary truncate text-left">{selectedOutputLabel}</div>
</div> </div>
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" className="text-txt-tertiary flex-shrink-0 ml-2"> <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 */} {/* Output Volume */}
<div className="px-4 py-3"> <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 <input
type="range" type="range"
min={0} min={0}
@@ -1125,7 +1128,7 @@ function UserAreaPanel({
onClick={() => onSettingsClick('voice')} onClick={() => onSettingsClick('voice')}
className="w-full px-4 py-3 flex items-center justify-between hover:bg-interactive-hover transition-colors" 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"> <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" /> <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> </svg>
@@ -1,4 +1,5 @@
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { useT } from '../../i18n';
import type { MemberWithUser, Activity } from '@backspace/shared'; import type { MemberWithUser, Activity } from '@backspace/shared';
import { useSpaceStore } from '../../stores/spaceStore'; import { useSpaceStore } from '../../stores/spaceStore';
import { useUIStore } from '../../stores/uiStore'; import { useUIStore } from '../../stores/uiStore';
@@ -21,7 +22,7 @@ function getMemberGroup(member: MemberWithUser, ownerId: string | undefined) {
const ownerRole = member.roles?.find(r => r.position > 0); const ownerRole = member.roles?.find(r => r.position > 0);
return { return {
key: '__owner__', key: '__owner__',
label: 'OWNER', label: '__OWNER__',
color: ownerRole?.color ?? 'rgb(var(--accent-rose))', color: ownerRole?.color ?? 'rgb(var(--accent-rose))',
position: Infinity, position: Infinity,
}; };
@@ -40,7 +41,10 @@ function getMemberGroup(member: MemberWithUser, ownerId: string | undefined) {
// No explicit roles — just @everyone // No explicit roles — just @everyone
return { return {
key: '__online__', key: '__online__',
label: 'ONLINE', // Marcado em vez de traduzido aqui: esta funcao nao e um componente e nao
// pode usar o hook. Nomes de cargo passam adiante sem traducao — sao dados
// do usuario, nao interface.
label: '__ONLINE__',
color: undefined, color: undefined,
position: -1, position: -1,
}; };
@@ -111,6 +115,7 @@ export function MemberSidebar() {
const currentSpaceId = useSpaceStore((s) => s.currentSpaceId); const currentSpaceId = useSpaceStore((s) => s.currentSpaceId);
const loadingSpaceId = useSpaceStore((s) => s.loadingSpaceId); const loadingSpaceId = useSpaceStore((s) => s.loadingSpaceId);
const memberListOpen = useUIStore((s) => s.memberListOpen); const memberListOpen = useUIStore((s) => s.memberListOpen);
const tr = useT();
const openUserProfile = useUIStore((s) => s.openUserProfile); const openUserProfile = useUIStore((s) => s.openUserProfile);
const userActivities = useActivityStore((s) => s.userActivities); const userActivities = useActivityStore((s) => s.userActivities);
@@ -183,7 +188,7 @@ export function MemberSidebar() {
return ( return (
<div className="w-60 bg-surface-members flex-shrink-0 overflow-y-auto select-none no-scrollbar hidden md:block border-l border-border-hard"> <div className="w-60 bg-surface-members flex-shrink-0 overflow-y-auto select-none no-scrollbar hidden md:block border-l border-border-hard">
{showMemberSkeleton ? ( {showMemberSkeleton ? (
<div className="px-3 pt-4" role="status" aria-label="Loading members"> <div className="px-3 pt-4" role="status" aria-label={tr('sidebar.loadingMembers')}>
{/* Role group 1 */} {/* Role group 1 */}
<div className="skeleton skeleton-bar h-2 w-[40%] mb-3" style={{ animationDelay: '0s' }} /> <div className="skeleton skeleton-bar h-2 w-[40%] mb-3" style={{ animationDelay: '0s' }} />
{Array.from({ length: 2 }, (_, i) => ( {Array.from({ length: 2 }, (_, i) => (
@@ -207,7 +212,11 @@ export function MemberSidebar() {
{roleGroups.map(([key, group]) => ( {roleGroups.map(([key, group]) => (
<div key={key} className="mb-4"> <div key={key} className="mb-4">
<h3 className="text-[10.5px] font-bold text-txt-tertiary uppercase tracking-[0.06em] px-2 mb-1"> <h3 className="text-[10.5px] font-bold text-txt-tertiary uppercase tracking-[0.06em] px-2 mb-1">
{group.label} {group.members.length} {group.label === '__ONLINE__'
? tr('sidebar.groupOnline')
: group.label === '__OWNER__'
? tr('sidebar.groupOwner')
: group.label} {group.members.length}
</h3> </h3>
{group.members.map((m) => renderMember(m))} {group.members.map((m) => renderMember(m))}
</div> </div>
+22
View File
@@ -56,6 +56,28 @@ export const en = {
'accountMenu.copyId': 'Copy User ID', 'accountMenu.copyId': 'Copy User ID',
'accountMenu.copied': 'Copied', 'accountMenu.copied': 'Copied',
// Sidebar — spaces, channels and members
'sidebar.friends': 'Friends',
'sidebar.directMessages': 'Direct Messages',
'sidebar.noChannels': 'No channels',
'sidebar.comingSoon': 'Coming Soon',
'sidebar.loadingChannels': 'Loading channels',
'sidebar.loadingMembers': 'Loading members',
'sidebar.groupOnline': 'ONLINE',
'sidebar.groupOwner': 'OWNER',
'sidebar.createChannel': 'Create Channel',
'sidebar.createCategory': 'Create Category',
'sidebar.categorySettings': 'Category Settings',
'sidebar.deleteCategory': 'Delete Category',
'sidebar.invitePeople': 'Invite People',
'sidebar.spaceSettings': 'Space Settings',
'sidebar.leaveGroup': 'Leave Group',
'sidebar.voiceSettings': 'Voice Settings',
'sidebar.inputDevice': 'Input Device',
'sidebar.inputVolume': 'Input Volume',
'sidebar.outputDevice': 'Output Device',
'sidebar.outputVolume': 'Output Volume',
// Chat — composer and messages // Chat — composer and messages
'chat.composer.placeholder': 'Message {channel}', 'chat.composer.placeholder': 'Message {channel}',
'chat.composer.attach': 'Attach file', 'chat.composer.attach': 'Attach file',
+22
View File
@@ -55,6 +55,28 @@ export const ptBR: Partial<Dictionary> = {
'accountMenu.copyId': 'Copiar ID do usuário', 'accountMenu.copyId': 'Copiar ID do usuário',
'accountMenu.copied': 'Copiado', 'accountMenu.copied': 'Copiado',
// Barra lateral — servidores, canais e membros
'sidebar.friends': 'Amigos',
'sidebar.directMessages': 'Mensagens diretas',
'sidebar.noChannels': 'Nenhum canal',
'sidebar.comingSoon': 'Em breve',
'sidebar.loadingChannels': 'Carregando canais',
'sidebar.loadingMembers': 'Carregando membros',
'sidebar.groupOnline': 'DISPONÍVEIS',
'sidebar.groupOwner': 'DONO',
'sidebar.createChannel': 'Criar canal',
'sidebar.createCategory': 'Criar categoria',
'sidebar.categorySettings': 'Configurações da categoria',
'sidebar.deleteCategory': 'Excluir categoria',
'sidebar.invitePeople': 'Convidar pessoas',
'sidebar.spaceSettings': 'Configurações do servidor',
'sidebar.leaveGroup': 'Sair do grupo',
'sidebar.voiceSettings': 'Configurações de voz',
'sidebar.inputDevice': 'Dispositivo de entrada',
'sidebar.inputVolume': 'Volume de entrada',
'sidebar.outputDevice': 'Dispositivo de saída',
'sidebar.outputVolume': 'Volume de saída',
// Chat — composer e mensagens // Chat — composer e mensagens
'chat.composer.placeholder': 'Conversar em {channel}', 'chat.composer.placeholder': 'Conversar em {channel}',
'chat.composer.attach': 'Anexar arquivo', 'chat.composer.attach': 'Anexar arquivo',