mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-28 13:32:23 +00:00
fix(tinyplace): paginate the Tiny Place ledger listing (#4952)
This commit is contained in:
@@ -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<GqlLedgerTransaction> {
|
||||
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(<LedgerSection />);
|
||||
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(<LedgerSection />);
|
||||
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(<LedgerSection />);
|
||||
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(<LedgerSection />);
|
||||
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(<LedgerSection />);
|
||||
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(<LedgerSection />);
|
||||
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', () => {
|
||||
|
||||
@@ -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<LedgerState>({ status: 'loading' });
|
||||
const [expandedTxId, setExpandedTxId] = useState<string | null>(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 = (
|
||||
<div className="rounded-lg border border-line bg-surface">
|
||||
{ledgerState.transactions.map(tx => (
|
||||
<TransactionRow
|
||||
key={tx.txId}
|
||||
tx={tx}
|
||||
expanded={expandedTxId === tx.txId}
|
||||
onToggle={() => setExpandedTxId(prev => (prev === tx.txId ? null : tx.txId))}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<>
|
||||
<div className="rounded-lg border border-line bg-surface">
|
||||
{transactions.map(tx => (
|
||||
<TransactionRow
|
||||
key={tx.txId}
|
||||
tx={tx}
|
||||
expanded={expandedTxId === tx.txId}
|
||||
onToggle={() => setExpandedTxId(prev => (prev === tx.txId ? null : tx.txId))}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{moreError && (
|
||||
<p className="mt-3 text-center text-xs text-red-600 dark:text-red-400">
|
||||
{t('agentWorld.ledger.loadMoreError')}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{hasMore && (
|
||||
<div className="mt-3 flex justify-center">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
disabled={loadingMore}
|
||||
onClick={() => loadMore(nextOffset)}>
|
||||
{loadingMore ? t('agentWorld.ledger.loadingMore') : t('agentWorld.ledger.loadMore')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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': 'استكشاف',
|
||||
|
||||
@@ -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': 'অন্বেষণ করুন',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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': 'एक्सप्लोर करें',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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': '탐색',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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': 'Исследовать',
|
||||
|
||||
@@ -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': '探索',
|
||||
|
||||
Reference in New Issue
Block a user