From a81252f65d01e027ff9372d099d65ad64eb546b2 Mon Sep 17 00:00:00 2001 From: Mega Mind <146339422+M3gA-Mind@users.noreply.github.com> Date: Fri, 17 Jul 2026 14:17:46 +0530 Subject: [PATCH] fix(tinyplace): paginate the Tiny Place ledger listing (#4952) --- .../agentworld/pages/LedgerSection.test.tsx | 121 ++++++++++++++ app/src/agentworld/pages/LedgerSection.tsx | 149 ++++++++++++++++-- app/src/lib/i18n/ar.ts | 3 + app/src/lib/i18n/bn.ts | 3 + app/src/lib/i18n/de.ts | 4 + app/src/lib/i18n/en.ts | 3 + app/src/lib/i18n/es.ts | 3 + app/src/lib/i18n/fr.ts | 3 + app/src/lib/i18n/hi.ts | 3 + app/src/lib/i18n/id.ts | 3 + app/src/lib/i18n/it.ts | 3 + app/src/lib/i18n/ko.ts | 3 + app/src/lib/i18n/pl.ts | 4 + app/src/lib/i18n/pt.ts | 3 + app/src/lib/i18n/ru.ts | 3 + app/src/lib/i18n/zh-CN.ts | 3 + 16 files changed, 298 insertions(+), 16 deletions(-) diff --git a/app/src/agentworld/pages/LedgerSection.test.tsx b/app/src/agentworld/pages/LedgerSection.test.tsx index 20113a7f5..3ad3249cb 100644 --- a/app/src/agentworld/pages/LedgerSection.test.tsx +++ b/app/src/agentworld/pages/LedgerSection.test.tsx @@ -17,6 +17,7 @@ import LedgerSection, { abbreviateAddress, formatAmount, formatLedgerAmount, + LEDGER_PAGE_SIZE, StatusBadge, } from './LedgerSection'; @@ -43,6 +44,15 @@ const sampleTransaction: GqlLedgerTransaction = { metadata: { identity: '@test-agent' }, }; +/** Build `n` distinct SALE rows with sequential ids, starting at `start`. */ +function buildPage(n: number, start = 0): Array { + return Array.from({ length: n }, (_, i) => ({ + ...sampleTransaction, + txId: `tx-${String(start + i).padStart(4, '0')}`, + type: 'SALE', + })); +} + beforeEach(() => { vi.clearAllMocks(); vi.mocked(apiClient.graphql.ledgerTransactions).mockResolvedValue({ transactions: [], count: 0 }); @@ -103,6 +113,117 @@ describe('Ledger list', () => { }); }); +// ── Pagination ──────────────────────────────────────────────────────────────── + +describe('Ledger pagination', () => { + test('requests the first page with limit + offset 0', async () => { + vi.mocked(apiClient.graphql.ledgerTransactions).mockResolvedValue({ + transactions: [sampleTransaction], + count: 1, + }); + render(); + await waitFor(() => { + expect(screen.getByText('REGISTRATION')).toBeInTheDocument(); + }); + expect(apiClient.graphql.ledgerTransactions).toHaveBeenNthCalledWith(1, { + limit: LEDGER_PAGE_SIZE, + offset: 0, + }); + }); + + test('hides Load more when the first page is shorter than a full page', async () => { + vi.mocked(apiClient.graphql.ledgerTransactions).mockResolvedValue({ + transactions: buildPage(3), + count: 3, + }); + render(); + await waitFor(() => { + expect(screen.getAllByText('SALE')).toHaveLength(3); + }); + expect(screen.queryByRole('button', { name: /load more/i })).not.toBeInTheDocument(); + }); + + test('shows Load more when the first page fills the page size', async () => { + vi.mocked(apiClient.graphql.ledgerTransactions).mockResolvedValue({ + transactions: buildPage(LEDGER_PAGE_SIZE), + count: LEDGER_PAGE_SIZE, + }); + render(); + await waitFor(() => { + expect(screen.getByRole('button', { name: /load more/i })).toBeInTheDocument(); + }); + }); + + test('clicking Load more fetches the next offset, appends rows, then stops', async () => { + const user = userEvent.setup(); + vi.mocked(apiClient.graphql.ledgerTransactions) + .mockResolvedValueOnce({ transactions: buildPage(LEDGER_PAGE_SIZE, 0), count: 53 }) + .mockResolvedValueOnce({ transactions: buildPage(3, LEDGER_PAGE_SIZE), count: 53 }); + + render(); + await waitFor(() => { + expect(screen.getAllByText('SALE')).toHaveLength(LEDGER_PAGE_SIZE); + }); + + await user.click(screen.getByRole('button', { name: /load more/i })); + + // Second page appended (50 + 3 = 53 rows) and the control disappears because + // the short page signals the ledger is exhausted. + await waitFor(() => { + expect(screen.getAllByText('SALE')).toHaveLength(LEDGER_PAGE_SIZE + 3); + }); + expect(apiClient.graphql.ledgerTransactions).toHaveBeenNthCalledWith(2, { + limit: LEDGER_PAGE_SIZE, + offset: LEDGER_PAGE_SIZE, + }); + expect(screen.queryByRole('button', { name: /load more/i })).not.toBeInTheDocument(); + }); + + test('deduplicates overlapping rows across pages', async () => { + const user = userEvent.setup(); + // Second page repeats the last id of the first page (tx-0049) plus one new row. + vi.mocked(apiClient.graphql.ledgerTransactions) + .mockResolvedValueOnce({ transactions: buildPage(LEDGER_PAGE_SIZE, 0), count: 60 }) + .mockResolvedValueOnce({ + transactions: buildPage(LEDGER_PAGE_SIZE, LEDGER_PAGE_SIZE - 1), + count: 60, + }); + + render(); + await waitFor(() => { + expect(screen.getAllByText('SALE')).toHaveLength(LEDGER_PAGE_SIZE); + }); + + await user.click(screen.getByRole('button', { name: /load more/i })); + + // 50 initial + 50 returned − 1 overlapping (tx-0049) = 99 unique rows. + await waitFor(() => { + expect(screen.getAllByText('SALE')).toHaveLength(2 * LEDGER_PAGE_SIZE - 1); + }); + }); + + test('keeps rows and surfaces an error when Load more fails', async () => { + const user = userEvent.setup(); + vi.mocked(apiClient.graphql.ledgerTransactions) + .mockResolvedValueOnce({ transactions: buildPage(LEDGER_PAGE_SIZE, 0), count: 99 }) + .mockRejectedValueOnce(new Error('network failure')); + + render(); + await waitFor(() => { + expect(screen.getByRole('button', { name: /load more/i })).toBeInTheDocument(); + }); + + await user.click(screen.getByRole('button', { name: /load more/i })); + + // Existing rows stay; an error message appears; the control remains for retry. + await waitFor(() => { + expect(screen.getByText(/could not load more transactions/i)).toBeInTheDocument(); + }); + expect(screen.getAllByText('SALE')).toHaveLength(LEDGER_PAGE_SIZE); + expect(screen.getByRole('button', { name: /load more/i })).toBeInTheDocument(); + }); +}); + // ── StatusBadge ─────────────────────────────────────────────────────────────── describe('StatusBadge colors', () => { diff --git a/app/src/agentworld/pages/LedgerSection.tsx b/app/src/agentworld/pages/LedgerSection.tsx index efb5397a7..c6e2fdff2 100644 --- a/app/src/agentworld/pages/LedgerSection.tsx +++ b/app/src/agentworld/pages/LedgerSection.tsx @@ -8,23 +8,50 @@ * * Pattern mirrors FeedSection: useState + useEffect fetch, PanelScaffold * wrapper, StatusBlock for loading/error/empty states. + * + * Pagination: the backend/SDK `ledgerTransactions` call accepts `limit`/`offset` + * (LedgerListParams) and returns a `count`, so the list is fetched a page at a + * time and extended via an offset-based "Load more" control. A page shorter than + * {@link LEDGER_PAGE_SIZE} means the ledger is exhausted (`hasMore=false`). */ -import { useEffect, useState } from 'react'; +import debug from 'debug'; +import { useCallback, useEffect, useRef, useState } from 'react'; import PanelScaffold from '../../components/layout/PanelScaffold'; +import Button from '../../components/ui/Button'; import { type GqlLedgerTransaction } from '../../lib/agentworld/invokeApiClient'; +import { useT } from '../../lib/i18n/I18nContext'; import { apiClient } from '../AgentWorldShell'; import { decimalsForAsset, resolveAssetSymbol } from '../assets'; import { formatUnits, friendlyNetwork } from '../components/X402ConfirmDialog'; import { explorerTxUrl } from '../hooks/useX402Buy'; import { relativeTime } from './relativeTime'; +const log = debug('agentworld:ledger'); + +/** Ledger rows fetched per page (also the initial page size). */ +export const LEDGER_PAGE_SIZE = 50; + // ── State types ─────────────────────────────────────────────────────────────── type LedgerState = | { status: 'loading' } | { status: 'error'; message: string } - | { status: 'ok'; transactions: GqlLedgerTransaction[] }; + | { + status: 'ok'; + transactions: GqlLedgerTransaction[]; + // Server-side cursor, in request units: how many rows to skip on the next + // page. Advances by LEDGER_PAGE_SIZE per fetch, decoupled from the client + // row count so dedupe never desyncs the offset. + nextOffset: number; + // A full page came back, so more rows may exist. + hasMore: boolean; + // A "Load more" fetch is in flight. + loadingMore: boolean; + // Non-null when the most recent "Load more" fetch failed (existing rows + // stay visible; the user can retry). + moreError: string | null; + }; // ── Helpers ─────────────────────────────────────────────────────────────────── @@ -298,24 +325,46 @@ function TransactionRow({ // ── LedgerSection (main export) ─────────────────────────────────────────────── export default function LedgerSection() { + const { t } = useT(); const [ledgerState, setLedgerState] = useState({ status: 'loading' }); const [expandedTxId, setExpandedTxId] = useState(null); - // ── Fetch ledger transactions ────────────────────────────────────────────── + // Guards async setState after unmount (the initial useEffect uses its own + // `cancelled` flag; "Load more" fetches outlive no single effect, so they read + // this ref instead). + const mountedRef = useRef(true); + useEffect(() => { + mountedRef.current = true; + return () => { + mountedRef.current = false; + }; + }, []); + + // ── Fetch first page of ledger transactions ──────────────────────────────── useEffect(() => { let cancelled = false; setLedgerState({ status: 'loading' }); + log('loading first ledger page', { limit: LEDGER_PAGE_SIZE }); - // TODO(phase-2-follow-up): implement pagination with offset or cursor. void apiClient.graphql - .ledgerTransactions({ limit: 50 }) + .ledgerTransactions({ limit: LEDGER_PAGE_SIZE, offset: 0 }) .then(result => { if (cancelled) return; const transactions = Array.isArray(result?.transactions) ? result.transactions : []; - setLedgerState({ status: 'ok', transactions }); + const hasMore = transactions.length >= LEDGER_PAGE_SIZE; + log('loaded first ledger page', { received: transactions.length, hasMore }); + setLedgerState({ + status: 'ok', + transactions, + nextOffset: LEDGER_PAGE_SIZE, + hasMore, + loadingMore: false, + moreError: null, + }); }) .catch((err: unknown) => { if (cancelled) return; + log('first ledger page failed', { error: String(err) }); setLedgerState({ status: 'error', message: String(err) }); }); @@ -324,6 +373,53 @@ export default function LedgerSection() { }; }, []); + // ── Fetch the next page and append it ────────────────────────────────────── + // `offset` is passed in from the rendered 'ok' state so the cursor stays a + // pure function of pages requested. Reentry is prevented by disabling the + // button while `loadingMore` is set. + const loadMore = useCallback((offset: number) => { + log('loading more ledger rows', { offset, limit: LEDGER_PAGE_SIZE }); + setLedgerState(prev => + prev.status === 'ok' ? { ...prev, loadingMore: true, moreError: null } : prev + ); + + void apiClient.graphql + .ledgerTransactions({ limit: LEDGER_PAGE_SIZE, offset }) + .then(result => { + if (!mountedRef.current) return; + const page = Array.isArray(result?.transactions) ? result.transactions : []; + const hasMore = page.length >= LEDGER_PAGE_SIZE; + setLedgerState(prev => { + if (prev.status !== 'ok') return prev; + // Dedupe by txId: if rows shifted between page fetches the overlap must + // not produce duplicate React keys or double-counted entries. + const seen = new Set(prev.transactions.map(tx => tx.txId)); + const fresh = page.filter(tx => !seen.has(tx.txId)); + log('appended ledger rows', { + received: page.length, + fresh: fresh.length, + total: prev.transactions.length + fresh.length, + hasMore, + }); + return { + status: 'ok', + transactions: [...prev.transactions, ...fresh], + nextOffset: offset + LEDGER_PAGE_SIZE, + hasMore, + loadingMore: false, + moreError: null, + }; + }); + }) + .catch((err: unknown) => { + if (!mountedRef.current) return; + log('load more failed', { error: String(err) }); + setLedgerState(prev => + prev.status === 'ok' ? { ...prev, loadingMore: false, moreError: String(err) } : prev + ); + }); + }, []); + // ── Render ───────────────────────────────────────────────────────────────── let body: React.ReactNode; @@ -351,17 +447,38 @@ export default function LedgerSection() { /> ); } else { + const { transactions, hasMore, loadingMore, moreError, nextOffset } = ledgerState; body = ( -
- {ledgerState.transactions.map(tx => ( - setExpandedTxId(prev => (prev === tx.txId ? null : tx.txId))} - /> - ))} -
+ <> +
+ {transactions.map(tx => ( + setExpandedTxId(prev => (prev === tx.txId ? null : tx.txId))} + /> + ))} +
+ + {moreError && ( +

+ {t('agentWorld.ledger.loadMoreError')} +

+ )} + + {hasMore && ( +
+ +
+ )} + ); } diff --git a/app/src/lib/i18n/ar.ts b/app/src/lib/i18n/ar.ts index c07639795..c3c55272f 100644 --- a/app/src/lib/i18n/ar.ts +++ b/app/src/lib/i18n/ar.ts @@ -440,6 +440,9 @@ const messages: TranslationMap = { 'agentWorld.world.rooms.outside.description': 'ساحة مفتوحة كبيرة تحيط بها المباني.', 'agentWorld.feed': 'التغذية', 'agentWorld.ledger': 'السجل', + 'agentWorld.ledger.loadMore': 'تحميل المزيد', + 'agentWorld.ledger.loadingMore': 'جارٍ تحميل المزيد…', + 'agentWorld.ledger.loadMoreError': 'تعذّر تحميل المزيد من المعاملات. حاول مرة أخرى.', 'agentWorld.jobs': 'الوظائف', 'agentWorld.bounties': 'مكافآت', 'agentWorld.explore': 'استكشاف', diff --git a/app/src/lib/i18n/bn.ts b/app/src/lib/i18n/bn.ts index a70c9b123..867299d24 100644 --- a/app/src/lib/i18n/bn.ts +++ b/app/src/lib/i18n/bn.ts @@ -456,6 +456,9 @@ const messages: TranslationMap = { 'agentWorld.world.rooms.outside.description': 'ভবনঘেরা বড় খোলা প্লাজা।', 'agentWorld.feed': 'ফিড', 'agentWorld.ledger': 'লেজার', + 'agentWorld.ledger.loadMore': 'আরও লোড করুন', + 'agentWorld.ledger.loadingMore': 'আরও লোড হচ্ছে…', + 'agentWorld.ledger.loadMoreError': 'আরও লেনদেন লোড করা যায়নি। আবার চেষ্টা করুন।', 'agentWorld.jobs': 'চাকরি', 'agentWorld.bounties': 'পুরস্কার', 'agentWorld.explore': 'অন্বেষণ করুন', diff --git a/app/src/lib/i18n/de.ts b/app/src/lib/i18n/de.ts index 951cd3954..58d708c91 100644 --- a/app/src/lib/i18n/de.ts +++ b/app/src/lib/i18n/de.ts @@ -480,6 +480,10 @@ const messages: TranslationMap = { 'agentWorld.world.rooms.outside.description': 'Ein großer offener Platz mit Gebäuden ringsum.', 'agentWorld.feed': 'Feed', 'agentWorld.ledger': 'Kontobuch', + 'agentWorld.ledger.loadMore': 'Mehr laden', + 'agentWorld.ledger.loadingMore': 'Wird geladen…', + 'agentWorld.ledger.loadMoreError': + 'Weitere Transaktionen konnten nicht geladen werden. Bitte erneut versuchen.', 'agentWorld.jobs': 'Aufträge', 'agentWorld.bounties': 'Prämien', 'agentWorld.explore': 'Entdecken', diff --git a/app/src/lib/i18n/en.ts b/app/src/lib/i18n/en.ts index bd932e3ab..a44e66941 100644 --- a/app/src/lib/i18n/en.ts +++ b/app/src/lib/i18n/en.ts @@ -185,6 +185,9 @@ const en: TranslationMap = { 'agentWorld.world.rooms.outside.description': 'A large open plaza ringed with buildings.', 'agentWorld.feed': 'Feed', 'agentWorld.ledger': 'Ledger', + 'agentWorld.ledger.loadMore': 'Load more', + 'agentWorld.ledger.loadingMore': 'Loading more…', + 'agentWorld.ledger.loadMoreError': 'Could not load more transactions. Try again.', 'agentWorld.jobs': 'Jobs', 'agentWorld.bounties': 'Bounties', 'agentWorld.explore': 'Explore', diff --git a/app/src/lib/i18n/es.ts b/app/src/lib/i18n/es.ts index 578a46773..453fa5c58 100644 --- a/app/src/lib/i18n/es.ts +++ b/app/src/lib/i18n/es.ts @@ -466,6 +466,9 @@ const messages: TranslationMap = { 'agentWorld.world.rooms.outside.description': 'Una gran plaza abierta rodeada de edificios.', 'agentWorld.feed': 'Noticias', 'agentWorld.ledger': 'Libro mayor', + '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.', 'agentWorld.jobs': 'Trabajos', 'agentWorld.bounties': 'Recompensas', 'agentWorld.explore': 'Explorar', diff --git a/app/src/lib/i18n/fr.ts b/app/src/lib/i18n/fr.ts index 21f2645b5..2944fbebf 100644 --- a/app/src/lib/i18n/fr.ts +++ b/app/src/lib/i18n/fr.ts @@ -476,6 +476,9 @@ const messages: TranslationMap = { 'agentWorld.world.rooms.outside.description': 'Une grande place ouverte entourée de bâtiments.', 'agentWorld.feed': 'Fil', 'agentWorld.ledger': 'Grand livre', + 'agentWorld.ledger.loadMore': 'Charger plus', + 'agentWorld.ledger.loadingMore': 'Chargement…', + 'agentWorld.ledger.loadMoreError': 'Impossible de charger plus de transactions. Réessayez.', 'agentWorld.jobs': 'Missions', 'agentWorld.bounties': 'Primes', 'agentWorld.explore': 'Explorer', diff --git a/app/src/lib/i18n/hi.ts b/app/src/lib/i18n/hi.ts index 9e0f6e5df..2d4a3d3b3 100644 --- a/app/src/lib/i18n/hi.ts +++ b/app/src/lib/i18n/hi.ts @@ -456,6 +456,9 @@ const messages: TranslationMap = { 'agentWorld.world.rooms.outside.description': 'इमारतों से घिरा बड़ा खुला प्लाजा।', 'agentWorld.feed': 'फ़ीड', 'agentWorld.ledger': 'खाता बही', + 'agentWorld.ledger.loadMore': 'और लोड करें', + 'agentWorld.ledger.loadingMore': 'और लोड हो रहा है…', + 'agentWorld.ledger.loadMoreError': 'अधिक लेन-देन लोड नहीं हो सके। फिर से प्रयास करें।', 'agentWorld.jobs': 'कार्य', 'agentWorld.bounties': 'इनाम', 'agentWorld.explore': 'एक्सप्लोर करें', diff --git a/app/src/lib/i18n/id.ts b/app/src/lib/i18n/id.ts index 4e6db5879..c84ddbf8b 100644 --- a/app/src/lib/i18n/id.ts +++ b/app/src/lib/i18n/id.ts @@ -462,6 +462,9 @@ const messages: TranslationMap = { 'agentWorld.world.rooms.outside.description': 'Plaza terbuka besar yang dikelilingi gedung.', 'agentWorld.feed': 'Feed', 'agentWorld.ledger': 'Buku Besar', + 'agentWorld.ledger.loadMore': 'Muat lebih banyak', + 'agentWorld.ledger.loadingMore': 'Memuat lagi…', + 'agentWorld.ledger.loadMoreError': 'Tidak dapat memuat transaksi lainnya. Coba lagi.', 'agentWorld.jobs': 'Pekerjaan', 'agentWorld.bounties': 'Hadiah', 'agentWorld.explore': 'Jelajahi', diff --git a/app/src/lib/i18n/it.ts b/app/src/lib/i18n/it.ts index f1f195218..406b6eb43 100644 --- a/app/src/lib/i18n/it.ts +++ b/app/src/lib/i18n/it.ts @@ -469,6 +469,9 @@ const messages: TranslationMap = { 'agentWorld.world.rooms.outside.description': 'Una grande piazza aperta circondata da edifici.', 'agentWorld.feed': 'Feed', 'agentWorld.ledger': 'Registro', + 'agentWorld.ledger.loadMore': 'Carica altro', + 'agentWorld.ledger.loadingMore': 'Caricamento…', + 'agentWorld.ledger.loadMoreError': 'Impossibile caricare altre transazioni. Riprova.', 'agentWorld.jobs': 'Lavori', 'agentWorld.bounties': 'Ricompense', 'agentWorld.explore': 'Esplora', diff --git a/app/src/lib/i18n/ko.ts b/app/src/lib/i18n/ko.ts index f9746c6e9..acb9e186d 100644 --- a/app/src/lib/i18n/ko.ts +++ b/app/src/lib/i18n/ko.ts @@ -449,6 +449,9 @@ const messages: TranslationMap = { 'agentWorld.world.rooms.outside.description': '건물로 둘러싸인 넓은 열린 광장.', 'agentWorld.feed': '피드', 'agentWorld.ledger': '원장', + 'agentWorld.ledger.loadMore': '더 보기', + 'agentWorld.ledger.loadingMore': '더 불러오는 중…', + 'agentWorld.ledger.loadMoreError': '거래를 더 불러오지 못했습니다. 다시 시도하세요.', 'agentWorld.jobs': '채용', 'agentWorld.bounties': '현상금', 'agentWorld.explore': '탐색', diff --git a/app/src/lib/i18n/pl.ts b/app/src/lib/i18n/pl.ts index 2c840e5ad..aefd8560e 100644 --- a/app/src/lib/i18n/pl.ts +++ b/app/src/lib/i18n/pl.ts @@ -467,6 +467,10 @@ const messages: TranslationMap = { 'agentWorld.world.rooms.outside.description': 'Duży otwarty plac otoczony budynkami.', 'agentWorld.feed': 'Kanał', 'agentWorld.ledger': 'Księga', + 'agentWorld.ledger.loadMore': 'Załaduj więcej', + 'agentWorld.ledger.loadingMore': 'Ładowanie…', + 'agentWorld.ledger.loadMoreError': + 'Nie udało się załadować kolejnych transakcji. Spróbuj ponownie.', 'agentWorld.jobs': 'Zlecenia', 'agentWorld.bounties': 'Nagrody', 'agentWorld.explore': 'Eksploruj', diff --git a/app/src/lib/i18n/pt.ts b/app/src/lib/i18n/pt.ts index f7973d5d9..dc84c0bf5 100644 --- a/app/src/lib/i18n/pt.ts +++ b/app/src/lib/i18n/pt.ts @@ -461,6 +461,9 @@ const messages: TranslationMap = { 'agentWorld.world.rooms.outside.description': 'Uma grande praça aberta cercada por edifícios.', 'agentWorld.feed': 'Feed', 'agentWorld.ledger': 'Livro-razão', + 'agentWorld.ledger.loadMore': 'Carregar mais', + 'agentWorld.ledger.loadingMore': 'Carregando mais…', + 'agentWorld.ledger.loadMoreError': 'Não foi possível carregar mais transações. Tente novamente.', 'agentWorld.jobs': 'Trabalhos', 'agentWorld.bounties': 'Recompensas', 'agentWorld.explore': 'Explorar', diff --git a/app/src/lib/i18n/ru.ts b/app/src/lib/i18n/ru.ts index 381232f9f..c1d7222ff 100644 --- a/app/src/lib/i18n/ru.ts +++ b/app/src/lib/i18n/ru.ts @@ -461,6 +461,9 @@ const messages: TranslationMap = { 'agentWorld.world.rooms.outside.description': 'Большая открытая площадь, окруженная зданиями.', 'agentWorld.feed': 'Лента', 'agentWorld.ledger': 'Реестр', + 'agentWorld.ledger.loadMore': 'Загрузить ещё', + 'agentWorld.ledger.loadingMore': 'Загрузка…', + 'agentWorld.ledger.loadMoreError': 'Не удалось загрузить больше транзакций. Попробуйте ещё раз.', 'agentWorld.jobs': 'Вакансии', 'agentWorld.bounties': 'Награды', 'agentWorld.explore': 'Исследовать', diff --git a/app/src/lib/i18n/zh-CN.ts b/app/src/lib/i18n/zh-CN.ts index 396cbf12c..337097b89 100644 --- a/app/src/lib/i18n/zh-CN.ts +++ b/app/src/lib/i18n/zh-CN.ts @@ -425,6 +425,9 @@ const messages: TranslationMap = { 'agentWorld.world.rooms.outside.description': '一座被建筑环绕的大型开放广场。', 'agentWorld.feed': '动态', 'agentWorld.ledger': '账本', + 'agentWorld.ledger.loadMore': '加载更多', + 'agentWorld.ledger.loadingMore': '正在加载…', + 'agentWorld.ledger.loadMoreError': '无法加载更多交易,请重试。', 'agentWorld.jobs': '工作', 'agentWorld.bounties': '赏金', 'agentWorld.explore': '探索',