From 4fd6ac890c13ceb7a2663332dfacf123b18111bc Mon Sep 17 00:00:00 2001 From: devsyncwrld Date: Wed, 2 Sep 2026 20:38:54 -0300 Subject: [PATCH] feat(i18n): translate five more systems MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Settings > Voice: video — preview, permission states, camera picker. This closes the Voice & Video tab; input, output and video are all done. - Create channel / category / space - Group DMs — new DM, add friends, group settings, leave - Join a space — discovery, invite code, and both federated auth phases - Delete account, transfer ownership, and the top-level error screen Three patterns worth noting. Strings that are chosen in a helper or held in state (the camera's hard-blocked copy, its preview errors, the space visibility options, the invite failure reasons) now travel as translation keys and are resolved where they render, so switching language re-labels what is already on screen. Sentences that wrap an emphasised literal — the host name when joining, the space and member names when transferring — are split around their placeholder instead of interpolated, leaving word order to the translation. And `CreateChannel` mapped over `(t) =>` for the channel type, which shadowed the translator; the loop variable is now `channelType`. The error boundary in main.tsx is a class component, so it reads the locale once per render via `translate` rather than the hook — correct here anyway, since an error screen has no reason to re-render on a language switch. Two GroupDmSettings tests located the name input by its aria-label, which this change translates. They now use the `data-group-dm-name-input` attribute the input already carries, matching how the rest of that file locates elements — and no longer break when copy changes. Dictionary is at 595 keys, en and pt-BR at exact parity. Verified: web build (tsc + vite), 490 web tests. Co-Authored-By: Claude Opus 5 --- docs/roadmap-resenha.md | 11 +- .../components/modals/AddDmMemberModal.tsx | 21 ++- .../src/components/modals/CreateCategory.tsx | 18 +- .../src/components/modals/CreateChannel.tsx | 50 ++--- .../web/src/components/modals/CreateSpace.tsx | 45 ++--- .../components/modals/DeleteAccountModal.tsx | 50 ++--- .../modals/GroupDmSettings.test.tsx | 4 +- .../src/components/modals/GroupDmSettings.tsx | 72 ++++---- .../web/src/components/modals/JoinSpace.tsx | 57 +++--- .../web/src/components/modals/NewDmModal.tsx | 8 +- .../modals/TransferOwnershipModal.tsx | 38 ++-- .../modals/settingsPanels/VideoSection.tsx | 84 +++++---- packages/web/src/i18n/locales/en.ts | 173 ++++++++++++++++++ packages/web/src/i18n/locales/pt-BR.ts | 173 ++++++++++++++++++ packages/web/src/main.tsx | 12 +- 15 files changed, 612 insertions(+), 204 deletions(-) diff --git a/docs/roadmap-resenha.md b/docs/roadmap-resenha.md index 1e3e8b08..0db21249 100644 --- a/docs/roadmap-resenha.md +++ b/docs/roadmap-resenha.md @@ -100,11 +100,18 @@ fixas por `t('chave')`. O que faltar cai para o inglês sozinho. - Configurações → Atalhos (incluindo os nomes das ações e o aviso de conflito) - Configurações → Conta (perfil, senha, conta desligada da origem, zona de perigo) - Explorar servidores (cartões, entrar, pedir para entrar) +- Configurações → Voz: vídeo (prévia, permissões, escolha de câmera) — fecha a aba +- Criar canal, categoria e servidor +- DMs em grupo (nova DM, adicionar amigos, configurações do grupo, sair) +- Entrar num servidor (descoberta, código de convite, conectar e login federado) +- Excluir conta, transferir propriedade e a tela de erro geral ### Fila sugerida de tradução -Configurações → Vídeo · Configurações → Instância · configurações do servidor e -cargos · telas de erro · DMs em grupo (`GroupDmSettings`, `AddDmMemberModal`) +Configurações → Instância (painéis: geral, registro, federação, transmissão, +armazenamento, usuários) · configurações do servidor e cargos +(`SpaceSettings` e `spaceSettingsPanels/`) · telas móveis (`Mobile*.tsx`) · +`CategorySettingsModal` · `ConnectedInstances` ## Correções pendentes diff --git a/packages/web/src/components/modals/AddDmMemberModal.tsx b/packages/web/src/components/modals/AddDmMemberModal.tsx index 85aaf25d..c32e3093 100644 --- a/packages/web/src/components/modals/AddDmMemberModal.tsx +++ b/packages/web/src/components/modals/AddDmMemberModal.tsx @@ -10,6 +10,7 @@ import { api } from '../../api/client'; import { isSelf, parseFederatedUsername } from '../../utils/identity'; import { useCanonicalUserView } from '../../utils/userViewLookup'; import type { User } from '@backspace/shared'; +import { useT } from '../../i18n'; function AddDmFriendRow({ friend, @@ -26,6 +27,7 @@ function AddDmFriendRow({ isAdding: boolean; onToggle: (id: string) => void; }) { + const t = useT(); const canonical = useCanonicalUserView(friend as unknown as User); const { baseName } = parseFederatedUsername(canonical.username); const friendDisplayName = canonical.displayName ?? baseName; @@ -54,7 +56,7 @@ function AddDmFriendRow({ {friendDisplayName}
- {isInDm ? 'Already in this DM' : `@${canonical.username}`} + {isInDm ? t('groupDm.add.alreadyIn') : `@${canonical.username}`}
{!isInDm && ( @@ -77,6 +79,7 @@ function AddDmFriendRow({ } export function AddDmMemberModal() { + const t = useT(); const [query, setQuery] = useState(''); const [selected, setSelected] = useState>(new Set()); const [error, setError] = useState(''); @@ -190,18 +193,20 @@ export function AddDmMemberModal() { closeModal(); } } catch (err) { - setError((err as Error).message || 'Failed to add members'); + setError((err as Error).message || t('groupDm.add.failed')); } finally { setIsAdding(false); } }; const buttonText = selectedFriends.length === 0 - ? 'Select Friends' - : `Add ${selectedFriends.length} Friend${selectedFriends.length > 1 ? 's' : ''}`; + ? t('groupDm.add.select') + : t(selectedFriends.length > 1 ? 'groupDm.add.submitPlural' : 'groupDm.add.submit', { + count: selectedFriends.length, + }); return ( - +
{/* Header with member count */}
@@ -239,7 +244,7 @@ export function AddDmMemberModal() { type="text" value={query} onChange={(e) => setQuery(e.target.value)} - placeholder="Search friends..." + placeholder={t('groupDm.add.search')} className="input-search w-full py-2 text-[14px]" disabled={remainingSlots <= 0} /> @@ -256,7 +261,7 @@ export function AddDmMemberModal() {
{filteredFriends.length === 0 && (
- {query.trim() ? 'No friends match your search' : 'No friends yet'} + {query.trim() ? t('groupDm.add.noMatch') : t('groupDm.add.noFriends')}
)} @@ -284,7 +289,7 @@ export function AddDmMemberModal() { disabled={selectedFriends.length === 0 || isAdding} className="w-full py-2 rounded-md text-[13px] font-semibold transition-colors bg-accent-mint text-surface-base hover:bg-accent-mint/90 disabled:opacity-50 disabled:cursor-not-allowed" > - {isAdding ? 'Adding...' : buttonText} + {isAdding ? t('groupDm.add.adding') : buttonText}
diff --git a/packages/web/src/components/modals/CreateCategory.tsx b/packages/web/src/components/modals/CreateCategory.tsx index b771a8b8..85d9ee6e 100644 --- a/packages/web/src/components/modals/CreateCategory.tsx +++ b/packages/web/src/components/modals/CreateCategory.tsx @@ -2,8 +2,10 @@ import React, { useState } from 'react'; import { Modal } from '../ui/Modal'; import { useUIStore } from '../../stores/uiStore'; import { useSpaceStore } from '../../stores/spaceStore'; +import { useT } from '../../i18n'; export function CreateCategoryModal() { + const t = useT(); const [name, setName] = useState(''); const [error, setError] = useState(''); const [isLoading, setIsLoading] = useState(false); @@ -19,12 +21,12 @@ export function CreateCategoryModal() { setError(''); if (!name.trim()) { - setError('Category name is required'); + setError(t('create.category.name.required')); return; } if (!currentSpaceId) { - setError('No space selected'); + setError(t('create.noSpace')); return; } @@ -34,14 +36,14 @@ export function CreateCategoryModal() { closeModal(); setName(''); } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to create category'); + setError(err instanceof Error ? err.message : t('create.category.failed')); } finally { setIsLoading(false); } }; return ( - +
{error && (
@@ -51,14 +53,14 @@ export function CreateCategoryModal() {
setName(e.target.value)} className="input-standard w-full" - placeholder="new-category" + placeholder={t('create.category.name.placeholder')} autoFocus />
@@ -71,14 +73,14 @@ export function CreateCategoryModal() { onClick={closeModal} className="px-3 py-1 text-sm text-txt-tertiary hover:text-txt-secondary transition-colors" > - Cancel + {t('create.cancel')}
diff --git a/packages/web/src/components/modals/CreateChannel.tsx b/packages/web/src/components/modals/CreateChannel.tsx index e8c11653..aae35766 100644 --- a/packages/web/src/components/modals/CreateChannel.tsx +++ b/packages/web/src/components/modals/CreateChannel.tsx @@ -2,8 +2,10 @@ import React, { useState, useEffect } from 'react'; import { Modal } from '../ui/Modal'; import { useUIStore } from '../../stores/uiStore'; import { useSpaceStore } from '../../stores/spaceStore'; +import { useT } from '../../i18n'; export function CreateChannelModal() { + const t = useT(); const [name, setName] = useState(''); const [type, setType] = useState<'text' | 'voice'>('text'); const [topic, setTopic] = useState(''); @@ -31,12 +33,12 @@ export function CreateChannelModal() { setError(''); if (!name.trim()) { - setError('Channel name is required'); + setError(t('create.channel.name.required')); return; } if (!currentSpaceId) { - setError('No space selected'); + setError(t('create.noSpace')); return; } @@ -49,14 +51,14 @@ export function CreateChannelModal() { setType('text'); setCategoryId(''); } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to create channel'); + setError(err instanceof Error ? err.message : t('create.channel.failed')); } finally { setIsLoading(false); } }; return ( - + {error && (
@@ -66,14 +68,14 @@ export function CreateChannelModal() {
- {(['text', 'voice'] as const).map((t) => ( + {(['text', 'voice'] as const).map((channelType) => ( @@ -108,14 +110,14 @@ export function CreateChannelModal() {
setName(e.target.value)} className="input-standard w-full" - placeholder="new-channel" + placeholder={t('create.channel.name.placeholder')} autoFocus />
@@ -123,14 +125,14 @@ export function CreateChannelModal() { {type === 'text' && (
setTopic(e.target.value)} className="input-standard w-full" - placeholder="What's this channel about?" + placeholder={t('create.channel.topic.placeholder')} />
)} @@ -138,14 +140,14 @@ export function CreateChannelModal() { {categories.length > 0 && (
setName(e.target.value)} className="input-standard w-full" - placeholder="My Awesome Space" + placeholder={t('create.space.name.placeholder')} autoFocus />
@@ -226,7 +229,7 @@ export function CreateSpaceModal() { {/* Visibility */}
- Visibility + {t('create.space.visibility')}
{visibilityOptions.map((opt) => ( @@ -247,8 +250,8 @@ export function CreateSpaceModal() { className="mt-0.5 accent-accent-primary" />
-
{opt.label}
-
{opt.desc}
+
{t(opt.labelKey)}
+
{t(opt.descKey)}
))} @@ -258,12 +261,12 @@ export function CreateSpaceModal() { {/* Description */}
- Description + {t('create.space.description')}