diff --git a/app/src/agentworld/pages/AgentWorld.tsx b/app/src/agentworld/pages/AgentWorld.tsx index 801c4af76..7b2f9e0ef 100644 --- a/app/src/agentworld/pages/AgentWorld.tsx +++ b/app/src/agentworld/pages/AgentWorld.tsx @@ -25,6 +25,7 @@ import LedgerSection from './LedgerSection'; import MarketplaceSection from './MarketplaceSection'; import MessagingSection from './MessagingSection'; import ProfilesSection from './ProfilesSection'; +import ProfileViewer from './ProfileViewer'; import WelcomeSection from './WelcomeSection'; import WorldSection from './WorldSection'; @@ -112,9 +113,12 @@ export default function AgentWorld() { // Derive the active slug from the current sub-path // e.g. /agent-world/explore → 'explore' - const pathParts = location.pathname.split('/'); - const activeSlug = pathParts[pathParts.length - 1] || 'welcome'; - const activeSection = activeSlug === 'agent-world' ? 'welcome' : activeSlug; + // Take the FIRST segment after `/agent-world`, not the last — nested routes + // like `/agent-world/profiles/:username` must still resolve to the `profiles` + // section (keeping the sidebar selection) rather than the trailing param. + const pathParts = location.pathname.split('/').filter(Boolean); + const awIndex = pathParts.indexOf('agent-world'); + const activeSection = (awIndex >= 0 ? pathParts[awIndex + 1] : undefined) || 'welcome'; const isWorld = activeSection === 'world'; // The welcome landing sits flush (no framed card) so its centered pitch reads // as part of the page, not boxed inside a panel. @@ -174,6 +178,8 @@ export default function AgentWorld() { {/* === AGENT-WORLD SECTION ROUTES (append one per section) === */} } /> } /> + {/* Public viewer for an arbitrary handle (own/other users/agents) — #4931 */} + } /> } /> } /> } /> diff --git a/app/src/agentworld/pages/ProfileViewer.test.tsx b/app/src/agentworld/pages/ProfileViewer.test.tsx new file mode 100644 index 000000000..67d78f8d6 --- /dev/null +++ b/app/src/agentworld/pages/ProfileViewer.test.tsx @@ -0,0 +1,182 @@ +/** + * Tests for ProfileViewer — the Agent World public profile viewer (#4931). + * + * The viewer renders an ARBITRARY handle's profile via `graphql.profile`, with a + * follow/unfollow button (`follows.follow`/`unfollow`, follow-state from + * `follows.following`) and a copy-link affordance. apiClient + wallet are mocked; + * all handles/ids are generic placeholders. These behaviours do not exist before + * this change (the route + component are new), so the file is the regression. + */ +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { MemoryRouter, Route, Routes } from 'react-router-dom'; +import { beforeEach, describe, expect, test, vi } from 'vitest'; + +import { type GqlProfile } from '../../lib/agentworld/invokeApiClient'; +import { fetchWalletStatus } from '../../services/walletApi'; +import { apiClient } from '../AgentWorldShell'; +import ProfileViewer from './ProfileViewer'; + +vi.mock('../AgentWorldShell', () => ({ + apiClient: { + graphql: { profile: vi.fn(), agentCard: vi.fn() }, + follows: { follow: vi.fn(), unfollow: vi.fn(), stats: vi.fn() }, + }, +})); +vi.mock('../../services/walletApi', () => ({ fetchWalletStatus: vi.fn() })); + +const graphqlProfile = vi.mocked(apiClient.graphql.profile); +const graphqlAgentCard = vi.mocked(apiClient.graphql.agentCard); +const followsFollow = vi.mocked(apiClient.follows.follow); +const followsUnfollow = vi.mocked(apiClient.follows.unfollow); +const followsStats = vi.mocked(apiClient.follows.stats); +const walletStatus = vi.mocked(fetchWalletStatus); + +/** Agent card carrying the signer-aware follow flag the viewer reads. */ +function agentCardFollowing(viewerIsFollowing: boolean) { + return { agentId: PROFILE_ADDR, viewerIsFollowing }; +} + +const PROFILE_ADDR = 'ProfiLeSoLanaAddr00000000001'; +const VIEWER_ADDR = 'ViewerSoLanaAddr00000000002'; + +function makeProfile(overrides: Partial = {}): GqlProfile { + return { + cryptoId: PROFILE_ADDR, + actorType: 'agent', + displayName: 'Alice Agent', + bio: 'An autonomous test agent.', + private: false, + createdAt: '2026-01-02T00:00:00Z', + updatedAt: '2026-01-02T00:00:00Z', + verified: true, + attestations: [], + agentCard: null, + identities: [ + { + username: 'alice', + cryptoId: PROFILE_ADDR, + publicKey: 'pk', + registeredAt: '2026-01-02T00:00:00Z', + expiresAt: '2027-01-02T00:00:00Z', + status: 'active', + updatedAt: '2026-01-02T00:00:00Z', + primary: true, + }, + ], + ...overrides, + }; +} + +function walletWith(address: string | null) { + return { accounts: address ? [{ chain: 'solana', address }] : [] } as unknown as Awaited< + ReturnType + >; +} + +function renderViewer(username = 'alice') { + return render( + + + } /> + + + ); +} + +beforeEach(() => { + vi.clearAllMocks(); + graphqlProfile.mockResolvedValue(makeProfile()); + // Default: viewer does not follow this agent yet. + graphqlAgentCard.mockResolvedValue(agentCardFollowing(false)); + followsFollow.mockResolvedValue({ follower: VIEWER_ADDR, followee: PROFILE_ADDR, createdAt: '' }); + followsUnfollow.mockResolvedValue(undefined); + followsStats.mockResolvedValue({ agentId: PROFILE_ADDR, followerCount: 3, followingCount: 5 }); + walletStatus.mockResolvedValue(walletWith(VIEWER_ADDR)); +}); + +describe('ProfileViewer', () => { + test('renders an arbitrary handle profile (not the wallet owner)', async () => { + renderViewer('alice'); + // Looked up by the route param, not the wallet. + await waitFor(() => expect(graphqlProfile).toHaveBeenCalledWith('alice')); + // '@alice' shows in the header and again in the owned-handles list; assert + // it renders at all (not a specific count) so the query stays unambiguous. + expect((await screen.findAllByText('@alice')).length).toBeGreaterThan(0); + expect(screen.getByText('An autonomous test agent.')).toBeInTheDocument(); + // Follower stats load in their own effect (a second async tick after the + // profile), so await rather than assert synchronously. + expect(await screen.findByText('3')).toBeInTheDocument(); + }); + + test('shows a not-found state when the profile does not exist', async () => { + graphqlProfile.mockResolvedValue(null); + renderViewer('ghost'); + expect(await screen.findByText(/profile not found/i)).toBeInTheDocument(); + }); + + test('follow button follows then unfollows another user', async () => { + const user = userEvent.setup(); + renderViewer('alice'); + + // Button appears once the wallet resolves and follow-state loads. + const followBtn = await screen.findByRole('button', { name: 'Follow' }); + await waitFor(() => expect(followBtn).toBeEnabled()); + + // Follower count starts at 3 (from follows.stats). + expect(await screen.findByText('3')).toBeInTheDocument(); + + await user.click(followBtn); + expect(followsFollow).toHaveBeenCalledWith(PROFILE_ADDR); + const followingBtn = await screen.findByRole('button', { name: 'Following' }); + // Count tracks the follow optimistically: 3 -> 4. + expect(await screen.findByText('4')).toBeInTheDocument(); + + await user.click(followingBtn); + expect(followsUnfollow).toHaveBeenCalledWith(PROFILE_ADDR); + expect(await screen.findByRole('button', { name: 'Follow' })).toBeInTheDocument(); + // ...and back to 3 on unfollow. + expect(await screen.findByText('3')).toBeInTheDocument(); + }); + + test('pre-selects the following state from the agent card follow flag', async () => { + graphqlAgentCard.mockResolvedValue(agentCardFollowing(true)); + renderViewer('alice'); + // viewerIsFollowing:true → button shows Following without a click. + expect(await screen.findByRole('button', { name: 'Following' })).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Follow' })).not.toBeInTheDocument(); + }); + + test('hides the follow button when the follow relationship is unknown', async () => { + // No agent card (e.g. a non-agent profile) → no follow flag → the button is + // hidden rather than defaulting to an inferred state. + graphqlAgentCard.mockResolvedValue(null); + renderViewer('alice'); + await screen.findByTestId('profile-copy-link'); + expect(screen.queryByRole('button', { name: 'Follow' })).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Following' })).not.toBeInTheDocument(); + }); + + test('hides the follow button and marks self when viewing own profile', async () => { + walletStatus.mockResolvedValue(walletWith(PROFILE_ADDR)); + renderViewer('alice'); + expect(await screen.findByText(/this is your profile/i)).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Follow' })).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Following' })).not.toBeInTheDocument(); + }); + + test('copy-link affordance copies the shareable deep link', async () => { + // Install a clipboard spy and drive the click with fireEvent — NOT + // userEvent, whose setup() replaces navigator.clipboard with its own stub + // and would shadow this spy. + const writeText = vi.fn().mockResolvedValue(undefined); + Object.defineProperty(navigator, 'clipboard', { value: { writeText }, configurable: true }); + + renderViewer('alice'); + const copyBtn = await screen.findByTestId('profile-copy-link'); + fireEvent.click(copyBtn); + + await waitFor(() => expect(writeText).toHaveBeenCalled()); + expect(writeText.mock.calls[0][0] as string).toContain('#/agent-world/profiles/alice'); + }); +}); diff --git a/app/src/agentworld/pages/ProfileViewer.tsx b/app/src/agentworld/pages/ProfileViewer.tsx new file mode 100644 index 000000000..38dc6f512 --- /dev/null +++ b/app/src/agentworld/pages/ProfileViewer.tsx @@ -0,0 +1,508 @@ +/** + * ProfileViewer — Agent World public profile viewer for an ARBITRARY handle. + * + * Route: `/agent-world/profiles/:username`. Where `ProfilesSection` is hard-wired + * to the connected wallet, this viewer renders any user's or agent's profile via + * the already-plumbed read handlers: `graphql.profile(username)` (a rich + * `GqlProfile`) and `graphql.agentCard(cryptoId)` (the agent card + the + * signer-aware `viewerIsFollowing` flag), plus `follows.stats`. It adds a + * follow / unfollow button (reusing `follows.follow` / `follows.unfollow`) and a + * copy-link affordance so a profile is shareable. + * + * Read-only. Profile EDITING lives in `ProfilesSection` and is tracked + * separately (#4930); this component never mutates the viewed profile. + */ +import debug from 'debug'; +import { useCallback, useEffect, useState } from 'react'; +import { useParams } from 'react-router-dom'; + +import PanelScaffold from '../../components/layout/PanelScaffold'; +import Button from '../../components/ui/Button'; +import { + type AgentCard, + type FollowStats, + type GqlAttestation, + type GqlProfile, + type Identity, + PaymentRequiredError, +} from '../../lib/agentworld/invokeApiClient'; +import { useT } from '../../lib/i18n/I18nContext'; +import { fetchWalletStatus } from '../../services/walletApi'; +import { apiClient } from '../AgentWorldShell'; + +const log = debug('agentworld:profileviewer'); + +// ── Helpers ───────────────────────────────────────────────────────────────────── + +/** Sanitized error label for diagnostics — an error NAME/kind only, never the + * raw message (which may carry backend internals / PII). */ +function errorKind(err: unknown): string { + if (err instanceof PaymentRequiredError) return 'payment_required'; + if (err instanceof Error) return err.name || 'Error'; + return 'unknown'; +} + +function truncateCryptoId(cryptoId: string): string { + if (cryptoId.length <= 12) return cryptoId; + return `${cryptoId.slice(0, 6)}…${cryptoId.slice(-4)}`; +} + +/** Format an ISO date using the runtime locale (never a hard-coded language). */ +function formatDate(iso: string): string { + const d = new Date(iso); + if (Number.isNaN(d.getTime())) return iso; + return d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' }); +} + +/** Pick the primary identity, else the first. */ +function pickPrimary(identities: T[]): T | undefined { + return identities.find(i => i.primary) ?? identities[0]; +} + +/** Read the signer-aware `viewerIsFollowing` flag off an agent card, if present. */ +function readViewerFollows(card: AgentCard | null): boolean | null { + const value = card?.['viewerIsFollowing']; + return typeof value === 'boolean' ? value : null; +} + +/** Resolve the wallet's Solana address (the viewer's tiny.place cryptoId). */ +function useMyAgentId(): string | null { + const [agentId, setAgentId] = useState(null); + useEffect(() => { + let cancelled = false; + log('[agentworld:profileviewer] resolving wallet agent id'); + void fetchWalletStatus() + .then(status => { + if (cancelled) return; + const solana = (status.accounts ?? []).find(a => a.chain === 'solana'); + if (solana?.address) setAgentId(solana.address); + else log('[agentworld:profileviewer] no solana wallet account; viewer is signed out'); + }) + .catch((err: unknown) => { + if (!cancelled) log('[agentworld:profileviewer] wallet resolve failed: %s', errorKind(err)); + }); + return () => { + cancelled = true; + }; + }, []); + return agentId; +} + +// ── Profile fetch ──────────────────────────────────────────────────────────────── + +type ViewerState = + | { status: 'loading' } + | { status: 'not_found' } + | { status: 'error' } + | { status: 'ok'; profile: GqlProfile }; + +/** + * Load the target handle's public profile, keyed by the normalized handle so a + * stale response for a previous handle can never surface the wrong profile. The + * effect never calls setState synchronously — the only state writes happen in + * the async resolve/reject, satisfying the repo's no-sync-setState-in-effect + * rule. `null` from the server → not found. + */ +function useProfile(handle: string): ViewerState { + const [entry, setEntry] = useState<{ key: string; state: ViewerState }>({ + key: '', + state: { status: 'loading' }, + }); + + useEffect(() => { + if (!handle) return; + let cancelled = false; + log('[agentworld:profileviewer] loading profile'); + void apiClient.graphql + .profile(handle) + .then(profile => { + if (cancelled) return; + log('[agentworld:profileviewer] profile %s', profile ? 'loaded' : 'not found'); + setEntry({ + key: handle, + state: profile ? { status: 'ok', profile } : { status: 'not_found' }, + }); + }) + .catch((err: unknown) => { + if (cancelled) return; + log('[agentworld:profileviewer] profile load failed: %s', errorKind(err)); + setEntry({ key: handle, state: { status: 'error' } }); + }); + return () => { + cancelled = true; + }; + }, [handle]); + + if (!handle) return { status: 'not_found' }; + // Until THIS handle's response lands, show loading — prevents a previous + // handle's profile (and its follow/share actions) from flashing. + return entry.key === handle ? entry.state : { status: 'loading' }; +} + +// ── Agent card (also carries the signer-aware follow flag) ─────────────────────── + +/** Fetch the agent card for `cryptoId`. Returns `null` for non-agent profiles or + * on failure — callers degrade to "unknown". */ +function useAgentCard(cryptoId: string): AgentCard | null { + const [card, setCard] = useState(null); + useEffect(() => { + if (!cryptoId) return; + let cancelled = false; + log('[agentworld:profileviewer] loading agent card'); + void apiClient.graphql + .agentCard(cryptoId) + .then(c => { + if (!cancelled) setCard(c); + }) + .catch((err: unknown) => { + if (!cancelled) + log('[agentworld:profileviewer] agent card load failed: %s', errorKind(err)); + }); + return () => { + cancelled = true; + }; + }, [cryptoId]); + return card; +} + +// ── Follow control ───────────────────────────────────────────────────────────── + +type FollowState = 'unknown' | 'following' | 'not_following'; + +/** + * Follow relationship for `targetCryptoId`, derived ONLY from the direct, + * signer-aware `viewerFollowsHint` (from the agent card). It never infers a + * relationship from partial or failed data: when the hint is absent it stays + * `'unknown'` and the button is hidden, rather than defaulting to "not + * following" and risking a follow() on an existing relationship. A user toggle + * takes optimistic precedence via `override`. + */ +function useFollow( + myAgentId: string | null, + targetCryptoId: string, + viewerFollowsHint: boolean | null, + onFollowChange?: (next: 'following' | 'not_following') => void +) { + const [override, setOverride] = useState<'following' | 'not_following' | null>(null); + const [busy, setBusy] = useState(false); + + const isSelf = myAgentId != null && myAgentId === targetCryptoId; + const enabled = myAgentId != null && targetCryptoId !== '' && !isSelf; + const state: FollowState = + override ?? + (viewerFollowsHint == null ? 'unknown' : viewerFollowsHint ? 'following' : 'not_following'); + + const toggle = useCallback(async () => { + if (busy || !enabled || state === 'unknown') return; + setBusy(true); + const next = state === 'following' ? 'not_following' : 'following'; + try { + if (state === 'following') await apiClient.follows.unfollow(targetCryptoId); + else await apiClient.follows.follow(targetCryptoId); + setOverride(next); + // Keep the follower count in step with the button (it is fetched once and + // would otherwise lag behind after a follow/unfollow). + onFollowChange?.(next); + // No PII: only the resulting action, never the address/handle. + log('[agentworld:profileviewer] follow toggled -> %s', next); + } catch (err) { + log('[agentworld:profileviewer] follow toggle failed: %s', errorKind(err)); + } finally { + setBusy(false); + } + }, [busy, enabled, state, targetCryptoId, onFollowChange]); + + return { state, busy, isSelf, enabled, toggle }; +} + +// ── Follow stats sub-hook ──────────────────────────────────────────────────────── + +function useFollowStats(cryptoId: string): FollowStats | null { + const [stats, setStats] = useState(null); + useEffect(() => { + if (!cryptoId) return; + let cancelled = false; + void apiClient.follows + .stats(cryptoId) + .then(s => { + if (!cancelled) setStats(s); + }) + .catch((err: unknown) => { + if (!cancelled) + log('[agentworld:profileviewer] follow stats load failed: %s', errorKind(err)); + }); + return () => { + cancelled = true; + }; + }, [cryptoId]); + return stats; +} + +// ── Presentational bits ────────────────────────────────────────────────────────── + +function StatusBlock({ tone, title, body }: { tone: string; title: string; body?: string }) { + return ( +
+

{title}

+ {body &&

{body}

} +
+ ); +} + +// ── Main export ────────────────────────────────────────────────────────────────── + +export default function ProfileViewer() { + const { username } = useParams<{ username: string }>(); + const { t } = useT(); + const routeHandle = (username ?? '').replace(/^@+/, '').trim(); + const state = useProfile(routeHandle); + + let body: React.ReactNode; + if (state.status === 'loading') { + body = ( +
+ {t('agentWorld.profileViewer.loading')} +
+ ); + } else if (state.status === 'not_found') { + body = ( + + ); + } else if (state.status === 'error') { + // Generic, translated copy only — never the raw external error string. + body = ( + + ); + } else { + body = ; + } + + return ( + {body} + ); +} + +// ── Profile card ───────────────────────────────────────────────────────────────── + +function ProfileCard({ profile, routeHandle }: { profile: GqlProfile; routeHandle: string }) { + const { t } = useT(); + const myAgentId = useMyAgentId(); + const cryptoId = profile.cryptoId; + const agentCard = useAgentCard(cryptoId); + // Optimistic follower-count adjustment so the count tracks the follow button + // (stats are fetched once and would otherwise lag after a toggle). + const [followerDelta, setFollowerDelta] = useState(0); + const follow = useFollow(myAgentId, cryptoId, readViewerFollows(agentCard), next => + setFollowerDelta(d => d + (next === 'following' ? 1 : -1)) + ); + const followStats = useFollowStats(cryptoId); + const [copied, setCopied] = useState(false); + // Fall back to the initials monogram if the avatar image fails to load. + const [avatarBroken, setAvatarBroken] = useState(false); + + const primaryIdentity = pickPrimary(profile.identities ?? []); + const primaryUsername = primaryIdentity?.username ?? null; + const hasHandle = primaryUsername !== null; + const usernameClean = (primaryUsername ?? profile.displayName ?? '').replace(/^@+/, ''); + const handle = hasHandle ? `@${usernameClean}` : usernameClean; + const displayName = profile.displayName || usernameClean || '?'; + const initials = displayName.slice(0, 2).toUpperCase(); + const skills = profile.tags ?? []; + const attestations: GqlAttestation[] = profile.attestations ?? []; + const ownedIdentities: Identity[] = profile.identities ?? []; + const actorType = profile.actorType ?? ''; + const isHuman = actorType.toLowerCase() === 'human'; + const showFollow = follow.enabled && follow.state !== 'unknown'; + // Read-only agent-card summary (only when it adds something over the profile). + const cardDescription = (agentCard?.description ?? '').trim(); + + const copyLink = useCallback(() => { + // Build the deep link from the ROUTE handle (what the user actually + // navigated to), not from `identities`/`displayName` — those may be null or + // differ from the selected route, producing a link that does not resolve. + const { origin, pathname } = window.location; + const link = `${origin}${pathname}#/agent-world/profiles/${encodeURIComponent(routeHandle)}`; + void navigator.clipboard + ?.writeText(link) + .then(() => { + setCopied(true); + log('[agentworld:profileviewer] copied share link'); + window.setTimeout(() => setCopied(false), 2000); + }) + .catch((err: unknown) => { + log('[agentworld:profileviewer] copy link failed: %s', errorKind(err)); + }); + }, [routeHandle]); + + return ( +
+ {/* Header row: identity + actions */} +
+ {profile.avatarUrl && !avatarBroken ? ( + {displayName} setAvatarBroken(true)} + className="h-14 w-14 shrink-0 rounded-full object-cover" + /> + ) : ( +
+ {initials} +
+ )} +
+

+ {handle} + {profile.verified && ( + + ✓ + + )} + {actorType && ( + + {isHuman + ? t('agentWorld.profileViewer.humanBadge') + : t('agentWorld.profileViewer.agentBadge')} + + )} +

+ {cryptoId && ( +

+ {truncateCryptoId(cryptoId)} +

+ )} + {profile.bio && ( +

{profile.bio}

+ )} +
+ + {/* Actions */} +
+ {follow.isSelf ? ( + + {t('agentWorld.profileViewer.ownProfile')} + + ) : showFollow ? ( + + ) : null} + +
+
+ + {cardDescription && cardDescription !== (profile.bio ?? '').trim() && ( +
+

+ {t('agentWorld.profileViewer.agentCard')} +

+

{cardDescription}

+
+ )} + + {skills.length > 0 && ( +
+

+ {t('agentWorld.profileViewer.skills')} +

+
+ {skills.map(skill => ( + + {skill} + + ))} +
+
+ )} + + {attestations.length > 0 && ( +
+

+ {t('agentWorld.profileViewer.verifiedAccounts')} +

+
+ {attestations.map(a => ( + + {a.platform}: {a.handle} + + ))} +
+
+ )} + + {ownedIdentities.length > 0 && ( +
+

+ {t('agentWorld.profileViewer.handlesOwned')} +

+
+ {ownedIdentities.map(id => ( + + @{id.username.replace(/^@+/, '')} + + ))} +
+
+ )} + + {followStats && ( +
+
+
+ + {Math.max(0, followStats.followerCount + followerDelta)} + + + {t('agentWorld.profileViewer.followers')} + +
+
+ + {followStats.followingCount} + + + {t('agentWorld.profileViewer.followingCount')} + +
+
+
+ )} + + {profile.createdAt && ( +
+ + {t('agentWorld.profileViewer.joined')} {formatDate(profile.createdAt)} + +
+ )} +
+ ); +} diff --git a/app/src/lib/i18n/ar.ts b/app/src/lib/i18n/ar.ts index 8df7581d5..ca7db0220 100644 --- a/app/src/lib/i18n/ar.ts +++ b/app/src/lib/i18n/ar.ts @@ -443,6 +443,25 @@ const messages: TranslationMap = { 'agentWorld.feed.loadingMore': 'جارٍ تحميل المزيد…', 'agentWorld.feed.loadMoreError': 'تعذّر تحميل المزيد من المنشورات. حاول مرة أخرى.', 'agentWorld.ledger': 'السجل', + 'agentWorld.profileViewer.description': 'الملف الشخصي العام', + 'agentWorld.profileViewer.agentCard': 'بطاقة الوكيل', + 'agentWorld.profileViewer.loading': 'جارٍ تحميل الملف الشخصي…', + 'agentWorld.profileViewer.notFoundTitle': 'الملف الشخصي غير موجود', + 'agentWorld.profileViewer.notFoundBody': 'لا يوجد ملف شخصي منشور لهذا المعرّف حتى الآن.', + 'agentWorld.profileViewer.errorTitle': 'تعذّر تحميل الملف الشخصي', + 'agentWorld.profileViewer.follow': 'متابعة', + 'agentWorld.profileViewer.following': 'تتابعه', + 'agentWorld.profileViewer.copyLink': 'نسخ الرابط', + 'agentWorld.profileViewer.linkCopied': 'تم نسخ الرابط', + 'agentWorld.profileViewer.skills': 'المهارات', + 'agentWorld.profileViewer.verifiedAccounts': 'الحسابات الموثّقة', + 'agentWorld.profileViewer.handlesOwned': 'المعرّفات المملوكة', + 'agentWorld.profileViewer.followers': 'متابِعون', + 'agentWorld.profileViewer.followingCount': 'يتابع', + 'agentWorld.profileViewer.joined': 'انضم في', + 'agentWorld.profileViewer.ownProfile': 'هذا ملفك الشخصي', + 'agentWorld.profileViewer.agentBadge': 'وكيل', + 'agentWorld.profileViewer.humanBadge': 'إنسان', 'agentWorld.ledger.loadMore': 'تحميل المزيد', 'agentWorld.ledger.loadingMore': 'جارٍ تحميل المزيد…', 'agentWorld.ledger.loadMoreError': 'تعذّر تحميل المزيد من المعاملات. حاول مرة أخرى.', diff --git a/app/src/lib/i18n/bn.ts b/app/src/lib/i18n/bn.ts index 2951c3c21..108f29635 100644 --- a/app/src/lib/i18n/bn.ts +++ b/app/src/lib/i18n/bn.ts @@ -459,6 +459,25 @@ const messages: TranslationMap = { 'agentWorld.feed.loadingMore': 'আরও লোড হচ্ছে…', 'agentWorld.feed.loadMoreError': 'আরও পোস্ট লোড করা যায়নি। আবার চেষ্টা করুন।', 'agentWorld.ledger': 'লেজার', + 'agentWorld.profileViewer.description': 'সর্বজনীন প্রোফাইল', + 'agentWorld.profileViewer.agentCard': 'এজেন্ট কার্ড', + 'agentWorld.profileViewer.loading': 'প্রোফাইল লোড হচ্ছে…', + 'agentWorld.profileViewer.notFoundTitle': 'প্রোফাইল পাওয়া যায়নি', + 'agentWorld.profileViewer.notFoundBody': 'এই হ্যান্ডেলের জন্য এখনও কোনো প্রকাশিত প্রোফাইল নেই।', + 'agentWorld.profileViewer.errorTitle': 'প্রোফাইল লোড করা যায়নি', + 'agentWorld.profileViewer.follow': 'অনুসরণ', + 'agentWorld.profileViewer.following': 'অনুসরণ করছেন', + 'agentWorld.profileViewer.copyLink': 'লিঙ্ক কপি করুন', + 'agentWorld.profileViewer.linkCopied': 'লিঙ্ক কপি হয়েছে', + 'agentWorld.profileViewer.skills': 'দক্ষতা', + 'agentWorld.profileViewer.verifiedAccounts': 'যাচাই করা অ্যাকাউন্ট', + 'agentWorld.profileViewer.handlesOwned': 'মালিকানাধীন হ্যান্ডেল', + 'agentWorld.profileViewer.followers': 'অনুসারী', + 'agentWorld.profileViewer.followingCount': 'অনুসরণ করছেন', + 'agentWorld.profileViewer.joined': 'যোগ দিয়েছেন', + 'agentWorld.profileViewer.ownProfile': 'এটি আপনার প্রোফাইল', + 'agentWorld.profileViewer.agentBadge': 'এজেন্ট', + 'agentWorld.profileViewer.humanBadge': 'মানুষ', 'agentWorld.ledger.loadMore': 'আরও লোড করুন', 'agentWorld.ledger.loadingMore': 'আরও লোড হচ্ছে…', 'agentWorld.ledger.loadMoreError': 'আরও লেনদেন লোড করা যায়নি। আবার চেষ্টা করুন।', diff --git a/app/src/lib/i18n/de.ts b/app/src/lib/i18n/de.ts index 74a78c31d..5ecd87f8d 100644 --- a/app/src/lib/i18n/de.ts +++ b/app/src/lib/i18n/de.ts @@ -484,6 +484,26 @@ const messages: TranslationMap = { 'agentWorld.feed.loadMoreError': 'Weitere Beiträge konnten nicht geladen werden. Bitte erneut versuchen.', 'agentWorld.ledger': 'Kontobuch', + 'agentWorld.profileViewer.description': 'Öffentliches Profil', + 'agentWorld.profileViewer.agentCard': 'Agentenkarte', + 'agentWorld.profileViewer.loading': 'Profil wird geladen…', + 'agentWorld.profileViewer.notFoundTitle': 'Profil nicht gefunden', + 'agentWorld.profileViewer.notFoundBody': + 'Für dieses Handle gibt es noch kein veröffentlichtes Profil.', + 'agentWorld.profileViewer.errorTitle': 'Profil konnte nicht geladen werden', + 'agentWorld.profileViewer.follow': 'Folgen', + 'agentWorld.profileViewer.following': 'Folge ich', + 'agentWorld.profileViewer.copyLink': 'Link kopieren', + 'agentWorld.profileViewer.linkCopied': 'Link kopiert', + 'agentWorld.profileViewer.skills': 'Fähigkeiten', + 'agentWorld.profileViewer.verifiedAccounts': 'Verifizierte Konten', + 'agentWorld.profileViewer.handlesOwned': 'Eigene Handles', + 'agentWorld.profileViewer.followers': 'Follower', + 'agentWorld.profileViewer.followingCount': 'folgt', + 'agentWorld.profileViewer.joined': 'Beigetreten', + 'agentWorld.profileViewer.ownProfile': 'Das ist dein Profil', + 'agentWorld.profileViewer.agentBadge': 'Agent', + 'agentWorld.profileViewer.humanBadge': 'Mensch', 'agentWorld.ledger.loadMore': 'Mehr laden', 'agentWorld.ledger.loadingMore': 'Wird geladen…', 'agentWorld.ledger.loadMoreError': diff --git a/app/src/lib/i18n/en.ts b/app/src/lib/i18n/en.ts index 63f6daf06..88a4b8fb2 100644 --- a/app/src/lib/i18n/en.ts +++ b/app/src/lib/i18n/en.ts @@ -188,6 +188,25 @@ const en: TranslationMap = { 'agentWorld.feed.loadingMore': 'Loading more…', 'agentWorld.feed.loadMoreError': 'Could not load more posts. Try again.', 'agentWorld.ledger': 'Ledger', + 'agentWorld.profileViewer.description': 'Public profile', + 'agentWorld.profileViewer.agentCard': 'Agent card', + 'agentWorld.profileViewer.loading': 'Loading profile…', + 'agentWorld.profileViewer.notFoundTitle': 'Profile not found', + 'agentWorld.profileViewer.notFoundBody': 'No published profile exists for this handle yet.', + 'agentWorld.profileViewer.errorTitle': 'Failed to load profile', + 'agentWorld.profileViewer.follow': 'Follow', + 'agentWorld.profileViewer.following': 'Following', + 'agentWorld.profileViewer.copyLink': 'Copy link', + 'agentWorld.profileViewer.linkCopied': 'Link copied', + 'agentWorld.profileViewer.skills': 'Skills', + 'agentWorld.profileViewer.verifiedAccounts': 'Verified accounts', + 'agentWorld.profileViewer.handlesOwned': 'Handles owned', + 'agentWorld.profileViewer.followers': 'followers', + 'agentWorld.profileViewer.followingCount': 'following', + 'agentWorld.profileViewer.joined': 'Joined', + 'agentWorld.profileViewer.ownProfile': 'This is your profile', + 'agentWorld.profileViewer.agentBadge': 'Agent', + 'agentWorld.profileViewer.humanBadge': 'Human', 'agentWorld.ledger.loadMore': 'Load more', 'agentWorld.ledger.loadingMore': 'Loading more…', 'agentWorld.ledger.loadMoreError': 'Could not load more transactions. Try again.', diff --git a/app/src/lib/i18n/es.ts b/app/src/lib/i18n/es.ts index 4680e29e6..f473afc9a 100644 --- a/app/src/lib/i18n/es.ts +++ b/app/src/lib/i18n/es.ts @@ -469,6 +469,26 @@ const messages: TranslationMap = { 'agentWorld.feed.loadingMore': 'Cargando más…', 'agentWorld.feed.loadMoreError': 'No se pudieron cargar más publicaciones. Inténtalo de nuevo.', 'agentWorld.ledger': 'Libro mayor', + 'agentWorld.profileViewer.description': 'Perfil público', + 'agentWorld.profileViewer.agentCard': 'Tarjeta del agente', + 'agentWorld.profileViewer.loading': 'Cargando perfil…', + 'agentWorld.profileViewer.notFoundTitle': 'Perfil no encontrado', + 'agentWorld.profileViewer.notFoundBody': + 'Todavía no existe un perfil publicado para este identificador.', + 'agentWorld.profileViewer.errorTitle': 'No se pudo cargar el perfil', + 'agentWorld.profileViewer.follow': 'Seguir', + 'agentWorld.profileViewer.following': 'Siguiendo', + 'agentWorld.profileViewer.copyLink': 'Copiar enlace', + 'agentWorld.profileViewer.linkCopied': 'Enlace copiado', + 'agentWorld.profileViewer.skills': 'Habilidades', + 'agentWorld.profileViewer.verifiedAccounts': 'Cuentas verificadas', + 'agentWorld.profileViewer.handlesOwned': 'Identificadores en propiedad', + 'agentWorld.profileViewer.followers': 'seguidores', + 'agentWorld.profileViewer.followingCount': 'siguiendo', + 'agentWorld.profileViewer.joined': 'Se unió', + 'agentWorld.profileViewer.ownProfile': 'Este es tu perfil', + 'agentWorld.profileViewer.agentBadge': 'Agente', + 'agentWorld.profileViewer.humanBadge': 'Humano', 'agentWorld.ledger.loadMore': 'Cargar más', 'agentWorld.ledger.loadingMore': 'Cargando más…', 'agentWorld.ledger.loadMoreError': 'No se pudieron cargar más transacciones. Inténtalo de nuevo.', diff --git a/app/src/lib/i18n/fr.ts b/app/src/lib/i18n/fr.ts index db1d9cd03..f281dd0a6 100644 --- a/app/src/lib/i18n/fr.ts +++ b/app/src/lib/i18n/fr.ts @@ -479,6 +479,25 @@ const messages: TranslationMap = { 'agentWorld.feed.loadingMore': 'Chargement…', 'agentWorld.feed.loadMoreError': 'Impossible de charger plus de publications. Réessayez.', 'agentWorld.ledger': 'Grand livre', + 'agentWorld.profileViewer.description': 'Profil public', + 'agentWorld.profileViewer.agentCard': 'Fiche agent', + 'agentWorld.profileViewer.loading': 'Chargement du profil…', + 'agentWorld.profileViewer.notFoundTitle': 'Profil introuvable', + 'agentWorld.profileViewer.notFoundBody': 'Aucun profil publié pour ce handle pour le moment.', + 'agentWorld.profileViewer.errorTitle': 'Échec du chargement du profil', + 'agentWorld.profileViewer.follow': 'Suivre', + 'agentWorld.profileViewer.following': 'Suivi', + 'agentWorld.profileViewer.copyLink': 'Copier le lien', + 'agentWorld.profileViewer.linkCopied': 'Lien copié', + 'agentWorld.profileViewer.skills': 'Compétences', + 'agentWorld.profileViewer.verifiedAccounts': 'Comptes vérifiés', + 'agentWorld.profileViewer.handlesOwned': 'Handles détenus', + 'agentWorld.profileViewer.followers': 'abonnés', + 'agentWorld.profileViewer.followingCount': 'abonnements', + 'agentWorld.profileViewer.joined': 'Inscrit le', + 'agentWorld.profileViewer.ownProfile': 'Ceci est votre profil', + 'agentWorld.profileViewer.agentBadge': 'Agent', + 'agentWorld.profileViewer.humanBadge': 'Humain', 'agentWorld.ledger.loadMore': 'Charger plus', 'agentWorld.ledger.loadingMore': 'Chargement…', 'agentWorld.ledger.loadMoreError': 'Impossible de charger plus de transactions. Réessayez.', diff --git a/app/src/lib/i18n/hi.ts b/app/src/lib/i18n/hi.ts index e709ef2a7..232e8a110 100644 --- a/app/src/lib/i18n/hi.ts +++ b/app/src/lib/i18n/hi.ts @@ -459,6 +459,25 @@ const messages: TranslationMap = { 'agentWorld.feed.loadingMore': 'और लोड हो रहा है…', 'agentWorld.feed.loadMoreError': 'अधिक पोस्ट लोड नहीं हो सके। फिर से प्रयास करें।', 'agentWorld.ledger': 'खाता बही', + 'agentWorld.profileViewer.description': 'सार्वजनिक प्रोफ़ाइल', + 'agentWorld.profileViewer.agentCard': 'एजेंट कार्ड', + 'agentWorld.profileViewer.loading': 'प्रोफ़ाइल लोड हो रही है…', + 'agentWorld.profileViewer.notFoundTitle': 'प्रोफ़ाइल नहीं मिली', + 'agentWorld.profileViewer.notFoundBody': 'इस हैंडल के लिए अभी तक कोई प्रकाशित प्रोफ़ाइल नहीं है।', + 'agentWorld.profileViewer.errorTitle': 'प्रोफ़ाइल लोड नहीं हो सकी', + 'agentWorld.profileViewer.follow': 'फ़ॉलो करें', + 'agentWorld.profileViewer.following': 'फ़ॉलो कर रहे हैं', + 'agentWorld.profileViewer.copyLink': 'लिंक कॉपी करें', + 'agentWorld.profileViewer.linkCopied': 'लिंक कॉपी हो गया', + 'agentWorld.profileViewer.skills': 'कौशल', + 'agentWorld.profileViewer.verifiedAccounts': 'सत्यापित खाते', + 'agentWorld.profileViewer.handlesOwned': 'स्वामित्व वाले हैंडल', + 'agentWorld.profileViewer.followers': 'फ़ॉलोअर्स', + 'agentWorld.profileViewer.followingCount': 'फ़ॉलोइंग', + 'agentWorld.profileViewer.joined': 'शामिल हुए', + 'agentWorld.profileViewer.ownProfile': 'यह आपकी प्रोफ़ाइल है', + 'agentWorld.profileViewer.agentBadge': 'एजेंट', + 'agentWorld.profileViewer.humanBadge': 'मानव', 'agentWorld.ledger.loadMore': 'और लोड करें', 'agentWorld.ledger.loadingMore': 'और लोड हो रहा है…', 'agentWorld.ledger.loadMoreError': 'अधिक लेन-देन लोड नहीं हो सके। फिर से प्रयास करें।', diff --git a/app/src/lib/i18n/id.ts b/app/src/lib/i18n/id.ts index 839ddc9de..77954eb44 100644 --- a/app/src/lib/i18n/id.ts +++ b/app/src/lib/i18n/id.ts @@ -465,6 +465,25 @@ const messages: TranslationMap = { 'agentWorld.feed.loadingMore': 'Memuat lagi…', 'agentWorld.feed.loadMoreError': 'Tidak dapat memuat postingan lainnya. Coba lagi.', 'agentWorld.ledger': 'Buku Besar', + 'agentWorld.profileViewer.description': 'Profil publik', + 'agentWorld.profileViewer.agentCard': 'Kartu agen', + 'agentWorld.profileViewer.loading': 'Memuat profil…', + 'agentWorld.profileViewer.notFoundTitle': 'Profil tidak ditemukan', + 'agentWorld.profileViewer.notFoundBody': 'Belum ada profil yang dipublikasikan untuk handel ini.', + 'agentWorld.profileViewer.errorTitle': 'Gagal memuat profil', + 'agentWorld.profileViewer.follow': 'Ikuti', + 'agentWorld.profileViewer.following': 'Mengikuti', + 'agentWorld.profileViewer.copyLink': 'Salin tautan', + 'agentWorld.profileViewer.linkCopied': 'Tautan disalin', + 'agentWorld.profileViewer.skills': 'Keterampilan', + 'agentWorld.profileViewer.verifiedAccounts': 'Akun terverifikasi', + 'agentWorld.profileViewer.handlesOwned': 'Handel yang dimiliki', + 'agentWorld.profileViewer.followers': 'pengikut', + 'agentWorld.profileViewer.followingCount': 'mengikuti', + 'agentWorld.profileViewer.joined': 'Bergabung', + 'agentWorld.profileViewer.ownProfile': 'Ini profil Anda', + 'agentWorld.profileViewer.agentBadge': 'Agen', + 'agentWorld.profileViewer.humanBadge': 'Manusia', 'agentWorld.ledger.loadMore': 'Muat lebih banyak', 'agentWorld.ledger.loadingMore': 'Memuat lagi…', 'agentWorld.ledger.loadMoreError': 'Tidak dapat memuat transaksi lainnya. Coba lagi.', diff --git a/app/src/lib/i18n/it.ts b/app/src/lib/i18n/it.ts index c922c9441..062d3539e 100644 --- a/app/src/lib/i18n/it.ts +++ b/app/src/lib/i18n/it.ts @@ -472,6 +472,26 @@ const messages: TranslationMap = { 'agentWorld.feed.loadingMore': 'Caricamento…', 'agentWorld.feed.loadMoreError': 'Impossibile caricare altri post. Riprova.', 'agentWorld.ledger': 'Registro', + 'agentWorld.profileViewer.description': 'Profilo pubblico', + 'agentWorld.profileViewer.agentCard': 'Scheda agente', + 'agentWorld.profileViewer.loading': 'Caricamento profilo…', + 'agentWorld.profileViewer.notFoundTitle': 'Profilo non trovato', + 'agentWorld.profileViewer.notFoundBody': + 'Non esiste ancora un profilo pubblicato per questo handle.', + 'agentWorld.profileViewer.errorTitle': 'Impossibile caricare il profilo', + 'agentWorld.profileViewer.follow': 'Segui', + 'agentWorld.profileViewer.following': 'Stai già seguendo', + 'agentWorld.profileViewer.copyLink': 'Copia link', + 'agentWorld.profileViewer.linkCopied': 'Link copiato', + 'agentWorld.profileViewer.skills': 'Competenze', + 'agentWorld.profileViewer.verifiedAccounts': 'Account verificati', + 'agentWorld.profileViewer.handlesOwned': 'Handle posseduti', + 'agentWorld.profileViewer.followers': 'follower', + 'agentWorld.profileViewer.followingCount': 'seguiti', + 'agentWorld.profileViewer.joined': 'Iscritto', + 'agentWorld.profileViewer.ownProfile': 'Questo è il tuo profilo', + 'agentWorld.profileViewer.agentBadge': 'Agente', + 'agentWorld.profileViewer.humanBadge': 'Umano', 'agentWorld.ledger.loadMore': 'Carica altro', 'agentWorld.ledger.loadingMore': 'Caricamento…', 'agentWorld.ledger.loadMoreError': 'Impossibile caricare altre transazioni. Riprova.', diff --git a/app/src/lib/i18n/ko.ts b/app/src/lib/i18n/ko.ts index e45921d3c..6766de9b2 100644 --- a/app/src/lib/i18n/ko.ts +++ b/app/src/lib/i18n/ko.ts @@ -452,6 +452,25 @@ const messages: TranslationMap = { 'agentWorld.feed.loadingMore': '더 불러오는 중…', 'agentWorld.feed.loadMoreError': '게시물을 더 불러오지 못했습니다. 다시 시도하세요.', 'agentWorld.ledger': '원장', + 'agentWorld.profileViewer.description': '공개 프로필', + 'agentWorld.profileViewer.agentCard': '에이전트 카드', + 'agentWorld.profileViewer.loading': '프로필 불러오는 중…', + 'agentWorld.profileViewer.notFoundTitle': '프로필을 찾을 수 없음', + 'agentWorld.profileViewer.notFoundBody': '이 핸들에 대해 게시된 프로필이 아직 없습니다.', + 'agentWorld.profileViewer.errorTitle': '프로필을 불러오지 못했습니다', + 'agentWorld.profileViewer.follow': '팔로우', + 'agentWorld.profileViewer.following': '팔로잉', + 'agentWorld.profileViewer.copyLink': '링크 복사', + 'agentWorld.profileViewer.linkCopied': '링크 복사됨', + 'agentWorld.profileViewer.skills': '기술', + 'agentWorld.profileViewer.verifiedAccounts': '인증된 계정', + 'agentWorld.profileViewer.handlesOwned': '보유한 핸들', + 'agentWorld.profileViewer.followers': '팔로워', + 'agentWorld.profileViewer.followingCount': '팔로잉', + 'agentWorld.profileViewer.joined': '가입일', + 'agentWorld.profileViewer.ownProfile': '내 프로필입니다', + 'agentWorld.profileViewer.agentBadge': '에이전트', + 'agentWorld.profileViewer.humanBadge': '사람', 'agentWorld.ledger.loadMore': '더 보기', 'agentWorld.ledger.loadingMore': '더 불러오는 중…', 'agentWorld.ledger.loadMoreError': '거래를 더 불러오지 못했습니다. 다시 시도하세요.', diff --git a/app/src/lib/i18n/pl.ts b/app/src/lib/i18n/pl.ts index 79754df75..241a1b624 100644 --- a/app/src/lib/i18n/pl.ts +++ b/app/src/lib/i18n/pl.ts @@ -470,6 +470,26 @@ const messages: TranslationMap = { 'agentWorld.feed.loadingMore': 'Ładowanie…', 'agentWorld.feed.loadMoreError': 'Nie udało się załadować kolejnych postów. Spróbuj ponownie.', 'agentWorld.ledger': 'Księga', + 'agentWorld.profileViewer.description': 'Profil publiczny', + 'agentWorld.profileViewer.agentCard': 'Karta agenta', + 'agentWorld.profileViewer.loading': 'Ładowanie profilu…', + 'agentWorld.profileViewer.notFoundTitle': 'Nie znaleziono profilu', + 'agentWorld.profileViewer.notFoundBody': + 'Dla tej nazwy użytkownika nie ma jeszcze opublikowanego profilu.', + 'agentWorld.profileViewer.errorTitle': 'Nie udało się załadować profilu', + 'agentWorld.profileViewer.follow': 'Obserwuj', + 'agentWorld.profileViewer.following': 'Obserwujesz', + 'agentWorld.profileViewer.copyLink': 'Kopiuj link', + 'agentWorld.profileViewer.linkCopied': 'Link skopiowany', + 'agentWorld.profileViewer.skills': 'Umiejętności', + 'agentWorld.profileViewer.verifiedAccounts': 'Zweryfikowane konta', + 'agentWorld.profileViewer.handlesOwned': 'Posiadane nazwy użytkownika', + 'agentWorld.profileViewer.followers': 'obserwujący', + 'agentWorld.profileViewer.followingCount': 'obserwowani', + 'agentWorld.profileViewer.joined': 'Dołączono', + 'agentWorld.profileViewer.ownProfile': 'To jest Twój profil', + 'agentWorld.profileViewer.agentBadge': 'Agent', + 'agentWorld.profileViewer.humanBadge': 'Człowiek', 'agentWorld.ledger.loadMore': 'Załaduj więcej', 'agentWorld.ledger.loadingMore': 'Ładowanie…', 'agentWorld.ledger.loadMoreError': diff --git a/app/src/lib/i18n/pt.ts b/app/src/lib/i18n/pt.ts index fba1a7752..928d198ef 100644 --- a/app/src/lib/i18n/pt.ts +++ b/app/src/lib/i18n/pt.ts @@ -464,6 +464,26 @@ const messages: TranslationMap = { 'agentWorld.feed.loadingMore': 'Carregando mais…', 'agentWorld.feed.loadMoreError': 'Não foi possível carregar mais publicações. Tente novamente.', 'agentWorld.ledger': 'Livro-razão', + 'agentWorld.profileViewer.description': 'Perfil público', + 'agentWorld.profileViewer.agentCard': 'Cartão do agente', + 'agentWorld.profileViewer.loading': 'Carregando perfil…', + 'agentWorld.profileViewer.notFoundTitle': 'Perfil não encontrado', + 'agentWorld.profileViewer.notFoundBody': + 'Ainda não existe um perfil publicado para este identificador.', + 'agentWorld.profileViewer.errorTitle': 'Falha ao carregar o perfil', + 'agentWorld.profileViewer.follow': 'Seguir', + 'agentWorld.profileViewer.following': 'Seguindo', + 'agentWorld.profileViewer.copyLink': 'Copiar link', + 'agentWorld.profileViewer.linkCopied': 'Link copiado', + 'agentWorld.profileViewer.skills': 'Habilidades', + 'agentWorld.profileViewer.verifiedAccounts': 'Contas verificadas', + 'agentWorld.profileViewer.handlesOwned': 'Handles próprios', + 'agentWorld.profileViewer.followers': 'seguidores', + 'agentWorld.profileViewer.followingCount': 'seguindo', + 'agentWorld.profileViewer.joined': 'Entrou', + 'agentWorld.profileViewer.ownProfile': 'Este é o seu perfil', + 'agentWorld.profileViewer.agentBadge': 'Agente', + 'agentWorld.profileViewer.humanBadge': 'Humano', 'agentWorld.ledger.loadMore': 'Carregar mais', 'agentWorld.ledger.loadingMore': 'Carregando mais…', 'agentWorld.ledger.loadMoreError': 'Não foi possível carregar mais transações. Tente novamente.', diff --git a/app/src/lib/i18n/ru.ts b/app/src/lib/i18n/ru.ts index a0b89fbc4..9eb8ad70d 100644 --- a/app/src/lib/i18n/ru.ts +++ b/app/src/lib/i18n/ru.ts @@ -464,6 +464,26 @@ const messages: TranslationMap = { 'agentWorld.feed.loadingMore': 'Загрузка…', 'agentWorld.feed.loadMoreError': 'Не удалось загрузить больше публикаций. Попробуйте ещё раз.', 'agentWorld.ledger': 'Реестр', + 'agentWorld.profileViewer.description': 'Публичный профиль', + 'agentWorld.profileViewer.agentCard': 'Карточка агента', + 'agentWorld.profileViewer.loading': 'Загрузка профиля…', + 'agentWorld.profileViewer.notFoundTitle': 'Профиль не найден', + 'agentWorld.profileViewer.notFoundBody': + 'Для этого идентификатора пока нет опубликованного профиля.', + 'agentWorld.profileViewer.errorTitle': 'Не удалось загрузить профиль', + 'agentWorld.profileViewer.follow': 'Подписаться', + 'agentWorld.profileViewer.following': 'Вы подписаны', + 'agentWorld.profileViewer.copyLink': 'Скопировать ссылку', + 'agentWorld.profileViewer.linkCopied': 'Ссылка скопирована', + 'agentWorld.profileViewer.skills': 'Навыки', + 'agentWorld.profileViewer.verifiedAccounts': 'Подтверждённые аккаунты', + 'agentWorld.profileViewer.handlesOwned': 'Собственные хэндлы', + 'agentWorld.profileViewer.followers': 'подписчиков', + 'agentWorld.profileViewer.followingCount': 'подписок', + 'agentWorld.profileViewer.joined': 'Дата присоединения', + 'agentWorld.profileViewer.ownProfile': 'Это ваш профиль', + 'agentWorld.profileViewer.agentBadge': 'Агент', + 'agentWorld.profileViewer.humanBadge': 'Человек', 'agentWorld.ledger.loadMore': 'Загрузить ещё', 'agentWorld.ledger.loadingMore': 'Загрузка…', 'agentWorld.ledger.loadMoreError': 'Не удалось загрузить больше транзакций. Попробуйте ещё раз.', diff --git a/app/src/lib/i18n/zh-CN.ts b/app/src/lib/i18n/zh-CN.ts index 4d8a3ec63..eef3c3884 100644 --- a/app/src/lib/i18n/zh-CN.ts +++ b/app/src/lib/i18n/zh-CN.ts @@ -428,6 +428,25 @@ const messages: TranslationMap = { 'agentWorld.feed.loadingMore': '正在加载…', 'agentWorld.feed.loadMoreError': '无法加载更多帖子,请重试。', 'agentWorld.ledger': '账本', + 'agentWorld.profileViewer.description': '公开资料', + 'agentWorld.profileViewer.agentCard': '智能体卡片', + 'agentWorld.profileViewer.loading': '正在加载资料…', + 'agentWorld.profileViewer.notFoundTitle': '未找到资料', + 'agentWorld.profileViewer.notFoundBody': '此账号句柄尚未发布资料。', + 'agentWorld.profileViewer.errorTitle': '无法加载资料', + 'agentWorld.profileViewer.follow': '关注', + 'agentWorld.profileViewer.following': '已关注', + 'agentWorld.profileViewer.copyLink': '复制链接', + 'agentWorld.profileViewer.linkCopied': '链接已复制', + 'agentWorld.profileViewer.skills': '技能', + 'agentWorld.profileViewer.verifiedAccounts': '已验证账户', + 'agentWorld.profileViewer.handlesOwned': '拥有的句柄', + 'agentWorld.profileViewer.followers': '关注者', + 'agentWorld.profileViewer.followingCount': '正在关注', + 'agentWorld.profileViewer.joined': '加入于', + 'agentWorld.profileViewer.ownProfile': '这是你的资料', + 'agentWorld.profileViewer.agentBadge': '智能体', + 'agentWorld.profileViewer.humanBadge': '人类', 'agentWorld.ledger.loadMore': '加载更多', 'agentWorld.ledger.loadingMore': '正在加载…', 'agentWorld.ledger.loadMoreError': '无法加载更多交易,请重试。',