diff --git a/app/src/components/settings/panels/TeamPanel.test.tsx b/app/src/components/settings/panels/TeamPanel.test.tsx index 2c62d7386..05b9f07de 100644 --- a/app/src/components/settings/panels/TeamPanel.test.tsx +++ b/app/src/components/settings/panels/TeamPanel.test.tsx @@ -101,17 +101,15 @@ describe('', () => { await waitFor(() => expect(refreshTeams).toHaveBeenCalled()); }); - it('disables the Create button until a name is typed and calls createTeam on submit', async () => { + it('does not render a create-team control and shows the personal-team note', async () => { const Panel = await importPanel(); renderPanel(Panel); - const createBtn = screen.getByRole('button', { name: 'Create' }); - expect(createBtn).toBeDisabled(); - - fireEvent.change(screen.getByPlaceholderText('Team name'), { target: { value: 'New Team' } }); - expect(createBtn).not.toBeDisabled(); - fireEvent.click(createBtn); - await waitFor(() => expect(teamApiMock.createTeam).toHaveBeenCalledWith('New Team')); + // Create was removed (issue #3723): backend caps one owned team per user. + expect(screen.queryByRole('button', { name: 'Create' })).not.toBeInTheDocument(); + expect(screen.queryByPlaceholderText('Team name')).not.toBeInTheDocument(); + expect(screen.getByText(/personal team is created automatically/i)).toBeInTheDocument(); + expect(teamApiMock.createTeam).not.toHaveBeenCalled(); }); it('joins a team when the user submits a join code', async () => { @@ -161,13 +159,13 @@ describe('', () => { expect(navigateToTeamManagement).toHaveBeenCalledWith('team-a'); }); - it('surfaces the localized error when createTeam rejects', async () => { - teamApiMock.createTeam.mockRejectedValueOnce(new Error('boom')); + it('falls back to the localized error when joinTeam rejects without a reason', async () => { + teamApiMock.joinTeam.mockRejectedValueOnce(new Error('boom')); const Panel = await importPanel(); renderPanel(Panel); - fireEvent.change(screen.getByPlaceholderText('Team name'), { target: { value: 'New Team' } }); - fireEvent.click(screen.getByRole('button', { name: 'Create' })); - await waitFor(() => expect(screen.getByText('Failed to create team')).toBeInTheDocument()); + fireEvent.change(screen.getByPlaceholderText('Invite code'), { target: { value: 'X' } }); + fireEvent.click(screen.getByRole('button', { name: 'Join' })); + await waitFor(() => expect(screen.getByText('boom')).toBeInTheDocument()); }); }); diff --git a/app/src/components/settings/panels/TeamPanel.tsx b/app/src/components/settings/panels/TeamPanel.tsx index 2a64bfc7c..9ddd44ffc 100644 --- a/app/src/components/settings/panels/TeamPanel.tsx +++ b/app/src/components/settings/panels/TeamPanel.tsx @@ -12,6 +12,7 @@ import Button from '../../ui/Button'; import { SettingsBadge, SettingsSection, SettingsTextField } from '../controls'; import { useSettingsNavigation } from '../hooks/useSettingsNavigation'; import SettingsPanel from '../layout/SettingsPanel'; +import { teamErrorMessage } from './teamErrorMessage'; const log = debug('core-rpc:error'); @@ -21,10 +22,8 @@ const TeamPanel = () => { const { snapshot, teams, refresh, refreshTeams } = useCoreState(); const user = snapshot.currentUser; - const [newTeamName, setNewTeamName] = useState(''); const [joinCode, setJoinCode] = useState(''); const [isLoading, setIsLoading] = useState(false); - const [isCreating, setIsCreating] = useState(false); const [isJoining, setIsJoining] = useState(false); const [isSwitching, setIsSwitching] = useState(null); const [isLeaving, setIsLeaving] = useState(null); @@ -60,26 +59,6 @@ const TeamPanel = () => { }); }, [refreshTeamsWithLoading]); - const handleCreateTeam = async () => { - const name = newTeamName.trim(); - if (!name) return; - setIsCreating(true); - setError(null); - try { - await teamApi.createTeam(name); - setNewTeamName(''); - await refreshTeamsWithLoading(); - } catch (err) { - setError( - err && typeof err === 'object' && 'error' in err - ? String(err.error) - : t('team.failedToCreate') - ); - } finally { - setIsCreating(false); - } - }; - const handleJoinTeam = async () => { const code = joinCode.trim(); if (!code) return; @@ -90,11 +69,7 @@ const TeamPanel = () => { setJoinCode(''); await Promise.all([refresh(), refreshTeamsWithLoading()]); } catch (err) { - setError( - err && typeof err === 'object' && 'error' in err - ? String(err.error) - : t('team.invalidInviteCode') - ); + setError(teamErrorMessage(err, t('team.invalidInviteCode'))); } finally { setIsJoining(false); } @@ -108,11 +83,7 @@ const TeamPanel = () => { await teamApi.switchTeam(teamId); await Promise.all([refresh(), refreshTeamsWithLoading()]); } catch (err) { - setError( - err && typeof err === 'object' && 'error' in err - ? String(err.error) - : t('team.failedToSwitch') - ); + setError(teamErrorMessage(err, t('team.failedToSwitch'))); } finally { setIsSwitching(null); } @@ -133,11 +104,7 @@ const TeamPanel = () => { await Promise.all([refresh(), refreshTeamsWithLoading()]); setTeamToLeave(null); } catch (err) { - setError( - err && typeof err === 'object' && 'error' in err - ? String(err.error) - : t('team.failedToLeave') - ); + setError(teamErrorMessage(err, t('team.failedToLeave'))); } finally { setIsLeaving(null); } @@ -260,29 +227,8 @@ const TeamPanel = () => { )} - -
- setNewTeamName(e.target.value)} - onKeyDown={e => e.key === 'Enter' && void handleCreateTeam()} - placeholder={t('team.teamName')} - aria-label={t('team.teamName')} - inputSize="sm" - /> - -
-
- +

{t('team.personalAutoCreatedNote')}

({ 'team.yourTeams': 'Your Teams', 'team.createNewTeam': 'Create New Team', 'team.joinExistingTeam': 'Join Existing Team', + 'team.personalAutoCreatedNote': 'Personal team is auto-created. Join below.', 'team.teamName': 'Team Name', 'team.inviteCode': 'Invite Code', 'team.creating': 'Creating...', @@ -151,37 +152,24 @@ describe('TeamPanel — role badge rendering (line 165)', () => { }); }); -describe('TeamPanel — create team (line 281)', () => { +describe('TeamPanel — create team removed (issue #3723)', () => { beforeEach(() => { vi.clearAllMocks(); - mockCreateTeam.mockResolvedValue({}); setupState({ teams: [] }); }); - it('calls createTeam when form is submitted (line 281)', async () => { + it('does not render a create-team control', () => { render(); - - const input = screen.getByPlaceholderText('Team Name') ?? screen.getByLabelText('Team Name'); - fireEvent.change(input, { target: { value: 'My New Team' } }); - - fireEvent.click(screen.getByRole('button', { name: 'Create' })); - - await waitFor(() => { - expect(mockCreateTeam).toHaveBeenCalledWith('My New Team'); - }); + // The "Create New Team" section is gone — backend caps one owned team per + // user, so the control could never succeed (always 404'd). + expect(screen.queryByRole('button', { name: 'Create' })).not.toBeInTheDocument(); + expect(screen.queryByLabelText('Team Name')).not.toBeInTheDocument(); + expect(mockCreateTeam).not.toHaveBeenCalled(); }); - it('shows an error banner when createTeam fails', async () => { - mockCreateTeam.mockRejectedValue({ error: 'Team limit reached' }); + it('renders the personal-team explanatory note', () => { render(); - - const input = screen.getByLabelText('Team Name'); - fireEvent.change(input, { target: { value: 'New Team' } }); - fireEvent.click(screen.getByRole('button', { name: 'Create' })); - - await waitFor(() => { - expect(screen.getByText('Team limit reached')).toBeInTheDocument(); - }); + expect(screen.getByText('Personal team is auto-created. Join below.')).toBeInTheDocument(); }); }); @@ -197,7 +185,6 @@ describe('TeamPanel — join team (line 304)', () => { const input = screen.getByLabelText('Invite Code'); fireEvent.change(input, { target: { value: 'CODE-XYZ' } }); - // Need to find the Join button — it's the second submit button (after Create) const joinBtn = screen.getByRole('button', { name: /^Join$/i }); fireEvent.click(joinBtn); @@ -218,6 +205,25 @@ describe('TeamPanel — join team (line 304)', () => { expect(screen.getByText('Code not found')).toBeInTheDocument(); }); }); + + it('surfaces the real backend reason from a CoreRpcError (issue #3723)', async () => { + // Production rejects with a CoreRpcError — which has no `.error` field, so + // the old `'error' in err` check dropped the reason. Verify it now shows. + mockJoinTeam.mockRejectedValue( + new CoreRpcError( + 'POST /teams/join failed (400 Bad Request): {"error":"Invite expired"}', + 'unknown' + ) + ); + render(); + + fireEvent.change(screen.getByLabelText('Invite Code'), { target: { value: 'OLD-CODE' } }); + fireEvent.click(screen.getByRole('button', { name: /^Join$/i })); + + await waitFor(() => { + expect(screen.getByText('Invite expired')).toBeInTheDocument(); + }); + }); }); describe('TeamPanel — leave team modal (line 327)', () => { diff --git a/app/src/components/settings/panels/teamErrorMessage.test.ts b/app/src/components/settings/panels/teamErrorMessage.test.ts new file mode 100644 index 000000000..31be01d2b --- /dev/null +++ b/app/src/components/settings/panels/teamErrorMessage.test.ts @@ -0,0 +1,73 @@ +import { describe, expect, it } from 'vitest'; + +import { CoreRpcError } from '../../../services/coreRpcClient'; +import { teamErrorMessage } from './teamErrorMessage'; + +const FALLBACK = 'Something went wrong'; + +describe('teamErrorMessage', () => { + it('prefers a structured reason from CoreRpcError.data', () => { + const err = new CoreRpcError('GET /teams failed (403 Forbidden)', 'unknown', undefined, { + message: 'Not a member of this team', + }); + expect(teamErrorMessage(err, FALLBACK)).toBe('Not a member of this team'); + }); + + it('lifts the human field out of a JSON body in CoreRpcError.message', () => { + const err = new CoreRpcError( + 'POST /teams/join failed (400 Bad Request): {"error":"Invite expired"}', + 'unknown' + ); + expect(teamErrorMessage(err, FALLBACK)).toBe('Invite expired'); + }); + + it('returns a clean message when there is no RPC prefix', () => { + const err = new CoreRpcError('Session expired. Please log in again.', 'auth_expired'); + expect(teamErrorMessage(err, FALLBACK)).toBe('Session expired. Please log in again.'); + }); + + it('falls back when the body is a raw HTML error page', () => { + const err = new CoreRpcError( + 'POST /teams failed (404 Not Found): Not Found', + 'unknown' + ); + expect(teamErrorMessage(err, FALLBACK)).toBe(FALLBACK); + }); + + it('falls back when a JSON body has no human-readable field', () => { + const err = new CoreRpcError( + 'POST /teams/join failed (400 Bad Request): {"code":"E_BAD"}', + 'unknown' + ); + expect(teamErrorMessage(err, FALLBACK)).toBe(FALLBACK); + }); + + it('falls back when nothing remains after the RPC prefix', () => { + const err = new CoreRpcError( + 'DELETE /teams/t1 failed (500 Internal Server Error): ', + 'unknown' + ); + expect(teamErrorMessage(err, FALLBACK)).toBe(FALLBACK); + }); + + it('handles the legacy plain { error } rejection shape', () => { + expect(teamErrorMessage({ error: 'Team limit reached' }, FALLBACK)).toBe('Team limit reached'); + }); + + it('surfaces a bare Error message', () => { + expect(teamErrorMessage(new Error('boom'), FALLBACK)).toBe('boom'); + }); + + it('falls back for non-object rejections', () => { + expect(teamErrorMessage(null, FALLBACK)).toBe(FALLBACK); + expect(teamErrorMessage(undefined, FALLBACK)).toBe(FALLBACK); + expect(teamErrorMessage('nope', FALLBACK)).toBe(FALLBACK); + }); + + it('caps an overly long reason', () => { + const long = 'x'.repeat(500); + const out = teamErrorMessage({ error: long }, FALLBACK); + expect(out.length).toBeLessThanOrEqual(200); + expect(out.endsWith('…')).toBe(true); + }); +}); diff --git a/app/src/components/settings/panels/teamErrorMessage.ts b/app/src/components/settings/panels/teamErrorMessage.ts new file mode 100644 index 000000000..586c950ed --- /dev/null +++ b/app/src/components/settings/panels/teamErrorMessage.ts @@ -0,0 +1,81 @@ +import { CoreRpcError } from '../../../services/coreRpcClient'; + +/** + * Extract a human-readable failure reason from a team RPC rejection. + * + * Team ops call the hosted backend through `callCoreRpc`, which rejects with a + * {@link CoreRpcError}. The real reason lives in either `err.data` (structured + * JSON-RPC `error.data`) or `err.message` — the latter shaped by the Rust + * `flatten_authed_error` net as `"POST /teams/join failed (400 Bad Request): + * "`. The previous TeamPanel catch checked `'error' in err`, which never + * matched a `CoreRpcError` (it has no `.error` field), so every failure fell + * back to a generic banner and the backend reason was dropped (issue #3723). + * + * This helper surfaces the backend reason when one is recoverable and falls + * back to the caller-supplied localized string otherwise. It also handles the + * legacy plain `{ error }` / `{ message }` rejection shape so existing call + * sites keep working. + */ + +const RPC_PREFIX = /^(?:GET|POST|PUT|DELETE|PATCH)\s+\S+\s+failed\s*\([^)]*\):?\s*/i; +const MAX_LEN = 200; + +function cap(value: string): string { + const oneLine = value.replace(/\s+/g, ' ').trim(); + return oneLine.length > MAX_LEN ? `${oneLine.slice(0, MAX_LEN - 1)}…` : oneLine; +} + +function firstString(obj: Record, keys: string[]): string | null { + for (const key of keys) { + const value = obj[key]; + if (typeof value === 'string' && value.trim()) return value.trim(); + } + return null; +} + +/** Pull a human field out of a structured error body / `error.data`. */ +function reasonFromData(data: unknown): string | null { + if (data && typeof data === 'object') { + const reason = firstString(data as Record, ['message', 'error', 'detail']); + if (reason) return cap(reason); + } + return null; +} + +/** + * Strip the `" failed ():"` prefix the core prepends and + * recover the meaningful tail. Returns `null` when nothing presentable remains + * (empty body, or a raw HTML error page that must never reach the user). + */ +function cleanRpcMessage(message: string): string | null { + const body = message.replace(RPC_PREFIX, '').trim(); + if (!body) return null; + // Never surface a raw HTML 404/error page (the POST /teams 404 case). + if (/^<(?:!doctype|html)/i.test(body)) return null; + // Backend errors arrive as a JSON body — lift the human field out of it. + if (body.startsWith('{')) { + try { + const fromJson = reasonFromData(JSON.parse(body)); + // Structured but no human field → let the caller fall back. + return fromJson; + } catch { + // Not valid JSON — fall through and surface the raw tail. + } + } + return cap(body); +} + +export function teamErrorMessage(err: unknown, fallback: string): string { + if (err instanceof CoreRpcError) { + return reasonFromData(err.data) ?? cleanRpcMessage(err.message) ?? fallback; + } + if (err && typeof err === 'object') { + const reason = firstString(err as Record, ['error', 'detail']); + if (reason) return cap(reason); + const message = (err as Record).message; + if (typeof message === 'string' && message.trim()) { + return cleanRpcMessage(message) ?? cap(message); + } + } + return fallback; +} diff --git a/app/src/lib/i18n/ar.ts b/app/src/lib/i18n/ar.ts index 5560c9ecb..d9a9756ad 100644 --- a/app/src/lib/i18n/ar.ts +++ b/app/src/lib/i18n/ar.ts @@ -1767,6 +1767,8 @@ const messages: TranslationMap = { 'team.teamName': 'اسم الفريق', 'team.creating': 'جارٍ الإنشاء...', 'team.joinExistingTeam': 'الانضمام إلى فريق موجود', + 'team.personalAutoCreatedNote': + 'يتم إنشاء فريقك الشخصي تلقائيًا. للتعاون، انضم إلى فريق موجود باستخدام رمز دعوة أدناه.', 'team.inviteCode': 'رمز الدعوة', 'team.joining': 'جارٍ الانضمام...', 'team.join': 'انضمام', diff --git a/app/src/lib/i18n/bn.ts b/app/src/lib/i18n/bn.ts index f24cf480f..3ea8a0034 100644 --- a/app/src/lib/i18n/bn.ts +++ b/app/src/lib/i18n/bn.ts @@ -1805,6 +1805,8 @@ const messages: TranslationMap = { 'team.teamName': 'টিমের নাম', 'team.creating': 'তৈরি হচ্ছে...', 'team.joinExistingTeam': 'বিদ্যমান টিমে যোগ দিন', + 'team.personalAutoCreatedNote': + 'আপনার ব্যক্তিগত টিম স্বয়ংক্রিয়ভাবে তৈরি হয়। সহযোগিতার জন্য, নিচে একটি আমন্ত্রণ কোড দিয়ে বিদ্যমান টিমে যোগ দিন।', 'team.inviteCode': 'আমন্ত্রণ কোড', 'team.joining': 'যোগ দেওয়া হচ্ছে...', 'team.join': 'যোগ দিন', diff --git a/app/src/lib/i18n/de.ts b/app/src/lib/i18n/de.ts index e79e704dd..752b7fd40 100644 --- a/app/src/lib/i18n/de.ts +++ b/app/src/lib/i18n/de.ts @@ -1852,6 +1852,8 @@ const messages: TranslationMap = { 'team.teamName': 'Teamname', 'team.creating': 'Erstellen...', 'team.joinExistingTeam': 'Tritt einem bestehenden Team bei', + 'team.personalAutoCreatedNote': + 'Dein persönliches Team wird automatisch erstellt. Um zusammenzuarbeiten, tritt unten mit einem Einladungscode einem bestehenden Team bei.', 'team.inviteCode': 'Einladungscode', 'team.joining': 'Beitritt...', 'team.join': 'Mach mit', diff --git a/app/src/lib/i18n/en.ts b/app/src/lib/i18n/en.ts index ab344f082..414919258 100644 --- a/app/src/lib/i18n/en.ts +++ b/app/src/lib/i18n/en.ts @@ -2170,6 +2170,8 @@ const en: TranslationMap = { 'team.teamName': 'Team name', 'team.creating': 'Creating...', 'team.joinExistingTeam': 'Join Existing Team', + 'team.personalAutoCreatedNote': + 'Your personal team is created automatically. To collaborate, join an existing team with an invite code below.', 'team.inviteCode': 'Invite code', 'team.joining': 'Joining...', 'team.join': 'Join', diff --git a/app/src/lib/i18n/es.ts b/app/src/lib/i18n/es.ts index 987669776..87ddadbc4 100644 --- a/app/src/lib/i18n/es.ts +++ b/app/src/lib/i18n/es.ts @@ -1840,6 +1840,8 @@ const messages: TranslationMap = { 'team.teamName': 'Nombre del equipo', 'team.creating': 'Creando...', 'team.joinExistingTeam': 'Unirse a un equipo existente', + 'team.personalAutoCreatedNote': + 'Tu equipo personal se crea automáticamente. Para colaborar, únete a un equipo existente con un código de invitación a continuación.', 'team.inviteCode': 'Código de invitación', 'team.joining': 'Uniéndose...', 'team.join': 'Unirse', diff --git a/app/src/lib/i18n/fr.ts b/app/src/lib/i18n/fr.ts index 408b578c1..3fd3d061e 100644 --- a/app/src/lib/i18n/fr.ts +++ b/app/src/lib/i18n/fr.ts @@ -1852,6 +1852,8 @@ const messages: TranslationMap = { 'team.teamName': "Nom de l'équipe", 'team.creating': 'Création…', 'team.joinExistingTeam': 'Rejoindre une équipe existante', + 'team.personalAutoCreatedNote': + 'Votre équipe personnelle est créée automatiquement. Pour collaborer, rejoignez une équipe existante avec un code d’invitation ci-dessous.', 'team.inviteCode': "Code d'invitation", 'team.joining': 'Adhésion…', 'team.join': 'Rejoindre', diff --git a/app/src/lib/i18n/hi.ts b/app/src/lib/i18n/hi.ts index 37c332f32..c02ca5e30 100644 --- a/app/src/lib/i18n/hi.ts +++ b/app/src/lib/i18n/hi.ts @@ -1803,6 +1803,8 @@ const messages: TranslationMap = { 'team.teamName': 'टीम का नाम', 'team.creating': 'बन रही है...', 'team.joinExistingTeam': 'मौजूदा टीम जॉइन करें', + 'team.personalAutoCreatedNote': + 'आपकी व्यक्तिगत टीम स्वतः बन जाती है। सहयोग के लिए, नीचे आमंत्रण कोड के साथ किसी मौजूदा टीम में शामिल हों।', 'team.inviteCode': 'इनवाइट कोड', 'team.joining': 'जॉइन हो रहे हैं...', 'team.join': 'जॉइन करें', diff --git a/app/src/lib/i18n/id.ts b/app/src/lib/i18n/id.ts index a64e001e7..b27393d6d 100644 --- a/app/src/lib/i18n/id.ts +++ b/app/src/lib/i18n/id.ts @@ -1812,6 +1812,8 @@ const messages: TranslationMap = { 'team.teamName': 'Nama tim', 'team.creating': 'Membuat...', 'team.joinExistingTeam': 'Bergabung dengan Tim yang Ada', + 'team.personalAutoCreatedNote': + 'Tim pribadi Anda dibuat secara otomatis. Untuk berkolaborasi, gabung ke tim yang sudah ada dengan kode undangan di bawah.', 'team.inviteCode': 'Kode undangan', 'team.joining': 'Bergabung...', 'team.join': 'Bergabung', diff --git a/app/src/lib/i18n/it.ts b/app/src/lib/i18n/it.ts index 404c3363c..9ccae327d 100644 --- a/app/src/lib/i18n/it.ts +++ b/app/src/lib/i18n/it.ts @@ -1837,6 +1837,8 @@ const messages: TranslationMap = { 'team.teamName': 'Nome team', 'team.creating': 'Creazione...', 'team.joinExistingTeam': 'Unisciti a un team esistente', + 'team.personalAutoCreatedNote': + 'Il tuo team personale viene creato automaticamente. Per collaborare, unisciti a un team esistente con un codice di invito qui sotto.', 'team.inviteCode': 'Codice invito', 'team.joining': 'Adesione...', 'team.join': 'Unisciti', diff --git a/app/src/lib/i18n/ko.ts b/app/src/lib/i18n/ko.ts index 62c418771..de72b31ff 100644 --- a/app/src/lib/i18n/ko.ts +++ b/app/src/lib/i18n/ko.ts @@ -1787,6 +1787,8 @@ const messages: TranslationMap = { 'team.teamName': '팀 이름', 'team.creating': '생성 중...', 'team.joinExistingTeam': '기존 팀 참가', + 'team.personalAutoCreatedNote': + '개인 팀은 자동으로 생성됩니다. 협업하려면 아래 초대 코드로 기존 팀에 참여하세요.', 'team.inviteCode': '초대 코드', 'team.joining': '참가 중...', 'team.join': '참가', diff --git a/app/src/lib/i18n/pl.ts b/app/src/lib/i18n/pl.ts index 16dc3cb13..514a8aac3 100644 --- a/app/src/lib/i18n/pl.ts +++ b/app/src/lib/i18n/pl.ts @@ -1825,6 +1825,8 @@ const messages: TranslationMap = { 'team.teamName': 'Nazwa zespołu', 'team.creating': 'Tworzenie...', 'team.joinExistingTeam': 'Dołącz do istniejącego zespołu', + 'team.personalAutoCreatedNote': + 'Twój zespół osobisty jest tworzony automatycznie. Aby współpracować, dołącz do istniejącego zespołu, używając kodu zaproszenia poniżej.', 'team.inviteCode': 'Kod zaproszenia', 'team.joining': 'Dołączanie...', 'team.join': 'Dołącz', diff --git a/app/src/lib/i18n/pt.ts b/app/src/lib/i18n/pt.ts index 2dc21b516..9da27b938 100644 --- a/app/src/lib/i18n/pt.ts +++ b/app/src/lib/i18n/pt.ts @@ -1844,6 +1844,8 @@ const messages: TranslationMap = { 'team.teamName': 'Nome da equipe', 'team.creating': 'Criando...', 'team.joinExistingTeam': 'Entrar em Equipe Existente', + 'team.personalAutoCreatedNote': + 'Sua equipe pessoal é criada automaticamente. Para colaborar, entre em uma equipe existente com um código de convite abaixo.', 'team.inviteCode': 'Código de convite', 'team.joining': 'Entrando...', 'team.join': 'Entrar', diff --git a/app/src/lib/i18n/ru.ts b/app/src/lib/i18n/ru.ts index f7c1d95ed..f48d2b405 100644 --- a/app/src/lib/i18n/ru.ts +++ b/app/src/lib/i18n/ru.ts @@ -1820,6 +1820,8 @@ const messages: TranslationMap = { 'team.teamName': 'Название команды', 'team.creating': 'Создание...', 'team.joinExistingTeam': 'Вступить в существующую команду', + 'team.personalAutoCreatedNote': + 'Ваша личная команда создаётся автоматически. Чтобы работать вместе, присоединитесь к существующей команде по коду приглашения ниже.', 'team.inviteCode': 'Код приглашения', 'team.joining': 'Вход...', 'team.join': 'Вступить', diff --git a/app/src/lib/i18n/zh-CN.ts b/app/src/lib/i18n/zh-CN.ts index caad352a7..0ad05df7a 100644 --- a/app/src/lib/i18n/zh-CN.ts +++ b/app/src/lib/i18n/zh-CN.ts @@ -1709,6 +1709,8 @@ const messages: TranslationMap = { 'team.teamName': '团队名称', 'team.creating': '创建中...', 'team.joinExistingTeam': '加入已有团队', + 'team.personalAutoCreatedNote': + '您的个人团队会自动创建。如需协作,请使用下方的邀请码加入现有团队。', 'team.inviteCode': '邀请码', 'team.joining': '加入中...', 'team.join': '加入',