diff --git a/.changeset/30588-marketplace-lazy-namespace.md b/.changeset/30588-marketplace-lazy-namespace.md new file mode 100644 index 00000000000..7e93fb57844 --- /dev/null +++ b/.changeset/30588-marketplace-lazy-namespace.md @@ -0,0 +1,20 @@ +--- +'@object-ui/i18n': patch +'@object-ui/app-shell': patch +--- + +perf(i18n, app-shell)!: the marketplace pages' strings leave the eager language packs for a lazy `marketplace` namespace + +Clause-②: yes (narrowing) +Metadata-protocol: none + + + +The console loads one language pack eagerly, `en`, and its chunk had grown to 59,951 of the 60,000 gzipped bytes its ceiling allows. About 3 KB of it was the `marketplace.*` group, read only by the two marketplace pages, which are lazy routes. That group now lives in `@object-ui/i18n/namespaces/marketplace`: ten catalogues, one per built-in language, each its own chunk, fetched when a marketplace page opens. The console's `en` chunk reads 57,010 bytes. + +- **The pages wait for their text.** App-shell's marketplace catalogue and package pages render their load skeleton until the catalogue for the console's language (and `en`, the fallback) has arrived, and again after a language switch, so a visitor never sees English in a translated console or a raw key. A catalogue that cannot be fetched shows the console's own "Something went wrong" with Try again, which fetches it again. +- **`useMarketplaceNamespace()`** (from the new subpath) is the hook behind that frame: it answers `loading`, `ready` or `failed` with a `retry()`, merges the catalogues into the page's i18next instance without overwriting a host's own `resources`, and uses a catalogue already made resident by `registerBuiltInLocales()` without fetching it. +- **What stays in the packs.** Studio's packages page, outside the marketplace route, draws marketplace installs with the same components: their keys (`marketplace.installedAdditiveNote`, `marketplace.notLoaded.*`, `marketplace.action.uninstall`, `marketplace.action.uninstalling`, `marketplace.action.details`, `marketplace.uninstall.confirm`, `marketplace.uninstall.confirmNotLoaded`, `marketplace.uninstall.successInList`, `marketplace.uninstall.successNotLoaded`, `marketplace.install.dependencyMissing.*` and `marketplace.install.protocolCheckSkipped.*`) stay in every pack. +- **Every string still in every language.** No text changed. `@object-ui/i18n/locales` (the all-ten door) hands out each language complete, its pack merged with its namespace catalogue, so key parity and `registerBuiltInLocales()` still cover the moved keys. + +**BREAKING** (`@object-ui/i18n`, TypeScript and the pack modules): the package entry's `en`, the exported `TranslationKeys` (`typeof en`) and the pack modules `@object-ui/i18n/locales/` no longer hold the moved `marketplace.*` keys. A `t()` call that names one still compiles, since its key is a `string`. A component outside app-shell's marketplace pages that reads one of those keys calls `useMarketplaceNamespace()` and renders it once the answer is `ready`; code that reads the catalogue objects directly reads them from `@object-ui/i18n/locales`, whose ten languages are complete. diff --git a/ui/apps/console/vite.config.ts b/ui/apps/console/vite.config.ts index f3277f2fbc9..645f2b11c11 100644 --- a/ui/apps/console/vite.config.ts +++ b/ui/apps/console/vite.config.ts @@ -1235,6 +1235,46 @@ export default defineConfig({ { name: 'i18n-ns-studio-en', test: /[\\/]packages[\\/]i18n[\\/]src[\\/]namespaces[\\/]studio[\\/]en\.ts$/, priority: 84 }, { name: 'i18n-ns-studio-zh', test: /[\\/]packages[\\/]i18n[\\/]src[\\/]namespaces[\\/]studio[\\/]zh\.ts$/, priority: 84 }, // + // The `marketplace` namespace (objectstack#30588): the pack group the + // two marketplace pages read, moved out of the ten packs so the + // eager `i18n-locale-en` chunk stops carrying it. All ten catalogues + // are `import()`ed by `packages/i18n/src/namespaces/marketplace/index.ts` + // when a marketplace page mounts, none is resident, so one group per + // catalogue for the rule above. The loader module gets a group too: + // without one it would fall to `i18n-runtime` below and be eager, + // while its only reader is the marketplace pages' frame. + // + // ⛔ Both the loader's group and the frame's (`marketplace-namespace-gate`, + // app-shell's `MarketplaceNamespaceGate.tsx`, the one module that + // imports the loader) take `includeDependenciesRecursively: false`, + // and the frame needs its group at all, for two measured reasons + // (console build of objectstack#30588, eager closure read from + // `eager-closure.json`): + // - with rolldown's default the loader's group followed its imports + // and took `locales/registry.ts` out of `i18n-runtime` and the + // dynamic-import preload helper out of `framework`; both are + // eager, so the group became an eager 2,374 B gz chunk; + // - without a group of its own the frame, shared by the two lazy + // marketplace pages, landed in the chunk those pages share with + // the eager Studio packages page (`MarketplaceRowActions.tsx` and + // the refusal readers), which is EAGER by that co-tenancy, and + // pulled the loader into the eager closure with it (+1,281 B gz). + // With both, the loader, the frame and the ten catalogues are lazy, + // the shared chunk is the one the base build emits, and the only + // change to the eager closure is `i18n-locale-en`'s. + { name: 'i18n-ns-marketplace', test: /[\\/]packages[\\/]i18n[\\/]src[\\/]namespaces[\\/]marketplace[\\/]index\.ts$/, priority: 84, includeDependenciesRecursively: false }, + { name: 'i18n-ns-marketplace-en', test: /[\\/]packages[\\/]i18n[\\/]src[\\/]namespaces[\\/]marketplace[\\/]en\.ts$/, priority: 84 }, + { name: 'i18n-ns-marketplace-zh', test: /[\\/]packages[\\/]i18n[\\/]src[\\/]namespaces[\\/]marketplace[\\/]zh\.ts$/, priority: 84 }, + { name: 'i18n-ns-marketplace-ja', test: /[\\/]packages[\\/]i18n[\\/]src[\\/]namespaces[\\/]marketplace[\\/]ja\.ts$/, priority: 84 }, + { name: 'i18n-ns-marketplace-ko', test: /[\\/]packages[\\/]i18n[\\/]src[\\/]namespaces[\\/]marketplace[\\/]ko\.ts$/, priority: 84 }, + { name: 'i18n-ns-marketplace-de', test: /[\\/]packages[\\/]i18n[\\/]src[\\/]namespaces[\\/]marketplace[\\/]de\.ts$/, priority: 84 }, + { name: 'i18n-ns-marketplace-fr', test: /[\\/]packages[\\/]i18n[\\/]src[\\/]namespaces[\\/]marketplace[\\/]fr\.ts$/, priority: 84 }, + { name: 'i18n-ns-marketplace-es', test: /[\\/]packages[\\/]i18n[\\/]src[\\/]namespaces[\\/]marketplace[\\/]es\.ts$/, priority: 84 }, + { name: 'i18n-ns-marketplace-pt', test: /[\\/]packages[\\/]i18n[\\/]src[\\/]namespaces[\\/]marketplace[\\/]pt\.ts$/, priority: 84 }, + { name: 'i18n-ns-marketplace-ru', test: /[\\/]packages[\\/]i18n[\\/]src[\\/]namespaces[\\/]marketplace[\\/]ru\.ts$/, priority: 84 }, + { name: 'i18n-ns-marketplace-ar', test: /[\\/]packages[\\/]i18n[\\/]src[\\/]namespaces[\\/]marketplace[\\/]ar\.ts$/, priority: 84 }, + { name: 'marketplace-namespace-gate', test: /[\\/]packages[\\/]app-shell[\\/]src[\\/]console[\\/]marketplace[\\/](?:MarketplaceNamespaceGate\.tsx|marketplaceFrameHolding\.ts)$/, priority: 84, includeDependenciesRecursively: false }, + // // ⛔ `i18n-runtime` takes `includeDependenciesRecursively: false` // for the reason `data-adapter` below does (objectui#9345, read out // there in full): it outranks `framework` (83 over 80), and since diff --git a/ui/packages/app-shell/src/console/marketplace/MarketplaceNamespaceGate.tsx b/ui/packages/app-shell/src/console/marketplace/MarketplaceNamespaceGate.tsx new file mode 100644 index 00000000000..58153f09a3c --- /dev/null +++ b/ui/packages/app-shell/src/console/marketplace/MarketplaceNamespaceGate.tsx @@ -0,0 +1,75 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * The marketplace pages' frame for their own strings (objectstack#30588). + * + * The `marketplace.*` keys these pages read are not in the console's eager + * language pack: they are `@object-ui/i18n`'s lazy `marketplace` namespace, + * fetched when a page here mounts (the namespace module's header says why). + * This frame renders the page only once the namespace answers `ready` for the + * instance's language, so a visitor sees the copy-free load skeleton the pages + * already use (`MarketplaceResolving`), never an English sentence in a + * translated console nor a raw key. A language switch while a page is open + * shows the skeleton again until that language's catalogue has arrived. + * + * That swap unmounts the page body while the visitor stays on the page, so an + * unmount cleanup that means "the visitor left" (`SampleDataLeaveBanner`) + * asks `useMarketplaceFrameHolding()` (`marketplaceFrameHolding.ts`) first. + * + * A catalogue that cannot be fetched is said once, in the console's own error + * words (resident pack keys, which need no fetch), with a Try again that asks + * for it again — never a skeleton that waits forever, and never a retry loop. + * + * ⛔ Every component in this directory that reads a `marketplace.*` key renders + * under this frame (the two pages wrap their bodies in it), or is one of the + * files Studio's packages page also renders — `MarketplaceRowActions.tsx`, + * `dependencyMissing.ts`, `protocolCheckSkipped.ts` — whose keys stayed in + * the packs for that reason and need no frame. + * `__tests__/marketplaceNamespaceBoundary.test.ts` holds both halves. + */ + +import { useCallback, useLayoutEffect, useRef, type ReactNode } from 'react'; +import { RotateCcw } from 'lucide-react'; +import { Button, Empty, EmptyTitle } from '@object-ui/components'; +import { useObjectTranslation } from '@object-ui/i18n'; +import { useMarketplaceNamespace } from '@object-ui/i18n/namespaces/marketplace'; +import { MarketplaceResolving } from './MarketplaceResolving.js'; +import { MarketplaceFrameHolding } from './marketplaceFrameHolding.js'; + +export function MarketplaceNamespaceGate({ children }: { children: ReactNode }) { + const namespace = useMarketplaceNamespace(); + const holding = useRef(false); + useLayoutEffect(() => { + holding.current = namespace.status !== 'ready'; + }); + const isHolding = useCallback(() => holding.current, []); + return ( + + {namespace.status === 'ready' ? ( + children + ) : namespace.status === 'failed' ? ( + + ) : ( + + )} + + ); +} + +function MarketplaceNamespaceFailed({ onRetry }: { onRetry: () => void }) { + const { t } = useObjectTranslation(); + return ( +
+ + {t('console.errors.somethingWentWrong')} + + +
+ ); +} diff --git a/ui/packages/app-shell/src/console/marketplace/MarketplacePackagePage.tsx b/ui/packages/app-shell/src/console/marketplace/MarketplacePackagePage.tsx index 3ab826d5e07..49011b336ca 100644 --- a/ui/packages/app-shell/src/console/marketplace/MarketplacePackagePage.tsx +++ b/ui/packages/app-shell/src/console/marketplace/MarketplacePackagePage.tsx @@ -112,6 +112,7 @@ import { SuggestedBindingsPanel, type SuggestedBindingsStrings } from '../../com import { ConnectorCredentialsStep } from './ConnectorCredentialsStep.js'; import type { SuggestedBinding } from '../../services/suggestedBindingsApi.js'; import { errorCodeIs } from '@object-ui/types'; +import { MarketplaceNamespaceGate } from './MarketplaceNamespaceGate.js'; /** * Whether a cloud install lands in the environment this console renders, read @@ -163,7 +164,19 @@ function versionEntryId(version: string): string { return `marketplace-version-${version}`; } +/** + * The page under the frame that fetches its strings first — the + * `marketplace` namespace (`MarketplaceNamespaceGate.tsx` says why). + */ export function MarketplacePackagePage() { + return ( + + + + ); +} + +function MarketplacePackagePageBody() { const navigate = useNavigate(); const { packageId, appName } = useParams<{ packageId?: string; appName?: string }>(); const { isAdmin, isResolved } = useWorkspaceAdminStatus(); diff --git a/ui/packages/app-shell/src/console/marketplace/MarketplacePage.tsx b/ui/packages/app-shell/src/console/marketplace/MarketplacePage.tsx index 8035064a8f8..b633e61c67a 100644 --- a/ui/packages/app-shell/src/console/marketplace/MarketplacePage.tsx +++ b/ui/packages/app-shell/src/console/marketplace/MarketplacePage.tsx @@ -56,6 +56,7 @@ import { useProtocolCheckSkippedText } from './protocolCheckSkipped.js'; import { ProtocolCheckSkippedNote } from './ProtocolCheckSkippedNote.js'; import { ConnectorCredentialsStep } from './ConnectorCredentialsStep.js'; import { packagesHref } from '../../views/metadata-admin/packageSource.js'; +import { MarketplaceNamespaceGate } from './MarketplaceNamespaceGate.js'; /** * The card grid's track (objectui#12616): a card is never narrower than 240px, @@ -306,7 +307,19 @@ type OrgDeployState = | { phase: 'served'; manifestId: string; version: string | undefined; name: string; app: ServedApp | null } | { phase: 'timeout'; manifestId: string; version: string | undefined; name: string }; +/** + * The page under the frame that fetches its strings first — the + * `marketplace` namespace (`MarketplaceNamespaceGate.tsx` says why). + */ export function MarketplacePage() { + return ( + + + + ); +} + +function MarketplacePageBody() { const navigate = useNavigate(); const { appName } = useParams(); const { isAdmin, isResolved } = useWorkspaceAdminStatus(); diff --git a/ui/packages/app-shell/src/console/marketplace/SampleDataLeaveBanner.tsx b/ui/packages/app-shell/src/console/marketplace/SampleDataLeaveBanner.tsx index f091bb4d125..400b22a6849 100644 --- a/ui/packages/app-shell/src/console/marketplace/SampleDataLeaveBanner.tsx +++ b/ui/packages/app-shell/src/console/marketplace/SampleDataLeaveBanner.tsx @@ -16,7 +16,10 @@ * It is raised only for a package this session recorded loading sample data * into (`sampleDataNotice.ts`), and only while a notification provider is * mounted: the page renders this component under that guard, since - * `useNotifications()` throws outside one. + * `useNotifications()` throws outside one. A language switch that sends the + * page's frame back to its load skeleton unmounts this component too, with + * the installer still on the page: that is not a leave, so no banner is + * raised (`useMarketplaceFrameHolding`). */ import { useEffect, useRef } from 'react'; @@ -24,11 +27,13 @@ import { useNavigate } from 'react-router-dom'; import { useNotifications } from '@object-ui/react'; import { useObjectTranslation } from '@object-ui/i18n'; import { attachBanner, requestClearConfirm, sampleDataNotice, withdrawBanner } from './sampleDataNotice.js'; +import { useMarketplaceFrameHolding } from './marketplaceFrameHolding.js'; export function SampleDataLeaveBanner({ manifestId }: { manifestId: string | null }) { const { notify, dismiss } = useNotifications(); const { t } = useObjectTranslation(); const navigate = useNavigate(); + const isFrameHolding = useMarketplaceFrameHolding(); // The cleanup below runs once the page is gone, so it reads the functions // of the last render rather than those of the render that set it up. const latest = useRef({ notify, dismiss, t, navigate }); @@ -41,6 +46,8 @@ export function SampleDataLeaveBanner({ manifestId }: { manifestId: string | nul // Back on the package page: its install note speaks for the sample data. withdrawBanner(manifestId); return () => { + // The frame took the page back for a language's strings: still here. + if (isFrameHolding?.()) return; const notice = sampleDataNotice(manifestId); if (!notice) return; const { notify: raise, dismiss: drop, t: tr, navigate: go } = latest.current; @@ -60,7 +67,7 @@ export function SampleDataLeaveBanner({ manifestId }: { manifestId: string | nul }); attachBanner(notice.manifestId, () => drop(id)); }; - }, [manifestId]); + }, [manifestId, isFrameHolding]); return null; } diff --git a/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePage.namespaceGate-30588.test.tsx b/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePage.namespaceGate-30588.test.tsx new file mode 100644 index 00000000000..6862792a878 --- /dev/null +++ b/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePage.namespaceGate-30588.test.tsx @@ -0,0 +1,277 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * objectstack#30588 — the marketplace pages' strings are a lazy namespace, and + * a visitor reading the console in another language never sees them in + * English or as raw keys while it loads. + * + * `marketplace.*` (bar the families Studio's packages page reads) left the ten + * language packs for `@object-ui/i18n/namespaces/marketplace`, fetched when a + * marketplace page mounts. Both pages wrap their bodies in + * `MarketplaceNamespaceGate`, which renders the copy-free load skeleton + * (`marketplace-resolving`) until the namespace is in, then the page — in the + * console's language from the first frame that has text. A catalogue that + * cannot be fetched is said in the console's own resident error words, with + * Try again. + * + * Every text node the page ever adds is recorded (a MutationObserver over the + * render container, attached before the first render), so "never English, + * never a raw key" is checked over the whole sequence, not only its end. + * + * ## The registry is put back in its production state + * + * The DOM setup calls `registerBuiltInLocales()`, which registers every + * language COMPLETE — the namespace included — so the page would never need a + * fetch. That is the all-ten door's promise, not what a console visitor gets. + * This file re-registers each language as its pack alone, at module scope — + * the registry a console that awaited `preloadBootstrapLocale()` holds. The + * `dom` project isolates each file's module graph, so this stays here. + * + * Harness: `MarketplacePage.searchPlaceholder-10900.test.tsx`'s and + * `MarketplacePackagePage.guardOrder.test.tsx`'s — the real runtime-config + * boot, an admin viewer, the API module stubbed — under a real `I18nProvider`. + */ +import '@testing-library/jest-dom/vitest'; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { useState, type ReactElement } from 'react'; +import { render, screen, fireEvent, cleanup, act } from '@testing-library/react'; + +const arChunk = vi.hoisted(() => ({ fails: false, requests: 0 })); + +vi.mock('react-router-dom', () => ({ + useNavigate: () => vi.fn(), + useParams: () => ({ packageId: 'com.acme.helpdesk' }), +})); + +vi.mock('@object-ui/auth', async (importOriginal) => ({ + ...(await importOriginal>()), + useWorkspaceAdminStatus: () => ({ isAdmin: true, isResolved: true }), +})); + +const PKG = { + id: 'pkg_1', + manifest_id: 'com.acme.helpdesk', + display_name: 'Service Desk', + description: 'Tickets and SLAs', + category: null, + latest_version: null, +}; + +vi.mock('../marketplaceApi', () => ({ + listMarketplacePackages: async () => ({ items: [PKG] }), + getMarketplacePackage: async () => ({ package: PKG, versions: [] }), + getCloudInstallationInfo: async () => null, + listLocalInstalls: async () => [], + listOrgPackages: async () => ({ items: [] }), + listInstalledPackages: async () => ({ items: [] }), + installPackage: async () => ({}), + installLocal: async () => ({}), + uninstallLocal: async () => ({}), + listCloudEnvironments: async () => [], + listInstallableOrgIds: async () => [], + cloudConsoleUrl: () => '', + reseedSampleData: async () => ({ ok: true }), + purgeSampleData: async () => ({ ok: true }), + reseedLocalSampleData: async () => ({ ok: true }), + purgeLocalSampleData: async () => ({ ok: true }), + countLocalSampleData: async () => null, +})); + +vi.mock('../../../assistant/assistantBus', () => ({ emitMetadataRefresh: () => {} })); +vi.mock('../../../providers/MetadataProvider', () => ({ useMetadata: () => ({ refresh: () => {} }) })); +vi.mock('../../../components/SuggestedBindingsPanel', () => ({ SuggestedBindingsPanel: () => null })); + +// The Arabic catalogue's chunk, failing while a case says so. +vi.mock('@object-ui/i18n/namespaces/marketplace/ar', async (importOriginal) => { + arChunk.requests += 1; + if (arChunk.fails) throw new Error('chunk offline'); + return { ...(await importOriginal>()) }; +}); + +import { I18nProvider, useI18nContext } from '@object-ui/i18n'; +import { NotificationProvider } from '@object-ui/react'; +import { NotificationBanners } from '@object-ui/components'; +import { builtInLocales } from '@object-ui/i18n/locales'; +import { BUILT_IN_LANGUAGE_CODES, registerBuiltInLocale } from '@object-ui/i18n/locales/registry'; +import en from '@object-ui/i18n/locales/en'; +import zh from '@object-ui/i18n/locales/zh'; +import ja from '@object-ui/i18n/locales/ja'; +import ko from '@object-ui/i18n/locales/ko'; +import de from '@object-ui/i18n/locales/de'; +import fr from '@object-ui/i18n/locales/fr'; +import es from '@object-ui/i18n/locales/es'; +import pt from '@object-ui/i18n/locales/pt'; +import ru from '@object-ui/i18n/locales/ru'; +import ar from '@object-ui/i18n/locales/ar'; +import { initRuntimeConfig, resetRuntimeConfigForTesting } from '../../../runtime-config'; +import { MarketplacePage } from '../MarketplacePage'; +import { MarketplacePackagePage } from '../MarketplacePackagePage'; +import { rememberSampleData, resetSampleDataSessionForTesting } from '../sampleDataNotice'; + +const PACKS: Record> = { en, zh, ja, ko, de, fr, es, pt, ru, ar }; +for (const code of BUILT_IN_LANGUAGE_CODES) registerBuiltInLocale(code, PACKS[code]); + +const ENGLISH = builtInLocales.en.marketplace; +const RAW_KEY = /\bmarketplace\.[A-Za-z]/; + +beforeEach(async () => { + arChunk.fails = false; + resetSampleDataSessionForTesting(); + resetRuntimeConfigForTesting(); + vi.stubGlobal( + 'fetch', + vi.fn(async () => ({ + ok: true, + status: 200, + json: async () => ({ + cloudUrl: 'https://cloud.objectos.ai', + singleEnvironment: true, + features: { installLocal: true, marketplace: true, aiStudio: true, autoPublishAiBuilds: true }, + branding: { productName: 'ObjectOS', productShortName: 'ObjectOS' }, + }), + })), + ); + await initRuntimeConfig(); +}); + +afterEach(() => { + cleanup(); + vi.unstubAllGlobals(); + resetRuntimeConfigForTesting(); +}); + +/** Render `page` in `language`, recording the text of every node it ever adds. */ +function renderRecording(page: ReactElement, language: string) { + const container = document.createElement('div'); + document.body.appendChild(container); + const texts: string[] = []; + const observer = new MutationObserver((records) => { + for (const record of records) { + if (record.type === 'characterData') texts.push(record.target.textContent ?? ''); + for (const node of Array.from(record.addedNodes)) texts.push(node.textContent ?? ''); + } + }); + observer.observe(container, { childList: true, subtree: true, characterData: true }); + const result = render( + + {page} + , + { container }, + ); + // The first commit is synchronous; record it whatever the observer batched. + texts.push(container.textContent ?? ''); + return { + ...result, + /** Everything the page has shown so far, once the observer has delivered. */ + async shown(): Promise { + await act(async () => { + await Promise.resolve(); + }); + for (const record of observer.takeRecords()) { + for (const node of Array.from(record.addedNodes)) texts.push(node.textContent ?? ''); + } + return texts; + }, + }; +} + +describe('the marketplace pages wait for their strings (objectstack#30588)', () => { + it('zh-CN: the catalogue page shows the load skeleton, then Chinese, and never English or a raw key', async () => { + const zhMarketplace = builtInLocales.zh.marketplace; + const view = renderRecording(, 'zh-CN'); + // The first frame holds no namespace text yet — in no language, and not + // as a key — only the skeleton. + const firstFrame = view.container.textContent ?? ''; + expect(firstFrame).not.toMatch(RAW_KEY); + expect(firstFrame).not.toContain(ENGLISH.title); + expect(screen.getByTestId('marketplace-resolving')).toBeInTheDocument(); + + expect(await screen.findByText(zhMarketplace.title)).toBeInTheDocument(); + expect(screen.getByText(zhMarketplace.subtitle)).toBeInTheDocument(); + + const shown = (await view.shown()).join('\n'); + expect(shown).not.toContain(ENGLISH.title); + expect(shown).not.toContain(ENGLISH.subtitle); + expect(shown).not.toMatch(RAW_KEY); + }); + + it('de: the package page shows the load skeleton, then German, and never English or a raw key', async () => { + const deMarketplace = builtInLocales.de.marketplace; + const view = renderRecording(, 'de'); + const firstFrame = view.container.textContent ?? ''; + expect(firstFrame).not.toMatch(RAW_KEY); + expect(firstFrame).not.toContain(ENGLISH.back); + expect(screen.getByTestId('marketplace-resolving')).toBeInTheDocument(); + + expect((await screen.findAllByText(deMarketplace.back)).length).toBeGreaterThan(0); + + const shown = (await view.shown()).join('\n'); + expect(shown).not.toContain(ENGLISH.back); + expect(shown).not.toMatch(RAW_KEY); + }); + + it('ar: a catalogue that cannot be fetched is said in resident words, and Try again fetches it', async () => { + arChunk.fails = true; + const view = renderRecording(, 'ar'); + + const failed = await screen.findByTestId('marketplace-namespace-failed'); + expect(failed).toHaveTextContent(ar.console.errors.somethingWentWrong); + expect(arChunk.requests).toBe(1); + + arChunk.fails = false; + await act(async () => { + fireEvent.click(screen.getByRole('button', { name: ar.console.errors.tryAgain })); + }); + expect(await screen.findByText(builtInLocales.ar.marketplace.title)).toBeInTheDocument(); + expect(arChunk.requests).toBe(2); + + const shown = (await view.shown()).join('\n'); + expect(shown).not.toContain(ENGLISH.title); + expect(shown).not.toMatch(RAW_KEY); + }); + + it('ja after zh-CN: a language switch on the package page raises no sample-data banner, and leaving the page raises it in Japanese', async () => { + // A package this session installed with sample data: leaving its page + // raises the banner that follows the installer (objectstack#30323). + rememberSampleData({ manifestId: PKG.manifest_id, name: PKG.display_name, count: 12, path: '/apps/setup/system/marketplace/pkg_1' }); + const banner = (pack: { marketplace: { detail: { sampleDataBannerTitle: string } } }) => + pack.marketplace.detail.sampleDataBannerTitle.replace('{{name}}', PKG.display_name); + + /** The console around the page: its banner surface, a language switch and a way out, all outside the frame. */ + function Console() { + const [onPage, setOnPage] = useState(true); + const { changeLanguage } = useI18nContext(); + return ( + + + + + {onPage ? :
} + + ); + } + + const view = renderRecording(, 'zh-CN'); + expect((await screen.findAllByText(builtInLocales.zh.marketplace.back)).length).toBeGreaterThan(0); + + // The switch sends the frame back to its skeleton until the Japanese + // catalogue lands, which unmounts the page body. That is not a leave. + await act(async () => { + fireEvent.click(screen.getByRole('button', { name: 'switch-language' })); + }); + expect((await screen.findAllByText(builtInLocales.ja.marketplace.back)).length).toBeGreaterThan(0); + const beforeLeaving = (await view.shown()).join('\n'); + for (const pack of [builtInLocales.en, builtInLocales.zh, builtInLocales.ja]) expect(beforeLeaving).not.toContain(banner(pack)); + + // Leaving the page is: the banner follows, in the console's language. + await act(async () => { + fireEvent.click(screen.getByRole('button', { name: 'leave-page' })); + }); + expect(await screen.findByText(banner(builtInLocales.ja))).toBeInTheDocument(); + + const shown = (await view.shown()).join('\n'); + expect(shown).not.toContain(banner(builtInLocales.en)); + expect(shown).not.toContain(ENGLISH.back); + expect(shown).not.toMatch(RAW_KEY); + }); +}); diff --git a/ui/packages/app-shell/src/console/marketplace/__tests__/marketplaceNamespaceBoundary.test.ts b/ui/packages/app-shell/src/console/marketplace/__tests__/marketplaceNamespaceBoundary.test.ts new file mode 100644 index 00000000000..9685ebb4e96 --- /dev/null +++ b/ui/packages/app-shell/src/console/marketplace/__tests__/marketplaceNamespaceBoundary.test.ts @@ -0,0 +1,225 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * objectstack#30588 — which `marketplace.*` keys may live in the lazy + * `marketplace` namespace, read off the sources rather than off a list. + * + * The namespace (`@object-ui/i18n/namespaces/marketplace`) is fetched by + * `MarketplaceNamespaceGate`, which the two marketplace pages wrap their + * bodies in. A key that is only in the namespace therefore renders only under + * that frame; read anywhere else it is a raw key (no catalogue fetched at all) + * or English in a translated console (only `en` fetched by an earlier visit). + * + * This directory is not only those two pages: Studio's packages page imports + * `MarketplaceRowActions.tsx`, `dependencyMissing.ts`, `protocolCheckSkipped.ts` + * and more from here, outside the frame. So this file: + * + * 1. finds every file in `ui/packages//src` and `ui/apps//src` that + * imports a module of this directory from outside it — the doors in; + * 2. holds that the two pages behind the frame wrap their exported component + * in ``; + * 3. walks every OTHER door's relative imports inside this directory + * (transitively) and holds that every `marketplace.*` key those files + * spell is in the RESIDENT pack — the package entry's `en`, which holds no + * namespace key. + * + * TypeScript's own parser reads the sources, so a key or an import written in + * a comment is never read as code. + */ + +import { describe, it, expect } from 'vitest'; +import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs'; +import { dirname, join, relative, resolve, sep } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import ts from 'typescript'; +import { en } from '@object-ui/i18n'; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const MARKETPLACE_DIR = resolve(HERE, '..'); +const UI_ROOT = resolve(MARKETPLACE_DIR, '../../../../..'); +const rel = (file: string) => relative(UI_ROOT, file).split(sep).join('/'); + +/** The pages behind the frame: what reaches them is gated by construction. */ +const GATED_PAGES = ['MarketplacePage.tsx', 'MarketplacePackagePage.tsx'].map((f) => join(MARKETPLACE_DIR, f)); + +const isSource = (name: string) => + /\.(ts|tsx)$/.test(name) && !/\.(test|spec)\.(ts|tsx)$/.test(name) && !name.endsWith('.d.ts'); + +function walk(dir: string, out: string[] = []): string[] { + for (const name of readdirSync(dir)) { + if (name === 'node_modules' || name === 'dist' || name === '__tests__') continue; + const full = join(dir, name); + if (statSync(full).isDirectory()) walk(full, out); + else if (isSource(name)) out.push(full); + } + return out; +} + +/** Every `src/` tree under `ui/packages/*` and `ui/apps/*`. */ +function allSources(): string[] { + const roots: string[] = []; + for (const group of ['packages', 'apps']) { + for (const name of readdirSync(join(UI_ROOT, group))) { + const src = join(UI_ROOT, group, name, 'src'); + if (existsSync(src) && statSync(src).isDirectory()) roots.push(src); + } + } + return roots.flatMap((root) => walk(root)); +} + +const parsed = new Map(); +function parse(file: string): ts.SourceFile { + let sf = parsed.get(file); + if (!sf) { + const kind = file.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS; + sf = ts.createSourceFile(file, readFileSync(file, 'utf8'), ts.ScriptTarget.Latest, true, kind); + parsed.set(file, sf); + } + return sf; +} + +/** A relative specifier resolved to a source file, or null (a package, an asset). */ +function resolveSpecifier(from: string, specifier: string): string | null { + if (!specifier.startsWith('.')) return null; + const base = resolve(dirname(from), specifier).replace(/\.js$/, ''); + for (const candidate of [`${base}.ts`, `${base}.tsx`, join(base, 'index.ts'), join(base, 'index.tsx')]) { + if (existsSync(candidate)) return candidate; + } + return null; +} + +/** Every module `file` imports or re-exports through a relative specifier, static or `import()`. */ +function importsOf(file: string): string[] { + const out: string[] = []; + const visit = (node: ts.Node) => { + let specifier: ts.Expression | undefined; + if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier) { + specifier = node.moduleSpecifier; + } else if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) { + specifier = node.arguments[0]; + } + if (specifier && ts.isStringLiteralLike(specifier)) { + const target = resolveSpecifier(file, specifier.text); + if (target) out.push(target); + } + ts.forEachChild(node, visit); + }; + visit(parse(file)); + return out; +} + +const inMarketplaceDir = (file: string) => file.startsWith(`${MARKETPLACE_DIR}${sep}`); + +/** `marketplace.*` keys a file spells: whole keys, and the fixed head of a template key. */ +function keysOf(file: string): { keys: string[]; prefixes: string[] } { + const keys: string[] = []; + const prefixes: string[] = []; + const visit = (node: ts.Node) => { + if ((ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) && /^marketplace\.[\w.-]+$/.test(node.text)) { + keys.push(node.text); + } else if (ts.isTemplateHead(node) && node.text.startsWith('marketplace.')) { + prefixes.push(node.text.replace(/\.$/, '')); + } + ts.forEachChild(node, visit); + }; + visit(parse(file)); + return { keys, prefixes }; +} + +function lookup(pack: unknown, path: string): unknown { + let node: unknown = pack; + for (const segment of path.split('.')) { + if (typeof node !== 'object' || node === null || !Object.prototype.hasOwnProperty.call(node, segment)) return undefined; + node = (node as Record)[segment]; + } + return node; +} + +/** A key the resident pack answers: a leaf, or a plural base whose `_other` form is a leaf. */ +const residentLeaf = (key: string) => + typeof lookup(en, key) === 'string' || typeof lookup(en, `${key}_other`) === 'string'; +const residentGroup = (key: string) => { + const node = lookup(en, key); + return typeof node === 'object' && node !== null; +}; + +// -- the census --------------------------------------------------------------- + +const doors = new Map>(); // marketplace file -> outside importers +for (const file of allSources()) { + if (inMarketplaceDir(file)) continue; + for (const target of importsOf(file)) { + if (!inMarketplaceDir(target)) continue; + if (!doors.has(target)) doors.set(target, new Set()); + doors.get(target)!.add(rel(file)); + } +} + +/** Every file in this directory reachable from an ungated door, following relative imports inside it. */ +const ungated = new Set(); +{ + const queue = [...doors.keys()].filter((file) => !GATED_PAGES.includes(file)); + while (queue.length > 0) { + const file = queue.pop()!; + if (ungated.has(file) || GATED_PAGES.includes(file)) continue; + ungated.add(file); + for (const target of importsOf(file)) if (inMarketplaceDir(target)) queue.push(target); + } +} + +describe('the marketplace namespace boundary (objectstack#30588)', () => { + it('finds the doors into this directory (the census is not over nothing)', () => { + const names = [...doors.keys()].map((file) => rel(file)); + // Lit controls: the two gated pages, and Studio's packages page's own imports. + expect(names).toEqual( + expect.arrayContaining([ + 'packages/app-shell/src/console/marketplace/MarketplacePage.tsx', + 'packages/app-shell/src/console/marketplace/MarketplacePackagePage.tsx', + 'packages/app-shell/src/console/marketplace/MarketplaceRowActions.tsx', + 'packages/app-shell/src/console/marketplace/dependencyMissing.ts', + 'packages/app-shell/src/console/marketplace/protocolCheckSkipped.ts', + ]), + ); + expect([...doors.get(join(MARKETPLACE_DIR, 'MarketplaceRowActions.tsx'))!]).toContain( + 'packages/app-shell/src/views/metadata-admin/PackagesPage.tsx', + ); + }); + + it.each(GATED_PAGES.map((file) => [rel(file), file]))( + '%s renders its body under ', + (_name, file) => { + const sf = parse(file); + const exported = sf.statements.find( + (s): s is ts.FunctionDeclaration => + ts.isFunctionDeclaration(s) && + !!s.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) && + /^Marketplace(Package)?Page$/.test(s.name?.text ?? ''), + ); + expect(exported, 'the exported page component').toBeDefined(); + const returns = exported!.body!.statements.filter(ts.isReturnStatement); + expect(returns).toHaveLength(1); + let expr = returns[0].expression!; + while (ts.isParenthesizedExpression(expr)) expr = expr.expression; + expect(ts.isJsxElement(expr) && expr.openingElement.tagName.getText(sf)).toBe('MarketplaceNamespaceGate'); + }, + ); + + it('every marketplace key a file outside the frame can render is a resident pack key', () => { + const outside: string[] = []; + let read = 0; + for (const file of ungated) { + const { keys, prefixes } = keysOf(file); + read += keys.length + prefixes.length; + for (const key of keys) if (!residentLeaf(key)) outside.push(`${key} (${rel(file)})`); + for (const prefix of prefixes) if (!residentGroup(prefix)) outside.push(`${prefix}.* (${rel(file)})`); + } + // Lit control: the walk reads the row actions' and the refusal readers' keys. + expect(read).toBeGreaterThan(15); + expect(outside).toEqual([]); + }); + + it('the probe key the frame waits on is NOT resident (the pack really is lighter)', () => { + expect(lookup(en, 'marketplace.title')).toBeUndefined(); + expect(typeof lookup(en, 'marketplace.action.uninstall')).toBe('string'); + }); +}); diff --git a/ui/packages/app-shell/src/console/marketplace/marketplaceFrameHolding.ts b/ui/packages/app-shell/src/console/marketplace/marketplaceFrameHolding.ts new file mode 100644 index 00000000000..c78a4fa796c --- /dev/null +++ b/ui/packages/app-shell/src/console/marketplace/marketplaceFrameHolding.ts @@ -0,0 +1,25 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Whether the marketplace pages' strings frame (`MarketplaceNamespaceGate`) is + * holding its page back right now (objectstack#30588). + * + * A language switch sends the frame back to its load skeleton until that + * language's catalogue has arrived, which unmounts the page body while the + * visitor stays on the page. An unmount cleanup that means "the visitor left + * the page" (`SampleDataLeaveBanner`) calls this first. + * + * The answer is a function, read in the cleanup, never during render. The + * frame sets it in the layout phase of the commit that swaps the page out, and + * that runs before the commit's passive cleanups. It stays false when the + * route itself is left, since the frame then unmounts without re-rendering. + * `null` outside a frame. + */ + +import { createContext, useContext } from 'react'; + +export const MarketplaceFrameHolding = createContext<(() => boolean) | null>(null); + +export function useMarketplaceFrameHolding(): (() => boolean) | null { + return useContext(MarketplaceFrameHolding); +} diff --git a/ui/packages/i18n/README.md b/ui/packages/i18n/README.md index f604a144957..08b749dabcd 100644 --- a/ui/packages/i18n/README.md +++ b/ui/packages/i18n/README.md @@ -421,6 +421,42 @@ and retired `check:i18n-designer-parity`, which paired the two homes. App-shell' still skips app-shell's translator **by declaration** (its `EXCLUDED_TRANSLATORS` entry), so a second local table cannot appear silently. +## The `marketplace` namespace — the marketplace pages' strings, fetched with them + +The `marketplace.*` keys are ordinary pack keys to their readers — app-shell's +marketplace pages ask `t('marketplace.…')` like any other component — but they +are not in the packs. They live in +[`src/namespaces/marketplace/`](./src/namespaces/marketplace/index.ts), reached +through the `@object-ui/i18n/namespaces/marketplace` subpath: + +- **Ten catalogues, all lazy.** `src/namespaces/marketplace/.ts` is a + partial pack — the same shape as `src/locales/.ts`, holding only the + `marketplace` group. Each is its own chunk; none is in the console's first + load, so the eager `en` pack does not carry two lazy pages' text. +- **A page waits for them.** `useMarketplaceNamespace()` fetches the catalogue + for the instance's language plus `en` (the packs' fallback) and deep-merges + them into the instance, never over a host's own `resources`. App-shell's + marketplace pages render a copy-free loading frame until it answers `ready`, + and again after a language switch, so a visitor never reads English in a + translated page nor a raw key. +- **What Studio's packages page reads stays in the packs.** That page, outside + the marketplace route, draws a marketplace install's row with app-shell's + `MarketplaceRowActions` / `MarketplaceNotLoaded` and an import's refusal and + protocol note with the `dependencyMissing` / `protocolCheckSkipped` readers, + so the keys those read (`marketplace.installedAdditiveNote`, + `marketplace.notLoaded.*`, three `marketplace.action.*` and four + `marketplace.uninstall.*` leaves, `marketplace.install.dependencyMissing.*`, + `marketplace.install.protocolCheckSkipped.*`) stay resident. App-shell's + `marketplaceNamespaceBoundary` test holds that set to what those files read. +- **Inside the pack gates.** `@object-ui/i18n/locales` hands out each language + COMPLETE — its pack merged with its namespace catalogue — so key parity and + every suite that walks `builtInLocales` still walk these keys, and + `registerBuiltInLocales()` makes them resident with the rest. The call-site, + dead-key and `en`-drift gates under `ui/scripts/` read the namespace files as + part of the packs and refuse a key defined in both. + +The package entry's `en` is the resident pack alone, without the namespace. + ## Links - 📦 [npm package](https://www.npmjs.com/package/@object-ui/i18n) diff --git a/ui/packages/i18n/package.json b/ui/packages/i18n/package.json index 641ccc97f2f..a577c6a558c 100644 --- a/ui/packages/i18n/package.json +++ b/ui/packages/i18n/package.json @@ -37,6 +37,11 @@ "types": "./dist/namespaces/studio/index.d.ts", "import": "./dist/namespaces/studio/index.js", "default": "./dist/namespaces/studio/index.js" + }, + "./namespaces/marketplace": { + "types": "./dist/namespaces/marketplace/index.d.ts", + "import": "./dist/namespaces/marketplace/index.js", + "default": "./dist/namespaces/marketplace/index.js" } }, "scripts": { diff --git a/ui/packages/i18n/src/__tests__/marketplace-namespace-lazy.test.tsx b/ui/packages/i18n/src/__tests__/marketplace-namespace-lazy.test.tsx new file mode 100644 index 00000000000..b22f9613f09 --- /dev/null +++ b/ui/packages/i18n/src/__tests__/marketplace-namespace-lazy.test.tsx @@ -0,0 +1,184 @@ +/** + * The `marketplace` namespace's lazy runtime (objectstack#30588) — what a + * marketplace page observes through `useMarketplaceNamespace()`, in order: + * + * 1. in a translated console, the namespace answers `loading` until the + * language's catalogue (and `en`, the packs' fallback) has landed, and a + * component behind it renders in that language from its FIRST render — + * never English, never the raw key; + * 2. a language switch answers `loading` again until the new language's + * catalogue lands; + * 3. a language the namespace has no catalogue for is served by `en`; + * 4. a host's own `resources` for one `marketplace.*` key win over the + * built-in text and do not stop the rest of the catalogue from loading; + * 5. a catalogue that cannot be fetched answers `failed` once — no retry + * loop — and `retry()` fetches it again. + * + * ## The registry is put back in its production state + * + * The DOM setup (`vitest.setup.i18n-catalogues.ts`) calls + * `registerBuiltInLocales()`, which registers every language COMPLETE — + * namespace included — so a page would never need a fetch, which is the + * all-ten door's promise and not what a console visitor gets. This file + * re-registers each language as its pack ALONE, at module scope: the registry + * a console that awaited `preloadBootstrapLocale()` holds. A `dom` project + * isolates each file's module graph, so nothing here reaches another file, + * and the namespace starts with no catalogue fetched. + */ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import React from 'react'; +import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'; + +import { I18nProvider, useI18nContext, useObjectTranslation } from '../provider.js'; +import { BUILT_IN_LANGUAGE_CODES, registerBuiltInLocale } from '../locales/registry.js'; +import enPack from '../locales/en.js'; +import zhPack from '../locales/zh.js'; +import jaPack from '../locales/ja.js'; +import koPack from '../locales/ko.js'; +import dePack from '../locales/de.js'; +import frPack from '../locales/fr.js'; +import esPack from '../locales/es.js'; +import ptPack from '../locales/pt.js'; +import ruPack from '../locales/ru.js'; +import arPack from '../locales/ar.js'; +import enNs from '../namespaces/marketplace/en.js'; +import zhNs from '../namespaces/marketplace/zh.js'; +import jaNs from '../namespaces/marketplace/ja.js'; +import { + isMarketplaceCatalogueLoaded, + useMarketplaceNamespace, +} from '../namespaces/marketplace/index.js'; + +/** `ko` is the catalogue whose fetch fails, until a case says otherwise. */ +const koChunk = vi.hoisted(() => ({ fails: true, requests: 0 })); +vi.mock('../namespaces/marketplace/ko.js', async (importOriginal) => { + koChunk.requests += 1; + if (koChunk.fails) throw new Error('chunk offline'); + return { ...(await importOriginal>()) }; +}); + +const PACKS: Record> = { + en: enPack, zh: zhPack, ja: jaPack, ko: koPack, de: dePack, + fr: frPack, es: esPack, pt: ptPack, ru: ruPack, ar: arPack, +}; +for (const code of BUILT_IN_LANGUAGE_CODES) registerBuiltInLocale(code, PACKS[code]); + +afterEach(cleanup); + +/** Every text the probe below rendered, with the language it rendered in. */ +let seen: string[] = []; + +function Title() { + const { t, i18n } = useObjectTranslation(); + const title = t('marketplace.title'); + const refresh = t('marketplace.refresh'); + seen.push(`${i18n.language}|${title}`); + return ( +
+

{title}

+ {refresh} +
+ ); +} + +function Page() { + const namespace = useMarketplaceNamespace(); + const { changeLanguage } = useI18nContext(); + return ( +
+ + {namespace.status === 'ready' ? ( + + ) : ( + <p data-testid="state"> + {namespace.status} + <button onClick={namespace.retry}>retry</button> + </p> + )} + </div> + ); +} + +function renderIn(language: string, resources?: Record<string, Record<string, unknown>>) { + seen = []; + return render( + <I18nProvider config={{ defaultLanguage: language, detectBrowserLanguage: false, resources }} persistLanguage={false}> + <Page /> + </I18nProvider>, + ); +} + +const ZH_TITLE = zhNs.marketplace.title; +const JA_TITLE = jaNs.marketplace.title; +const EN_TITLE = enNs.marketplace.title; + +describe('marketplace namespace — lazy', () => { + it('a Chinese page waits, then renders Chinese from its first render', async () => { + expect(isMarketplaceCatalogueLoaded('zh')).toBe(false); + expect(isMarketplaceCatalogueLoaded('en')).toBe(false); + + renderIn('zh-CN'); + // The first paint: the fetch cannot have resolved synchronously. + expect(screen.getByTestId('state').textContent).toContain('loading'); + expect(screen.queryByTestId('title')).toBeNull(); + + expect(await screen.findByText(ZH_TITLE)).toBeTruthy(); + expect(screen.getByTestId('refresh').textContent).toBe(zhNs.marketplace.refresh); + // Every render the component made was Chinese: no English, no raw key. + expect(seen.length).toBeGreaterThan(0); + expect(seen.filter((entry) => entry !== `zh-CN|${ZH_TITLE}`)).toEqual([]); + // What it fetched: its own catalogue and the fallback, nothing else. + expect(isMarketplaceCatalogueLoaded('zh')).toBe(true); + expect(isMarketplaceCatalogueLoaded('en')).toBe(true); + expect(isMarketplaceCatalogueLoaded('ja')).toBe(false); + }); + + it('a language switch waits again for the new language', async () => { + renderIn('zh-CN'); + expect(await screen.findByText(ZH_TITLE)).toBeTruthy(); + + seen = []; + await act(async () => { + fireEvent.click(screen.getByText('switch')); + }); + expect(await screen.findByText(JA_TITLE)).toBeTruthy(); + // Between the two, the component never rendered Japanese-console English. + expect(seen.filter((entry) => entry.startsWith('ja|') && entry !== `ja|${JA_TITLE}`)).toEqual([]); + expect(seen).toContain(`ja|${JA_TITLE}`); + }); + + it('a language with no catalogue of its own is served by en', async () => { + renderIn('sv'); + expect(await screen.findByText(EN_TITLE)).toBeTruthy(); + expect(seen.filter((entry) => entry !== `sv|${EN_TITLE}`)).toEqual([]); + }); + + it("a host's own key wins, and the rest of the catalogue still loads", async () => { + // A fresh instance whose zh resources spell the probe key themselves. + renderIn('zh-CN', { zh: { marketplace: { title: '宿主自己的市场' } } }); + expect(await screen.findByText('宿主自己的市场')).toBeTruthy(); + expect(screen.getByTestId('refresh').textContent).toBe(zhNs.marketplace.refresh); + }); + + it('a catalogue that cannot be fetched says so once, and retry fetches it again', async () => { + expect(koChunk.requests).toBe(0); + renderIn('ko'); + expect(await screen.findByText(/failed/)).toBeTruthy(); + expect(koChunk.requests).toBe(1); + + // No retry loop: the failure stays a failure until the visitor asks. + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 50)); + }); + expect(koChunk.requests).toBe(1); + expect(screen.getByTestId('state').textContent).toContain('failed'); + + koChunk.fails = false; + await act(async () => { + fireEvent.click(screen.getByText('retry')); + }); + const ko = await vi.importActual<typeof import('../namespaces/marketplace/ko.js')>('../namespaces/marketplace/ko.js'); + expect(await screen.findByText(ko.default.marketplace.title)).toBeTruthy(); + expect(koChunk.requests).toBe(2); + }); +}); diff --git a/ui/packages/i18n/src/__tests__/marketplace-namespace.test.ts b/ui/packages/i18n/src/__tests__/marketplace-namespace.test.ts new file mode 100644 index 00000000000..501b1e216d8 --- /dev/null +++ b/ui/packages/i18n/src/__tests__/marketplace-namespace.test.ts @@ -0,0 +1,185 @@ +/** + * The `marketplace` namespace (objectstack#30588): its shape, and how it sits + * beside the ten packs it was cut out of. + * + * 1. LAZY BY CONSTRUCTION. `namespaces/marketplace/index.ts` reaches its ten + * catalogues through ten literal `import()`s and nothing else — no static + * import, no template-literal specifier — so each is a chunk of its own + * and none is in the console's first load. + * 2. CUT, NOT COPIED. Every catalogue holds only the `marketplace` group and + * the probe key the page frame waits on; no pack holds the probe key, and + * no leaf is in both a pack and its catalogue. + * 3. THE ALL-TEN DOOR IS COMPLETE. `locales/index.ts` hands out each language + * as its pack merged with its catalogue, so the parity suites that walk + * `builtInLocales` still walk these keys; the package entry's `en` is the + * resident pack alone. + * 4. LANGUAGE RESOLUTION and the memoised loader. + * + * The runtime a page observes — loading, the fetch, the re-render, a switch, a + * failure and its retry — is `marketplace-namespace-lazy.test.tsx`'s, in a DOM + * project that isolates its module graph; this file runs in `unit`, which + * shares one per worker, so "nothing fetched yet" is not a state it assumes. + */ +import { describe, expect, it } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import ts from 'typescript'; + +import { en as entryEn } from '../index.js'; +import { BUILT_IN_LANGUAGE_CODES } from '../locales/registry.js'; +import { builtInLocales } from '../locales/index.js'; +import enPack from '../locales/en.js'; +import zhPack from '../locales/zh.js'; +import jaPack from '../locales/ja.js'; +import koPack from '../locales/ko.js'; +import dePack from '../locales/de.js'; +import frPack from '../locales/fr.js'; +import esPack from '../locales/es.js'; +import ptPack from '../locales/pt.js'; +import ruPack from '../locales/ru.js'; +import arPack from '../locales/ar.js'; +import enNs from '../namespaces/marketplace/en.js'; +import zhNs from '../namespaces/marketplace/zh.js'; +import jaNs from '../namespaces/marketplace/ja.js'; +import koNs from '../namespaces/marketplace/ko.js'; +import deNs from '../namespaces/marketplace/de.js'; +import frNs from '../namespaces/marketplace/fr.js'; +import esNs from '../namespaces/marketplace/es.js'; +import ptNs from '../namespaces/marketplace/pt.js'; +import ruNs from '../namespaces/marketplace/ru.js'; +import arNs from '../namespaces/marketplace/ar.js'; +import { + MARKETPLACE_LANGUAGE_CODES, + MARKETPLACE_NAMESPACE, + MARKETPLACE_NAMESPACE_PROBE_KEY, + isMarketplaceCatalogueLoaded, + loadMarketplaceCatalogue, + resolveMarketplaceLanguage, +} from '../namespaces/marketplace/index.js'; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const INDEX_PATH = join(HERE, '..', 'namespaces', 'marketplace', 'index.ts'); + +type Tree = Record<string, unknown>; +const PACKS: Record<string, Tree> = { + en: enPack, zh: zhPack, ja: jaPack, ko: koPack, de: dePack, + fr: frPack, es: esPack, pt: ptPack, ru: ruPack, ar: arPack, +}; +const CATALOGUES: Record<string, Tree> = { + en: enNs, zh: zhNs, ja: jaNs, ko: koNs, de: deNs, + fr: frNs, es: esNs, pt: ptNs, ru: ruNs, ar: arNs, +}; + +function leaves(node: unknown, prefix = ''): string[] { + if (typeof node !== 'object' || node === null) return [prefix]; + return Object.entries(node).flatMap(([k, v]) => leaves(v, prefix ? `${prefix}.${k}` : k)); +} + +function lookup(tree: unknown, path: string): unknown { + let node: unknown = tree; + for (const segment of path.split('.')) { + if (typeof node !== 'object' || node === null || !Object.prototype.hasOwnProperty.call(node, segment)) return undefined; + node = (node as Tree)[segment]; + } + return node; +} + +describe('marketplace namespace — lazy by construction', () => { + const source = readFileSync(INDEX_PATH, 'utf8'); + const file = ts.createSourceFile('index.ts', source, ts.ScriptTarget.Latest, true); + const staticSpecifiers: string[] = []; + const dynamicSpecifiers: string[] = []; + const nonLiteralImports: string[] = []; + const visit = (node: ts.Node) => { + if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) { + staticSpecifiers.push(node.moduleSpecifier.text); + } else if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) { + const arg = node.arguments[0]; + if (arg && ts.isStringLiteral(arg)) dynamicSpecifiers.push(arg.text); + else nonLiteralImports.push(node.getText(file)); + } + ts.forEachChild(node, visit); + }; + visit(file); + + it('imports one literal `import()` per catalogue, one catalogue per built-in language', () => { + expect([...dynamicSpecifiers].sort()).toEqual(BUILT_IN_LANGUAGE_CODES.map((code) => `./${code}.js`).sort()); + expect(nonLiteralImports).toEqual([]); + }); + + it('statically imports no catalogue (nothing of it rides in with the page frame)', () => { + expect(staticSpecifiers.filter((s) => /^\.\/[a-z]{2}\.js$/.test(s))).toEqual([]); + // Lit control: the static imports were read. + expect(staticSpecifiers).toContain('../../locales/registry.js'); + }); + + it('ships the ten built-in languages', () => { + expect([...MARKETPLACE_LANGUAGE_CODES].sort()).toEqual([...BUILT_IN_LANGUAGE_CODES].sort()); + }); +}); + +describe('marketplace namespace — cut out of the packs, not copied', () => { + it.each(BUILT_IN_LANGUAGE_CODES.map((code) => [code]))('%s: the catalogue holds the marketplace group only', (code) => { + expect(Object.keys(CATALOGUES[code])).toEqual([MARKETPLACE_NAMESPACE]); + }); + + it.each(BUILT_IN_LANGUAGE_CODES.map((code) => [code]))('%s: the probe key is in the catalogue and not in the pack', (code) => { + expect(typeof lookup(CATALOGUES[code], MARKETPLACE_NAMESPACE_PROBE_KEY)).toBe('string'); + expect(lookup(PACKS[code], MARKETPLACE_NAMESPACE_PROBE_KEY)).toBeUndefined(); + }); + + it.each(BUILT_IN_LANGUAGE_CODES.map((code) => [code]))('%s: no leaf is in both the pack and the catalogue', (code) => { + const packLeaves = new Set(leaves(PACKS[code])); + const nsLeaves = leaves(CATALOGUES[code]); + // Lit controls: both halves of the marketplace group are populated. + expect(nsLeaves.length).toBeGreaterThan(150); + expect([...packLeaves].filter((k) => k.startsWith('marketplace.')).length).toBeGreaterThan(10); + expect(nsLeaves.filter((key) => packLeaves.has(key))).toEqual([]); + }); +}); + +describe('marketplace namespace — the all-ten door hands out complete languages', () => { + it.each(BUILT_IN_LANGUAGE_CODES.map((code) => [code]))('%s: pack keys and namespace keys are both there', (code) => { + const complete = (builtInLocales as unknown as Record<string, Tree>)[code]; + const expected = [...leaves(PACKS[code]), ...leaves(CATALOGUES[code])].sort(); + expect(leaves(complete).sort()).toEqual(expected); + expect(lookup(complete, MARKETPLACE_NAMESPACE_PROBE_KEY)).toBe(lookup(CATALOGUES[code], MARKETPLACE_NAMESPACE_PROBE_KEY)); + // A family Studio's packages page reads, still served from the pack. + expect(lookup(complete, 'marketplace.action.uninstall')).toBe(lookup(PACKS[code], 'marketplace.action.uninstall')); + }); + + it("the package entry's `en` is the resident pack alone", () => { + expect(lookup(entryEn, MARKETPLACE_NAMESPACE_PROBE_KEY)).toBeUndefined(); + expect(typeof lookup(entryEn, 'marketplace.install.dependencyMissing.heading')).toBe('string'); + }); +}); + +describe('marketplace namespace — language resolution and the loader', () => { + it.each([ + ['zh-CN', 'zh'], + ['zh', 'zh'], + ['zh-TW', 'zh'], + ['pt-BR', 'pt'], + ['ar-EG', 'ar'], + ['en-US', 'en'], + ['de', 'de'], + ['sv', null], + ['', null], + [undefined, null], + ])('%s is served by the %s catalogue', (lang, code) => { + expect(resolveMarketplaceLanguage(lang)).toBe(code); + }); + + it('refuses a code it ships no catalogue for', async () => { + await expect(loadMarketplaceCatalogue('tlh')).rejects.toThrow(/ships no `tlh` catalogue/); + expect(isMarketplaceCatalogueLoaded('tlh')).toBe(false); + }); + + it('loads a catalogue once and answers the same object after', async () => { + const first = await loadMarketplaceCatalogue('de'); + expect(lookup(first, MARKETPLACE_NAMESPACE_PROBE_KEY)).toBe(lookup(deNs, MARKETPLACE_NAMESPACE_PROBE_KEY)); + expect(isMarketplaceCatalogueLoaded('de')).toBe(true); + await expect(loadMarketplaceCatalogue('de')).resolves.toBe(first); + }); +}); diff --git a/ui/packages/i18n/src/locales/ar.ts b/ui/packages/i18n/src/locales/ar.ts index c7d4f69a3ed..88eb8a5b87a 100644 --- a/ui/packages/i18n/src/locales/ar.ts +++ b/ui/packages/i18n/src/locales/ar.ts @@ -4433,42 +4433,16 @@ const ar = { connect: "اتصال", }, }, + // The rest of `marketplace.*` is the lazy `marketplace` namespace + // (`../namespaces/marketplace/ar.ts`), fetched when a marketplace page + // mounts. What stays here is read by a page outside the marketplace route: + // Studio's packages page draws a marketplace install's row through + // `MarketplaceRowActions` and `MarketplaceNotLoaded` (the Not loaded badge + // and reason, Uninstall with its confirmation and result), and an import's + // refusal and protocol note through `useDependencyMissingText` and + // `useProtocolCheckSkippedText`. marketplace: { - suggestedBindings: { - promptEveryone: "يقترح هذا التطبيق منح «{{set}}» لجميع المستخدمين المسجّلين (موضع Everyone).", - promptGuest: "يقترح هذا التطبيق منح «{{set}}» للزوار غير المصادَق عليهم (موضع Guest).", - confirm: "منح", - confirming: "جارٍ المنح…", - dismiss: "تجاهل", - confirmedToast: "تم الآن منح «{{set}}» إلى {{anchor}}.", - dismissedToast: "تم تجاهل اقتراح «{{set}}».", - guestUnsupported: "لا يمكن المنح هنا: لا تُشتق مجموعات موضع Guest من الاقتراحات المقبولة.", - acceptedState: "تم قبول «{{set}}» لجميع المستخدمين المسجّلين (موضع Everyone).", - notInForceState: "تم قبول «{{set}}»، لكنه لم يسرِ بعد: لم يتغير موضع Everyone في هذا الطلب. يوضّح سجل الخادم السبب.", - dismissedState: "«{{set}}» غير ممنوح: تم تجاهل الاقتراح أو إلغاؤه.", - revoke: "إلغاء", - revoking: "جارٍ الإلغاء…", - revokedToast: "تم إلغاء قبول «{{set}}».", - reopen: "إعادة النظر", - reopening: "جارٍ إعادة الفتح…", - reopenedToast: "أُعيد فتح اقتراح «{{set}}»: لا يُمنح شيء حتى يُقبل.", - }, - title: "سوق التطبيقات", - subtitle: "استعرض التطبيقات المعتمدة المنشورة في كتالوج ObjectStack. انقر على تطبيق لرؤية التفاصيل وتثبيته في أحد بيئاتك.", - searchPlaceholder: 'ابحث عن التطبيقات بالاسم أو معرف التطبيق…', - searchAria: "البحث في تطبيقات السوق", - installed: "مثبت", - installedCount: "مثبت ({{count}})", - refresh: "تحديث", - all: "الكل", - noApprovedYet: "لا توجد تطبيقات معتمدة للسوق بعد.", - noMatchFilters: "لا توجد تطبيقات تطابق الفلاتر.", - noDescription: "لا وصف متاح.", - back: "العودة إلى السوق", - browseLink: "استعراض السوق →", installedAdditiveNote: "يزيل إلغاء التثبيت التطبيق من النواة الجارية فوراً، ومعه مجموعات الأذونات الخاصة به وتعييناتها، ويحذف المانيفست المخزن مؤقتاً لكي لا يتم تحميل الحزمة عند التشغيل التالي. تبقى السجلات المخزنة في كائناته في قاعدة البيانات.", - versionBadge: "v{{version}}", - installedBadge: "مثبت v{{version}}", // objectui#11645 — an install-local entry this runtime refused to load at // startup (the listing's `notLoaded` marker), read on Installed Apps. notLoaded: { @@ -4476,106 +4450,18 @@ const ar = { protocolIncompatible: "لم يقم وقت التشغيل هذا بتحميل هذه الحزمة: فهي تستهدف البروتوكول {{requiredRange}}، وهو بروتوكول لا يدعمه وقت التشغيل هذا.", otherReason: "لم يقم وقت التشغيل هذا بتحميل هذه الحزمة ({{code}}).", }, - load: { - failed: "تعذر تحميل السوق", - failedHintConfigured: "يصل وقت التشغيل هذا إلى السوق عبر مستوى التحكم على {{url}}. تحقق من أنه متصل بالإنترنت ويمكن الوصول إليه من هنا.", - failedHintSameOrigin: "يتم الوصول إلى كتالوج السوق عبر وقت التشغيل هذا. تأكد من أن وقت التشغيل متصل بالإنترنت ويمكنه الوصول إلى الكتالوج.", - loading: "جارٍ تحميل كتالوج السوق…", - slow: "لا يزال التحميل جاريًا — كتالوج السوق بطيء في الاستجابة.", - retry: "إعادة المحاولة", - unreachable: "تعذر الوصول إلى كتالوج السوق.", - answeredError: "استجاب كتالوج السوق بخطأ (HTTP {{status}}).", - packageFailed: "تعذر تحميل الحزمة", - notFound: "غير موجود.", - }, - disabled: { - title: "سوق التطبيقات معطّل", - description: "لا يوجد سوق مُهيّأ لوقت التشغيل هذا، لذا لا يوجد ما يمكن تصفحه أو تثبيته من هنا.", - hint: "هذه هي الحالة المتوقعة عند تشغيل وقت التشغيل باستخدام <code>OS_CLOUD_URL=off</code> (أو <code>none</code> أو <code>local</code> أو <code>disabled</code>). لتفعيل السوق، وجّه <code>OS_CLOUD_URL</code> إلى مستوى تحكم وأعد تشغيل وقت التشغيل.", - }, - detail: { - homepage: "الصفحة الرئيسية", - installedV: "مثبت · v{{version}}", - about: "حول", - noReadme: "لا يوجد readme متاح.", - versions: "الإصدارات", - noApprovedVersions: "لا توجد إصدارات معتمدة.", - prerelease: "ما قبل الإصدار", - moreOptions: "مزيد من خيارات التثبيت", - uninstallFromRuntime: "إلغاء التثبيت من هذا الوقت التشغيل", - addSampleData: 'إضافة بيانات تجريبية', - reseedAgain: 'إعادة تحميل البيانات التجريبية', - purgeSampleData: 'حذف البيانات التجريبية', - purgeConfirm: 'حذف جميع السجلات التجريبية التي أنشأتها هذه الحزمة؟ لن تتأثر السجلات التي أضافها المستخدم.', - purgeSuccess: 'تم حذف {{count}} سجل تجريبي.', - purgeSuccess_zero: 'تم حذف {{count}} سجل تجريبي.', - purgeSuccess_one: 'تم حذف {{count}} سجل تجريبي.', - purgeSuccess_two: 'تم حذف سجلين تجريبيين ({{count}}).', - purgeSuccess_few: 'تم حذف {{count}} سجلات تجريبية.', - purgeSuccess_many: 'تم حذف {{count}} سجلًا تجريبيًا.', - purgeSuccess_other: 'تم حذف {{count}} سجل تجريبي.', - purgeNoData: 'لا توجد سجلات تجريبية للحذف.', - // objectstack#30323 — the install note's sample-data count, its clear and the app banner. - sampleDataLoaded: 'حمّل هذا التثبيت سجلات تجريبية عددها {{count}} لترى التطبيق يعمل. احذفها عندما تبدأ استخدامه فعليًا.', - sampleDataLoaded_zero: 'حمّل هذا التثبيت سجلات تجريبية عددها {{count}} لترى التطبيق يعمل. احذفها عندما تبدأ استخدامه فعليًا.', - sampleDataLoaded_one: 'حمّل هذا التثبيت سجلات تجريبية عددها {{count}} لترى التطبيق يعمل. احذفها عندما تبدأ استخدامه فعليًا.', - sampleDataLoaded_two: 'حمّل هذا التثبيت سجلات تجريبية عددها {{count}} لترى التطبيق يعمل. احذفها عندما تبدأ استخدامه فعليًا.', - sampleDataLoaded_few: 'حمّل هذا التثبيت سجلات تجريبية عددها {{count}} لترى التطبيق يعمل. احذفها عندما تبدأ استخدامه فعليًا.', - sampleDataLoaded_many: 'حمّل هذا التثبيت سجلات تجريبية عددها {{count}} لترى التطبيق يعمل. احذفها عندما تبدأ استخدامه فعليًا.', - sampleDataLoaded_other: 'حمّل هذا التثبيت سجلات تجريبية عددها {{count}} لترى التطبيق يعمل. احذفها عندما تبدأ استخدامه فعليًا.', - sampleDataReloaded: 'يؤدي التثبيت مرة أخرى إلى تحميل البيانات التجريبية من جديد، بما فيها ما حذفته.', - purgeConfirmAtMost: 'سيُحذف من السجلات التجريبية {{count}} على الأكثر.', - purgeConfirmAtMost_zero: 'سيُحذف من السجلات التجريبية {{count}} على الأكثر.', - purgeConfirmAtMost_one: 'سيُحذف من السجلات التجريبية {{count}} على الأكثر.', - purgeConfirmAtMost_two: 'سيُحذف من السجلات التجريبية {{count}} على الأكثر.', - purgeConfirmAtMost_few: 'سيُحذف من السجلات التجريبية {{count}} على الأكثر.', - purgeConfirmAtMost_many: 'سيُحذف من السجلات التجريبية {{count}} على الأكثر.', - purgeConfirmAtMost_other: 'سيُحذف من السجلات التجريبية {{count}} على الأكثر.', - sampleDataBannerTitle: 'يحتوي "{{name}}" على بيانات تجريبية.', - sampleDataBannerMessage: 'إنها موجودة لترى التطبيق يعمل. احذفها عندما تبدأ استخدامه فعليًا.', - reseedQueued: 'ستتم إعادة تحميل البيانات التجريبية عند الوصول التالي إلى البيئة.', - reseedLocalSuccess: 'تمت إعادة تحميل البيانات التجريبية: {{inserted}} مضافة، {{updated}} محدثة.', - reseedPartialErrors: '(فشل في كتابة {{count}} سجل)', - reseedPartialErrors_zero: '(فشل في كتابة {{count}} سجل)', - reseedPartialErrors_one: '(فشل في كتابة {{count}} سجل)', - reseedPartialErrors_two: '(فشل في كتابة سجلين: {{count}})', - reseedPartialErrors_few: '(فشل في كتابة {{count}} سجلات)', - reseedPartialErrors_many: '(فشل في كتابة {{count}} سجلًا)', - reseedPartialErrors_other: '(فشل في كتابة {{count}} سجل)', - sampleDataKernelUnavailable: 'لا يحتوي مستوى التحكم هذا على نواة بيئة، لذا لا يمكن إعادة تحميل البيانات التجريبية أو حذفها من هنا. نفّذ ذلك من وقت التشغيل الخاص بالبيئة نفسها.', - updateAvailable: 'يتوفر تحديث', - }, action: { - installed: "مثبَّت", - install: "تثبيت", - reinstall: "إعادة التثبيت", - working: "جارٍ المعالجة…", - installToCloud: "تثبيت في السحابة…", - installing: "جارٍ التثبيت…", - updating: "جارٍ التحديث…", uninstall: "إلغاء التثبيت", uninstalling: "جارٍ إلغاء التثبيت…", details: "التفاصيل", - close: "إغلاق", - dismiss: "رفض", - openOnCloud: "فتح في السحابة", - backHome: "العودة إلى الرئيسية", - updateTo: "تحديث", + }, + uninstall: { + confirm: "إلغاء تثبيت {{manifestId}} v{{version}} من وقت التشغيل هذا؟\n\nستتم إزالة التطبيق من النواة الجارية فوراً، ومعه مجموعات الأذونات الخاصة به وتعييناتها، وستتم إزالة المانيفست المخزن مؤقتاً. تبقى السجلات المخزنة في كائناته في قاعدة البيانات.", + confirmNotLoaded: "إلغاء تثبيت {{manifestId}} v{{version}} من وقت التشغيل هذا؟\n\nسيتم إزالة المانيفست المخزن مؤقتاً. لم يقم وقت التشغيل هذا بتحميل الحزمة، لذا لا يعمل أي جزء منها.", + successInList: "تمت إزالة {{manifestId}}: خرج التطبيق من النواة الجارية، ومعه مجموعات الأذونات الخاصة به وتعييناتها. تبقى السجلات المخزنة في كائناته في قاعدة البيانات.", + successNotLoaded: "تمت إزالة {{manifestId}}. لم تكن الحزمة محمّلة، لذا لا حاجة لإعادة التشغيل.", }, install: { - dialogTitle: "تثبيت {{name}}", - dialogDescCurrent: "تثبيت في هذه البيئة ({{host}}).", - dialogDescPicker: "اختر بيئة لتثبيت هذا التطبيق. يجب أن تكون مسجلاً في ObjectOS Cloud.", - environment: "البيئة", - environmentPlaceholder: "تحديد بيئة", - includeSampleData: "تضمين بيانات نموذجية", - noEnvs: "لم يتم العثور على بيئات في مؤسستك النشطة.", - noPermission: "ليس لديك إذن لتثبيت التطبيقات. فقط مالكو المؤسسة والمشرفون يمكنهم التثبيت — تواصل مع مشرف مؤسستك.", - signInFirst: "يجب عليك أولاً تسجيل الدخول إلى ObjectOS Cloud.", - success: "تم التثبيت بنجاح. افتح البيئة لرؤية التطبيق الجديد.", - localSuccess: 'تم تثبيت v{{version}} في وقت التشغيل هذا. أصبح "{{name}}" الآن في مبدّل التطبيقات.', - localLoadsAtRestart: "تم تثبيت v{{version}} في وقت التشغيل هذا. سيُحمَّل \"{{name}}\" عند إعادة التشغيل التالية لوقت التشغيل، ولن يظهر في مبدّل التطبيقات قبل ذلك.", - localManifestConflict: "{{message}}\nملاحظة: تطبيق محلي يمتلك بالفعل هذا المعرف. أزله أولاً من objectstack.config.ts.", // ADR-0141 D2 — the install refusal DEPENDENCY_MISSING, one line per unmet entry. dependencyMissing: { heading: "تم رفض التثبيت: تبعية تعلنها هذه الحزمة غير مستوفاة في هذه البيئة، لذا لم يُثبَّت أي شيء.", @@ -4589,115 +4475,6 @@ const ar = { noRange: "لا تحدد الحزمة {{packageId}} بروتوكول المنصة الذي تدعمه، لذلك ثُبّتت تحت البروتوكول {{protocolVersion}} دون فحص التوافق وقد لا تعمل على إصدار المنصة هذا. يُفحص عند التثبيت أي إصدار من الحزمة يصرّح بالبروتوكول الذي يدعمه (engines.protocol).", unparsedRange: "تصرّح الحزمة {{packageId}} ببروتوكول المنصة على أنه {{requiredRange}}، وهو ما لا تستطيع بيئة التشغيل هذه قراءته، لذلك ثُبّتت تحت البروتوكول {{protocolVersion}} دون فحص التوافق وقد لا تعمل على إصدار المنصة هذا. يُفحص عند التثبيت أي إصدار من الحزمة يصرّح بنطاق مقروء (engines.protocol).", }, - localUnauthorized: "سجل الدخول إلى هذا الوقت التشغيل أولاً.", - localMarketplaceUnavailable: "هذا الوقت التشغيل لا يحتوي على OS_CLOUD_URL مُهيأ.", - updateTo: "تحديث → v{{version}}", - installedVersion: "مثبت v{{version}}", - deploying: "جارٍ نشر التطبيق…", - deployed: "تم نشر التطبيق وهو جاهز.", - openApp: "فتح {{name}}", - deployTimeout: "تم تسجيل التثبيت، لكن هذه البيئة لا تقدّم التطبيق بعد. قد يكون النشر لا يزال جاريًا — تحقّق مرة أخرى بعد قليل. إذا لم يظهر أبدًا، فربما لم تُحمَّل الحزمة، أو لا تتضمن أي تطبيق، أو يتطلب تطبيقها إذنًا لا تملكه.", - checkAgain: "تحقّق مرة أخرى", - // ADR-0131 D6 — the install modes a package's manifest permits, and - // what an unmanaged install answers (MarketplacePackagePage). - mode: { - label: "طريقة التثبيت", - managed: "مُدار", - managedHint: "تبقى الحزمة هي المرجع: تُسجَّل كتعليمات برمجية وتتلقى ترقياتها، ولا تُحرَّر عناصرها هنا.", - unmanaged: "نسخة غير مُدارة", - unmanagedHint: "تُنسخ مرة واحدة إلى وقت التشغيل هذا كبيانات وصفية خاصة به: كل عنصر قابل للتحرير هنا، ولا تتلقى النسخة أي ترقية.", - required: "اختر طريقة التثبيت لتثبيت هذه الحزمة.", - }, - unmanagedSuccess: "تم نسخ v{{version}} من \"{{name}}\" إلى وقت التشغيل هذا كبيانات وصفية خاصة به، في القاعدة {{base}}. النسخة قابلة للتحرير ولا تتلقى أي ترقية.", - unmanagedStrippedLocks: "كانت الحزمة تقفل هذه العناصر. النسخة قابلة للتحرير، لذلك لم تُنسخ أقفالها:", - unmanagedIgnored: "لا يدعم وقت التشغيل هذا التثبيت غير المُدار، لذلك ثُبّت \"{{name}}\" كحزمة مُدارة (مسجّلة كتعليمات برمجية) بدلاً من نسخه. رقِّ وقت التشغيل، ثم ألغِ تثبيت الحزمة وثبّتها مرة أخرى كنسخة غير مُدارة.", - }, - // objectui#12146 — the install dialog opened as an UPDATE of the copy - // this environment holds (MarketplacePackagePage). - update: { - dialogTitle: "تحديث {{name}}", - dialogDescCurrent: "تحديث النسخة المثبتة في هذه البيئة ({{host}}).", - dialogDescPicker: "اختر البيئة التي تريد تحديث النسخة المثبتة فيها. يجب أن تكون مسجلاً في ObjectOS Cloud.", - majorNotice: "هذا تحديث إلى إصدار رئيسي جديد (v{{fromMajor}} → v{{toMajor}})، وقد يتضمن تغييرات غير متوافقة.", - releaseNotes: "اقرأ ملاحظات الإصدار v{{version}}", - success: "تم التحديث إلى v{{version}}.", - deployTimeout: "تم تسجيل التحديث، لكن هذه البيئة لا تقدّم v{{version}} بعد. قد يكون النشر لا يزال جاريًا — تحقّق مرة أخرى بعد قليل.", - }, - // objectui#3546 — the MarketplacePage "Your organization" strip. - org: { - heading: "مؤسستك", - install: "تثبيت", - installed: "تم تثبيت {{name}}", - installedBadge: "مثبت", - installing: "جارٍ التثبيت…", - }, - // ADR-0025 PD4 §3.5/§3.11 — the pre-install consent panel - // (PluginDisclosure.tsx). `runtime` is a CLOSED enum: spec - // PluginRuntimeSchema = z.enum(['node', 'sandbox', 'worker']). - disclosure: { - containsCode: "تحتوي هذه الحزمة على تعليمات برمجية", - reviewed: "تمت المراجعة والموافقة", - unreviewed: "لم تتم المراجعة بعد", - signed: "موقَّع", - grantsIntro: "تطلب هذه الحزمة:", - notEnforced: "تُسجَّل هذه القائمة عند التثبيت ويُعاد تأكيدها إذا طلب إصدار لاحق المزيد، لكن بيئة التشغيل لا تقصر الحزمة على هذه القائمة بعد.", - services: "خدمات المنصة", - hooks: "خطافات دورة الحياة", - network: "الوصول إلى الشبكة", - fs: "الوصول إلى نظام الملفات", - noPermissions: "لا تطلب أي أذونات خاصة.", - acknowledge: "أفهم أن هذه الحزمة تنفّذ تعليمات برمجية وتحصل على الأذونات المذكورة أعلاه.", - runtime: { - node: "داخل العملية · ثقة كاملة", - sandbox: "في بيئة معزولة", - worker: "خارج العملية", - }, - }, - uninstall: { - confirm: "إلغاء تثبيت {{manifestId}} v{{version}} من وقت التشغيل هذا؟\n\nستتم إزالة التطبيق من النواة الجارية فوراً، ومعه مجموعات الأذونات الخاصة به وتعييناتها، وستتم إزالة المانيفست المخزن مؤقتاً. تبقى السجلات المخزنة في كائناته في قاعدة البيانات.", - successInList: "تمت إزالة {{manifestId}}: خرج التطبيق من النواة الجارية، ومعه مجموعات الأذونات الخاصة به وتعييناتها. تبقى السجلات المخزنة في كائناته في قاعدة البيانات.", - successInDetail: "تمت إزالة {{manifestId}} والمانيفست المخزن مؤقتاً الخاص به: خرج التطبيق من النواة الجارية، ومعه مجموعات الأذونات الخاصة به وتعييناتها. تبقى السجلات المخزنة في كائناته في قاعدة البيانات.", - confirmNotLoaded: "إلغاء تثبيت {{manifestId}} v{{version}} من وقت التشغيل هذا؟\n\nسيتم إزالة المانيفست المخزن مؤقتاً. لم يقم وقت التشغيل هذا بتحميل الحزمة، لذا لا يعمل أي جزء منها.", - successNotLoaded: "تمت إزالة {{manifestId}}. لم تكن الحزمة محمّلة، لذا لا حاجة لإعادة التشغيل.", - }, - accessDenied: { - title: "سوق التطبيقات للمشرفين فقط", - description: "ليس لديك إذن لتثبيت التطبيقات في هذه البيئة.", - }, - category: { - crm: "CRM", - erp: "ERP", - hr: "الموارد البشرية", - finance: "المالية", - project: "إدارة المشاريع", - collaboration: "التعاون", - analytics: "التحليلات", - integration: "التكامل", - automation: "الأتمتة", - ai: "الذكاء الاصطناعي", - security: "الأمان", - "developer-tools": "أدوات المطور", - "ui-theme": "سمة واجهة المستخدم", - storage: "التخزين", - other: "أخرى", - }, - relativeTime: { - today: "اليوم", - daysAgo: "منذ {{count}} ي", - monthsAgo: "منذ {{count}} شهر(أشهر)", - monthsAgo_zero: "منذ {{count}} شهر", - monthsAgo_one: "منذ {{count}} شهر", - monthsAgo_two: "منذ شهرين ({{count}})", - monthsAgo_few: "منذ {{count}} أشهر", - monthsAgo_many: "منذ {{count}} شهرًا", - monthsAgo_other: "منذ {{count}} شهر", - yearsAgo: "منذ {{count}} سنة(سنوات)", - yearsAgo_zero: "منذ {{count}} سنة", - yearsAgo_one: "منذ {{count}} سنة", - yearsAgo_two: "منذ سنتين ({{count}})", - yearsAgo_few: "منذ {{count}} سنوات", - yearsAgo_many: "منذ {{count}} سنة", - yearsAgo_other: "منذ {{count}} سنة", }, }, approvalsInbox: { diff --git a/ui/packages/i18n/src/locales/de.ts b/ui/packages/i18n/src/locales/de.ts index 8eacefe52c8..8a88328b824 100644 --- a/ui/packages/i18n/src/locales/de.ts +++ b/ui/packages/i18n/src/locales/de.ts @@ -4096,42 +4096,16 @@ const de = { connect: "Verbinden", }, }, + // The rest of `marketplace.*` is the lazy `marketplace` namespace + // (`../namespaces/marketplace/de.ts`), fetched when a marketplace page + // mounts. What stays here is read by a page outside the marketplace route: + // Studio's packages page draws a marketplace install's row through + // `MarketplaceRowActions` and `MarketplaceNotLoaded` (the Not loaded badge + // and reason, Uninstall with its confirmation and result), and an import's + // refusal and protocol note through `useDependencyMissingText` and + // `useProtocolCheckSkippedText`. marketplace: { - suggestedBindings: { - promptEveryone: "Diese App empfiehlt, „{{set}}“ allen angemeldeten Benutzern zu gewähren (Position „Everyone“).", - promptGuest: "Diese App empfiehlt, „{{set}}“ nicht authentifizierten Besuchern zu gewähren (Position „Guest“).", - confirm: "Gewähren", - confirming: "Wird gewährt…", - dismiss: "Ausblenden", - confirmedToast: "„{{set}}“ ist jetzt {{anchor}} gewährt.", - dismissedToast: "Vorschlag für „{{set}}“ ausgeblendet.", - guestUnsupported: "Hier nicht gewährbar: Die Sets der Position „Guest“ werden nicht aus angenommenen Vorschlägen abgeleitet.", - acceptedState: "„{{set}}“ wurde für alle angemeldeten Benutzer angenommen (Position „Everyone“).", - notInForceState: "„{{set}}“ wurde angenommen, ist aber noch nicht wirksam: Die Position „Everyone“ hat sich bei dieser Anfrage nicht geändert. Der Grund steht im Serverprotokoll.", - dismissedState: "„{{set}}“ ist nicht gewährt: Der Vorschlag wurde ausgeblendet oder widerrufen.", - revoke: "Widerrufen", - revoking: "Wird widerrufen…", - revokedToast: "Annahme von „{{set}}“ widerrufen.", - reopen: "Neu bewerten", - reopening: "Wird wieder geöffnet…", - reopenedToast: "Vorschlag für „{{set}}“ wieder geöffnet: Nichts wird gewährt, bis er angenommen wird.", - }, - title: "App-Marktplatz", - subtitle: "Durchsuchen Sie genehmigte Apps, die im ObjectStack-Katalog veröffentlicht wurden. Klicken Sie auf eine App, um Details zu sehen und sie in eine Ihrer Umgebungen zu installieren.", - searchPlaceholder: 'Apps nach Name oder App-ID suchen…', - searchAria: "Marktplatz-Apps suchen", - installed: "Installiert", - installedCount: "Installiert ({{count}})", - refresh: "Aktualisieren", - all: "Alle", - noApprovedYet: "Noch keine Apps für den Marktplatz genehmigt.", - noMatchFilters: "Keine Apps entsprechen Ihren Filtern.", - noDescription: "Keine Beschreibung vorhanden.", - back: "Zurück zum Marktplatz", - browseLink: "Marktplatz durchsuchen →", installedAdditiveNote: "Die Deinstallation entfernt die App sofort aus dem laufenden Kernel, samt ihren Berechtigungssets und deren Zuweisungen, und löscht das zwischengespeicherte Manifest, damit das Paket beim nächsten Start nicht geladen wird. Datensätze in ihren Objekten bleiben in der Datenbank erhalten.", - versionBadge: "v{{version}}", - installedBadge: "Installiert v{{version}}", // objectui#11645 — an install-local entry this runtime refused to load at // startup (the listing's `notLoaded` marker), read on Installed Apps. notLoaded: { @@ -4139,90 +4113,18 @@ const de = { protocolIncompatible: "Dieses Laufzeitsystem hat das Paket nicht geladen: Es zielt auf das Protokoll {{requiredRange}} ab, das dieses Laufzeitsystem nicht unterstützt.", otherReason: "Dieses Laufzeitsystem hat das Paket nicht geladen ({{code}}).", }, - load: { - failed: "Marktplatz konnte nicht geladen werden", - failedHintConfigured: "Diese Laufzeitumgebung erreicht den Marktplatz über die Control Plane unter {{url}}. Prüfen Sie, ob diese online und von hier aus erreichbar ist.", - failedHintSameOrigin: "Der Marktplatz-Katalog wird über diese Laufzeitumgebung erreicht. Prüfen Sie, ob die Laufzeitumgebung online ist und den Katalog erreichen kann.", - loading: "Marktplatz-Katalog wird geladen…", - slow: "Wird noch geladen — der Marktplatz-Katalog antwortet langsam.", - retry: "Erneut versuchen", - unreachable: "Der Marktplatz-Katalog war nicht erreichbar.", - answeredError: "Der Marktplatz-Katalog hat mit einem Fehler geantwortet (HTTP {{status}}).", - packageFailed: "Paket konnte nicht geladen werden", - notFound: "Nicht gefunden.", - }, - disabled: { - title: "App-Marktplatz ist deaktiviert", - description: "Für diese Laufzeitumgebung ist kein Marktplatz konfiguriert; es gibt hier nichts zu durchsuchen oder zu installieren.", - hint: "Das ist der erwartete Zustand, wenn die Laufzeitumgebung mit <code>OS_CLOUD_URL=off</code> (oder <code>none</code>, <code>local</code>, <code>disabled</code>) gestartet wird. Um den Marktplatz zu aktivieren, richten Sie <code>OS_CLOUD_URL</code> auf eine Control Plane und starten Sie die Laufzeitumgebung neu.", - }, - detail: { - homepage: "Startseite", - installedV: "Installiert · v{{version}}", - about: "Über", - noReadme: "Kein Readme vorhanden.", - versions: "Versionen", - noApprovedVersions: "Keine genehmigten Versionen.", - prerelease: "Vorab", - moreOptions: "Weitere Installationsoptionen", - uninstallFromRuntime: "Aus diesem Laufzeitsystem deinstallieren", - addSampleData: 'Beispieldaten hinzufügen', - reseedAgain: 'Beispieldaten neu laden', - purgeSampleData: 'Beispieldaten löschen', - purgeConfirm: 'Alle von diesem Paket erzeugten Beispieldatensätze löschen? Vom Benutzer hinzugefügte Datensätze bleiben unberührt.', - purgeSuccess: '{{count}} Beispieldatensatz/-sätze entfernt.', - purgeSuccess_one: '{{count}} Beispieldatensatz entfernt.', - purgeSuccess_other: '{{count}} Beispieldatensätze entfernt.', - purgeNoData: 'Keine Beispieldatensätze zum Löschen gefunden.', - // objectstack#30323 — the install note's sample-data count, its clear and the app banner. - sampleDataLoaded: 'Diese Installation hat {{count}} Beispieldatensatz/-sätze geladen, damit Sie die App in Aktion sehen. Löschen Sie sie, sobald Sie die App wirklich nutzen.', - sampleDataLoaded_one: 'Diese Installation hat {{count}} Beispieldatensatz geladen, damit Sie die App in Aktion sehen. Löschen Sie ihn, sobald Sie die App wirklich nutzen.', - sampleDataLoaded_other: 'Diese Installation hat {{count}} Beispieldatensätze geladen, damit Sie die App in Aktion sehen. Löschen Sie sie, sobald Sie die App wirklich nutzen.', - sampleDataReloaded: 'Eine erneute Installation lädt die Beispieldaten wieder, auch die bereits gelöschten.', - purgeConfirmAtMost: 'Höchstens {{count}} Beispieldatensatz/-sätze werden gelöscht.', - purgeConfirmAtMost_one: 'Höchstens {{count}} Beispieldatensatz wird gelöscht.', - purgeConfirmAtMost_other: 'Höchstens {{count}} Beispieldatensätze werden gelöscht.', - sampleDataBannerTitle: '„{{name}}“ enthält Beispieldaten.', - sampleDataBannerMessage: 'Sie zeigen, wie die App arbeitet. Löschen Sie sie, sobald Sie die App wirklich nutzen.', - reseedQueued: 'Beispieldaten werden beim nächsten Zugriff auf die Umgebung neu geladen.', - reseedLocalSuccess: 'Beispieldaten neu geladen: {{inserted}} hinzugefügt, {{updated}} aktualisiert.', - reseedPartialErrors: '({{count}} Datensatz/-sätze konnten nicht geschrieben werden)', - reseedPartialErrors_one: '({{count}} Datensatz konnte nicht geschrieben werden)', - reseedPartialErrors_other: '({{count}} Datensätze konnten nicht geschrieben werden)', - sampleDataKernelUnavailable: 'Diese Control Plane hat keinen Umgebungs-Kernel, daher können Beispieldaten hier weder neu geladen noch gelöscht werden. Führen Sie dies im eigenen Laufzeitsystem der Umgebung aus.', - updateAvailable: 'Update verfügbar', - }, action: { - installed: "Installiert", - install: "Installieren", - reinstall: "Neu installieren", - working: "Verarbeite…", - installToCloud: "In der Cloud installieren…", - installing: "Installiere…", - updating: "Aktualisiere…", uninstall: "Deinstallieren", uninstalling: "Deinstalliere…", details: "Details", - close: "Schließen", - dismiss: "Schließen", - openOnCloud: "In der Cloud öffnen", - backHome: "Zurück zur Startseite", - updateTo: "Aktualisieren", + }, + uninstall: { + confirm: "{{manifestId}} v{{version}} aus diesem Laufzeitsystem deinstallieren?\n\nDie App wird sofort aus dem laufenden Kernel entfernt, samt ihren Berechtigungssets und deren Zuweisungen, und das zwischengespeicherte Manifest wird gelöscht. Datensätze in ihren Objekten bleiben in der Datenbank erhalten.", + confirmNotLoaded: "{{manifestId}} v{{version}} aus diesem Laufzeitsystem deinstallieren?\n\nDas zwischengespeicherte Manifest wird entfernt. Dieses Laufzeitsystem hat das Paket nicht geladen, daher läuft nichts davon.", + successInList: "{{manifestId}} entfernt: Die App ist aus dem laufenden Kernel entfernt, samt ihren Berechtigungssets und deren Zuweisungen. Datensätze in ihren Objekten bleiben in der Datenbank erhalten.", + successNotLoaded: "{{manifestId}} entfernt. Es war nicht geladen, ein Neustart ist nicht nötig.", }, install: { - dialogTitle: "{{name}} installieren", - dialogDescCurrent: "In dieser Umgebung installieren ({{host}}).", - dialogDescPicker: "Wählen Sie eine Umgebung, in der Sie diese App installieren möchten. Sie müssen bei ObjectOS Cloud angemeldet sein.", - environment: "Umgebung", - environmentPlaceholder: "Umgebung auswählen", - includeSampleData: "Beispieldaten einschließen", - noEnvs: "Keine Umgebungen in Ihrer aktiven Organisation gefunden.", - noPermission: "Sie haben keine Berechtigung, Apps in einer Umgebung zu installieren. Nur Organisationsinhaber und Administratoren können installieren — wenden Sie sich an den Administrator Ihrer Organisation.", - signInFirst: "Sie müssen sich zuerst bei ObjectOS Cloud anmelden.", - success: "Erfolgreich installiert. Öffnen Sie die Umgebung, um die neue App zu sehen.", - localSuccess: 'v{{version}} in diesem Laufzeitsystem installiert. „{{name}}“ ist jetzt im App-Wechsler.', - localLoadsAtRestart: "v{{version}} in diesem Laufzeitsystem installiert. „{{name}}“ wird beim nächsten Neustart des Laufzeitsystems geladen und erscheint erst dann im App-Wechsler.", - localManifestConflict: "{{message}}\nHinweis: Eine lokale App besitzt bereits diese manifest_id. Entfernen Sie sie zuerst aus objectstack.config.ts.", // ADR-0141 D2 — the install refusal DEPENDENCY_MISSING, one line per unmet entry. dependencyMissing: { heading: "Installation abgelehnt: Eine von diesem Paket deklarierte Abhängigkeit ist in dieser Umgebung nicht erfüllt, daher wurde nichts installiert.", @@ -4236,107 +4138,6 @@ const de = { noRange: "{{packageId}} gibt nicht an, welches Plattformprotokoll es unterstützt. Es wurde daher ohne Kompatibilitätsprüfung unter Protokoll {{protocolVersion}} installiert und funktioniert auf dieser Plattformversion möglicherweise nicht. Ein Build des Pakets, der sein unterstütztes Protokoll (engines.protocol) angibt, wird bei der Installation geprüft.", unparsedRange: "{{packageId}} gibt sein Plattformprotokoll als {{requiredRange}} an, was diese Laufzeitumgebung nicht lesen kann. Es wurde daher ohne Kompatibilitätsprüfung unter Protokoll {{protocolVersion}} installiert und funktioniert auf dieser Plattformversion möglicherweise nicht. Ein Build des Pakets mit lesbarem Bereich (engines.protocol) wird bei der Installation geprüft.", }, - localUnauthorized: "Melden Sie sich zuerst in diesem Laufzeitsystem an.", - localMarketplaceUnavailable: "Dieses Laufzeitsystem hat keine OS_CLOUD_URL konfiguriert.", - updateTo: "Aktualisieren → v{{version}}", - installedVersion: "Installiert v{{version}}", - deploying: "App wird bereitgestellt…", - deployed: "Die App ist bereitgestellt und einsatzbereit.", - openApp: "{{name}} öffnen", - deployTimeout: "Die Installation wurde erfasst, aber diese Umgebung stellt die App noch nicht bereit. Die Bereitstellung läuft möglicherweise noch – prüfen Sie es gleich erneut. Erscheint sie nie, wurde das Paket womöglich nicht geladen, enthält keine App, oder die App erfordert eine Berechtigung, die Ihnen fehlt.", - checkAgain: "Erneut prüfen", - // ADR-0131 D6 — the install modes a package's manifest permits, and - // what an unmanaged install answers (MarketplacePackagePage). - mode: { - label: "Installationsmodus", - managed: "Verwaltet", - managedHint: "Das Paket bleibt maßgeblich: Es wird als Code registriert, erhält seine Upgrades, und seine Elemente werden hier nicht bearbeitet.", - unmanaged: "Nicht verwaltete Kopie", - unmanagedHint: "Einmalig als eigene Metadaten in dieses Laufzeitsystem kopiert: Jedes Element ist hier bearbeitbar, und die Kopie erhält kein Upgrade.", - required: "Wählen Sie einen Installationsmodus, um dieses Paket zu installieren.", - }, - unmanagedSuccess: "v{{version}} von „{{name}}“ als eigene Metadaten in dieses Laufzeitsystem kopiert, in die Basis {{base}}. Die Kopie ist bearbeitbar und erhält kein Upgrade.", - unmanagedStrippedLocks: "Das Paket hat diese Elemente gesperrt. Die Kopie ist bearbeitbar, daher wurden ihre Sperren nicht kopiert:", - unmanagedIgnored: "Dieses Laufzeitsystem unterstützt keine nicht verwalteten Installationen, daher wurde „{{name}}“ verwaltet installiert (als Code registriert) statt kopiert. Aktualisieren Sie das Laufzeitsystem, deinstallieren Sie dann das Paket und installieren Sie es erneut als nicht verwaltete Kopie.", - }, - // objectui#12146 — the install dialog opened as an UPDATE of the copy - // this environment holds (MarketplacePackagePage). - update: { - dialogTitle: "{{name}} aktualisieren", - dialogDescCurrent: "Die in dieser Umgebung installierte Kopie aktualisieren ({{host}}).", - dialogDescPicker: "Wählen Sie die Umgebung, deren installierte Kopie Sie aktualisieren möchten. Sie müssen bei ObjectOS Cloud angemeldet sein.", - majorNotice: "Dies ist ein Update auf eine neue Hauptversion (v{{fromMajor}} → v{{toMajor}}), das inkompatible Änderungen enthalten kann.", - releaseNotes: "Versionshinweise zu v{{version}} lesen", - success: "Auf v{{version}} aktualisiert.", - deployTimeout: "Das Update wurde erfasst, aber diese Umgebung stellt v{{version}} noch nicht bereit. Die Bereitstellung läuft möglicherweise noch – prüfen Sie es gleich erneut.", - }, - // objectui#3546 — the MarketplacePage "Your organization" strip. - org: { - heading: "Ihre Organisation", - install: "Installieren", - installed: "{{name}} installiert", - installedBadge: "Installiert", - installing: "Installiere…", - }, - // ADR-0025 PD4 §3.5/§3.11 — the pre-install consent panel - // (PluginDisclosure.tsx). `runtime` is a CLOSED enum: spec - // PluginRuntimeSchema = z.enum(['node', 'sandbox', 'worker']). - disclosure: { - containsCode: "Dieses Paket enthält Code", - reviewed: "Geprüft und genehmigt", - unreviewed: "Noch nicht geprüft", - signed: "Signiert", - grantsIntro: "Dieses Paket fordert an:", - notEnforced: "Diese Liste wird bei der Installation erfasst und erneut bestätigt, wenn eine spätere Version mehr anfordert – die Laufzeit beschränkt das Paket jedoch noch nicht darauf.", - services: "Plattformdienste", - hooks: "Lifecycle-Hooks", - network: "Netzwerkzugriff", - fs: "Dateisystemzugriff", - noPermissions: "Fordert keine besonderen Berechtigungen an.", - acknowledge: "Ich habe verstanden, dass dieses Paket Code ausführt und die oben genannten Berechtigungen erhält.", - runtime: { - node: "Im Prozess · volles Vertrauen", - sandbox: "Sandbox-isoliert", - worker: "Außerhalb des Prozesses", - }, - }, - uninstall: { - confirm: "{{manifestId}} v{{version}} aus diesem Laufzeitsystem deinstallieren?\n\nDie App wird sofort aus dem laufenden Kernel entfernt, samt ihren Berechtigungssets und deren Zuweisungen, und das zwischengespeicherte Manifest wird gelöscht. Datensätze in ihren Objekten bleiben in der Datenbank erhalten.", - successInList: "{{manifestId}} entfernt: Die App ist aus dem laufenden Kernel entfernt, samt ihren Berechtigungssets und deren Zuweisungen. Datensätze in ihren Objekten bleiben in der Datenbank erhalten.", - successInDetail: "{{manifestId}} und das zugehörige zwischengespeicherte Manifest entfernt: Die App ist aus dem laufenden Kernel entfernt, samt ihren Berechtigungssets und deren Zuweisungen. Datensätze in ihren Objekten bleiben in der Datenbank erhalten.", - confirmNotLoaded: "{{manifestId}} v{{version}} aus diesem Laufzeitsystem deinstallieren?\n\nDas zwischengespeicherte Manifest wird entfernt. Dieses Laufzeitsystem hat das Paket nicht geladen, daher läuft nichts davon.", - successNotLoaded: "{{manifestId}} entfernt. Es war nicht geladen, ein Neustart ist nicht nötig.", - }, - accessDenied: { - title: "App-Marktplatz nur für Administratoren", - description: "Sie haben keine Berechtigung, Apps in dieser Umgebung zu installieren.", - }, - category: { - crm: "CRM", - erp: "ERP", - hr: "Personalwesen", - finance: "Finanzen", - project: "Projektmanagement", - collaboration: "Zusammenarbeit", - analytics: "Analyse", - integration: "Integration", - automation: "Automatisierung", - ai: "KI", - security: "Sicherheit", - "developer-tools": "Entwicklertools", - "ui-theme": "UI-Thema", - storage: "Speicher", - other: "Sonstiges", - }, - relativeTime: { - today: "heute", - daysAgo: "vor {{count}}d", - monthsAgo: "vor {{count}} Monaten", - monthsAgo_one: "vor {{count}} Monat", - monthsAgo_other: "vor {{count}} Monaten", - yearsAgo: "vor {{count}} Jahren", - yearsAgo_one: "vor {{count}} Jahr", - yearsAgo_other: "vor {{count}} Jahren", }, }, approvalsInbox: { diff --git a/ui/packages/i18n/src/locales/en.ts b/ui/packages/i18n/src/locales/en.ts index 038308ea631..af0d7f6f88c 100644 --- a/ui/packages/i18n/src/locales/en.ts +++ b/ui/packages/i18n/src/locales/en.ts @@ -4917,23 +4917,16 @@ const en = { connect: 'Connect', }, }, + // The rest of `marketplace.*` is the lazy `marketplace` namespace + // (`../namespaces/marketplace/en.ts`), fetched when a marketplace page + // mounts. What stays here is read by a page outside the marketplace route: + // Studio's packages page draws a marketplace install's row through + // `MarketplaceRowActions` and `MarketplaceNotLoaded` (the Not loaded badge + // and reason, Uninstall with its confirmation and result), and an import's + // refusal and protocol note through `useDependencyMissingText` and + // `useProtocolCheckSkippedText`. marketplace: { - title: 'App Marketplace', - subtitle: 'Browse approved apps published to the ObjectStack catalog. Click an app to view details and install it into one of your environments.', - searchPlaceholder: 'Search apps by name or app ID…', - searchAria: 'Search marketplace apps', - installed: 'Installed', - installedCount: 'Installed ({{count}})', - refresh: 'Refresh', - all: 'All', - noApprovedYet: 'No apps have been approved for the marketplace yet.', - noMatchFilters: 'No apps match your filters.', - noDescription: 'No description provided.', - back: 'Back to marketplace', - browseLink: 'Browse the marketplace →', installedAdditiveNote: 'Uninstall removes the app from the running kernel at once, together with its permission sets and their grants, and deletes the cached manifest so the package does not load at the next start. Records stored in its objects stay in the database.', - versionBadge: 'v{{version}}', - installedBadge: 'Installed v{{version}}', // objectui#11645 — an install-local entry this runtime refused to load at // startup (the listing's `notLoaded` marker), read on Installed Apps. notLoaded: { @@ -4941,93 +4934,18 @@ const en = { protocolIncompatible: 'This runtime did not load this package: it targets protocol {{requiredRange}}, which this runtime does not support.', otherReason: 'This runtime did not load this package ({{code}}).', }, - load: { - failed: 'Failed to load marketplace', - failedHintConfigured: 'This runtime reaches the marketplace through the control plane at {{url}}. Check that it is online and reachable from here.', - failedHintSameOrigin: 'The marketplace catalog is reached through this runtime. Check that the runtime is online and can reach the catalog.', - loading: 'Loading the marketplace catalog…', - slow: 'Still loading — the marketplace catalog is slow to answer.', - retry: 'Retry', - unreachable: 'The marketplace catalog could not be reached.', - answeredError: 'The marketplace catalog answered with an error (HTTP {{status}}).', - packageFailed: 'Failed to load package', - notFound: 'Not found.', - }, - disabled: { - title: 'App Marketplace is turned off', - description: 'This runtime has no marketplace configured, so there is nothing to browse or install from here.', - hint: 'That is the expected state when the runtime is started with <code>OS_CLOUD_URL=off</code> (or <code>none</code>, <code>local</code>, <code>disabled</code>). To turn the marketplace on, point <code>OS_CLOUD_URL</code> at a control plane and restart the runtime.', - }, - detail: { - homepage: 'Homepage', - installedV: 'Installed · v{{version}}', - about: 'About', - noReadme: 'No readme provided.', - versions: 'Versions', - noApprovedVersions: 'No approved versions.', - prerelease: 'pre', - moreOptions: 'More install options', - uninstallFromRuntime: 'Uninstall from this runtime', - addSampleData: 'Add sample data', - reseedAgain: 'Re-seed sample data', - purgeSampleData: 'Purge sample data', - purgeConfirm: 'Delete all sample records seeded by this package? User-added records will NOT be touched.', - purgeSuccess: 'Removed {{count}} sample record(s).', - purgeSuccess_one: 'Removed {{count}} sample record.', - purgeSuccess_other: 'Removed {{count}} sample records.', - purgeNoData: 'No sample records found to purge.', - // objectstack#30323 — the install note's sample-data count, its clear and the app banner. - sampleDataLoaded: 'This install loaded {{count}} sample record(s) so you can see the app working. Clear them when you start using it for real.', - sampleDataLoaded_one: 'This install loaded {{count}} sample record so you can see the app working. Clear it when you start using the app for real.', - sampleDataLoaded_other: 'This install loaded {{count}} sample records so you can see the app working. Clear them when you start using it for real.', - sampleDataReloaded: 'Installing again loads the sample data again, including any you had cleared.', - purgeConfirmAtMost: 'Up to {{count}} sample record(s) will be deleted.', - purgeConfirmAtMost_one: 'Up to {{count}} sample record will be deleted.', - purgeConfirmAtMost_other: 'Up to {{count}} sample records will be deleted.', - sampleDataBannerTitle: '"{{name}}" has sample data.', - sampleDataBannerMessage: 'It is there so you can see the app working. Clear it when you start using the app for real.', - reseedQueued: 'Sample data will be re-seeded on next environment access.', - reseedLocalSuccess: 'Re-seeded sample data: {{inserted}} inserted, {{updated}} updated.', - reseedPartialErrors: '({{count}} record(s) failed to write)', - reseedPartialErrors_one: '({{count}} record failed to write)', - reseedPartialErrors_other: '({{count}} records failed to write)', - sampleDataKernelUnavailable: 'This control plane has no environment kernel, so sample data cannot be re-seeded or purged from here. Do it from the environment\'s own runtime.', - updateAvailable: 'Update available', - }, - action: { - install: 'Install', - reinstall: 'Reinstall', - working: 'Working…', - installToCloud: 'Install to cloud…', - installed: 'Installed', - installing: 'Installing…', - updating: 'Updating…', + action: { uninstall: 'Uninstall', uninstalling: 'Uninstalling…', details: 'Details', - close: 'Close', - dismiss: 'Dismiss', - openOnCloud: 'Open on cloud', - backHome: 'Back to home', - updateTo: 'Update', - }, - install: { - dialogTitle: 'Install {{name}}', - dialogDescCurrent: 'Install into this environment ({{host}}).', - dialogDescPicker: 'Choose an environment to install this app into. You need to be signed into ObjectOS Cloud.', - environment: 'Environment', - environmentPlaceholder: 'Pick an environment', - includeSampleData: 'Include sample data', - noEnvs: 'No environments found in your active organization.', - noPermission: 'You do not have permission to install apps in any environment. Only organization owners and admins can install — ask your organization admin.', - signInFirst: 'You need to sign into ObjectOS Cloud first. Click "Open on cloud" below.', - success: 'Installed successfully. Open the environment to see the new app.', - localSuccess: 'Installed v{{version}} to this runtime. "{{name}}" is now in the app switcher.', - // objectui#12098 — the install answered `hotLoaded: false`: installed, and - // the running kernel did not register it (objectstack#22695), so nothing - // of it is served before the next restart. - localLoadsAtRestart: 'Installed v{{version}} to this runtime. "{{name}}" loads when the runtime next restarts; it does not appear in the app switcher until then.', - localManifestConflict: '{{message}}\nTip: a local app already owns this manifest_id. Remove it from objectstack.config.ts first.', + }, + uninstall: { + confirm: 'Uninstall {{manifestId}} v{{version}} from this runtime?\n\nThe app will leave the running kernel at once, together with its permission sets and their grants, and its cached manifest will be removed. Records stored in its objects stay in the database.', + confirmNotLoaded: 'Uninstall {{manifestId}} v{{version}} from this runtime?\n\nThe cached manifest will be removed. This runtime did not load the package, so none of it is running.', + successInList: 'Removed {{manifestId}}: the app has left the running kernel, with its permission sets and their grants. Records stored in its objects stay in the database.', + successNotLoaded: 'Removed {{manifestId}}. It was not loaded, so no restart is needed.', + }, + install: { // ADR-0141 D2 — the install refusal DEPENDENCY_MISSING, one line per unmet entry. dependencyMissing: { heading: 'Install refused: a dependency this package declares is not satisfied in this environment, so nothing was installed.', @@ -5042,133 +4960,8 @@ const en = { noRange: '{{packageId}} does not state which platform protocol it supports, so it was installed under protocol {{protocolVersion}} without a compatibility check and may not work on this platform version. A build of the package that declares the protocol it supports (engines.protocol) is checked at install.', unparsedRange: '{{packageId}} states its platform protocol as {{requiredRange}}, which this runtime cannot read, so it was installed under protocol {{protocolVersion}} without a compatibility check and may not work on this platform version. A build of the package that declares a range this runtime reads (engines.protocol) is checked at install.', }, - localUnauthorized: 'Sign in to this runtime first, then try again.', - localMarketplaceUnavailable: 'This runtime has no OS_CLOUD_URL configured, so the marketplace catalog is unreachable.', - updateTo: 'Update → v{{version}}', - installedVersion: 'Installed v{{version}}', - // objectui#12087 — an install into THIS environment, from its answer - // until the runtime serves the package's app (MarketplacePackagePage). - deploying: 'Deploying the app…', - deployed: 'The app is deployed and ready.', - openApp: 'Open {{name}}', - deployTimeout: 'The install was recorded, but this environment is not serving the app yet. It may still be deploying — check again shortly. If it never appears, the package may not have loaded, may include no app, or its app may need a permission you do not hold.', - checkAgain: 'Check again', - // ADR-0131 D6 — the install modes a package's manifest permits, and - // what an unmanaged install answers (MarketplacePackagePage). - mode: { - label: 'Install mode', - managed: 'Managed', - managedHint: 'The package stays the authority: it is registered as code, receives its upgrades, and its items are not edited here.', - unmanaged: 'Unmanaged copy', - unmanagedHint: 'Copied once into this runtime as its own metadata: every item is editable here, and the copy receives no upgrade.', - required: 'Choose an install mode to install this package.', - }, - unmanagedSuccess: 'Copied v{{version}} of "{{name}}" into this runtime as its own metadata, in the base {{base}}. The copy is editable and receives no upgrade.', - unmanagedStrippedLocks: 'The package locked these items. The copy is editable, so their locks were not copied:', - unmanagedIgnored: 'This runtime does not support unmanaged installs, so "{{name}}" was installed managed (registered as code) instead of copied. Upgrade the runtime, then uninstall the package and install it again as an unmanaged copy.', - }, - // objectui#12146 — the install dialog opened as an UPDATE of the copy - // this environment holds (MarketplacePackagePage). - update: { - dialogTitle: 'Update {{name}}', - dialogDescCurrent: 'Update the copy installed in this environment ({{host}}).', - dialogDescPicker: 'Choose the environment whose installed copy to update. You need to be signed into ObjectOS Cloud.', - majorNotice: 'This is a major version update (v{{fromMajor}} → v{{toMajor}}), which may include breaking changes.', - releaseNotes: 'Read the v{{version}} release notes', - success: 'Updated to v{{version}}.', - deployTimeout: 'The update was recorded, but this environment is not serving v{{version}} yet. It may still be deploying — check again shortly.', - }, - // objectui#3546 — the MarketplacePage "Your organization" strip. - org: { - heading: 'Your organization', - install: 'Install', - installed: 'Installed {{name}}', - installedBadge: 'Installed', - installing: 'Installing…', - }, - // ADR-0025 PD4 §3.5/§3.11 — the pre-install consent panel - // (PluginDisclosure.tsx). `runtime` is a CLOSED enum: spec - // PluginRuntimeSchema = z.enum(['node', 'sandbox', 'worker']). - disclosure: { - containsCode: 'This package contains code', - reviewed: 'Reviewed & approved', - unreviewed: 'Not yet reviewed', - signed: 'Signed', - grantsIntro: 'This package requests:', - notEnforced: 'Recorded at install, and re-confirmed if a later version asks for more — but the runtime does not yet restrict the package to this list.', - services: 'Platform services', - hooks: 'Lifecycle hooks', - network: 'Network access', - fs: 'Filesystem access', - noPermissions: 'Requests no special permissions.', - acknowledge: 'I understand this package runs code and grants the permissions above.', - runtime: { - node: 'In-process · full trust', - sandbox: 'Sandboxed', - worker: 'Out-of-process', - }, - }, - // ADR-0090 D5/D9 — a package's isDefault permission set is an - // install-time suggestion to bind it to the everyone/guest position; - // the admin confirms here, the server never auto-binds. - suggestedBindings: { - promptEveryone: 'This app suggests granting "{{set}}" to all signed-in users (the Everyone position).', - promptGuest: 'This app suggests granting "{{set}}" to unauthenticated visitors (the Guest position).', - confirm: 'Grant', - confirming: 'Granting…', - dismiss: 'Dismiss', - confirmedToast: '"{{set}}" is now granted to {{anchor}}.', - dismissedToast: 'Suggestion for "{{set}}" dismissed.', - guestUnsupported: 'Cannot be granted here: the Guest position takes no sets from accepted suggestions.', - acceptedState: '"{{set}}" was accepted for all signed-in users (the Everyone position).', - notInForceState: '"{{set}}" was accepted but is not in force yet: the Everyone position did not change on this request. The server log says why.', - dismissedState: '"{{set}}" is not granted: the suggestion was dismissed or revoked.', - revoke: 'Revoke', - revoking: 'Revoking…', - revokedToast: 'Acceptance of "{{set}}" revoked.', - reopen: 'Reconsider', - reopening: 'Reopening…', - reopenedToast: 'Suggestion for "{{set}}" reopened: nothing is granted until it is accepted.', - }, - uninstall: { - confirm: 'Uninstall {{manifestId}} v{{version}} from this runtime?\n\nThe app will leave the running kernel at once, together with its permission sets and their grants, and its cached manifest will be removed. Records stored in its objects stay in the database.', - successInList: 'Removed {{manifestId}}: the app has left the running kernel, with its permission sets and their grants. Records stored in its objects stay in the database.', - successInDetail: 'Removed {{manifestId}} and its cached manifest: the app has left the running kernel, with its permission sets and their grants. Records stored in its objects stay in the database.', - confirmNotLoaded: 'Uninstall {{manifestId}} v{{version}} from this runtime?\n\nThe cached manifest will be removed. This runtime did not load the package, so none of it is running.', - successNotLoaded: 'Removed {{manifestId}}. It was not loaded, so no restart is needed.', - }, - accessDenied: { - title: 'App Marketplace is admin-only', - description: 'You don\'t have permission to install apps in this environment. Ask an owner or admin of this organization for access.', - }, - category: { - crm: 'CRM', - erp: 'ERP', - hr: 'Human Resources', - finance: 'Finance', - project: 'Project Management', - collaboration: 'Collaboration', - analytics: 'Analytics', - integration: 'Integration', - automation: 'Automation', - ai: 'AI', - security: 'Security', - 'developer-tools': 'Developer Tools', - 'ui-theme': 'UI Theme', - storage: 'Storage', - other: 'Other', - }, - relativeTime: { - today: 'today', - daysAgo: '{{count}}d ago', - monthsAgo: '{{count}}mo ago', - monthsAgo_one: '{{count}}mo ago', - monthsAgo_other: '{{count}}mo ago', - yearsAgo: '{{count}}y ago', - yearsAgo_one: '{{count}}y ago', - yearsAgo_other: '{{count}}y ago', - }, }, + }, approvalsInbox: { recordUnresolvable: 'This record cannot be opened', loadMore: 'Load more', diff --git a/ui/packages/i18n/src/locales/es.ts b/ui/packages/i18n/src/locales/es.ts index 296e74383fe..a6dd2baf56a 100644 --- a/ui/packages/i18n/src/locales/es.ts +++ b/ui/packages/i18n/src/locales/es.ts @@ -4181,42 +4181,16 @@ const es = { connect: "Conectar", }, }, + // The rest of `marketplace.*` is the lazy `marketplace` namespace + // (`../namespaces/marketplace/es.ts`), fetched when a marketplace page + // mounts. What stays here is read by a page outside the marketplace route: + // Studio's packages page draws a marketplace install's row through + // `MarketplaceRowActions` and `MarketplaceNotLoaded` (the Not loaded badge + // and reason, Uninstall with its confirmation and result), and an import's + // refusal and protocol note through `useDependencyMissingText` and + // `useProtocolCheckSkippedText`. marketplace: { - suggestedBindings: { - promptEveryone: "Esta aplicación sugiere conceder «{{set}}» a todos los usuarios con sesión iniciada (la posición Everyone).", - promptGuest: "Esta aplicación sugiere conceder «{{set}}» a los visitantes no autenticados (la posición Guest).", - confirm: "Conceder", - confirming: "Concediendo…", - dismiss: "Descartar", - confirmedToast: "«{{set}}» ya está concedido a {{anchor}}.", - dismissedToast: "Sugerencia para «{{set}}» descartada.", - guestUnsupported: "No se puede conceder aquí: los conjuntos de la posición Guest no se derivan de sugerencias aceptadas.", - acceptedState: "«{{set}}» se aceptó para todos los usuarios con sesión iniciada (la posición Everyone).", - notInForceState: "«{{set}}» se aceptó, pero aún no está en vigor: la posición Everyone no cambió con esta solicitud. El registro del servidor indica el motivo.", - dismissedState: "«{{set}}» no está concedido: la sugerencia se descartó o se revocó.", - revoke: "Revocar", - revoking: "Revocando…", - revokedToast: "Aceptación de «{{set}}» revocada.", - reopen: "Reconsiderar", - reopening: "Reabriendo…", - reopenedToast: "Sugerencia para «{{set}}» reabierta: no se concede nada hasta que se acepte.", - }, - title: "Marketplace de aplicaciones", - subtitle: "Explore aplicaciones aprobadas publicadas en el catálogo de ObjectStack. Haga clic en una aplicación para ver detalles e instalarla en uno de sus entornos.", - searchPlaceholder: 'Buscar aplicaciones por nombre o ID de aplicación…', - searchAria: "Buscar aplicaciones del marketplace", - installed: "Instalado", - installedCount: "Instaladas ({{count}})", - refresh: "Actualizar", - all: "Todas", - noApprovedYet: "Aún no hay aplicaciones aprobadas para el marketplace.", - noMatchFilters: "Ninguna aplicación coincide con sus filtros.", - noDescription: "Sin descripción disponible.", - back: "Volver al marketplace", - browseLink: "Explorar marketplace →", installedAdditiveNote: "La desinstalación retira la aplicación del kernel en ejecución de inmediato, junto con sus conjuntos de permisos y sus asignaciones, y elimina el manifiesto en caché para que el paquete no se cargue en el próximo inicio. Los registros almacenados en sus objetos permanecen en la base de datos.", - versionBadge: "v{{version}}", - installedBadge: "Instalado v{{version}}", // objectui#11645 — an install-local entry this runtime refused to load at // startup (the listing's `notLoaded` marker), read on Installed Apps. notLoaded: { @@ -4224,94 +4198,18 @@ const es = { protocolIncompatible: "Este runtime no cargó este paquete: está dirigido al protocolo {{requiredRange}}, que este runtime no admite.", otherReason: "Este runtime no cargó este paquete ({{code}}).", }, - load: { - failed: "No se pudo cargar el marketplace", - failedHintConfigured: "Este runtime accede al marketplace a través del plano de control en {{url}}. Compruebe que esté en línea y sea accesible desde aquí.", - failedHintSameOrigin: "Al catálogo del marketplace se accede a través de este runtime. Compruebe que el runtime esté en línea y pueda acceder al catálogo.", - loading: "Cargando el catálogo del marketplace…", - slow: "Sigue cargando — el catálogo del marketplace tarda en responder.", - retry: "Reintentar", - unreachable: "No se pudo acceder al catálogo del marketplace.", - answeredError: "El catálogo del marketplace respondió con un error (HTTP {{status}}).", - packageFailed: "No se pudo cargar el paquete", - notFound: "No encontrado.", - }, - disabled: { - title: "El marketplace de aplicaciones está desactivado", - description: "Este runtime no tiene ningún marketplace configurado, por lo que no hay nada que explorar ni instalar desde aquí.", - hint: "Ese es el estado esperado cuando el runtime se inicia con <code>OS_CLOUD_URL=off</code> (o <code>none</code>, <code>local</code>, <code>disabled</code>). Para activar el marketplace, apunte <code>OS_CLOUD_URL</code> a un plano de control y reinicie el runtime.", - }, - detail: { - homepage: "Página de inicio", - installedV: "Instalado · v{{version}}", - about: "Acerca de", - noReadme: "Sin readme disponible.", - versions: "Versiones", - noApprovedVersions: "Sin versiones aprobadas.", - prerelease: "Prelanzamiento", - moreOptions: "Más opciones de instalación", - uninstallFromRuntime: "Desinstalar de este runtime", - addSampleData: 'Agregar datos de ejemplo', - reseedAgain: 'Volver a cargar datos de ejemplo', - purgeSampleData: 'Eliminar datos de ejemplo', - purgeConfirm: '¿Eliminar todos los registros de ejemplo creados por este paquete? Los registros añadidos por el usuario NO se verán afectados.', - purgeSuccess: 'Se eliminaron {{count}} registro(s) de ejemplo.', - purgeSuccess_one: 'Se eliminó {{count}} registro de ejemplo.', - purgeSuccess_many: 'Se eliminaron {{count}} de registros de ejemplo.', - purgeSuccess_other: 'Se eliminaron {{count}} registros de ejemplo.', - purgeNoData: 'No se encontraron registros de ejemplo para eliminar.', - // objectstack#30323 — the install note's sample-data count, its clear and the app banner. - sampleDataLoaded: 'Esta instalación cargó {{count}} registro(s) de ejemplo para que vea la aplicación en funcionamiento. Elimínelos cuando empiece a usarla de verdad.', - sampleDataLoaded_one: 'Esta instalación cargó {{count}} registro de ejemplo para que vea la aplicación en funcionamiento. Elimínelo cuando empiece a usarla de verdad.', - sampleDataLoaded_many: 'Esta instalación cargó {{count}} de registros de ejemplo para que vea la aplicación en funcionamiento. Elimínelos cuando empiece a usarla de verdad.', - sampleDataLoaded_other: 'Esta instalación cargó {{count}} registros de ejemplo para que vea la aplicación en funcionamiento. Elimínelos cuando empiece a usarla de verdad.', - sampleDataReloaded: 'Instalar de nuevo vuelve a cargar los datos de ejemplo, incluidos los que haya eliminado.', - purgeConfirmAtMost: 'Se eliminarán como máximo {{count}} registro(s) de ejemplo.', - purgeConfirmAtMost_one: 'Se eliminará como máximo {{count}} registro de ejemplo.', - purgeConfirmAtMost_many: 'Se eliminarán como máximo {{count}} de registros de ejemplo.', - purgeConfirmAtMost_other: 'Se eliminarán como máximo {{count}} registros de ejemplo.', - sampleDataBannerTitle: '"{{name}}" tiene datos de ejemplo.', - sampleDataBannerMessage: 'Están ahí para que vea la aplicación en funcionamiento. Elimínelos cuando empiece a usarla de verdad.', - reseedQueued: 'Los datos de ejemplo se volverán a cargar en el próximo acceso al entorno.', - reseedLocalSuccess: 'Datos de ejemplo recargados: {{inserted}} insertados, {{updated}} actualizados.', - reseedPartialErrors: '({{count}} registro(s) no se pudieron escribir)', - reseedPartialErrors_one: '({{count}} registro no se pudo escribir)', - reseedPartialErrors_many: '({{count}} de registros no se pudieron escribir)', - reseedPartialErrors_other: '({{count}} registros no se pudieron escribir)', - sampleDataKernelUnavailable: 'Este plano de control no tiene un kernel de entorno, por lo que los datos de ejemplo no se pueden volver a cargar ni eliminar desde aquí. Hágalo desde el propio runtime del entorno.', - updateAvailable: 'Actualización disponible', - }, action: { - installed: "Instalada", - install: "Instalar", - reinstall: "Reinstalar", - working: "Procesando…", - installToCloud: "Instalar en la nube…", - installing: "Instalando…", - updating: "Actualizando…", uninstall: "Desinstalar", uninstalling: "Desinstalando…", details: "Detalles", - close: "Cerrar", - dismiss: "Descartar", - openOnCloud: "Abrir en la nube", - backHome: "Volver al inicio", - updateTo: "Actualizar", + }, + uninstall: { + confirm: "¿Desinstalar {{manifestId}} v{{version}} de este runtime?\n\nLa aplicación se retirará de inmediato del kernel en ejecución, junto con sus conjuntos de permisos y sus asignaciones, y el manifiesto en caché será eliminado. Los registros almacenados en sus objetos permanecen en la base de datos.", + confirmNotLoaded: "¿Desinstalar {{manifestId}} v{{version}} de este runtime?\n\nEl manifiesto en caché será eliminado. Este runtime no cargó el paquete, así que ninguna parte de él está en ejecución.", + successInList: "{{manifestId}} eliminado: la aplicación se retiró del kernel en ejecución, junto con sus conjuntos de permisos y sus asignaciones. Los registros almacenados en sus objetos permanecen en la base de datos.", + successNotLoaded: "{{manifestId}} eliminado. No estaba cargado, así que no es necesario reiniciar.", }, install: { - dialogTitle: "Instalar {{name}}", - dialogDescCurrent: "Instalar en este entorno ({{host}}).", - dialogDescPicker: "Elija un entorno donde instalar esta aplicación. Debe haber iniciado sesión en ObjectOS Cloud.", - environment: "Entorno", - environmentPlaceholder: "Seleccionar entorno", - includeSampleData: "Incluir datos de muestra", - noEnvs: "No se encontraron entornos en su organización activa.", - noPermission: "No tiene permiso para instalar aplicaciones en un entorno. Solo los propietarios y administradores de la organización pueden instalar; pida ayuda al administrador de su organización.", - signInFirst: "Primero debe iniciar sesión en ObjectOS Cloud.", - success: "Instalado correctamente. Abra el entorno para ver la nueva aplicación.", - localSuccess: 'v{{version}} instalado en este runtime. "{{name}}" ya está en el selector de aplicaciones.', - localLoadsAtRestart: "v{{version}} instalado en este runtime. \"{{name}}\" se cargará en el próximo reinicio del runtime y no aparecerá en el selector de aplicaciones hasta entonces.", - localManifestConflict: "{{message}}\nNota: Una aplicación local ya tiene este manifest_id. Primero elimínela de objectstack.config.ts.", // ADR-0141 D2 — the install refusal DEPENDENCY_MISSING, one line per unmet entry. dependencyMissing: { heading: "Instalación rechazada: una dependencia que declara este paquete no se cumple en este entorno, por lo que no se instaló nada.", @@ -4325,109 +4223,6 @@ const es = { noRange: "{{packageId}} no indica qué protocolo de plataforma admite, por lo que se instaló con el protocolo {{protocolVersion}} sin comprobar la compatibilidad y es posible que no funcione en esta versión de la plataforma. Una compilación del paquete que declare el protocolo que admite (engines.protocol) se comprueba al instalarla.", unparsedRange: "{{packageId}} declara su protocolo de plataforma como {{requiredRange}}, que este entorno de ejecución no puede leer, por lo que se instaló con el protocolo {{protocolVersion}} sin comprobar la compatibilidad y es posible que no funcione en esta versión de la plataforma. Una compilación del paquete que declare un rango legible (engines.protocol) se comprueba al instalarla.", }, - localUnauthorized: "Primero inicie sesión en este runtime.", - localMarketplaceUnavailable: "Este runtime no tiene configurado OS_CLOUD_URL.", - updateTo: "Actualizar → v{{version}}", - installedVersion: "Instalado v{{version}}", - deploying: "Desplegando la aplicación…", - deployed: "La aplicación está desplegada y lista.", - openApp: "Abrir {{name}}", - deployTimeout: "La instalación se registró, pero este entorno aún no ofrece la aplicación. Puede que siga desplegándose; vuelve a comprobarlo en breve. Si nunca aparece, es posible que el paquete no se haya cargado, que no incluya ninguna aplicación o que su aplicación requiera un permiso que no tienes.", - checkAgain: "Comprobar de nuevo", - // ADR-0131 D6 — the install modes a package's manifest permits, and - // what an unmanaged install answers (MarketplacePackagePage). - mode: { - label: "Modo de instalación", - managed: "Administrado", - managedHint: "El paquete sigue siendo la autoridad: se registra como código, recibe sus actualizaciones y sus elementos no se editan aquí.", - unmanaged: "Copia no administrada", - unmanagedHint: "Se copia una sola vez en este runtime como sus propios metadatos: cada elemento se puede editar aquí y la copia no recibe actualizaciones.", - required: "Elige un modo de instalación para instalar este paquete.", - }, - unmanagedSuccess: "v{{version}} de \"{{name}}\" copiado en este runtime como sus propios metadatos, en la base {{base}}. La copia se puede editar y no recibe actualizaciones.", - unmanagedStrippedLocks: "El paquete bloqueaba estos elementos. La copia se puede editar, así que sus bloqueos no se copiaron:", - unmanagedIgnored: "Este runtime no admite instalaciones no administradas, así que \"{{name}}\" se instaló como administrado (registrado como código) en lugar de copiarse. Actualiza el runtime, luego desinstala el paquete y vuelve a instalarlo como copia no administrada.", - }, - // objectui#12146 — the install dialog opened as an UPDATE of the copy - // this environment holds (MarketplacePackagePage). - update: { - dialogTitle: "Actualizar {{name}}", - dialogDescCurrent: "Actualizar la copia instalada en este entorno ({{host}}).", - dialogDescPicker: "Elija el entorno cuya copia instalada desea actualizar. Debe haber iniciado sesión en ObjectOS Cloud.", - majorNotice: "Esta es una actualización de versión principal (v{{fromMajor}} → v{{toMajor}}), que puede incluir cambios incompatibles.", - releaseNotes: "Leer las notas de la versión v{{version}}", - success: "Actualizado a la v{{version}}.", - deployTimeout: "La actualización se registró, pero este entorno aún no ofrece la v{{version}}. Puede que siga desplegándose; vuelva a comprobarlo en breve.", - }, - // objectui#3546 — the MarketplacePage "Your organization" strip. - org: { - heading: "Su organización", - install: "Instalar", - installed: "{{name}} instalado", - installedBadge: "Instalado", - installing: "Instalando…", - }, - // ADR-0025 PD4 §3.5/§3.11 — the pre-install consent panel - // (PluginDisclosure.tsx). `runtime` is a CLOSED enum: spec - // PluginRuntimeSchema = z.enum(['node', 'sandbox', 'worker']). - disclosure: { - containsCode: "Este paquete contiene código", - reviewed: "Revisado y aprobado", - unreviewed: "Sin revisar todavía", - signed: "Firmado", - grantsIntro: "Este paquete solicita:", - notEnforced: "Esta lista se registra al instalar y se vuelve a confirmar si una versión posterior solicita más, pero el runtime aún no restringe el paquete a ella.", - services: "Servicios de la plataforma", - hooks: "Hooks de ciclo de vida", - network: "Acceso a la red", - fs: "Acceso al sistema de archivos", - noPermissions: "No solicita permisos especiales.", - acknowledge: "Entiendo que este paquete ejecuta código y recibe los permisos anteriores.", - runtime: { - node: "En el proceso · confianza total", - sandbox: "En sandbox", - worker: "Fuera del proceso", - }, - }, - uninstall: { - confirm: "¿Desinstalar {{manifestId}} v{{version}} de este runtime?\n\nLa aplicación se retirará de inmediato del kernel en ejecución, junto con sus conjuntos de permisos y sus asignaciones, y el manifiesto en caché será eliminado. Los registros almacenados en sus objetos permanecen en la base de datos.", - successInList: "{{manifestId}} eliminado: la aplicación se retiró del kernel en ejecución, junto con sus conjuntos de permisos y sus asignaciones. Los registros almacenados en sus objetos permanecen en la base de datos.", - successInDetail: "{{manifestId}} y su manifiesto en caché eliminados: la aplicación se retiró del kernel en ejecución, junto con sus conjuntos de permisos y sus asignaciones. Los registros almacenados en sus objetos permanecen en la base de datos.", - confirmNotLoaded: "¿Desinstalar {{manifestId}} v{{version}} de este runtime?\n\nEl manifiesto en caché será eliminado. Este runtime no cargó el paquete, así que ninguna parte de él está en ejecución.", - successNotLoaded: "{{manifestId}} eliminado. No estaba cargado, así que no es necesario reiniciar.", - }, - accessDenied: { - title: "Marketplace solo para administradores", - description: "No tiene permiso para instalar aplicaciones en este entorno.", - }, - category: { - crm: "CRM", - erp: "ERP", - hr: "RRHH", - finance: "Finanzas", - project: "Gestión de proyectos", - collaboration: "Colaboración", - analytics: "Analítica", - integration: "Integración", - automation: "Automatización", - ai: "IA", - security: "Seguridad", - "developer-tools": "Herramientas de desarrollador", - "ui-theme": "Tema de UI", - storage: "Almacenamiento", - other: "Otro", - }, - relativeTime: { - today: "hoy", - daysAgo: "hace {{count}}d", - monthsAgo: "hace {{count}} meses", - monthsAgo_one: "hace {{count}} mes", - monthsAgo_many: "hace {{count}} meses", - monthsAgo_other: "hace {{count}} meses", - yearsAgo: "hace {{count}} años", - yearsAgo_one: "hace {{count}} año", - yearsAgo_many: "hace {{count}} años", - yearsAgo_other: "hace {{count}} años", }, }, approvalsInbox: { diff --git a/ui/packages/i18n/src/locales/fr.ts b/ui/packages/i18n/src/locales/fr.ts index bfc8d5db061..56842635951 100644 --- a/ui/packages/i18n/src/locales/fr.ts +++ b/ui/packages/i18n/src/locales/fr.ts @@ -4177,42 +4177,16 @@ const fr = { connect: "Connecter", }, }, + // The rest of `marketplace.*` is the lazy `marketplace` namespace + // (`../namespaces/marketplace/fr.ts`), fetched when a marketplace page + // mounts. What stays here is read by a page outside the marketplace route: + // Studio's packages page draws a marketplace install's row through + // `MarketplaceRowActions` and `MarketplaceNotLoaded` (the Not loaded badge + // and reason, Uninstall with its confirmation and result), and an import's + // refusal and protocol note through `useDependencyMissingText` and + // `useProtocolCheckSkippedText`. marketplace: { - suggestedBindings: { - promptEveryone: "Cette application suggère d'accorder « {{set}} » à tous les utilisateurs connectés (position Everyone).", - promptGuest: "Cette application suggère d'accorder « {{set}} » aux visiteurs non authentifiés (position Guest).", - confirm: "Accorder", - confirming: "Attribution…", - dismiss: "Ignorer", - confirmedToast: "« {{set}} » est désormais accordé à {{anchor}}.", - dismissedToast: "Suggestion pour « {{set}} » ignorée.", - guestUnsupported: "Impossible de l'accorder ici : les ensembles de la position Guest ne sont pas dérivés des suggestions acceptées.", - acceptedState: "« {{set}} » a été accepté pour tous les utilisateurs connectés (position Everyone).", - notInForceState: "« {{set}} » a été accepté, mais n'est pas encore en vigueur : la position Everyone n'a pas changé lors de cette requête. Le journal du serveur indique pourquoi.", - dismissedState: "« {{set}} » n'est pas accordé : la suggestion a été ignorée ou révoquée.", - revoke: "Révoquer", - revoking: "Révocation…", - revokedToast: "Acceptation de « {{set}} » révoquée.", - reopen: "Reconsidérer", - reopening: "Réouverture…", - reopenedToast: "Suggestion pour « {{set}} » rouverte : rien n'est accordé tant qu'elle n'est pas acceptée.", - }, - title: "Marketplace d'applications", - subtitle: "Parcourez les applications approuvées publiées dans le catalogue ObjectStack. Cliquez sur une application pour voir les détails et l'installer dans l'un de vos environnements.", - searchPlaceholder: "Rechercher des applications par nom ou ID d'application…", - searchAria: "Rechercher des applications du marketplace", - installed: "Installé", - installedCount: "Installées ({{count}})", - refresh: "Actualiser", - all: "Toutes", - noApprovedYet: "Aucune application approuvée pour le marketplace pour l'instant.", - noMatchFilters: "Aucune application ne correspond à vos filtres.", - noDescription: "Aucune description disponible.", - back: "Retour au marketplace", - browseLink: "Parcourir le marketplace →", installedAdditiveNote: "La désinstallation retire immédiatement l'application du noyau en cours d'exécution, avec ses jeux d'autorisations et leurs attributions, et supprime le manifeste en cache pour que le paquet ne se charge pas au prochain démarrage. Les enregistrements stockés dans ses objets restent dans la base de données.", - versionBadge: "v{{version}}", - installedBadge: "Installé v{{version}}", // objectui#11645 — an install-local entry this runtime refused to load at // startup (the listing's `notLoaded` marker), read on Installed Apps. notLoaded: { @@ -4220,94 +4194,18 @@ const fr = { protocolIncompatible: "Ce runtime n'a pas chargé ce paquet : il cible le protocole {{requiredRange}}, que ce runtime ne prend pas en charge.", otherReason: "Ce runtime n'a pas chargé ce paquet ({{code}}).", }, - load: { - failed: "Impossible de charger le marketplace", - failedHintConfigured: "Ce runtime accède à la place de marché via le plan de contrôle situé à {{url}}. Vérifiez qu'il est en ligne et joignable depuis ici.", - failedHintSameOrigin: "Le catalogue de la place de marché est accessible via ce runtime. Vérifiez que le runtime est en ligne et qu'il peut joindre le catalogue.", - loading: "Chargement du catalogue de la place de marché…", - slow: "Chargement en cours — le catalogue de la place de marché tarde à répondre.", - retry: "Réessayer", - unreachable: "Le catalogue de la place de marché est injoignable.", - answeredError: "Le catalogue de la place de marché a répondu par une erreur (HTTP {{status}}).", - packageFailed: "Impossible de charger le paquet", - notFound: "Introuvable.", - }, - disabled: { - title: "La place de marché des applications est désactivée", - description: "Aucune place de marché n'est configurée pour ce runtime : il n'y a donc rien à parcourir ni à installer ici.", - hint: "C'est l'état attendu lorsque le runtime démarre avec <code>OS_CLOUD_URL=off</code> (ou <code>none</code>, <code>local</code>, <code>disabled</code>). Pour activer la place de marché, faites pointer <code>OS_CLOUD_URL</code> vers un plan de contrôle puis redémarrez le runtime.", - }, - detail: { - homepage: "Page d'accueil", - installedV: "Installé · v{{version}}", - about: "À propos", - noReadme: "Aucun readme disponible.", - versions: "Versions", - noApprovedVersions: "Aucune version approuvée.", - prerelease: "Prélancement", - moreOptions: "Plus d'options d'installation", - uninstallFromRuntime: "Désinstaller de ce runtime", - addSampleData: 'Ajouter des données d\'exemple', - reseedAgain: 'Recharger les données d\'exemple', - purgeSampleData: 'Supprimer les données d\'exemple', - purgeConfirm: 'Supprimer tous les enregistrements d\'exemple créés par ce paquet ? Les enregistrements ajoutés par l\'utilisateur ne seront PAS touchés.', - purgeSuccess: '{{count}} enregistrement(s) d\'exemple supprimé(s).', - purgeSuccess_one: "{{count}} enregistrement d'exemple supprimé.", - purgeSuccess_many: "{{count}} d'enregistrements d'exemple supprimés.", - purgeSuccess_other: "{{count}} enregistrements d'exemple supprimés.", - purgeNoData: 'Aucun enregistrement d\'exemple à supprimer.', - // objectstack#30323 — the install note's sample-data count, its clear and the app banner. - sampleDataLoaded: 'Cette installation a chargé {{count}} enregistrement(s) d\'exemple pour que vous voyiez l\'application fonctionner. Supprimez-les quand vous commencez à l\'utiliser pour de bon.', - sampleDataLoaded_one: 'Cette installation a chargé {{count}} enregistrement d\'exemple pour que vous voyiez l\'application fonctionner. Supprimez-le quand vous commencez à l\'utiliser pour de bon.', - sampleDataLoaded_many: 'Cette installation a chargé {{count}} d\'enregistrements d\'exemple pour que vous voyiez l\'application fonctionner. Supprimez-les quand vous commencez à l\'utiliser pour de bon.', - sampleDataLoaded_other: 'Cette installation a chargé {{count}} enregistrements d\'exemple pour que vous voyiez l\'application fonctionner. Supprimez-les quand vous commencez à l\'utiliser pour de bon.', - sampleDataReloaded: 'Réinstaller recharge les données d\'exemple, y compris celles que vous avez supprimées.', - purgeConfirmAtMost: 'Au plus {{count}} enregistrement(s) d\'exemple seront supprimés.', - purgeConfirmAtMost_one: 'Au plus {{count}} enregistrement d\'exemple sera supprimé.', - purgeConfirmAtMost_many: 'Au plus {{count}} d\'enregistrements d\'exemple seront supprimés.', - purgeConfirmAtMost_other: 'Au plus {{count}} enregistrements d\'exemple seront supprimés.', - sampleDataBannerTitle: '« {{name}} » contient des données d\'exemple.', - sampleDataBannerMessage: 'Elles sont là pour que vous voyiez l\'application fonctionner. Supprimez-les quand vous commencez à l\'utiliser pour de bon.', - reseedQueued: 'Les données d\'exemple seront rechargées au prochain accès à l\'environnement.', - reseedLocalSuccess: 'Données d\'exemple rechargées : {{inserted}} ajoutés, {{updated}} mis à jour.', - reseedPartialErrors: '({{count}} enregistrement(s) n\'ont pas pu être écrits)', - reseedPartialErrors_one: "({{count}} enregistrement n'a pas pu être écrit)", - reseedPartialErrors_many: "({{count}} d'enregistrements n'ont pas pu être écrits)", - reseedPartialErrors_other: "({{count}} enregistrements n'ont pas pu être écrits)", - sampleDataKernelUnavailable: 'Ce plan de contrôle n\'a pas de noyau d\'environnement : les données d\'exemple ne peuvent donc être ni rechargées ni supprimées ici. Faites-le depuis le runtime propre à l\'environnement.', - updateAvailable: 'Mise à jour disponible', - }, action: { - installed: "Installé", - install: "Installer", - reinstall: "Réinstaller", - working: "Traitement…", - installToCloud: "Installer dans le cloud…", - installing: "Installation…", - updating: "Mise à jour…", uninstall: "Désinstaller", uninstalling: "Désinstallation…", details: "Détails", - close: "Fermer", - dismiss: "Fermer", - openOnCloud: "Ouvrir dans le cloud", - backHome: "Retour à l'accueil", - updateTo: "Mettre à jour", + }, + uninstall: { + confirm: "Désinstaller {{manifestId}} v{{version}} de ce runtime ?\n\nL'application sera retirée immédiatement du noyau en cours d'exécution, avec ses jeux d'autorisations et leurs attributions, et le manifeste en cache sera supprimé. Les enregistrements stockés dans ses objets restent dans la base de données.", + confirmNotLoaded: "Désinstaller {{manifestId}} v{{version}} de ce runtime ?\n\nLe manifeste en cache sera supprimé. Ce runtime n'a pas chargé le paquet, donc aucune partie n'est en cours d'exécution.", + successInList: "{{manifestId}} supprimé : l'application a été retirée du noyau en cours d'exécution, avec ses jeux d'autorisations et leurs attributions. Les enregistrements stockés dans ses objets restent dans la base de données.", + successNotLoaded: "{{manifestId}} supprimé. Il n'était pas chargé, aucun redémarrage n'est nécessaire.", }, install: { - dialogTitle: "Installer {{name}}", - dialogDescCurrent: "Installer dans cet environnement ({{host}}).", - dialogDescPicker: "Choisissez un environnement où installer cette application. Vous devez être connecté à ObjectOS Cloud.", - environment: "Environnement", - environmentPlaceholder: "Sélectionner un environnement", - includeSampleData: "Inclure des données d'exemple", - noEnvs: "Aucun environnement trouvé dans votre organisation active.", - noPermission: "Vous n'avez pas la permission d'installer des applications dans un environnement. Seuls les propriétaires et administrateurs de l'organisation peuvent installer — contactez l'administrateur de votre organisation.", - signInFirst: "Vous devez d'abord vous connecter à ObjectOS Cloud.", - success: "Installé avec succès. Ouvrez l'environnement pour voir la nouvelle application.", - localSuccess: 'v{{version}} installé dans ce runtime. « {{name}} » est maintenant dans le sélecteur d\'applications.', - localLoadsAtRestart: "v{{version}} installé dans ce runtime. \"{{name}}\" sera chargé au prochain redémarrage du runtime et n'apparaîtra pas dans le sélecteur d'applications d'ici là.", - localManifestConflict: "{{message}}\nRemarque : Une application locale possède déjà ce manifest_id. Supprimez-la d'abord de objectstack.config.ts.", // ADR-0141 D2 — the install refusal DEPENDENCY_MISSING, one line per unmet entry. dependencyMissing: { heading: "Installation refusée : une dépendance déclarée par ce paquet n’est pas satisfaite dans cet environnement, rien n’a donc été installé.", @@ -4321,109 +4219,6 @@ const fr = { noRange: "{{packageId}} n’indique pas quel protocole de plateforme il prend en charge : il a donc été installé sous le protocole {{protocolVersion}} sans vérification de compatibilité et risque de ne pas fonctionner sur cette version de la plateforme. Une version du paquet qui déclare le protocole pris en charge (engines.protocol) est vérifiée à l’installation.", unparsedRange: "{{packageId}} déclare son protocole de plateforme comme {{requiredRange}}, que ce runtime ne sait pas lire : il a donc été installé sous le protocole {{protocolVersion}} sans vérification de compatibilité et risque de ne pas fonctionner sur cette version de la plateforme. Une version du paquet qui déclare une plage lisible (engines.protocol) est vérifiée à l’installation.", }, - localUnauthorized: "Connectez-vous d'abord à ce runtime.", - localMarketplaceUnavailable: "Ce runtime n'a pas de OS_CLOUD_URL configuré.", - updateTo: "Mettre à jour → v{{version}}", - installedVersion: "Installé v{{version}}", - deploying: "Déploiement de l'application…", - deployed: "L'application est déployée et prête.", - openApp: "Ouvrir {{name}}", - deployTimeout: "L'installation a été enregistrée, mais cet environnement ne sert pas encore l'application. Le déploiement est peut-être encore en cours : vérifiez à nouveau dans un instant. Si elle n'apparaît jamais, le paquet n'a peut-être pas été chargé, ne contient aucune application, ou son application exige une autorisation que vous n'avez pas.", - checkAgain: "Vérifier à nouveau", - // ADR-0131 D6 — the install modes a package's manifest permits, and - // what an unmanaged install answers (MarketplacePackagePage). - mode: { - label: "Mode d'installation", - managed: "Géré", - managedHint: "Le paquet reste l'autorité : il est enregistré comme code, reçoit ses mises à niveau, et ses éléments ne sont pas modifiés ici.", - unmanaged: "Copie non gérée", - unmanagedHint: "Copié une seule fois dans ce runtime comme ses propres métadonnées : chaque élément est modifiable ici, et la copie ne reçoit aucune mise à niveau.", - required: "Choisissez un mode d'installation pour installer ce paquet.", - }, - unmanagedSuccess: "v{{version}} de \"{{name}}\" copié dans ce runtime comme ses propres métadonnées, dans la base {{base}}. La copie est modifiable et ne reçoit aucune mise à niveau.", - unmanagedStrippedLocks: "Le paquet verrouillait ces éléments. La copie est modifiable, leurs verrous n'ont donc pas été copiés :", - unmanagedIgnored: "Ce runtime ne prend pas en charge les installations non gérées : \"{{name}}\" a donc été installé en mode géré (enregistré comme code) au lieu d'être copié. Mettez à niveau le runtime, puis désinstallez le paquet et réinstallez-le comme copie non gérée.", - }, - // objectui#12146 — the install dialog opened as an UPDATE of the copy - // this environment holds (MarketplacePackagePage). - update: { - dialogTitle: "Mettre à jour {{name}}", - dialogDescCurrent: "Mettre à jour la copie installée dans cet environnement ({{host}}).", - dialogDescPicker: "Choisissez l'environnement dont la copie installée doit être mise à jour. Vous devez être connecté à ObjectOS Cloud.", - majorNotice: "Il s'agit d'une mise à jour de version majeure (v{{fromMajor}} → v{{toMajor}}), qui peut comporter des changements incompatibles.", - releaseNotes: "Lire les notes de version de la v{{version}}", - success: "Mis à jour vers la v{{version}}.", - deployTimeout: "La mise à jour a été enregistrée, mais cet environnement ne sert pas encore la v{{version}}. Le déploiement est peut-être encore en cours : vérifiez à nouveau dans un instant.", - }, - // objectui#3546 — the MarketplacePage "Your organization" strip. - org: { - heading: "Votre organisation", - install: "Installer", - installed: "{{name}} installé", - installedBadge: "Installé", - installing: "Installation…", - }, - // ADR-0025 PD4 §3.5/§3.11 — the pre-install consent panel - // (PluginDisclosure.tsx). `runtime` is a CLOSED enum: spec - // PluginRuntimeSchema = z.enum(['node', 'sandbox', 'worker']). - disclosure: { - containsCode: "Ce paquet contient du code", - reviewed: "Examiné et approuvé", - unreviewed: "Pas encore examiné", - signed: "Signé", - grantsIntro: "Ce paquet demande :", - notEnforced: "Cette liste est enregistrée à l'installation et reconfirmée si une version ultérieure en demande davantage, mais l'exécution ne restreint pas encore le paquet à cette liste.", - services: "Services de la plateforme", - hooks: "Hooks de cycle de vie", - network: "Accès réseau", - fs: "Accès au système de fichiers", - noPermissions: "Ne demande aucune autorisation particulière.", - acknowledge: "Je comprends que ce paquet exécute du code et obtient les autorisations ci-dessus.", - runtime: { - node: "Dans le processus · confiance totale", - sandbox: "Bac à sable", - worker: "Hors processus", - }, - }, - uninstall: { - confirm: "Désinstaller {{manifestId}} v{{version}} de ce runtime ?\n\nL'application sera retirée immédiatement du noyau en cours d'exécution, avec ses jeux d'autorisations et leurs attributions, et le manifeste en cache sera supprimé. Les enregistrements stockés dans ses objets restent dans la base de données.", - successInList: "{{manifestId}} supprimé : l'application a été retirée du noyau en cours d'exécution, avec ses jeux d'autorisations et leurs attributions. Les enregistrements stockés dans ses objets restent dans la base de données.", - successInDetail: "{{manifestId}} et son manifeste en cache supprimés : l'application a été retirée du noyau en cours d'exécution, avec ses jeux d'autorisations et leurs attributions. Les enregistrements stockés dans ses objets restent dans la base de données.", - confirmNotLoaded: "Désinstaller {{manifestId}} v{{version}} de ce runtime ?\n\nLe manifeste en cache sera supprimé. Ce runtime n'a pas chargé le paquet, donc aucune partie n'est en cours d'exécution.", - successNotLoaded: "{{manifestId}} supprimé. Il n'était pas chargé, aucun redémarrage n'est nécessaire.", - }, - accessDenied: { - title: "Marketplace réservé aux administrateurs", - description: "Vous n'avez pas la permission d'installer des applications dans cet environnement.", - }, - category: { - crm: "CRM", - erp: "ERP", - hr: "RH", - finance: "Finance", - project: "Gestion de projet", - collaboration: "Collaboration", - analytics: "Analytique", - integration: "Intégration", - automation: "Automatisation", - ai: "IA", - security: "Sécurité", - "developer-tools": "Outils développeur", - "ui-theme": "Thème UI", - storage: "Stockage", - other: "Autre", - }, - relativeTime: { - today: "aujourd'hui", - daysAgo: "il y a {{count}}j", - monthsAgo: "il y a {{count}} mois", - monthsAgo_one: "il y a {{count}} mois", - monthsAgo_many: "il y a {{count}} mois", - monthsAgo_other: "il y a {{count}} mois", - yearsAgo: "il y a {{count}} ans", - yearsAgo_one: "il y a {{count}} an", - yearsAgo_many: "il y a {{count}} ans", - yearsAgo_other: "il y a {{count}} ans", }, }, approvalsInbox: { diff --git a/ui/packages/i18n/src/locales/index.ts b/ui/packages/i18n/src/locales/index.ts index e97dfe0f4ba..a144fdd22fa 100644 --- a/ui/packages/i18n/src/locales/index.ts +++ b/ui/packages/i18n/src/locales/index.ts @@ -16,36 +16,91 @@ * that says what it costs. * * ⚠️ Anything on a page-load path belongs on `./registry.js`, not here. + * + * ## Each catalogue here is COMPLETE: the pack plus its lazy namespaces + * + * Some of a language's strings are not in its pack: the `marketplace.*` group + * is a lazy namespace (`../namespaces/marketplace/`), fetched when a + * marketplace page mounts so the console's eager `en` pack does not carry it. + * This door's two purposes both want every string, so each language it hands + * out is its pack deep-merged with its catalogue from every such namespace: + * the parity / drift suites keep walking the moved keys, and an app that makes + * every catalogue resident gets the marketplace text resident too. A key in + * both places is refused at module evaluation — the gates refuse it as well. + * + * ⚠️ So `en` here is not the same object as the package entry's `en`, which is + * the RESIDENT pack alone (what an instance holds before any namespace has + * loaded), nor as `@object-ui/i18n/locales/en`, the pack module itself. */ -export { default as en, type TranslationKeys } from './en.js'; -export { default as zh } from './zh.js'; -export { default as ja } from './ja.js'; -export { default as ko } from './ko.js'; -export { default as de } from './de.js'; -export { default as fr } from './fr.js'; -export { default as es } from './es.js'; -export { default as pt } from './pt.js'; -export { default as ru } from './ru.js'; -export { default as ar } from './ar.js'; +import enPack from './en.js'; +import zhPack from './zh.js'; +import jaPack from './ja.js'; +import koPack from './ko.js'; +import dePack from './de.js'; +import frPack from './fr.js'; +import esPack from './es.js'; +import ptPack from './pt.js'; +import ruPack from './ru.js'; +import arPack from './ar.js'; +import enMarketplace from '../namespaces/marketplace/en.js'; +import zhMarketplace from '../namespaces/marketplace/zh.js'; +import jaMarketplace from '../namespaces/marketplace/ja.js'; +import koMarketplace from '../namespaces/marketplace/ko.js'; +import deMarketplace from '../namespaces/marketplace/de.js'; +import frMarketplace from '../namespaces/marketplace/fr.js'; +import esMarketplace from '../namespaces/marketplace/es.js'; +import ptMarketplace from '../namespaces/marketplace/pt.js'; +import ruMarketplace from '../namespaces/marketplace/ru.js'; +import arMarketplace from '../namespaces/marketplace/ar.js'; + +export type { TranslationKeys } from './en.js'; + +const isPlainObject = (value: unknown): value is Record<string, unknown> => + typeof value === 'object' && value !== null && !Array.isArray(value); /** - * Map of all built-in locales keyed by language code + * `pack` deep-merged with a namespace's partial pack. Groups both sides hold + * (`marketplace`, `marketplace.install`) merge; a LEAF both sides hold is a + * key defined twice, which no reader could resolve the same way twice, and + * throws. */ -import en from './en.js'; -import zh from './zh.js'; -import ja from './ja.js'; -import ko from './ko.js'; -import de from './de.js'; -import fr from './fr.js'; -import es from './es.js'; -import pt from './pt.js'; -import ru from './ru.js'; -import ar from './ar.js'; +function complete<P extends object, N extends object>(pack: P, partial: N, label: string): P & N { + const merge = (into: Record<string, unknown>, from: Record<string, unknown>, at: string) => { + const out: Record<string, unknown> = { ...into }; + for (const key of Object.keys(from)) { + if (key === '__proto__') continue; + const path = at ? `${at}.${key}` : key; + if (isPlainObject(out[key]) && isPlainObject(from[key])) { + out[key] = merge(out[key] as Record<string, unknown>, from[key] as Record<string, unknown>, path); + } else if (Object.prototype.hasOwnProperty.call(out, key)) { + throw new Error(`${label}: \`${path}\` is defined both in the pack and in a namespace catalogue.`); + } else { + out[key] = from[key]; + } + } + return out; + }; + return merge(pack as Record<string, unknown>, partial as Record<string, unknown>, '') as P & N; +} + +// Annotated rather than inferred: the inferred type spells the packs' literal +// types out inline in the emitted declaration, which outgrew what tsc will +// serialize (TS7056). Each `typeof` is emitted as a reference. +export const en: typeof enPack & typeof enMarketplace = complete(enPack, enMarketplace, 'en'); +export const zh: typeof zhPack & typeof zhMarketplace = complete(zhPack, zhMarketplace, 'zh'); +export const ja: typeof jaPack & typeof jaMarketplace = complete(jaPack, jaMarketplace, 'ja'); +export const ko: typeof koPack & typeof koMarketplace = complete(koPack, koMarketplace, 'ko'); +export const de: typeof dePack & typeof deMarketplace = complete(dePack, deMarketplace, 'de'); +export const fr: typeof frPack & typeof frMarketplace = complete(frPack, frMarketplace, 'fr'); +export const es: typeof esPack & typeof esMarketplace = complete(esPack, esMarketplace, 'es'); +export const pt: typeof ptPack & typeof ptMarketplace = complete(ptPack, ptMarketplace, 'pt'); +export const ru: typeof ruPack & typeof ruMarketplace = complete(ruPack, ruMarketplace, 'ru'); +export const ar: typeof arPack & typeof arMarketplace = complete(arPack, arMarketplace, 'ar'); -// Annotated rather than inferred: the inferred type spells all ten packs' -// literal types out inline in the emitted declaration, which outgrew what tsc -// will serialize (TS7056). Each `typeof <pack>` is the same type, emitted as a -// reference. +/** + * Map of all built-in locales keyed by language code — each one complete (see + * the header). + */ export const builtInLocales: { readonly en: typeof en; readonly zh: typeof zh; diff --git a/ui/packages/i18n/src/locales/ja.ts b/ui/packages/i18n/src/locales/ja.ts index d5b0ac5b3ea..515f8adfb27 100644 --- a/ui/packages/i18n/src/locales/ja.ts +++ b/ui/packages/i18n/src/locales/ja.ts @@ -4098,42 +4098,16 @@ const ja = { connect: "接続", }, }, + // The rest of `marketplace.*` is the lazy `marketplace` namespace + // (`../namespaces/marketplace/ja.ts`), fetched when a marketplace page + // mounts. What stays here is read by a page outside the marketplace route: + // Studio's packages page draws a marketplace install's row through + // `MarketplaceRowActions` and `MarketplaceNotLoaded` (the Not loaded badge + // and reason, Uninstall with its confirmation and result), and an import's + // refusal and protocol note through `useDependencyMissingText` and + // `useProtocolCheckSkippedText`. marketplace: { - suggestedBindings: { - promptEveryone: "このアプリは、サインイン済みの全ユーザー(Everyone ポジション)に「{{set}}」を付与することを推奨しています。", - promptGuest: "このアプリは、未認証の訪問者(Guest ポジション)に「{{set}}」を付与することを推奨しています。", - confirm: "付与", - confirming: "付与しています…", - dismiss: "閉じる", - confirmedToast: "「{{set}}」を {{anchor}} に付与しました。", - dismissedToast: "「{{set}}」の提案を閉じました。", - guestUnsupported: "ここでは付与できません。Guest ポジションのセットは承認された提案から導出されません。", - acceptedState: "「{{set}}」は、サインイン済みの全ユーザー(Everyone ポジション)向けに承認されています。", - notInForceState: "「{{set}}」は承認されましたが、まだ有効ではありません。このリクエストでは Everyone ポジションは変更されませんでした。理由はサーバーログに記録されています。", - dismissedState: "「{{set}}」は付与されていません。提案は閉じられたか、取り消されました。", - revoke: "取り消す", - revoking: "取り消しています…", - revokedToast: "「{{set}}」の承認を取り消しました。", - reopen: "再検討", - reopening: "再オープン中…", - reopenedToast: "「{{set}}」の提案を再オープンしました。承認されるまで何も付与されません。", - }, - title: "アプリマーケットプレイス", - subtitle: "ObjectStackカタログに公開された承認済みアプリを参照します。アプリをクリックして詳細を表示し、環境にインストールしてください。", - searchPlaceholder: '名前またはアプリIDでアプリを検索…', - searchAria: "マーケットプレイスアプリを検索", - installed: "インストール済み", - installedCount: "インストール済み({{count}})", - refresh: "更新", - all: "すべて", - noApprovedYet: "マーケットプレイスにはまだ承認されたアプリがありません。", - noMatchFilters: "フィルターに一致するアプリがありません。", - noDescription: "説明がありません。", - back: "マーケットプレイスに戻る", - browseLink: "マーケットプレイスを参照 →", installedAdditiveNote: "アンインストールすると、アプリは実行中のカーネルから直ちに削除され、その権限セットと権限セットの付与も併せて削除されます。キャッシュされたマニフェストも削除されるため、次回起動時にパッケージは読み込まれません。オブジェクトに保存されたレコードはデータベースに残ります。", - versionBadge: "v{{version}}", - installedBadge: "インストール済み v{{version}}", // objectui#11645 — an install-local entry this runtime refused to load at // startup (the listing's `notLoaded` marker), read on Installed Apps. notLoaded: { @@ -4141,90 +4115,18 @@ const ja = { protocolIncompatible: "このランタイムはこのパッケージを読み込んでいません。このパッケージはプロトコル {{requiredRange}} を対象としていますが、このランタイムはそのプロトコルをサポートしていません。", otherReason: "このランタイムはこのパッケージを読み込んでいません({{code}})。", }, - load: { - failed: "マーケットプレイスの読み込みに失敗しました", - failedHintConfigured: "このランタイムは {{url}} のコントロールプレーン経由でマーケットプレイスに接続します。オンラインであること、ここから到達できることを確認してください。", - failedHintSameOrigin: "マーケットプレイスのカタログには、このランタイムを経由してアクセスします。ランタイムがオンラインで、カタログに到達できることを確認してください。", - loading: "マーケットプレイスのカタログを読み込んでいます…", - slow: "まだ読み込み中です — マーケットプレイスのカタログの応答が遅れています。", - retry: "再試行", - unreachable: "マーケットプレイスのカタログに到達できませんでした。", - answeredError: "マーケットプレイスのカタログがエラーを返しました(HTTP {{status}})。", - packageFailed: "パッケージの読み込みに失敗しました", - notFound: "見つかりません。", - }, - disabled: { - title: "アプリマーケットプレイスは無効です", - description: "このランタイムにはマーケットプレイスが設定されていないため、ここから閲覧またはインストールできるものはありません。", - hint: "ランタイムを <code>OS_CLOUD_URL=off</code>(または <code>none</code>、<code>local</code>、<code>disabled</code>)で起動した場合、これは想定どおりの状態です。マーケットプレイスを有効にするには、<code>OS_CLOUD_URL</code> をコントロールプレーンに向けてランタイムを再起動してください。", - }, - detail: { - homepage: "ホームページ", - installedV: "インストール済み · v{{version}}", - about: "概要", - noReadme: "READMEがありません。", - versions: "バージョン", - noApprovedVersions: "承認済みバージョンがありません。", - prerelease: "プレ", - moreOptions: "その他のインストールオプション", - uninstallFromRuntime: "このランタイムからアンインストール", - addSampleData: 'サンプルデータを追加', - reseedAgain: 'サンプルデータを再投入', - purgeSampleData: 'サンプルデータを削除', - purgeConfirm: 'このパッケージが投入したサンプルレコードをすべて削除しますか?ユーザーが追加したレコードは削除されません。', - purgeSuccess: '{{count}} 件のサンプルレコードを削除しました。', - purgeSuccess_one: '{{count}} 件のサンプルレコードを削除しました。', - purgeSuccess_other: '{{count}} 件のサンプルレコードを削除しました。', - purgeNoData: '削除するサンプルレコードがありません。', - // objectstack#30323 — the install note's sample-data count, its clear and the app banner. - sampleDataLoaded: 'このインストールで {{count}} 件のサンプルレコードを読み込みました。アプリの動作を確認できます。本番で使い始める前に削除できます。', - sampleDataLoaded_one: 'このインストールで {{count}} 件のサンプルレコードを読み込みました。アプリの動作を確認できます。本番で使い始める前に削除できます。', - sampleDataLoaded_other: 'このインストールで {{count}} 件のサンプルレコードを読み込みました。アプリの動作を確認できます。本番で使い始める前に削除できます。', - sampleDataReloaded: 'もう一度インストールすると、削除したものも含めてサンプルデータが再び読み込まれます。', - purgeConfirmAtMost: '削除されるサンプルレコードは最大 {{count}} 件です。', - purgeConfirmAtMost_one: '削除されるサンプルレコードは最大 {{count}} 件です。', - purgeConfirmAtMost_other: '削除されるサンプルレコードは最大 {{count}} 件です。', - sampleDataBannerTitle: '「{{name}}」にはサンプルデータがあります。', - sampleDataBannerMessage: 'アプリの動作を確認するためのデータです。本番で使い始める前に削除できます。', - reseedQueued: 'サンプルデータは次回の環境アクセス時に再投入されます。', - reseedLocalSuccess: 'サンプルデータを再投入しました:{{inserted}} 件追加、{{updated}} 件更新。', - reseedPartialErrors: '({{count}} 件のレコードの書き込みに失敗しました)', - reseedPartialErrors_one: '({{count}} 件のレコードの書き込みに失敗しました)', - reseedPartialErrors_other: '({{count}} 件のレコードの書き込みに失敗しました)', - sampleDataKernelUnavailable: 'このコントロールプレーンには環境カーネルがないため、ここではサンプルデータを再投入・削除できません。環境自身のランタイムから実行してください。', - updateAvailable: 'アップデートあり', - }, action: { - installed: "インストール済み", - install: "インストール", - reinstall: "再インストール", - working: "処理中…", - installToCloud: "クラウドにインストール…", - installing: "インストール中…", - updating: "更新中…", uninstall: "アンインストール", uninstalling: "アンインストール中…", details: "詳細", - close: "閉じる", - dismiss: "閉じる", - openOnCloud: "クラウドで開く", - backHome: "ホームに戻る", - updateTo: "更新", + }, + uninstall: { + confirm: "このランタイムから {{manifestId}} v{{version}} をアンインストールしますか?\n\nアプリは実行中のカーネルから直ちに削除され、その権限セットと権限セットの付与も併せて削除されます。キャッシュされたマニフェストも削除されます。オブジェクトに保存されたレコードはデータベースに残ります。", + confirmNotLoaded: "このランタイムから {{manifestId}} v{{version}} をアンインストールしますか?\n\nキャッシュされたマニフェストが削除されます。このランタイムはこのパッケージを読み込んでいないため、実行中の部分はありません。", + successInList: "{{manifestId}} を削除しました。アプリは実行中のカーネルから削除され、その権限セットと権限セットの付与も削除されました。オブジェクトに保存されたレコードはデータベースに残ります。", + successNotLoaded: "{{manifestId}} を削除しました。読み込まれていなかったため、再起動は不要です。", }, install: { - dialogTitle: "{{name}} をインストール", - dialogDescCurrent: "この環境({{host}})にインストールします。", - dialogDescPicker: "このアプリをインストールする環境を選択します。ObjectOS Cloudにサインインする必要があります。", - environment: "環境", - environmentPlaceholder: "環境を選択", - includeSampleData: "サンプルデータを含む", - noEnvs: "アクティブな組織に環境が見つかりません。", - noPermission: "どの環境にもアプリをインストールする権限がありません。組織のオーナーと管理者のみインストールできます。組織の管理者にお問い合わせください。", - signInFirst: "最初にObjectOS Cloudにサインインしてください。", - success: "インストールが完了しました。環境を開いて新しいアプリを確認してください。", - localSuccess: 'このランタイムに v{{version}} をインストールしました。「{{name}}」はアプリスイッチャーに表示されています。', - localLoadsAtRestart: "このランタイムに v{{version}} をインストールしました。「{{name}}」はランタイムの次回再起動時に読み込まれ、それまではアプリスイッチャーに表示されません。", - localManifestConflict: "{{message}}\nヒント:ローカルアプリがすでにこのmanifest_idを使用しています。最初にobjectstack.config.tsから削除してください。", // ADR-0141 D2 — the install refusal DEPENDENCY_MISSING, one line per unmet entry. dependencyMissing: { heading: "インストールが拒否されました:このパッケージが宣言する依存関係がこの環境で満たされていないため、何もインストールされていません。", @@ -4238,107 +4140,6 @@ const ja = { noRange: "{{packageId}} は対応するプラットフォームプロトコルを宣言していないため、互換性チェックなしでプロトコル {{protocolVersion}} の下にインストールされました。このプラットフォームバージョンでは正しく動作しない可能性があります。対応プロトコル(engines.protocol)を宣言したパッケージのビルドはインストール時にチェックされます。", unparsedRange: "{{packageId}} はプラットフォームプロトコルを {{requiredRange}} と宣言していますが、このランタイムでは解釈できないため、互換性チェックなしでプロトコル {{protocolVersion}} の下にインストールされました。このプラットフォームバージョンでは正しく動作しない可能性があります。解釈可能な範囲(engines.protocol)を宣言したパッケージのビルドはインストール時にチェックされます。", }, - localUnauthorized: "最初にこのランタイムにサインインしてから再試行してください。", - localMarketplaceUnavailable: "このランタイムにはOS_CLOUD_URLが設定されていないため、マーケットプレイスカタログにアクセスできません。", - updateTo: "更新 → v{{version}}", - installedVersion: "インストール済み v{{version}}", - deploying: "アプリをデプロイしています…", - deployed: "アプリのデプロイが完了しました。", - openApp: "{{name}} を開く", - deployTimeout: "インストールは記録されましたが、この環境ではまだアプリが提供されていません。デプロイ中の可能性があります。しばらくしてから再確認してください。表示されない場合は、パッケージが読み込まれていない、アプリが含まれていない、またはアプリにあなたが持っていない権限が必要な可能性があります。", - checkAgain: "再確認", - // ADR-0131 D6 — the install modes a package's manifest permits, and - // what an unmanaged install answers (MarketplacePackagePage). - mode: { - label: "インストール方式", - managed: "マネージド", - managedHint: "パッケージが管理元のままです。コードとして登録され、アップグレードを受け取り、その項目はここでは編集しません。", - unmanaged: "アンマネージドコピー", - unmanagedHint: "このランタイム自身のメタデータとして一度だけコピーされます。すべての項目をここで編集でき、コピーはアップグレードを受け取りません。", - required: "このパッケージをインストールするには、インストール方式を選択してください。", - }, - unmanagedSuccess: "「{{name}}」の v{{version}} を、このランタイム自身のメタデータとしてベース {{base}} にコピーしました。コピーは編集でき、アップグレードは受け取りません。", - unmanagedStrippedLocks: "パッケージはこれらの項目をロックしていました。コピーは編集可能なため、ロックはコピーされていません:", - unmanagedIgnored: "このランタイムはアンマネージドインストールに対応していないため、「{{name}}」はコピーではなくマネージド(コードとして登録)でインストールされました。ランタイムをアップグレードしてから、パッケージをアンインストールし、アンマネージドコピーとして再度インストールしてください。", - }, - // objectui#12146 — the install dialog opened as an UPDATE of the copy - // this environment holds (MarketplacePackagePage). - update: { - dialogTitle: "{{name}} を更新", - dialogDescCurrent: "この環境({{host}})にインストール済みのアプリを更新します。", - dialogDescPicker: "インストール済みのアプリを更新する環境を選択します。ObjectOS Cloudにサインインする必要があります。", - majorNotice: "メジャーバージョンの更新です(v{{fromMajor}} → v{{toMajor}})。互換性のない変更が含まれる可能性があります。", - releaseNotes: "v{{version}} のリリースノートを読む", - success: "v{{version}} に更新しました。", - deployTimeout: "更新は記録されましたが、この環境ではまだ v{{version}} が提供されていません。デプロイ中の可能性があります。しばらくしてから再確認してください。", - }, - // objectui#3546 — the MarketplacePage "Your organization" strip. - org: { - heading: "組織", - install: "インストール", - installed: "{{name}} をインストールしました", - installedBadge: "インストール済み", - installing: "インストール中…", - }, - // ADR-0025 PD4 §3.5/§3.11 — the pre-install consent panel - // (PluginDisclosure.tsx). `runtime` is a CLOSED enum: spec - // PluginRuntimeSchema = z.enum(['node', 'sandbox', 'worker']). - disclosure: { - containsCode: "このパッケージにはコードが含まれています", - reviewed: "審査・承認済み", - unreviewed: "未審査", - signed: "署名済み", - grantsIntro: "このパッケージは次の権限を要求します:", - notEnforced: "この一覧はインストール時に記録され、新しいバージョンがより多くを要求した場合は再確認されます。ただしランタイムはまだ、このパッケージをこの一覧に制限しません。", - services: "プラットフォームサービス", - hooks: "ライフサイクルフック", - network: "ネットワークアクセス", - fs: "ファイルシステムアクセス", - noPermissions: "特別な権限は要求しません。", - acknowledge: "このパッケージがコードを実行し、上記の権限を取得することを理解しました。", - runtime: { - node: "プロセス内 · 完全信頼", - sandbox: "サンドボックス", - worker: "プロセス外", - }, - }, - uninstall: { - confirm: "このランタイムから {{manifestId}} v{{version}} をアンインストールしますか?\n\nアプリは実行中のカーネルから直ちに削除され、その権限セットと権限セットの付与も併せて削除されます。キャッシュされたマニフェストも削除されます。オブジェクトに保存されたレコードはデータベースに残ります。", - successInList: "{{manifestId}} を削除しました。アプリは実行中のカーネルから削除され、その権限セットと権限セットの付与も削除されました。オブジェクトに保存されたレコードはデータベースに残ります。", - successInDetail: "{{manifestId}} とそのキャッシュされたマニフェストを削除しました。アプリは実行中のカーネルから削除され、その権限セットと権限セットの付与も削除されました。オブジェクトに保存されたレコードはデータベースに残ります。", - confirmNotLoaded: "このランタイムから {{manifestId}} v{{version}} をアンインストールしますか?\n\nキャッシュされたマニフェストが削除されます。このランタイムはこのパッケージを読み込んでいないため、実行中の部分はありません。", - successNotLoaded: "{{manifestId}} を削除しました。読み込まれていなかったため、再起動は不要です。", - }, - accessDenied: { - title: "アプリマーケットプレイスは管理者専用です", - description: "この環境にアプリをインストールする権限がありません。アクセスのために組織のオーナーまたは管理者に連絡してください。", - }, - category: { - crm: "CRM", - erp: "ERP", - hr: "人事", - finance: "財務", - project: "プロジェクト管理", - collaboration: "コラボレーション", - analytics: "分析", - integration: "統合", - automation: "自動化", - ai: "AI", - security: "セキュリティ", - "developer-tools": "開発者ツール", - "ui-theme": "UIテーマ", - storage: "ストレージ", - other: "その他", - }, - relativeTime: { - today: "今日", - daysAgo: "{{count}}日前", - monthsAgo: "{{count}}ヶ月前", - monthsAgo_one: "{{count}}ヶ月前", - monthsAgo_other: "{{count}}ヶ月前", - yearsAgo: "{{count}}年前", - yearsAgo_one: "{{count}}年前", - yearsAgo_other: "{{count}}年前", }, }, approvalsInbox: { diff --git a/ui/packages/i18n/src/locales/ko.ts b/ui/packages/i18n/src/locales/ko.ts index a2fe8011f8f..5647c9361c6 100644 --- a/ui/packages/i18n/src/locales/ko.ts +++ b/ui/packages/i18n/src/locales/ko.ts @@ -4095,42 +4095,16 @@ const ko = { connect: "연결", }, }, + // The rest of `marketplace.*` is the lazy `marketplace` namespace + // (`../namespaces/marketplace/ko.ts`), fetched when a marketplace page + // mounts. What stays here is read by a page outside the marketplace route: + // Studio's packages page draws a marketplace install's row through + // `MarketplaceRowActions` and `MarketplaceNotLoaded` (the Not loaded badge + // and reason, Uninstall with its confirmation and result), and an import's + // refusal and protocol note through `useDependencyMissingText` and + // `useProtocolCheckSkippedText`. marketplace: { - suggestedBindings: { - promptEveryone: "이 앱은 로그인한 모든 사용자(Everyone 위치)에게 \"{{set}}\"을(를) 부여할 것을 제안합니다.", - promptGuest: "이 앱은 인증되지 않은 방문자(Guest 위치)에게 \"{{set}}\"을(를) 부여할 것을 제안합니다.", - confirm: "부여", - confirming: "부여하는 중…", - dismiss: "닫기", - confirmedToast: "\"{{set}}\"이(가) 이제 {{anchor}}에 부여되었습니다.", - dismissedToast: "\"{{set}}\" 제안을 닫았습니다.", - guestUnsupported: "여기서는 부여할 수 없습니다. Guest 위치의 세트는 승인된 제안에서 파생되지 않습니다.", - acceptedState: "\"{{set}}\"이(가) 로그인한 모든 사용자(Everyone 위치)에 대해 승인되었습니다.", - notInForceState: "\"{{set}}\"이(가) 승인되었지만 아직 적용되지 않았습니다. 이 요청으로 Everyone 위치가 변경되지 않았습니다. 이유는 서버 로그에 기록되어 있습니다.", - dismissedState: "\"{{set}}\"은(는) 부여되지 않았습니다. 제안이 닫혔거나 취소되었습니다.", - revoke: "취소", - revoking: "취소하는 중…", - revokedToast: "\"{{set}}\" 승인을 취소했습니다.", - reopen: "다시 검토", - reopening: "다시 여는 중…", - reopenedToast: "\"{{set}}\" 제안을 다시 열었습니다. 승인될 때까지 아무것도 부여되지 않습니다.", - }, - title: "앱 마켓플레이스", - subtitle: "ObjectStack 카탈로그에 게시된 승인된 앱을 탐색하세요. 앱을 클릭하여 세부 정보를 보고 환경 중 하나에 설치하세요.", - searchPlaceholder: '이름 또는 앱 ID로 앱 검색…', - searchAria: "마켓플레이스 앱 검색", - installed: "설치됨", - installedCount: "설치됨 ({{count}})", - refresh: "새로고침", - all: "전체", - noApprovedYet: "아직 마켓플레이스에 승인된 앱이 없습니다.", - noMatchFilters: "필터와 일치하는 앱이 없습니다.", - noDescription: "사용 가능한 설명이 없습니다.", - back: "마켓플레이스로 돌아가기", - browseLink: "마켓플레이스 탐색 →", installedAdditiveNote: "제거하면 앱이 실행 중인 커널에서 즉시 제거되며, 앱의 권한 세트와 그 부여 내역도 함께 제거됩니다. 캐시된 매니페스트도 삭제되므로 다음 시작 시 패키지가 로드되지 않습니다. 앱의 객체에 저장된 레코드는 데이터베이스에 남습니다.", - versionBadge: "v{{version}}", - installedBadge: "v{{version}} 설치됨", // objectui#11645 — an install-local entry this runtime refused to load at // startup (the listing's `notLoaded` marker), read on Installed Apps. notLoaded: { @@ -4138,90 +4112,18 @@ const ko = { protocolIncompatible: "이 런타임은 이 패키지를 로드하지 않았습니다. 이 패키지는 프로토콜 {{requiredRange}}을(를) 대상으로 하지만 이 런타임은 해당 프로토콜을 지원하지 않습니다.", otherReason: "이 런타임은 이 패키지를 로드하지 않았습니다({{code}}).", }, - load: { - failed: "마켓플레이스를 로드할 수 없습니다", - failedHintConfigured: "이 런타임은 {{url}} 의 컨트롤 플레인을 통해 마켓플레이스에 접속합니다. 해당 주소가 온라인이고 여기에서 접근 가능한지 확인하세요.", - failedHintSameOrigin: "마켓플레이스 카탈로그는 이 런타임을 통해 접근합니다. 런타임이 온라인이고 카탈로그에 접근할 수 있는지 확인하세요.", - loading: "마켓플레이스 카탈로그를 불러오는 중…", - slow: "아직 불러오는 중입니다 — 마켓플레이스 카탈로그의 응답이 느립니다.", - retry: "다시 시도", - unreachable: "마켓플레이스 카탈로그에 연결할 수 없습니다.", - answeredError: "마켓플레이스 카탈로그가 오류로 응답했습니다(HTTP {{status}}).", - packageFailed: "패키지를 로드할 수 없습니다", - notFound: "찾을 수 없습니다.", - }, - disabled: { - title: "앱 마켓플레이스가 꺼져 있습니다", - description: "이 런타임에는 마켓플레이스가 구성되어 있지 않아 여기에서 둘러보거나 설치할 항목이 없습니다.", - hint: "런타임을 <code>OS_CLOUD_URL=off</code>(또는 <code>none</code>, <code>local</code>, <code>disabled</code>)로 시작하면 이것이 정상 상태입니다. 마켓플레이스를 켜려면 <code>OS_CLOUD_URL</code>을 컨트롤 플레인으로 지정하고 런타임을 다시 시작하세요.", - }, - detail: { - homepage: "홈페이지", - installedV: "설치됨 · v{{version}}", - about: "정보", - noReadme: "사용 가능한 README 없음.", - versions: "버전", - noApprovedVersions: "승인된 버전 없음.", - prerelease: "프리릴리즈", - moreOptions: "추가 설치 옵션", - uninstallFromRuntime: "이 런타임에서 제거", - addSampleData: '샘플 데이터 추가', - reseedAgain: '샘플 데이터 다시 적재', - purgeSampleData: '샘플 데이터 삭제', - purgeConfirm: '이 패키지가 생성한 모든 샘플 레코드를 삭제하시겠습니까? 사용자가 추가한 레코드는 삭제되지 않습니다.', - purgeSuccess: '샘플 레코드 {{count}}개를 삭제했습니다.', - purgeSuccess_one: '샘플 레코드 {{count}}개를 삭제했습니다.', - purgeSuccess_other: '샘플 레코드 {{count}}개를 삭제했습니다.', - purgeNoData: '삭제할 샘플 레코드가 없습니다.', - // objectstack#30323 — the install note's sample-data count, its clear and the app banner. - sampleDataLoaded: '이번 설치로 샘플 레코드 {{count}}개를 불러왔습니다. 앱이 동작하는 모습을 확인할 수 있습니다. 실제로 사용하기 전에 삭제할 수 있습니다.', - sampleDataLoaded_one: '이번 설치로 샘플 레코드 {{count}}개를 불러왔습니다. 앱이 동작하는 모습을 확인할 수 있습니다. 실제로 사용하기 전에 삭제할 수 있습니다.', - sampleDataLoaded_other: '이번 설치로 샘플 레코드 {{count}}개를 불러왔습니다. 앱이 동작하는 모습을 확인할 수 있습니다. 실제로 사용하기 전에 삭제할 수 있습니다.', - sampleDataReloaded: '다시 설치하면 삭제한 것을 포함해 샘플 데이터를 다시 불러옵니다.', - purgeConfirmAtMost: '최대 {{count}}개의 샘플 레코드가 삭제됩니다.', - purgeConfirmAtMost_one: '최대 {{count}}개의 샘플 레코드가 삭제됩니다.', - purgeConfirmAtMost_other: '최대 {{count}}개의 샘플 레코드가 삭제됩니다.', - sampleDataBannerTitle: '"{{name}}"에 샘플 데이터가 있습니다.', - sampleDataBannerMessage: '앱이 동작하는 모습을 확인하기 위한 데이터입니다. 실제로 사용하기 전에 삭제할 수 있습니다.', - reseedQueued: '샘플 데이터는 다음 환경 접속 시 다시 적재됩니다.', - reseedLocalSuccess: '샘플 데이터를 다시 적재했습니다: {{inserted}}개 추가, {{updated}}개 업데이트.', - reseedPartialErrors: '({{count}}개 레코드 쓰기 실패)', - reseedPartialErrors_one: '({{count}}개 레코드 쓰기 실패)', - reseedPartialErrors_other: '({{count}}개 레코드 쓰기 실패)', - sampleDataKernelUnavailable: '이 컨트롤 플레인에는 환경 커널이 없어 여기서는 샘플 데이터를 다시 적재하거나 삭제할 수 없습니다. 해당 환경 자체의 런타임에서 실행하세요.', - updateAvailable: '업데이트 가능', - }, action: { - installed: "설치됨", - install: "설치", - reinstall: "재설치", - working: "처리 중…", - installToCloud: "클라우드에 설치…", - installing: "설치 중…", - updating: "업데이트 중…", uninstall: "제거", uninstalling: "제거 중…", details: "세부 정보", - close: "닫기", - dismiss: "닫기", - openOnCloud: "클라우드에서 열기", - backHome: "홈으로 돌아가기", - updateTo: "업데이트", + }, + uninstall: { + confirm: "이 런타임에서 {{manifestId}} v{{version}}을(를) 제거하시겠습니까?\n\n앱이 실행 중인 커널에서 즉시 제거되며, 앱의 권한 세트와 그 부여 내역도 함께 제거됩니다. 캐시된 매니페스트도 삭제됩니다. 앱의 객체에 저장된 레코드는 데이터베이스에 남습니다.", + confirmNotLoaded: "이 런타임에서 {{manifestId}} v{{version}}을(를) 제거하시겠습니까?\n\n캐시된 매니페스트가 삭제됩니다. 이 런타임은 이 패키지를 로드하지 않았으므로 실행 중인 부분이 없습니다.", + successInList: "{{manifestId}} 제거됨. 앱이 실행 중인 커널에서 제거되었으며, 권한 세트와 그 부여 내역도 함께 제거되었습니다. 앱의 객체에 저장된 레코드는 데이터베이스에 남습니다.", + successNotLoaded: "{{manifestId}} 제거됨. 로드되지 않았으므로 재시작할 필요가 없습니다.", }, install: { - dialogTitle: "{{name}} 설치", - dialogDescCurrent: "이 환경에 설치 ({{host}}).", - dialogDescPicker: "이 앱을 설치할 환경을 선택하세요. ObjectOS Cloud에 로그인되어 있어야 합니다.", - environment: "환경", - environmentPlaceholder: "환경 선택", - includeSampleData: "샘플 데이터 포함", - noEnvs: "활성 조직에서 환경을 찾을 수 없습니다.", - noPermission: "환경에 앱을 설치할 권한이 없습니다. 조직 소유자 및 관리자만 설치할 수 있습니다 — 조직 관리자에게 문의하세요.", - signInFirst: "먼저 ObjectOS Cloud에 로그인해야 합니다.", - success: "설치 성공. 환경을 열어 새 앱을 확인하세요.", - localSuccess: '이 런타임에 v{{version}}을(를) 설치했습니다. 이제 앱 전환기에 "{{name}}"이(가) 있습니다.', - localLoadsAtRestart: "이 런타임에 v{{version}} 설치됨. \"{{name}}\"은(는) 런타임이 다음에 다시 시작될 때 로드되며, 그때까지는 앱 전환기에 표시되지 않습니다.", - localManifestConflict: "{{message}}\n참고: 로컬 앱이 이미 이 manifest_id를 가지고 있습니다. 먼저 objectstack.config.ts에서 제거하세요.", // ADR-0141 D2 — the install refusal DEPENDENCY_MISSING, one line per unmet entry. dependencyMissing: { heading: "설치가 거부되었습니다: 이 패키지가 선언한 종속성이 이 환경에서 충족되지 않아 아무것도 설치되지 않았습니다.", @@ -4235,107 +4137,6 @@ const ko = { noRange: "{{packageId}}은(는) 지원하는 플랫폼 프로토콜을 명시하지 않아 호환성 검사 없이 프로토콜 {{protocolVersion}}에서 설치되었으며, 이 플랫폼 버전에서 작동하지 않을 수 있습니다. 지원 프로토콜(engines.protocol)을 명시한 패키지 빌드는 설치 시 검사됩니다.", unparsedRange: "{{packageId}}은(는) 플랫폼 프로토콜을 {{requiredRange}}(으)로 명시했지만 이 런타임이 읽을 수 없어 호환성 검사 없이 프로토콜 {{protocolVersion}}에서 설치되었으며, 이 플랫폼 버전에서 작동하지 않을 수 있습니다. 읽을 수 있는 범위(engines.protocol)를 명시한 패키지 빌드는 설치 시 검사됩니다.", }, - localUnauthorized: "먼저 이 런타임에 로그인하세요.", - localMarketplaceUnavailable: "이 런타임에 OS_CLOUD_URL이 구성되지 않았습니다.", - updateTo: "업데이트 → v{{version}}", - installedVersion: "v{{version}} 설치됨", - deploying: "앱을 배포하는 중…", - deployed: "앱이 배포되어 사용할 준비가 되었습니다.", - openApp: "{{name}} 열기", - deployTimeout: "설치는 기록되었지만 이 환경에서 아직 앱을 제공하지 않습니다. 아직 배포 중일 수 있으니 잠시 후 다시 확인하세요. 계속 나타나지 않으면 패키지가 로드되지 않았거나, 앱이 포함되어 있지 않거나, 앱에 사용자에게 없는 권한이 필요할 수 있습니다.", - checkAgain: "다시 확인", - // ADR-0131 D6 — the install modes a package's manifest permits, and - // what an unmanaged install answers (MarketplacePackagePage). - mode: { - label: "설치 방식", - managed: "관리형", - managedHint: "패키지가 계속 원본으로 남습니다. 코드로 등록되고 업그레이드를 받으며, 항목은 여기에서 편집하지 않습니다.", - unmanaged: "비관리형 사본", - unmanagedHint: "이 런타임의 자체 메타데이터로 한 번 복사됩니다. 모든 항목을 여기에서 편집할 수 있으며, 사본은 업그레이드를 받지 않습니다.", - required: "이 패키지를 설치하려면 설치 방식을 선택하세요.", - }, - unmanagedSuccess: "\"{{name}}\" v{{version}}을(를) 이 런타임의 자체 메타데이터로 베이스 {{base}}에 복사했습니다. 사본은 편집할 수 있으며 업그레이드를 받지 않습니다.", - unmanagedStrippedLocks: "패키지가 이 항목들을 잠갔습니다. 사본은 편집할 수 있으므로 잠금은 복사되지 않았습니다:", - unmanagedIgnored: "이 런타임은 비관리형 설치를 지원하지 않으므로 \"{{name}}\"이(가) 복사되지 않고 관리형(코드로 등록)으로 설치되었습니다. 런타임을 업그레이드한 다음 패키지를 제거하고 비관리형 사본으로 다시 설치하세요.", - }, - // objectui#12146 — the install dialog opened as an UPDATE of the copy - // this environment holds (MarketplacePackagePage). - update: { - dialogTitle: "{{name}} 업데이트", - dialogDescCurrent: "이 환경({{host}})에 설치된 앱을 업데이트합니다.", - dialogDescPicker: "설치된 앱을 업데이트할 환경을 선택하세요. ObjectOS Cloud에 로그인되어 있어야 합니다.", - majorNotice: "메이저 버전 업데이트입니다(v{{fromMajor}} → v{{toMajor}}). 호환되지 않는 변경이 포함될 수 있습니다.", - releaseNotes: "v{{version}} 릴리스 노트 읽기", - success: "v{{version}}(으)로 업데이트했습니다.", - deployTimeout: "업데이트는 기록되었지만 이 환경에서 아직 v{{version}}을(를) 제공하지 않습니다. 아직 배포 중일 수 있으니 잠시 후 다시 확인하세요.", - }, - // objectui#3546 — the MarketplacePage "Your organization" strip. - org: { - heading: "조직", - install: "설치", - installed: "{{name}} 설치됨", - installedBadge: "설치됨", - installing: "설치 중…", - }, - // ADR-0025 PD4 §3.5/§3.11 — the pre-install consent panel - // (PluginDisclosure.tsx). `runtime` is a CLOSED enum: spec - // PluginRuntimeSchema = z.enum(['node', 'sandbox', 'worker']). - disclosure: { - containsCode: "이 패키지에는 코드가 포함되어 있습니다", - reviewed: "검토 및 승인됨", - unreviewed: "아직 검토되지 않음", - signed: "서명됨", - grantsIntro: "이 패키지가 요청하는 권한:", - notEnforced: "이 목록은 설치 시 기록되며 이후 버전이 더 많은 권한을 요청하면 다시 확인합니다. 다만 런타임은 아직 패키지를 이 목록으로 제한하지 않습니다.", - services: "플랫폼 서비스", - hooks: "라이프사이클 후크", - network: "네트워크 액세스", - fs: "파일 시스템 액세스", - noPermissions: "특별한 권한을 요청하지 않습니다.", - acknowledge: "이 패키지가 코드를 실행하고 위 권한을 부여받는다는 점을 이해합니다.", - runtime: { - node: "프로세스 내 · 완전 신뢰", - sandbox: "샌드박스", - worker: "프로세스 외부", - }, - }, - uninstall: { - confirm: "이 런타임에서 {{manifestId}} v{{version}}을(를) 제거하시겠습니까?\n\n앱이 실행 중인 커널에서 즉시 제거되며, 앱의 권한 세트와 그 부여 내역도 함께 제거됩니다. 캐시된 매니페스트도 삭제됩니다. 앱의 객체에 저장된 레코드는 데이터베이스에 남습니다.", - successInList: "{{manifestId}} 제거됨. 앱이 실행 중인 커널에서 제거되었으며, 권한 세트와 그 부여 내역도 함께 제거되었습니다. 앱의 객체에 저장된 레코드는 데이터베이스에 남습니다.", - successInDetail: "{{manifestId}} 및 캐시된 매니페스트 제거됨. 앱이 실행 중인 커널에서 제거되었으며, 권한 세트와 그 부여 내역도 함께 제거되었습니다. 앱의 객체에 저장된 레코드는 데이터베이스에 남습니다.", - confirmNotLoaded: "이 런타임에서 {{manifestId}} v{{version}}을(를) 제거하시겠습니까?\n\n캐시된 매니페스트가 삭제됩니다. 이 런타임은 이 패키지를 로드하지 않았으므로 실행 중인 부분이 없습니다.", - successNotLoaded: "{{manifestId}} 제거됨. 로드되지 않았으므로 재시작할 필요가 없습니다.", - }, - accessDenied: { - title: "관리자 전용 앱 마켓플레이스", - description: "이 환경에 앱을 설치할 권한이 없습니다.", - }, - category: { - crm: "CRM", - erp: "ERP", - hr: "인사", - finance: "재무", - project: "프로젝트 관리", - collaboration: "협업", - analytics: "분석", - integration: "통합", - automation: "자동화", - ai: "AI", - security: "보안", - "developer-tools": "개발자 도구", - "ui-theme": "UI 테마", - storage: "스토리지", - other: "기타", - }, - relativeTime: { - today: "오늘", - daysAgo: "{{count}}일 전", - monthsAgo: "{{count}}개월 전", - monthsAgo_one: "{{count}}개월 전", - monthsAgo_other: "{{count}}개월 전", - yearsAgo: "{{count}}년 전", - yearsAgo_one: "{{count}}년 전", - yearsAgo_other: "{{count}}년 전", }, }, approvalsInbox: { diff --git a/ui/packages/i18n/src/locales/pt.ts b/ui/packages/i18n/src/locales/pt.ts index c50265a414e..a3d29014641 100644 --- a/ui/packages/i18n/src/locales/pt.ts +++ b/ui/packages/i18n/src/locales/pt.ts @@ -4176,42 +4176,16 @@ const pt = { connect: "Conectar", }, }, + // The rest of `marketplace.*` is the lazy `marketplace` namespace + // (`../namespaces/marketplace/pt.ts`), fetched when a marketplace page + // mounts. What stays here is read by a page outside the marketplace route: + // Studio's packages page draws a marketplace install's row through + // `MarketplaceRowActions` and `MarketplaceNotLoaded` (the Not loaded badge + // and reason, Uninstall with its confirmation and result), and an import's + // refusal and protocol note through `useDependencyMissingText` and + // `useProtocolCheckSkippedText`. marketplace: { - suggestedBindings: { - promptEveryone: "Este app sugere conceder \"{{set}}\" a todos os usuários autenticados (a posição Everyone).", - promptGuest: "Este app sugere conceder \"{{set}}\" a visitantes não autenticados (a posição Guest).", - confirm: "Conceder", - confirming: "Concedendo…", - dismiss: "Dispensar", - confirmedToast: "\"{{set}}\" agora está concedido a {{anchor}}.", - dismissedToast: "Sugestão de \"{{set}}\" dispensada.", - guestUnsupported: "Não é possível conceder aqui: os conjuntos da posição Guest não são derivados de sugestões aceitas.", - acceptedState: "\"{{set}}\" foi aceito para todos os usuários autenticados (a posição Everyone).", - notInForceState: "\"{{set}}\" foi aceito, mas ainda não está em vigor: a posição Everyone não mudou nesta solicitação. O log do servidor informa o motivo.", - dismissedState: "\"{{set}}\" não está concedido: a sugestão foi dispensada ou revogada.", - revoke: "Revogar", - revoking: "Revogando…", - revokedToast: "Aceitação de \"{{set}}\" revogada.", - reopen: "Reconsiderar", - reopening: "Reabrindo…", - reopenedToast: "Sugestão para \"{{set}}\" reaberta: nada é concedido até que seja aceita.", - }, - title: "Marketplace de aplicativos", - subtitle: "Explore aplicativos aprovados publicados no catálogo ObjectStack. Clique em um aplicativo para ver os detalhes e instalá-lo em um de seus ambientes.", - searchPlaceholder: 'Pesquisar aplicativos por nome ou ID do aplicativo…', - searchAria: "Pesquisar aplicativos do marketplace", - installed: "Instalado", - installedCount: "Instalados ({{count}})", - refresh: "Atualizar", - all: "Todos", - noApprovedYet: "Nenhum aplicativo aprovado para o marketplace ainda.", - noMatchFilters: "Nenhum aplicativo corresponde aos seus filtros.", - noDescription: "Sem descrição disponível.", - back: "Voltar ao marketplace", - browseLink: "Explorar marketplace →", installedAdditiveNote: "A desinstalação remove o aplicativo do kernel em execução imediatamente, junto com seus conjuntos de permissões e as respectivas atribuições, e exclui o manifesto em cache para que o pacote não seja carregado na próxima inicialização. Os registros armazenados em seus objetos permanecem no banco de dados.", - versionBadge: "v{{version}}", - installedBadge: "Instalado v{{version}}", // objectui#11645 — an install-local entry this runtime refused to load at // startup (the listing's `notLoaded` marker), read on Installed Apps. notLoaded: { @@ -4219,94 +4193,18 @@ const pt = { protocolIncompatible: "Este runtime não carregou este pacote: ele é destinado ao protocolo {{requiredRange}}, que este runtime não suporta.", otherReason: "Este runtime não carregou este pacote ({{code}}).", }, - load: { - failed: "Não foi possível carregar o marketplace", - failedHintConfigured: "Este runtime acessa o marketplace através do plano de controle em {{url}}. Verifique se ele está online e acessível a partir daqui.", - failedHintSameOrigin: "O catálogo do marketplace é acessado através deste runtime. Verifique se o runtime está online e consegue acessar o catálogo.", - loading: "Carregando o catálogo do marketplace…", - slow: "Ainda carregando — o catálogo do marketplace está demorando para responder.", - retry: "Tentar novamente", - unreachable: "Não foi possível acessar o catálogo do marketplace.", - answeredError: "O catálogo do marketplace respondeu com um erro (HTTP {{status}}).", - packageFailed: "Não foi possível carregar o pacote", - notFound: "Não encontrado.", - }, - disabled: { - title: "O marketplace de aplicativos está desativado", - description: "Este runtime não tem marketplace configurado, portanto não há nada para explorar ou instalar aqui.", - hint: "Esse é o estado esperado quando o runtime é iniciado com <code>OS_CLOUD_URL=off</code> (ou <code>none</code>, <code>local</code>, <code>disabled</code>). Para ativar o marketplace, aponte <code>OS_CLOUD_URL</code> para um plano de controle e reinicie o runtime.", - }, - detail: { - homepage: "Página inicial", - installedV: "Instalado · v{{version}}", - about: "Sobre", - noReadme: "Sem readme disponível.", - versions: "Versões", - noApprovedVersions: "Sem versões aprovadas.", - prerelease: "Pré-lançamento", - moreOptions: "Mais opções de instalação", - uninstallFromRuntime: "Desinstalar deste runtime", - addSampleData: 'Adicionar dados de exemplo', - reseedAgain: 'Recarregar dados de exemplo', - purgeSampleData: 'Remover dados de exemplo', - purgeConfirm: 'Excluir todos os registros de exemplo criados por este pacote? Os registros adicionados pelo usuário NÃO serão afetados.', - purgeSuccess: '{{count}} registro(s) de exemplo removido(s).', - purgeSuccess_one: '{{count}} registro de exemplo removido.', - purgeSuccess_many: '{{count}} de registros de exemplo removidos.', - purgeSuccess_other: '{{count}} registros de exemplo removidos.', - purgeNoData: 'Nenhum registro de exemplo encontrado para remover.', - // objectstack#30323 — the install note's sample-data count, its clear and the app banner. - sampleDataLoaded: 'Esta instalação carregou {{count}} registro(s) de exemplo para você ver o aplicativo funcionando. Remova-os quando começar a usá-lo de verdade.', - sampleDataLoaded_one: 'Esta instalação carregou {{count}} registro de exemplo para você ver o aplicativo funcionando. Remova-o quando começar a usá-lo de verdade.', - sampleDataLoaded_many: 'Esta instalação carregou {{count}} de registros de exemplo para você ver o aplicativo funcionando. Remova-os quando começar a usá-lo de verdade.', - sampleDataLoaded_other: 'Esta instalação carregou {{count}} registros de exemplo para você ver o aplicativo funcionando. Remova-os quando começar a usá-lo de verdade.', - sampleDataReloaded: 'Instalar de novo carrega os dados de exemplo outra vez, inclusive os que você removeu.', - purgeConfirmAtMost: 'No máximo {{count}} registro(s) de exemplo serão excluídos.', - purgeConfirmAtMost_one: 'No máximo {{count}} registro de exemplo será excluído.', - purgeConfirmAtMost_many: 'No máximo {{count}} de registros de exemplo serão excluídos.', - purgeConfirmAtMost_other: 'No máximo {{count}} registros de exemplo serão excluídos.', - sampleDataBannerTitle: '"{{name}}" tem dados de exemplo.', - sampleDataBannerMessage: 'Eles estão lá para você ver o aplicativo funcionando. Remova-os quando começar a usá-lo de verdade.', - reseedQueued: 'Os dados de exemplo serão recarregados no próximo acesso ao ambiente.', - reseedLocalSuccess: 'Dados de exemplo recarregados: {{inserted}} inseridos, {{updated}} atualizados.', - reseedPartialErrors: '({{count}} registro(s) não puderam ser gravados)', - reseedPartialErrors_one: '({{count}} registro não pôde ser gravado)', - reseedPartialErrors_many: '({{count}} de registros não puderam ser gravados)', - reseedPartialErrors_other: '({{count}} registros não puderam ser gravados)', - sampleDataKernelUnavailable: 'Este plano de controle não tem um kernel de ambiente, portanto os dados de exemplo não podem ser recarregados nem removidos aqui. Faça isso no runtime do próprio ambiente.', - updateAvailable: 'Atualização disponível', - }, action: { - installed: "Instalado", - install: "Instalar", - reinstall: "Reinstalar", - working: "Processando…", - installToCloud: "Instalar na nuvem…", - installing: "Instalando…", - updating: "Atualizando…", uninstall: "Desinstalar", uninstalling: "Desinstalando…", details: "Detalhes", - close: "Fechar", - dismiss: "Fechar", - openOnCloud: "Abrir na nuvem", - backHome: "Voltar ao início", - updateTo: "Atualizar", + }, + uninstall: { + confirm: "Desinstalar {{manifestId}} v{{version}} deste runtime?\n\nO aplicativo será removido do kernel em execução imediatamente, junto com seus conjuntos de permissões e as respectivas atribuições, e o manifesto em cache será removido. Os registros armazenados em seus objetos permanecem no banco de dados.", + confirmNotLoaded: "Desinstalar {{manifestId}} v{{version}} deste runtime?\n\nO manifesto em cache será removido. Este runtime não carregou o pacote, então nenhuma parte dele está em execução.", + successInList: "{{manifestId}} removido: o aplicativo saiu do kernel em execução, junto com seus conjuntos de permissões e as respectivas atribuições. Os registros armazenados em seus objetos permanecem no banco de dados.", + successNotLoaded: "{{manifestId}} removido. Não estava carregado, então não é necessário reiniciar.", }, install: { - dialogTitle: "Instalar {{name}}", - dialogDescCurrent: "Instalar neste ambiente ({{host}}).", - dialogDescPicker: "Escolha um ambiente para instalar este aplicativo. Você deve estar conectado ao ObjectOS Cloud.", - environment: "Ambiente", - environmentPlaceholder: "Selecionar ambiente", - includeSampleData: "Incluir dados de amostra", - noEnvs: "Nenhum ambiente encontrado em sua organização ativa.", - noPermission: "Você não tem permissão para instalar aplicativos em um ambiente. Somente proprietários e administradores da organização podem instalar — fale com o administrador da sua organização.", - signInFirst: "Você deve primeiro fazer login no ObjectOS Cloud.", - success: "Instalado com sucesso. Abra o ambiente para ver o novo aplicativo.", - localSuccess: 'v{{version}} instalado neste runtime. "{{name}}" já está no seletor de aplicativos.', - localLoadsAtRestart: "v{{version}} instalado neste runtime. \"{{name}}\" será carregado na próxima reinicialização do runtime e não aparecerá no seletor de aplicativos até lá.", - localManifestConflict: "{{message}}\nNota: Um aplicativo local já possui este manifest_id. Remova-o primeiro de objectstack.config.ts.", // ADR-0141 D2 — the install refusal DEPENDENCY_MISSING, one line per unmet entry. dependencyMissing: { heading: "Instalação recusada: uma dependência declarada por este pacote não é satisfeita neste ambiente, por isso nada foi instalado.", @@ -4320,109 +4218,6 @@ const pt = { noRange: "{{packageId}} não indica qual protocolo de plataforma suporta, por isso foi instalado sob o protocolo {{protocolVersion}} sem verificação de compatibilidade e pode não funcionar nesta versão da plataforma. Uma compilação do pacote que declara o protocolo suportado (engines.protocol) é verificada na instalação.", unparsedRange: "{{packageId}} declara o seu protocolo de plataforma como {{requiredRange}}, que este runtime não consegue ler, por isso foi instalado sob o protocolo {{protocolVersion}} sem verificação de compatibilidade e pode não funcionar nesta versão da plataforma. Uma compilação do pacote que declara um intervalo legível (engines.protocol) é verificada na instalação.", }, - localUnauthorized: "Faça login neste runtime primeiro.", - localMarketplaceUnavailable: "Este runtime não tem OS_CLOUD_URL configurado.", - updateTo: "Atualizar → v{{version}}", - installedVersion: "Instalado v{{version}}", - deploying: "Implantando o aplicativo…", - deployed: "O aplicativo foi implantado e está pronto.", - openApp: "Abrir {{name}}", - deployTimeout: "A instalação foi registrada, mas este ambiente ainda não está servindo o aplicativo. Ele pode ainda estar sendo implantado — verifique novamente em instantes. Se nunca aparecer, o pacote pode não ter sido carregado, pode não incluir nenhum aplicativo ou o aplicativo pode exigir uma permissão que você não tem.", - checkAgain: "Verificar novamente", - // ADR-0131 D6 — the install modes a package's manifest permits, and - // what an unmanaged install answers (MarketplacePackagePage). - mode: { - label: "Modo de instalação", - managed: "Gerenciado", - managedHint: "O pacote continua sendo a autoridade: é registrado como código, recebe suas atualizações e seus itens não são editados aqui.", - unmanaged: "Cópia não gerenciada", - unmanagedHint: "Copiado uma única vez para este runtime como seus próprios metadados: cada item pode ser editado aqui, e a cópia não recebe atualizações.", - required: "Escolha um modo de instalação para instalar este pacote.", - }, - unmanagedSuccess: "v{{version}} de \"{{name}}\" copiado para este runtime como seus próprios metadados, na base {{base}}. A cópia pode ser editada e não recebe atualizações.", - unmanagedStrippedLocks: "O pacote bloqueava estes itens. A cópia pode ser editada, então seus bloqueios não foram copiados:", - unmanagedIgnored: "Este runtime não oferece suporte a instalações não gerenciadas, então \"{{name}}\" foi instalado como gerenciado (registrado como código) em vez de copiado. Atualize o runtime, depois desinstale o pacote e instale-o novamente como cópia não gerenciada.", - }, - // objectui#12146 — the install dialog opened as an UPDATE of the copy - // this environment holds (MarketplacePackagePage). - update: { - dialogTitle: "Atualizar {{name}}", - dialogDescCurrent: "Atualizar a cópia instalada neste ambiente ({{host}}).", - dialogDescPicker: "Escolha o ambiente cuja cópia instalada deseja atualizar. Você deve estar conectado ao ObjectOS Cloud.", - majorNotice: "Esta é uma atualização de versão principal (v{{fromMajor}} → v{{toMajor}}), que pode incluir alterações incompatíveis.", - releaseNotes: "Ler as notas da versão v{{version}}", - success: "Atualizado para a v{{version}}.", - deployTimeout: "A atualização foi registrada, mas este ambiente ainda não está servindo a v{{version}}. Ela pode ainda estar sendo implantada — verifique novamente em instantes.", - }, - // objectui#3546 — the MarketplacePage "Your organization" strip. - org: { - heading: "Sua organização", - install: "Instalar", - installed: "{{name}} instalado", - installedBadge: "Instalado", - installing: "Instalando…", - }, - // ADR-0025 PD4 §3.5/§3.11 — the pre-install consent panel - // (PluginDisclosure.tsx). `runtime` is a CLOSED enum: spec - // PluginRuntimeSchema = z.enum(['node', 'sandbox', 'worker']). - disclosure: { - containsCode: "Este pacote contém código", - reviewed: "Revisado e aprovado", - unreviewed: "Ainda não revisado", - signed: "Assinado", - grantsIntro: "Este pacote solicita:", - notEnforced: "Esta lista é registada na instalação e reconfirmada se uma versão posterior pedir mais, mas o runtime ainda não restringe o pacote a ela.", - services: "Serviços da plataforma", - hooks: "Hooks de ciclo de vida", - network: "Acesso à rede", - fs: "Acesso ao sistema de arquivos", - noPermissions: "Não solicita permissões especiais.", - acknowledge: "Entendo que este pacote executa código e recebe as permissões acima.", - runtime: { - node: "No processo · confiança total", - sandbox: "Em sandbox", - worker: "Fora do processo", - }, - }, - uninstall: { - confirm: "Desinstalar {{manifestId}} v{{version}} deste runtime?\n\nO aplicativo será removido do kernel em execução imediatamente, junto com seus conjuntos de permissões e as respectivas atribuições, e o manifesto em cache será removido. Os registros armazenados em seus objetos permanecem no banco de dados.", - successInList: "{{manifestId}} removido: o aplicativo saiu do kernel em execução, junto com seus conjuntos de permissões e as respectivas atribuições. Os registros armazenados em seus objetos permanecem no banco de dados.", - successInDetail: "{{manifestId}} e seu manifesto em cache removidos: o aplicativo saiu do kernel em execução, junto com seus conjuntos de permissões e as respectivas atribuições. Os registros armazenados em seus objetos permanecem no banco de dados.", - confirmNotLoaded: "Desinstalar {{manifestId}} v{{version}} deste runtime?\n\nO manifesto em cache será removido. Este runtime não carregou o pacote, então nenhuma parte dele está em execução.", - successNotLoaded: "{{manifestId}} removido. Não estava carregado, então não é necessário reiniciar.", - }, - accessDenied: { - title: "Marketplace somente para administradores", - description: "Você não tem permissão para instalar aplicativos neste ambiente.", - }, - category: { - crm: "CRM", - erp: "ERP", - hr: "RH", - finance: "Finanças", - project: "Gestão de projetos", - collaboration: "Colaboração", - analytics: "Análise", - integration: "Integração", - automation: "Automação", - ai: "IA", - security: "Segurança", - "developer-tools": "Ferramentas de desenvolvedor", - "ui-theme": "Tema de UI", - storage: "Armazenamento", - other: "Outro", - }, - relativeTime: { - today: "hoje", - daysAgo: "há {{count}}d", - monthsAgo: "há {{count}} meses", - monthsAgo_one: "há {{count}} mês", - monthsAgo_many: "há {{count}} meses", - monthsAgo_other: "há {{count}} meses", - yearsAgo: "há {{count}} anos", - yearsAgo_one: "há {{count}} ano", - yearsAgo_many: "há {{count}} anos", - yearsAgo_other: "há {{count}} anos", }, }, approvalsInbox: { diff --git a/ui/packages/i18n/src/locales/ru.ts b/ui/packages/i18n/src/locales/ru.ts index 0cab7d5abad..4514bef8fcb 100644 --- a/ui/packages/i18n/src/locales/ru.ts +++ b/ui/packages/i18n/src/locales/ru.ts @@ -4275,42 +4275,16 @@ const ru = { connect: "Подключить", }, }, + // The rest of `marketplace.*` is the lazy `marketplace` namespace + // (`../namespaces/marketplace/ru.ts`), fetched when a marketplace page + // mounts. What stays here is read by a page outside the marketplace route: + // Studio's packages page draws a marketplace install's row through + // `MarketplaceRowActions` and `MarketplaceNotLoaded` (the Not loaded badge + // and reason, Uninstall with its confirmation and result), and an import's + // refusal and protocol note through `useDependencyMissingText` and + // `useProtocolCheckSkippedText`. marketplace: { - suggestedBindings: { - promptEveryone: "Приложение предлагает предоставить «{{set}}» всем вошедшим пользователям (позиция Everyone).", - promptGuest: "Приложение предлагает предоставить «{{set}}» неаутентифицированным посетителям (позиция Guest).", - confirm: "Предоставить", - confirming: "Предоставление…", - dismiss: "Закрыть", - confirmedToast: "«{{set}}» теперь предоставлен: {{anchor}}.", - dismissedToast: "Предложение по «{{set}}» закрыто.", - guestUnsupported: "Здесь предоставить нельзя: наборы позиции Guest не выводятся из принятых предложений.", - acceptedState: "«{{set}}» принят для всех вошедших пользователей (позиция Everyone).", - notInForceState: "«{{set}}» принят, но ещё не действует: этот запрос не изменил позицию Everyone. Причина указана в журнале сервера.", - dismissedState: "«{{set}}» не предоставлен: предложение закрыто или отозвано.", - revoke: "Отозвать", - revoking: "Отзыв…", - revokedToast: "Принятие «{{set}}» отозвано.", - reopen: "Пересмотреть", - reopening: "Открывается снова…", - reopenedToast: "Предложение для «{{set}}» снова открыто: ничего не предоставляется, пока оно не принято.", - }, - title: "Маркетплейс приложений", - subtitle: "Просматривайте одобренные приложения из каталога ObjectStack. Нажмите на приложение, чтобы увидеть подробности и установить его.", - searchPlaceholder: 'Поиск приложений по имени или ID приложения…', - searchAria: "Поиск в маркетплейсе", - installed: "Установлено", - installedCount: "Установлено ({{count}})", - refresh: "Обновить", - all: "Все", - noApprovedYet: "Пока нет одобренных приложений.", - noMatchFilters: "Нет приложений, соответствующих фильтрам.", - noDescription: "Описание отсутствует.", - back: "Назад в маркетплейс", - browseLink: "Маркетплейс →", installedAdditiveNote: "Удаление сразу убирает приложение из работающего ядра вместе с его наборами разрешений и их назначениями, а также удаляет кэшированный манифест, чтобы пакет не загружался при следующем запуске. Записи, хранящиеся в его объектах, остаются в базе данных.", - versionBadge: "v{{version}}", - installedBadge: "Установлено v{{version}}", // objectui#11645 — an install-local entry this runtime refused to load at // startup (the listing's `notLoaded` marker), read on Installed Apps. notLoaded: { @@ -4318,98 +4292,18 @@ const ru = { protocolIncompatible: "Рантайм не загрузил этот пакет: он рассчитан на протокол {{requiredRange}}, который этот рантайм не поддерживает.", otherReason: "Рантайм не загрузил этот пакет ({{code}}).", }, - load: { - failed: "Не удалось загрузить маркетплейс", - failedHintConfigured: "Этот рантайм обращается к маркетплейсу через control plane по адресу {{url}}. Убедитесь, что он доступен и достижим отсюда.", - failedHintSameOrigin: "Каталог маркетплейса доступен через этот рантайм. Убедитесь, что рантайм онлайн и имеет доступ к каталогу.", - loading: "Загрузка каталога маркетплейса…", - slow: "Всё ещё загружается — каталог маркетплейса медленно отвечает.", - retry: "Повторить", - unreachable: "Не удалось связаться с каталогом маркетплейса.", - answeredError: "Каталог маркетплейса ответил ошибкой (HTTP {{status}}).", - packageFailed: "Не удалось загрузить пакет", - notFound: "Не найдено.", - }, - disabled: { - title: "Маркетплейс приложений отключён", - description: "Для этого рантайма не настроен маркетплейс, поэтому здесь нечего просматривать или устанавливать.", - hint: "Это ожидаемое состояние, когда рантайм запущен с <code>OS_CLOUD_URL=off</code> (или <code>none</code>, <code>local</code>, <code>disabled</code>). Чтобы включить маркетплейс, укажите в <code>OS_CLOUD_URL</code> адрес control plane и перезапустите рантайм.", - }, - detail: { - homepage: "Сайт", - installedV: "Установлено · v{{version}}", - about: "О приложении", - noReadme: "README отсутствует.", - versions: "Версии", - noApprovedVersions: "Нет одобренных версий.", - prerelease: "Пред-релиз", - moreOptions: "Другие варианты установки", - uninstallFromRuntime: "Удалить из рантайма", - addSampleData: 'Добавить демоданные', - reseedAgain: 'Перезагрузить демоданные', - purgeSampleData: 'Удалить демоданные', - purgeConfirm: 'Удалить все демонстрационные записи, созданные этим пакетом? Записи, добавленные пользователем, НЕ будут затронуты.', - purgeSuccess: 'Удалено демонстрационных записей: {{count}}.', - purgeSuccess_one: 'Удалено демонстрационных записей: {{count}}.', - purgeSuccess_few: 'Удалено демонстрационных записей: {{count}}.', - purgeSuccess_many: 'Удалено демонстрационных записей: {{count}}.', - purgeSuccess_other: 'Удалено демонстрационных записей: {{count}}.', - purgeNoData: 'Демонстрационные записи для удаления не найдены.', - // objectstack#30323 — the install note's sample-data count, its clear and the app banner. - sampleDataLoaded: 'Эта установка загрузила демонстрационных записей: {{count}}, чтобы вы увидели приложение в работе. Удалите их, когда начнёте работать по-настоящему.', - sampleDataLoaded_one: 'Эта установка загрузила демонстрационных записей: {{count}}, чтобы вы увидели приложение в работе. Удалите их, когда начнёте работать по-настоящему.', - sampleDataLoaded_few: 'Эта установка загрузила демонстрационных записей: {{count}}, чтобы вы увидели приложение в работе. Удалите их, когда начнёте работать по-настоящему.', - sampleDataLoaded_many: 'Эта установка загрузила демонстрационных записей: {{count}}, чтобы вы увидели приложение в работе. Удалите их, когда начнёте работать по-настоящему.', - sampleDataLoaded_other: 'Эта установка загрузила демонстрационных записей: {{count}}, чтобы вы увидели приложение в работе. Удалите их, когда начнёте работать по-настоящему.', - sampleDataReloaded: 'Повторная установка снова загружает демоданные, включая удалённые.', - purgeConfirmAtMost: 'Будет удалено не более {{count}} демонстрационных записей.', - purgeConfirmAtMost_one: 'Будет удалено не более {{count}} демонстрационной записи.', - purgeConfirmAtMost_few: 'Будет удалено не более {{count}} демонстрационных записей.', - purgeConfirmAtMost_many: 'Будет удалено не более {{count}} демонстрационных записей.', - purgeConfirmAtMost_other: 'Будет удалено не более {{count}} демонстрационных записей.', - sampleDataBannerTitle: 'В «{{name}}» есть демоданные.', - sampleDataBannerMessage: 'Они нужны, чтобы вы увидели приложение в работе. Удалите их, когда начнёте работать по-настоящему.', - reseedQueued: 'Демоданные будут перезагружены при следующем доступе к среде.', - reseedLocalSuccess: 'Демоданные перезагружены: добавлено {{inserted}}, обновлено {{updated}}.', - reseedPartialErrors: '(не удалось записать записей: {{count}})', - reseedPartialErrors_one: '(не удалось записать записей: {{count}})', - reseedPartialErrors_few: '(не удалось записать записей: {{count}})', - reseedPartialErrors_many: '(не удалось записать записей: {{count}})', - reseedPartialErrors_other: '(не удалось записать записей: {{count}})', - sampleDataKernelUnavailable: 'У этой control plane нет ядра среды, поэтому здесь нельзя перезагрузить или удалить демоданные. Выполните это действие в собственном рантайме среды.', - updateAvailable: 'Доступно обновление', - }, action: { - installed: "Установлено", - install: "Установить", - reinstall: "Переустановить", - working: "Обработка…", - installToCloud: "Установить в облако…", - installing: "Установка…", - updating: "Обновление…", uninstall: "Удалить", uninstalling: "Удаление…", details: "Подробности", - close: "Закрыть", - dismiss: "Закрыть", - openOnCloud: "Открыть в облаке", - backHome: "На главную", - updateTo: "Обновить", + }, + uninstall: { + confirm: "Удалить {{manifestId}} v{{version}}?\n\nПриложение сразу будет убрано из работающего ядра вместе с его наборами разрешений и их назначениями, а кэшированный манифест будет удалён. Записи, хранящиеся в его объектах, остаются в базе данных.", + confirmNotLoaded: "Удалить {{manifestId}} v{{version}}?\n\nКэшированный манифест будет удалён. Рантайм не загружал этот пакет, поэтому ничего из него не запущено.", + successInList: "{{manifestId}} удалён: приложение убрано из работающего ядра вместе с его наборами разрешений и их назначениями. Записи, хранящиеся в его объектах, остаются в базе данных.", + successNotLoaded: "{{manifestId}} удалён. Пакет не был загружен, перезапуск не требуется.", }, install: { - dialogTitle: "Установить {{name}}", - dialogDescCurrent: "Установить в эту среду ({{host}}).", - dialogDescPicker: "Выберите среду для установки. Требуется вход в ObjectOS Cloud.", - environment: "Среда", - environmentPlaceholder: "Выбрать среду", - includeSampleData: "Включить образцы данных", - noEnvs: "В активной организации нет сред.", - noPermission: "Нет прав на установку. Только владельцы и администраторы организации могут устанавливать — обратитесь к администратору вашей организации.", - signInFirst: "Сначала войдите в ObjectOS Cloud.", - success: "Установлено. Откройте среду, чтобы увидеть новое приложение.", - localSuccess: 'v{{version}} установлено в эту среду выполнения. «{{name}}» теперь есть в переключателе приложений.', - localLoadsAtRestart: "v{{version}} установлено. \"{{name}}\" загрузится при следующем перезапуске среды выполнения и до этого не появится в переключателе.", - localManifestConflict: "{{message}}\nПримечание: Локальное приложение уже имеет этот manifest_id. Сначала удалите его из objectstack.config.ts.", // ADR-0141 D2 — the install refusal DEPENDENCY_MISSING, one line per unmet entry. dependencyMissing: { heading: "Установка отклонена: зависимость, объявленная этим пакетом, не удовлетворена в этой среде, поэтому ничего не установлено.", @@ -4423,111 +4317,6 @@ const ru = { noRange: "{{packageId}} не указывает, какой протокол платформы он поддерживает, поэтому он установлен под протоколом {{protocolVersion}} без проверки совместимости и может не работать в этой версии платформы. Сборка пакета, в которой указан поддерживаемый протокол (engines.protocol), проверяется при установке.", unparsedRange: "{{packageId}} указывает протокол платформы как {{requiredRange}}, но эта среда выполнения не может его прочитать, поэтому пакет установлен под протоколом {{protocolVersion}} без проверки совместимости и может не работать в этой версии платформы. Сборка пакета с читаемым диапазоном (engines.protocol) проверяется при установке.", }, - localUnauthorized: "Сначала войдите в этот рантайм.", - localMarketplaceUnavailable: "OS_CLOUD_URL не настроен.", - updateTo: "Обновить → v{{version}}", - installedVersion: "Установлено v{{version}}", - deploying: "Развёртывание приложения…", - deployed: "Приложение развёрнуто и готово к работе.", - openApp: "Открыть {{name}}", - deployTimeout: "Установка записана, но эта среда пока не предоставляет приложение. Возможно, развёртывание ещё идёт — проверьте снова чуть позже. Если оно так и не появится, пакет мог не загрузиться, не содержать приложения или требовать разрешение, которого у вас нет.", - checkAgain: "Проверить снова", - // ADR-0131 D6 — the install modes a package's manifest permits, and - // what an unmanaged install answers (MarketplacePackagePage). - mode: { - label: "Способ установки", - managed: "Управляемая", - managedHint: "Пакет остаётся источником истины: он регистрируется как код, получает свои обновления, а его элементы здесь не редактируются.", - unmanaged: "Неуправляемая копия", - unmanagedHint: "Однократно копируется в эту среду выполнения как её собственные метаданные: каждый элемент здесь можно редактировать, а копия не получает обновлений.", - required: "Выберите способ установки, чтобы установить этот пакет.", - }, - unmanagedSuccess: "v{{version}} пакета \"{{name}}\" скопирована в эту среду выполнения как её собственные метаданные, в базу {{base}}. Копию можно редактировать, обновлений она не получает.", - unmanagedStrippedLocks: "Пакет блокировал эти элементы. Копию можно редактировать, поэтому их блокировки не скопированы:", - unmanagedIgnored: "Эта среда выполнения не поддерживает неуправляемую установку, поэтому \"{{name}}\" установлен как управляемый (зарегистрирован как код), а не скопирован. Обновите среду выполнения, затем удалите пакет и установите его снова как неуправляемую копию.", - }, - // objectui#12146 — the install dialog opened as an UPDATE of the copy - // this environment holds (MarketplacePackagePage). - update: { - dialogTitle: "Обновить {{name}}", - dialogDescCurrent: "Обновить копию, установленную в этой среде ({{host}}).", - dialogDescPicker: "Выберите среду, в которой нужно обновить установленную копию. Требуется вход в ObjectOS Cloud.", - majorNotice: "Это обновление основной версии (v{{fromMajor}} → v{{toMajor}}), оно может содержать несовместимые изменения.", - releaseNotes: "Прочитать примечания к выпуску v{{version}}", - success: "Обновлено до v{{version}}.", - deployTimeout: "Обновление записано, но эта среда пока не предоставляет v{{version}}. Возможно, развёртывание ещё идёт — проверьте снова чуть позже.", - }, - // objectui#3546 — the MarketplacePage "Your organization" strip. - org: { - heading: "Ваша организация", - install: "Установить", - installed: "Установлено {{name}}", - installedBadge: "Установлено", - installing: "Установка…", - }, - // ADR-0025 PD4 §3.5/§3.11 — the pre-install consent panel - // (PluginDisclosure.tsx). `runtime` is a CLOSED enum: spec - // PluginRuntimeSchema = z.enum(['node', 'sandbox', 'worker']). - disclosure: { - containsCode: "Этот пакет содержит код", - reviewed: "Проверено и одобрено", - unreviewed: "Ещё не проверено", - signed: "Подписано", - grantsIntro: "Пакет запрашивает:", - notEnforced: "Список записывается при установке и подтверждается заново, если более поздняя версия запросит больше, но среда выполнения пока не ограничивает пакет этим списком.", - services: "Сервисы платформы", - hooks: "Хуки жизненного цикла", - network: "Доступ к сети", - fs: "Доступ к файловой системе", - noPermissions: "Не запрашивает особых разрешений.", - acknowledge: "Я понимаю, что этот пакет выполняет код и получает указанные выше разрешения.", - runtime: { - node: "В процессе · полное доверие", - sandbox: "В песочнице", - worker: "Вне процесса", - }, - }, - uninstall: { - confirm: "Удалить {{manifestId}} v{{version}}?\n\nПриложение сразу будет убрано из работающего ядра вместе с его наборами разрешений и их назначениями, а кэшированный манифест будет удалён. Записи, хранящиеся в его объектах, остаются в базе данных.", - successInList: "{{manifestId}} удалён: приложение убрано из работающего ядра вместе с его наборами разрешений и их назначениями. Записи, хранящиеся в его объектах, остаются в базе данных.", - successInDetail: "{{manifestId}} и его кэшированный манифест удалены: приложение убрано из работающего ядра вместе с его наборами разрешений и их назначениями. Записи, хранящиеся в его объектах, остаются в базе данных.", - confirmNotLoaded: "Удалить {{manifestId}} v{{version}}?\n\nКэшированный манифест будет удалён. Рантайм не загружал этот пакет, поэтому ничего из него не запущено.", - successNotLoaded: "{{manifestId}} удалён. Пакет не был загружен, перезапуск не требуется.", - }, - accessDenied: { - title: "Только для администраторов", - description: "Нет прав на установку приложений.", - }, - category: { - crm: "CRM", - erp: "ERP", - hr: "HR", - finance: "Финансы", - project: "Управление проектами", - collaboration: "Совместная работа", - analytics: "Аналитика", - integration: "Интеграция", - automation: "Автоматизация", - ai: "ИИ", - security: "Безопасность", - "developer-tools": "Инструменты разработчика", - "ui-theme": "Тема UI", - storage: "Хранилище", - other: "Другое", - }, - relativeTime: { - today: "сегодня", - daysAgo: "{{count}} д назад", - monthsAgo: "{{count}} мес назад", - monthsAgo_one: "{{count}} мес назад", - monthsAgo_few: "{{count}} мес назад", - monthsAgo_many: "{{count}} мес назад", - monthsAgo_other: "{{count}} мес назад", - yearsAgo: "{{count}} лет назад", - yearsAgo_one: "{{count}} год назад", - yearsAgo_few: "{{count}} года назад", - yearsAgo_many: "{{count}} лет назад", - yearsAgo_other: "{{count}} года назад", }, }, approvalsInbox: { diff --git a/ui/packages/i18n/src/locales/zh.ts b/ui/packages/i18n/src/locales/zh.ts index a91341eb7c9..785a5193de9 100644 --- a/ui/packages/i18n/src/locales/zh.ts +++ b/ui/packages/i18n/src/locales/zh.ts @@ -4302,23 +4302,16 @@ const zh = { connect: '连接', }, }, + // The rest of `marketplace.*` is the lazy `marketplace` namespace + // (`../namespaces/marketplace/zh.ts`), fetched when a marketplace page + // mounts. What stays here is read by a page outside the marketplace route: + // Studio's packages page draws a marketplace install's row through + // `MarketplaceRowActions` and `MarketplaceNotLoaded` (the Not loaded badge + // and reason, Uninstall with its confirmation and result), and an import's + // refusal and protocol note through `useDependencyMissingText` and + // `useProtocolCheckSkippedText`. marketplace: { - title: '应用市场', - subtitle: '浏览已通过审核、发布到 ObjectStack 目录中的应用。点击应用查看详情并安装到你的某个环境中。', - searchPlaceholder: '按名称或标识搜索应用…', - searchAria: '搜索市场应用', - installed: '已安装', - installedCount: '已安装({{count}})', - refresh: '刷新', - all: '全部', - noApprovedYet: '应用市场暂无已通过审核的应用。', - noMatchFilters: '没有应用匹配当前筛选条件。', - noDescription: '未提供描述。', - back: '返回应用市场', - browseLink: '去应用市场看看 →', installedAdditiveNote: '卸载会立即将该应用从运行中的内核中移除,其权限集及相应授权也一并移除;同时删除缓存的清单,下次启动时不再加载该包。其对象中已存储的记录仍保留在数据库中。', - versionBadge: 'v{{version}}', - installedBadge: '已安装 v{{version}}', // objectui#11645 — an install-local entry this runtime refused to load at // startup (the listing's `notLoaded` marker), read on Installed Apps. notLoaded: { @@ -4326,90 +4319,18 @@ const zh = { protocolIncompatible: '本运行时未加载此包:它面向协议 {{requiredRange}},而本运行时不支持该协议。', otherReason: '本运行时未加载此包({{code}})。', }, - load: { - failed: '应用市场加载失败', - failedHintConfigured: '本运行时通过 {{url}} 上的控制面访问应用市场。请检查该地址是否在线、能否从本运行时访问。', - failedHintSameOrigin: '应用市场目录通过本运行时访问。请检查运行时是否在线、能否访问该目录。', - loading: '正在加载应用市场目录…', - slow: '仍在加载 —— 应用市场目录响应缓慢。', - retry: '重试', - unreachable: '无法访问应用市场目录。', - answeredError: '应用市场目录返回了错误(HTTP {{status}})。', - packageFailed: '应用加载失败', - notFound: '未找到。', - }, - disabled: { - title: '应用市场已关闭', - description: '本运行时未配置应用市场,因此这里没有可浏览或安装的内容。', - hint: '当运行时以 <code>OS_CLOUD_URL=off</code>(或 <code>none</code>、<code>local</code>、<code>disabled</code>)启动时,这就是预期状态。如需开启应用市场,请把 <code>OS_CLOUD_URL</code> 指向一个控制面并重启运行时。', - }, - detail: { - homepage: '主页', - installedV: '已安装 · v{{version}}', - about: '简介', - noReadme: '未提供 README。', - versions: '版本', - noApprovedVersions: '暂无已审核版本。', - prerelease: '预发布', - moreOptions: '更多安装选项', - uninstallFromRuntime: '从本运行时卸载', - addSampleData: '添加示例数据', - reseedAgain: '重新加载示例数据', - purgeSampleData: '清除示例数据', - purgeConfirm: '删除此应用预置的所有示例记录?用户自行添加的记录不会被删除。', - purgeSuccess: '已删除 {{count}} 条示例记录。', - purgeSuccess_one: '已删除 {{count}} 条示例记录。', - purgeSuccess_other: '已删除 {{count}} 条示例记录。', - purgeNoData: '没有可清除的示例记录。', - // objectstack#30323 — the install note's sample-data count, its clear and the app banner. - sampleDataLoaded: '本次安装载入了 {{count}} 条示例记录,方便查看应用效果。正式使用前可以清除。', - sampleDataLoaded_one: '本次安装载入了 {{count}} 条示例记录,方便查看应用效果。正式使用前可以清除。', - sampleDataLoaded_other: '本次安装载入了 {{count}} 条示例记录,方便查看应用效果。正式使用前可以清除。', - sampleDataReloaded: '再次安装会重新载入示例数据,包括已清除的示例数据。', - purgeConfirmAtMost: '最多删除 {{count}} 条示例记录。', - purgeConfirmAtMost_one: '最多删除 {{count}} 条示例记录。', - purgeConfirmAtMost_other: '最多删除 {{count}} 条示例记录。', - sampleDataBannerTitle: '"{{name}}" 中有示例数据。', - sampleDataBannerMessage: '这些数据用于查看应用效果,正式使用前可以清除。', - reseedQueued: '示例数据将在下次访问该环境时重新加载。', - reseedLocalSuccess: '已重新加载示例数据:新增 {{inserted}} 条,更新 {{updated}} 条。', - reseedPartialErrors: '(有 {{count}} 条记录写入失败)', - reseedPartialErrors_one: '(有 {{count}} 条记录写入失败)', - reseedPartialErrors_other: '(有 {{count}} 条记录写入失败)', - sampleDataKernelUnavailable: '此控制面没有环境内核,因此无法在这里重新加载或清除示例数据。请在该环境自己的运行时中操作。', - updateAvailable: '有可用更新', - }, - action: { - install: '安装', - reinstall: '重新安装', - working: '处理中…', - installToCloud: '安装到云端…', - installed: '已安装', - installing: '安装中…', - updating: '更新中…', + action: { uninstall: '卸载', uninstalling: '卸载中…', details: '详情', - close: '关闭', - dismiss: '关闭', - openOnCloud: '在云端打开', - backHome: '返回首页', - updateTo: '更新', - }, - install: { - dialogTitle: '安装 {{name}}', - dialogDescCurrent: '安装到当前环境({{host}})。', - dialogDescPicker: '选择要将此应用安装到的环境。需要先登录 ObjectOS Cloud。', - environment: '环境', - environmentPlaceholder: '选择一个环境', - includeSampleData: '包含示例数据', - noEnvs: '当前组织没有可用的环境。', - noPermission: '你没有权限在任何环境中安装应用。仅组织所有者和管理员可以安装 — 请联系你的组织管理员。', - signInFirst: '请先登录 ObjectOS Cloud,然后点击下方"在云端打开"。', - success: '安装成功。打开环境即可看到新应用。', - localSuccess: '已将 v{{version}} 安装到本运行时。"{{name}}" 已出现在应用切换器中。', - localLoadsAtRestart: '已将 v{{version}} 安装到本运行时。"{{name}}" 将在运行时下次重启时加载,在此之前不会出现在应用切换器中。', - localManifestConflict: '{{message}}\n提示:本地已有应用占用此 manifest_id,请先从 objectstack.config.ts 中移除。', + }, + uninstall: { + confirm: '从本运行时卸载 {{manifestId}} v{{version}}?\n\n该应用将立即从运行中的内核中移除,其权限集及相应授权也一并移除,缓存的清单也将被删除。其对象中已存储的记录仍保留在数据库中。', + confirmNotLoaded: '从本运行时卸载 {{manifestId}} v{{version}}?\n\n磁盘上的清单缓存将被移除。本运行时并未加载该包,因此它没有任何部分在运行。', + successInList: '已移除 {{manifestId}}:该应用已从运行中的内核中移除,其权限集及相应授权也已一并移除。其对象中已存储的记录仍保留在数据库中。', + successNotLoaded: '已移除 {{manifestId}}。该包未被加载,无需重启运行时。', + }, + install: { // ADR-0141 D2 — the install refusal DEPENDENCY_MISSING, one line per unmet entry. dependencyMissing: { heading: '安装被拒绝:此软件包声明的依赖在当前环境中未满足,未安装任何内容。', @@ -4424,130 +4345,8 @@ const zh = { noRange: '{{packageId}} 未声明它支持的平台协议版本,因此已在平台协议 {{protocolVersion}} 下安装,未做兼容性检查,可能无法在当前平台版本上正常运行。声明了所支持协议(engines.protocol)的软件包版本会在安装时接受检查。', unparsedRange: '{{packageId}} 声明的平台协议为 {{requiredRange}},本运行时无法识别,因此已在平台协议 {{protocolVersion}} 下安装,未做兼容性检查,可能无法在当前平台版本上正常运行。声明了可识别协议范围(engines.protocol)的软件包版本会在安装时接受检查。', }, - localUnauthorized: '请先登录本运行时再试。', - localMarketplaceUnavailable: '本运行时未配置 OS_CLOUD_URL,无法访问应用市场目录。', - updateTo: '更新 → v{{version}}', - installedVersion: '已安装 v{{version}}', - deploying: '正在部署应用…', - deployed: '应用已部署完成。', - openApp: '打开 {{name}}', - deployTimeout: '安装已记录,但当前环境尚未提供该应用。它可能仍在部署中,请稍后再次检查。如果一直没有出现,可能是软件包未能加载、不包含应用,或其应用需要你尚未拥有的权限。', - checkAgain: '再次检查', - // ADR-0131 D6 — the install modes a package's manifest permits, and - // what an unmanaged install answers (MarketplacePackagePage). - mode: { - label: '安装方式', - managed: '托管', - managedHint: '软件包保持权威:以代码形式注册并接收其升级,其中的条目不在此处编辑。', - unmanaged: '非托管副本', - unmanagedHint: '一次性复制到本运行时,成为它自己的元数据:每个条目都可在此编辑,副本不再接收升级。', - required: '请先选择安装方式,再安装此软件包。', - }, - unmanagedSuccess: '已将 "{{name}}" 的 v{{version}} 复制到本运行时,成为它自己的元数据,位于基础包 {{base}} 中。副本可编辑,且不再接收升级。', - unmanagedStrippedLocks: '软件包锁定了以下条目。副本可编辑,因此这些锁未被复制:', - unmanagedIgnored: '本运行时不支持非托管安装,因此 "{{name}}" 以托管方式安装(以代码形式注册),而不是被复制。请先升级运行时,然后卸载该软件包,再以非托管副本重新安装。', - }, - // objectui#12146 — the install dialog opened as an UPDATE of the copy - // this environment holds (MarketplacePackagePage). - update: { - dialogTitle: '更新 {{name}}', - dialogDescCurrent: '更新当前环境({{host}})中已安装的应用。', - dialogDescPicker: '选择要更新已安装应用的环境。需要先登录 ObjectOS Cloud。', - majorNotice: '这是一次主版本更新(v{{fromMajor}} → v{{toMajor}}),可能包含不兼容的变更。', - releaseNotes: '查看 v{{version}} 的版本说明', - success: '已更新到 v{{version}}。', - deployTimeout: '更新已记录,但当前环境尚未提供 v{{version}}。可能仍在部署中,请稍后再次检查。', - }, - // objectui#3546 — the MarketplacePage "Your organization" strip. - org: { - heading: '你的组织', - install: '安装', - installed: '已安装 {{name}}', - installedBadge: '已安装', - installing: '安装中…', - }, - // ADR-0025 PD4 §3.5/§3.11 — the pre-install consent panel - // (PluginDisclosure.tsx). `runtime` is a CLOSED enum: spec - // PluginRuntimeSchema = z.enum(['node', 'sandbox', 'worker']). - disclosure: { - containsCode: '此软件包包含代码', - reviewed: '已审核并批准', - unreviewed: '尚未审核', - signed: '已签名', - grantsIntro: '此软件包请求以下权限:', - notEnforced: '安装时会记录这份清单,后续版本要求更多权限时会再次征求同意;但运行时目前还不会把该软件包限制在此清单内。', - services: '平台服务', - hooks: '生命周期钩子', - network: '网络访问', - fs: '文件系统访问', - noPermissions: '不请求任何特殊权限。', - acknowledge: '我了解此软件包会运行代码,并将获得上述权限。', - runtime: { - node: '进程内 · 完全信任', - sandbox: '沙箱隔离', - worker: '进程外', - }, - }, - // ADR-0090 D5/D9 — 安装时建议:包声明 isDefault 的权限集建议绑定到 - // everyone/guest 岗位,由管理员在此确认,服务端绝不自动绑定。 - suggestedBindings: { - promptEveryone: '此应用建议将「{{set}}」授予所有已登录用户(Everyone 岗位)。', - promptGuest: '此应用建议将「{{set}}」授予未登录访客(Guest 岗位)。', - confirm: '授予', - confirming: '授予中…', - dismiss: '忽略', - confirmedToast: '「{{set}}」已授予 {{anchor}}。', - dismissedToast: '已忽略「{{set}}」的建议。', - guestUnsupported: '此处无法授予:Guest 岗位的权限集不由已接受的建议派生。', - acceptedState: '「{{set}}」已为所有已登录用户(Everyone 岗位)接受。', - notInForceState: '「{{set}}」已接受,但尚未生效:本次请求未改变 Everyone 岗位。原因见服务器日志。', - dismissedState: '「{{set}}」未授予:该建议已被忽略或撤销。', - revoke: '撤销', - revoking: '撤销中…', - revokedToast: '已撤销对「{{set}}」的接受。', - reopen: '重新考虑', - reopening: '重新打开中…', - reopenedToast: '已重新打开「{{set}}」的建议:接受之前不授予任何权限。', - }, - uninstall: { - confirm: '从本运行时卸载 {{manifestId}} v{{version}}?\n\n该应用将立即从运行中的内核中移除,其权限集及相应授权也一并移除,缓存的清单也将被删除。其对象中已存储的记录仍保留在数据库中。', - successInList: '已移除 {{manifestId}}:该应用已从运行中的内核中移除,其权限集及相应授权也已一并移除。其对象中已存储的记录仍保留在数据库中。', - successInDetail: '已移除 {{manifestId}} 及其清单缓存:该应用已从运行中的内核中移除,其权限集及相应授权也已一并移除。其对象中已存储的记录仍保留在数据库中。', - confirmNotLoaded: '从本运行时卸载 {{manifestId}} v{{version}}?\n\n磁盘上的清单缓存将被移除。本运行时并未加载该包,因此它没有任何部分在运行。', - successNotLoaded: '已移除 {{manifestId}}。该包未被加载,无需重启运行时。', - }, - accessDenied: { - title: '应用市场仅限管理员使用', - description: '你没有权限在本环境中安装应用。请联系组织的所有者或管理员申请权限。', - }, - category: { - crm: '客户关系', - erp: '企业资源', - hr: '人力资源', - finance: '财务', - project: '项目管理', - collaboration: '协作', - analytics: '分析', - integration: '集成', - automation: '自动化', - ai: '人工智能', - security: '安全', - 'developer-tools': '开发者工具', - 'ui-theme': '界面主题', - storage: '存储', - other: '其他', - }, - relativeTime: { - today: '今天', - daysAgo: '{{count}} 天前', - monthsAgo: '{{count}} 个月前', - monthsAgo_one: '{{count}} 个月前', - monthsAgo_other: '{{count}} 个月前', - yearsAgo: '{{count}} 年前', - yearsAgo_one: '{{count}} 年前', - yearsAgo_other: '{{count}} 年前', - }, }, + }, approvalsInbox: { recordUnresolvable: '无法打开此记录', loadMore: '加载更多', diff --git a/ui/packages/i18n/src/namespaces/marketplace/ar.ts b/ui/packages/i18n/src/namespaces/marketplace/ar.ts new file mode 100644 index 00000000000..907037a6c53 --- /dev/null +++ b/ui/packages/i18n/src/namespaces/marketplace/ar.ts @@ -0,0 +1,254 @@ +/** + * Arabic (ar) — the lazy `marketplace` namespace's catalogue. + * + * A PARTIAL pack: the same shape as `../../locales/ar.ts`, holding only + * the `marketplace` group, which `./index.ts` deep-merges into an i18next + * instance when a marketplace page mounts. `./index.ts` says what the + * namespace is, why it is lazy, and which gates read this file. + * + * ⛔ Keep the declaration `const ar = { … }` and `export default ar`: + * the i18n gates under `ui/scripts/` read this file by AST under that name. + */ +const ar = { + marketplace: { + suggestedBindings: { + promptEveryone: "يقترح هذا التطبيق منح «{{set}}» لجميع المستخدمين المسجّلين (موضع Everyone).", + promptGuest: "يقترح هذا التطبيق منح «{{set}}» للزوار غير المصادَق عليهم (موضع Guest).", + confirm: "منح", + confirming: "جارٍ المنح…", + dismiss: "تجاهل", + confirmedToast: "تم الآن منح «{{set}}» إلى {{anchor}}.", + dismissedToast: "تم تجاهل اقتراح «{{set}}».", + guestUnsupported: "لا يمكن المنح هنا: لا تُشتق مجموعات موضع Guest من الاقتراحات المقبولة.", + acceptedState: "تم قبول «{{set}}» لجميع المستخدمين المسجّلين (موضع Everyone).", + notInForceState: "تم قبول «{{set}}»، لكنه لم يسرِ بعد: لم يتغير موضع Everyone في هذا الطلب. يوضّح سجل الخادم السبب.", + dismissedState: "«{{set}}» غير ممنوح: تم تجاهل الاقتراح أو إلغاؤه.", + revoke: "إلغاء", + revoking: "جارٍ الإلغاء…", + revokedToast: "تم إلغاء قبول «{{set}}».", + reopen: "إعادة النظر", + reopening: "جارٍ إعادة الفتح…", + reopenedToast: "أُعيد فتح اقتراح «{{set}}»: لا يُمنح شيء حتى يُقبل.", + }, + title: "سوق التطبيقات", + subtitle: "استعرض التطبيقات المعتمدة المنشورة في كتالوج ObjectStack. انقر على تطبيق لرؤية التفاصيل وتثبيته في أحد بيئاتك.", + searchPlaceholder: 'ابحث عن التطبيقات بالاسم أو معرف التطبيق…', + searchAria: "البحث في تطبيقات السوق", + installed: "مثبت", + installedCount: "مثبت ({{count}})", + refresh: "تحديث", + all: "الكل", + noApprovedYet: "لا توجد تطبيقات معتمدة للسوق بعد.", + noMatchFilters: "لا توجد تطبيقات تطابق الفلاتر.", + noDescription: "لا وصف متاح.", + back: "العودة إلى السوق", + browseLink: "استعراض السوق →", + versionBadge: "v{{version}}", + installedBadge: "مثبت v{{version}}", + load: { + failed: "تعذر تحميل السوق", + failedHintConfigured: "يصل وقت التشغيل هذا إلى السوق عبر مستوى التحكم على {{url}}. تحقق من أنه متصل بالإنترنت ويمكن الوصول إليه من هنا.", + failedHintSameOrigin: "يتم الوصول إلى كتالوج السوق عبر وقت التشغيل هذا. تأكد من أن وقت التشغيل متصل بالإنترنت ويمكنه الوصول إلى الكتالوج.", + loading: "جارٍ تحميل كتالوج السوق…", + slow: "لا يزال التحميل جاريًا — كتالوج السوق بطيء في الاستجابة.", + retry: "إعادة المحاولة", + unreachable: "تعذر الوصول إلى كتالوج السوق.", + answeredError: "استجاب كتالوج السوق بخطأ (HTTP {{status}}).", + packageFailed: "تعذر تحميل الحزمة", + notFound: "غير موجود.", + }, + disabled: { + title: "سوق التطبيقات معطّل", + description: "لا يوجد سوق مُهيّأ لوقت التشغيل هذا، لذا لا يوجد ما يمكن تصفحه أو تثبيته من هنا.", + hint: "هذه هي الحالة المتوقعة عند تشغيل وقت التشغيل باستخدام <code>OS_CLOUD_URL=off</code> (أو <code>none</code> أو <code>local</code> أو <code>disabled</code>). لتفعيل السوق، وجّه <code>OS_CLOUD_URL</code> إلى مستوى تحكم وأعد تشغيل وقت التشغيل.", + }, + detail: { + homepage: "الصفحة الرئيسية", + installedV: "مثبت · v{{version}}", + about: "حول", + noReadme: "لا يوجد readme متاح.", + versions: "الإصدارات", + noApprovedVersions: "لا توجد إصدارات معتمدة.", + prerelease: "ما قبل الإصدار", + moreOptions: "مزيد من خيارات التثبيت", + uninstallFromRuntime: "إلغاء التثبيت من هذا الوقت التشغيل", + addSampleData: 'إضافة بيانات تجريبية', + reseedAgain: 'إعادة تحميل البيانات التجريبية', + purgeSampleData: 'حذف البيانات التجريبية', + purgeConfirm: 'حذف جميع السجلات التجريبية التي أنشأتها هذه الحزمة؟ لن تتأثر السجلات التي أضافها المستخدم.', + purgeSuccess: 'تم حذف {{count}} سجل تجريبي.', + purgeSuccess_zero: 'تم حذف {{count}} سجل تجريبي.', + purgeSuccess_one: 'تم حذف {{count}} سجل تجريبي.', + purgeSuccess_two: 'تم حذف سجلين تجريبيين ({{count}}).', + purgeSuccess_few: 'تم حذف {{count}} سجلات تجريبية.', + purgeSuccess_many: 'تم حذف {{count}} سجلًا تجريبيًا.', + purgeSuccess_other: 'تم حذف {{count}} سجل تجريبي.', + purgeNoData: 'لا توجد سجلات تجريبية للحذف.', + // objectstack#30323 — the install note's sample-data count, its clear and the app banner. + sampleDataLoaded: 'حمّل هذا التثبيت سجلات تجريبية عددها {{count}} لترى التطبيق يعمل. احذفها عندما تبدأ استخدامه فعليًا.', + sampleDataLoaded_zero: 'حمّل هذا التثبيت سجلات تجريبية عددها {{count}} لترى التطبيق يعمل. احذفها عندما تبدأ استخدامه فعليًا.', + sampleDataLoaded_one: 'حمّل هذا التثبيت سجلات تجريبية عددها {{count}} لترى التطبيق يعمل. احذفها عندما تبدأ استخدامه فعليًا.', + sampleDataLoaded_two: 'حمّل هذا التثبيت سجلات تجريبية عددها {{count}} لترى التطبيق يعمل. احذفها عندما تبدأ استخدامه فعليًا.', + sampleDataLoaded_few: 'حمّل هذا التثبيت سجلات تجريبية عددها {{count}} لترى التطبيق يعمل. احذفها عندما تبدأ استخدامه فعليًا.', + sampleDataLoaded_many: 'حمّل هذا التثبيت سجلات تجريبية عددها {{count}} لترى التطبيق يعمل. احذفها عندما تبدأ استخدامه فعليًا.', + sampleDataLoaded_other: 'حمّل هذا التثبيت سجلات تجريبية عددها {{count}} لترى التطبيق يعمل. احذفها عندما تبدأ استخدامه فعليًا.', + sampleDataReloaded: 'يؤدي التثبيت مرة أخرى إلى تحميل البيانات التجريبية من جديد، بما فيها ما حذفته.', + purgeConfirmAtMost: 'سيُحذف من السجلات التجريبية {{count}} على الأكثر.', + purgeConfirmAtMost_zero: 'سيُحذف من السجلات التجريبية {{count}} على الأكثر.', + purgeConfirmAtMost_one: 'سيُحذف من السجلات التجريبية {{count}} على الأكثر.', + purgeConfirmAtMost_two: 'سيُحذف من السجلات التجريبية {{count}} على الأكثر.', + purgeConfirmAtMost_few: 'سيُحذف من السجلات التجريبية {{count}} على الأكثر.', + purgeConfirmAtMost_many: 'سيُحذف من السجلات التجريبية {{count}} على الأكثر.', + purgeConfirmAtMost_other: 'سيُحذف من السجلات التجريبية {{count}} على الأكثر.', + sampleDataBannerTitle: 'يحتوي "{{name}}" على بيانات تجريبية.', + sampleDataBannerMessage: 'إنها موجودة لترى التطبيق يعمل. احذفها عندما تبدأ استخدامه فعليًا.', + reseedQueued: 'ستتم إعادة تحميل البيانات التجريبية عند الوصول التالي إلى البيئة.', + reseedLocalSuccess: 'تمت إعادة تحميل البيانات التجريبية: {{inserted}} مضافة، {{updated}} محدثة.', + reseedPartialErrors: '(فشل في كتابة {{count}} سجل)', + reseedPartialErrors_zero: '(فشل في كتابة {{count}} سجل)', + reseedPartialErrors_one: '(فشل في كتابة {{count}} سجل)', + reseedPartialErrors_two: '(فشل في كتابة سجلين: {{count}})', + reseedPartialErrors_few: '(فشل في كتابة {{count}} سجلات)', + reseedPartialErrors_many: '(فشل في كتابة {{count}} سجلًا)', + reseedPartialErrors_other: '(فشل في كتابة {{count}} سجل)', + sampleDataKernelUnavailable: 'لا يحتوي مستوى التحكم هذا على نواة بيئة، لذا لا يمكن إعادة تحميل البيانات التجريبية أو حذفها من هنا. نفّذ ذلك من وقت التشغيل الخاص بالبيئة نفسها.', + updateAvailable: 'يتوفر تحديث', + }, + action: { + installed: "مثبَّت", + install: "تثبيت", + reinstall: "إعادة التثبيت", + working: "جارٍ المعالجة…", + installToCloud: "تثبيت في السحابة…", + installing: "جارٍ التثبيت…", + updating: "جارٍ التحديث…", + close: "إغلاق", + dismiss: "رفض", + openOnCloud: "فتح في السحابة", + backHome: "العودة إلى الرئيسية", + updateTo: "تحديث", + }, + install: { + dialogTitle: "تثبيت {{name}}", + dialogDescCurrent: "تثبيت في هذه البيئة ({{host}}).", + dialogDescPicker: "اختر بيئة لتثبيت هذا التطبيق. يجب أن تكون مسجلاً في ObjectOS Cloud.", + environment: "البيئة", + environmentPlaceholder: "تحديد بيئة", + includeSampleData: "تضمين بيانات نموذجية", + noEnvs: "لم يتم العثور على بيئات في مؤسستك النشطة.", + noPermission: "ليس لديك إذن لتثبيت التطبيقات. فقط مالكو المؤسسة والمشرفون يمكنهم التثبيت — تواصل مع مشرف مؤسستك.", + signInFirst: "يجب عليك أولاً تسجيل الدخول إلى ObjectOS Cloud.", + success: "تم التثبيت بنجاح. افتح البيئة لرؤية التطبيق الجديد.", + localSuccess: 'تم تثبيت v{{version}} في وقت التشغيل هذا. أصبح "{{name}}" الآن في مبدّل التطبيقات.', + localLoadsAtRestart: "تم تثبيت v{{version}} في وقت التشغيل هذا. سيُحمَّل \"{{name}}\" عند إعادة التشغيل التالية لوقت التشغيل، ولن يظهر في مبدّل التطبيقات قبل ذلك.", + localManifestConflict: "{{message}}\nملاحظة: تطبيق محلي يمتلك بالفعل هذا المعرف. أزله أولاً من objectstack.config.ts.", + localUnauthorized: "سجل الدخول إلى هذا الوقت التشغيل أولاً.", + localMarketplaceUnavailable: "هذا الوقت التشغيل لا يحتوي على OS_CLOUD_URL مُهيأ.", + updateTo: "تحديث → v{{version}}", + installedVersion: "مثبت v{{version}}", + deploying: "جارٍ نشر التطبيق…", + deployed: "تم نشر التطبيق وهو جاهز.", + openApp: "فتح {{name}}", + deployTimeout: "تم تسجيل التثبيت، لكن هذه البيئة لا تقدّم التطبيق بعد. قد يكون النشر لا يزال جاريًا — تحقّق مرة أخرى بعد قليل. إذا لم يظهر أبدًا، فربما لم تُحمَّل الحزمة، أو لا تتضمن أي تطبيق، أو يتطلب تطبيقها إذنًا لا تملكه.", + checkAgain: "تحقّق مرة أخرى", + // ADR-0131 D6 — the install modes a package's manifest permits, and + // what an unmanaged install answers (MarketplacePackagePage). + mode: { + label: "طريقة التثبيت", + managed: "مُدار", + managedHint: "تبقى الحزمة هي المرجع: تُسجَّل كتعليمات برمجية وتتلقى ترقياتها، ولا تُحرَّر عناصرها هنا.", + unmanaged: "نسخة غير مُدارة", + unmanagedHint: "تُنسخ مرة واحدة إلى وقت التشغيل هذا كبيانات وصفية خاصة به: كل عنصر قابل للتحرير هنا، ولا تتلقى النسخة أي ترقية.", + required: "اختر طريقة التثبيت لتثبيت هذه الحزمة.", + }, + unmanagedSuccess: "تم نسخ v{{version}} من \"{{name}}\" إلى وقت التشغيل هذا كبيانات وصفية خاصة به، في القاعدة {{base}}. النسخة قابلة للتحرير ولا تتلقى أي ترقية.", + unmanagedStrippedLocks: "كانت الحزمة تقفل هذه العناصر. النسخة قابلة للتحرير، لذلك لم تُنسخ أقفالها:", + unmanagedIgnored: "لا يدعم وقت التشغيل هذا التثبيت غير المُدار، لذلك ثُبّت \"{{name}}\" كحزمة مُدارة (مسجّلة كتعليمات برمجية) بدلاً من نسخه. رقِّ وقت التشغيل، ثم ألغِ تثبيت الحزمة وثبّتها مرة أخرى كنسخة غير مُدارة.", + }, + // objectui#12146 — the install dialog opened as an UPDATE of the copy + // this environment holds (MarketplacePackagePage). + update: { + dialogTitle: "تحديث {{name}}", + dialogDescCurrent: "تحديث النسخة المثبتة في هذه البيئة ({{host}}).", + dialogDescPicker: "اختر البيئة التي تريد تحديث النسخة المثبتة فيها. يجب أن تكون مسجلاً في ObjectOS Cloud.", + majorNotice: "هذا تحديث إلى إصدار رئيسي جديد (v{{fromMajor}} → v{{toMajor}})، وقد يتضمن تغييرات غير متوافقة.", + releaseNotes: "اقرأ ملاحظات الإصدار v{{version}}", + success: "تم التحديث إلى v{{version}}.", + deployTimeout: "تم تسجيل التحديث، لكن هذه البيئة لا تقدّم v{{version}} بعد. قد يكون النشر لا يزال جاريًا — تحقّق مرة أخرى بعد قليل.", + }, + // objectui#3546 — the MarketplacePage "Your organization" strip. + org: { + heading: "مؤسستك", + install: "تثبيت", + installed: "تم تثبيت {{name}}", + installedBadge: "مثبت", + installing: "جارٍ التثبيت…", + }, + // ADR-0025 PD4 §3.5/§3.11 — the pre-install consent panel + // (PluginDisclosure.tsx). `runtime` is a CLOSED enum: spec + // PluginRuntimeSchema = z.enum(['node', 'sandbox', 'worker']). + disclosure: { + containsCode: "تحتوي هذه الحزمة على تعليمات برمجية", + reviewed: "تمت المراجعة والموافقة", + unreviewed: "لم تتم المراجعة بعد", + signed: "موقَّع", + grantsIntro: "تطلب هذه الحزمة:", + notEnforced: "تُسجَّل هذه القائمة عند التثبيت ويُعاد تأكيدها إذا طلب إصدار لاحق المزيد، لكن بيئة التشغيل لا تقصر الحزمة على هذه القائمة بعد.", + services: "خدمات المنصة", + hooks: "خطافات دورة الحياة", + network: "الوصول إلى الشبكة", + fs: "الوصول إلى نظام الملفات", + noPermissions: "لا تطلب أي أذونات خاصة.", + acknowledge: "أفهم أن هذه الحزمة تنفّذ تعليمات برمجية وتحصل على الأذونات المذكورة أعلاه.", + runtime: { + node: "داخل العملية · ثقة كاملة", + sandbox: "في بيئة معزولة", + worker: "خارج العملية", + }, + }, + uninstall: { + successInDetail: "تمت إزالة {{manifestId}} والمانيفست المخزن مؤقتاً الخاص به: خرج التطبيق من النواة الجارية، ومعه مجموعات الأذونات الخاصة به وتعييناتها. تبقى السجلات المخزنة في كائناته في قاعدة البيانات.", + }, + accessDenied: { + title: "سوق التطبيقات للمشرفين فقط", + description: "ليس لديك إذن لتثبيت التطبيقات في هذه البيئة.", + }, + category: { + crm: "CRM", + erp: "ERP", + hr: "الموارد البشرية", + finance: "المالية", + project: "إدارة المشاريع", + collaboration: "التعاون", + analytics: "التحليلات", + integration: "التكامل", + automation: "الأتمتة", + ai: "الذكاء الاصطناعي", + security: "الأمان", + "developer-tools": "أدوات المطور", + "ui-theme": "سمة واجهة المستخدم", + storage: "التخزين", + other: "أخرى", + }, + relativeTime: { + today: "اليوم", + daysAgo: "منذ {{count}} ي", + monthsAgo: "منذ {{count}} شهر(أشهر)", + monthsAgo_zero: "منذ {{count}} شهر", + monthsAgo_one: "منذ {{count}} شهر", + monthsAgo_two: "منذ شهرين ({{count}})", + monthsAgo_few: "منذ {{count}} أشهر", + monthsAgo_many: "منذ {{count}} شهرًا", + monthsAgo_other: "منذ {{count}} شهر", + yearsAgo: "منذ {{count}} سنة(سنوات)", + yearsAgo_zero: "منذ {{count}} سنة", + yearsAgo_one: "منذ {{count}} سنة", + yearsAgo_two: "منذ سنتين ({{count}})", + yearsAgo_few: "منذ {{count}} سنوات", + yearsAgo_many: "منذ {{count}} سنة", + yearsAgo_other: "منذ {{count}} سنة", + }, + }, +}; + +export default ar; diff --git a/ui/packages/i18n/src/namespaces/marketplace/de.ts b/ui/packages/i18n/src/namespaces/marketplace/de.ts new file mode 100644 index 00000000000..e6ecd3c5790 --- /dev/null +++ b/ui/packages/i18n/src/namespaces/marketplace/de.ts @@ -0,0 +1,230 @@ +/** + * German (de) — the lazy `marketplace` namespace's catalogue. + * + * A PARTIAL pack: the same shape as `../../locales/de.ts`, holding only + * the `marketplace` group, which `./index.ts` deep-merges into an i18next + * instance when a marketplace page mounts. `./index.ts` says what the + * namespace is, why it is lazy, and which gates read this file. + * + * ⛔ Keep the declaration `const de = { … }` and `export default de`: + * the i18n gates under `ui/scripts/` read this file by AST under that name. + */ +const de = { + marketplace: { + suggestedBindings: { + promptEveryone: "Diese App empfiehlt, „{{set}}“ allen angemeldeten Benutzern zu gewähren (Position „Everyone“).", + promptGuest: "Diese App empfiehlt, „{{set}}“ nicht authentifizierten Besuchern zu gewähren (Position „Guest“).", + confirm: "Gewähren", + confirming: "Wird gewährt…", + dismiss: "Ausblenden", + confirmedToast: "„{{set}}“ ist jetzt {{anchor}} gewährt.", + dismissedToast: "Vorschlag für „{{set}}“ ausgeblendet.", + guestUnsupported: "Hier nicht gewährbar: Die Sets der Position „Guest“ werden nicht aus angenommenen Vorschlägen abgeleitet.", + acceptedState: "„{{set}}“ wurde für alle angemeldeten Benutzer angenommen (Position „Everyone“).", + notInForceState: "„{{set}}“ wurde angenommen, ist aber noch nicht wirksam: Die Position „Everyone“ hat sich bei dieser Anfrage nicht geändert. Der Grund steht im Serverprotokoll.", + dismissedState: "„{{set}}“ ist nicht gewährt: Der Vorschlag wurde ausgeblendet oder widerrufen.", + revoke: "Widerrufen", + revoking: "Wird widerrufen…", + revokedToast: "Annahme von „{{set}}“ widerrufen.", + reopen: "Neu bewerten", + reopening: "Wird wieder geöffnet…", + reopenedToast: "Vorschlag für „{{set}}“ wieder geöffnet: Nichts wird gewährt, bis er angenommen wird.", + }, + title: "App-Marktplatz", + subtitle: "Durchsuchen Sie genehmigte Apps, die im ObjectStack-Katalog veröffentlicht wurden. Klicken Sie auf eine App, um Details zu sehen und sie in eine Ihrer Umgebungen zu installieren.", + searchPlaceholder: 'Apps nach Name oder App-ID suchen…', + searchAria: "Marktplatz-Apps suchen", + installed: "Installiert", + installedCount: "Installiert ({{count}})", + refresh: "Aktualisieren", + all: "Alle", + noApprovedYet: "Noch keine Apps für den Marktplatz genehmigt.", + noMatchFilters: "Keine Apps entsprechen Ihren Filtern.", + noDescription: "Keine Beschreibung vorhanden.", + back: "Zurück zum Marktplatz", + browseLink: "Marktplatz durchsuchen →", + versionBadge: "v{{version}}", + installedBadge: "Installiert v{{version}}", + load: { + failed: "Marktplatz konnte nicht geladen werden", + failedHintConfigured: "Diese Laufzeitumgebung erreicht den Marktplatz über die Control Plane unter {{url}}. Prüfen Sie, ob diese online und von hier aus erreichbar ist.", + failedHintSameOrigin: "Der Marktplatz-Katalog wird über diese Laufzeitumgebung erreicht. Prüfen Sie, ob die Laufzeitumgebung online ist und den Katalog erreichen kann.", + loading: "Marktplatz-Katalog wird geladen…", + slow: "Wird noch geladen — der Marktplatz-Katalog antwortet langsam.", + retry: "Erneut versuchen", + unreachable: "Der Marktplatz-Katalog war nicht erreichbar.", + answeredError: "Der Marktplatz-Katalog hat mit einem Fehler geantwortet (HTTP {{status}}).", + packageFailed: "Paket konnte nicht geladen werden", + notFound: "Nicht gefunden.", + }, + disabled: { + title: "App-Marktplatz ist deaktiviert", + description: "Für diese Laufzeitumgebung ist kein Marktplatz konfiguriert; es gibt hier nichts zu durchsuchen oder zu installieren.", + hint: "Das ist der erwartete Zustand, wenn die Laufzeitumgebung mit <code>OS_CLOUD_URL=off</code> (oder <code>none</code>, <code>local</code>, <code>disabled</code>) gestartet wird. Um den Marktplatz zu aktivieren, richten Sie <code>OS_CLOUD_URL</code> auf eine Control Plane und starten Sie die Laufzeitumgebung neu.", + }, + detail: { + homepage: "Startseite", + installedV: "Installiert · v{{version}}", + about: "Über", + noReadme: "Kein Readme vorhanden.", + versions: "Versionen", + noApprovedVersions: "Keine genehmigten Versionen.", + prerelease: "Vorab", + moreOptions: "Weitere Installationsoptionen", + uninstallFromRuntime: "Aus diesem Laufzeitsystem deinstallieren", + addSampleData: 'Beispieldaten hinzufügen', + reseedAgain: 'Beispieldaten neu laden', + purgeSampleData: 'Beispieldaten löschen', + purgeConfirm: 'Alle von diesem Paket erzeugten Beispieldatensätze löschen? Vom Benutzer hinzugefügte Datensätze bleiben unberührt.', + purgeSuccess: '{{count}} Beispieldatensatz/-sätze entfernt.', + purgeSuccess_one: '{{count}} Beispieldatensatz entfernt.', + purgeSuccess_other: '{{count}} Beispieldatensätze entfernt.', + purgeNoData: 'Keine Beispieldatensätze zum Löschen gefunden.', + // objectstack#30323 — the install note's sample-data count, its clear and the app banner. + sampleDataLoaded: 'Diese Installation hat {{count}} Beispieldatensatz/-sätze geladen, damit Sie die App in Aktion sehen. Löschen Sie sie, sobald Sie die App wirklich nutzen.', + sampleDataLoaded_one: 'Diese Installation hat {{count}} Beispieldatensatz geladen, damit Sie die App in Aktion sehen. Löschen Sie ihn, sobald Sie die App wirklich nutzen.', + sampleDataLoaded_other: 'Diese Installation hat {{count}} Beispieldatensätze geladen, damit Sie die App in Aktion sehen. Löschen Sie sie, sobald Sie die App wirklich nutzen.', + sampleDataReloaded: 'Eine erneute Installation lädt die Beispieldaten wieder, auch die bereits gelöschten.', + purgeConfirmAtMost: 'Höchstens {{count}} Beispieldatensatz/-sätze werden gelöscht.', + purgeConfirmAtMost_one: 'Höchstens {{count}} Beispieldatensatz wird gelöscht.', + purgeConfirmAtMost_other: 'Höchstens {{count}} Beispieldatensätze werden gelöscht.', + sampleDataBannerTitle: '„{{name}}“ enthält Beispieldaten.', + sampleDataBannerMessage: 'Sie zeigen, wie die App arbeitet. Löschen Sie sie, sobald Sie die App wirklich nutzen.', + reseedQueued: 'Beispieldaten werden beim nächsten Zugriff auf die Umgebung neu geladen.', + reseedLocalSuccess: 'Beispieldaten neu geladen: {{inserted}} hinzugefügt, {{updated}} aktualisiert.', + reseedPartialErrors: '({{count}} Datensatz/-sätze konnten nicht geschrieben werden)', + reseedPartialErrors_one: '({{count}} Datensatz konnte nicht geschrieben werden)', + reseedPartialErrors_other: '({{count}} Datensätze konnten nicht geschrieben werden)', + sampleDataKernelUnavailable: 'Diese Control Plane hat keinen Umgebungs-Kernel, daher können Beispieldaten hier weder neu geladen noch gelöscht werden. Führen Sie dies im eigenen Laufzeitsystem der Umgebung aus.', + updateAvailable: 'Update verfügbar', + }, + action: { + installed: "Installiert", + install: "Installieren", + reinstall: "Neu installieren", + working: "Verarbeite…", + installToCloud: "In der Cloud installieren…", + installing: "Installiere…", + updating: "Aktualisiere…", + close: "Schließen", + dismiss: "Schließen", + openOnCloud: "In der Cloud öffnen", + backHome: "Zurück zur Startseite", + updateTo: "Aktualisieren", + }, + install: { + dialogTitle: "{{name}} installieren", + dialogDescCurrent: "In dieser Umgebung installieren ({{host}}).", + dialogDescPicker: "Wählen Sie eine Umgebung, in der Sie diese App installieren möchten. Sie müssen bei ObjectOS Cloud angemeldet sein.", + environment: "Umgebung", + environmentPlaceholder: "Umgebung auswählen", + includeSampleData: "Beispieldaten einschließen", + noEnvs: "Keine Umgebungen in Ihrer aktiven Organisation gefunden.", + noPermission: "Sie haben keine Berechtigung, Apps in einer Umgebung zu installieren. Nur Organisationsinhaber und Administratoren können installieren — wenden Sie sich an den Administrator Ihrer Organisation.", + signInFirst: "Sie müssen sich zuerst bei ObjectOS Cloud anmelden.", + success: "Erfolgreich installiert. Öffnen Sie die Umgebung, um die neue App zu sehen.", + localSuccess: 'v{{version}} in diesem Laufzeitsystem installiert. „{{name}}“ ist jetzt im App-Wechsler.', + localLoadsAtRestart: "v{{version}} in diesem Laufzeitsystem installiert. „{{name}}“ wird beim nächsten Neustart des Laufzeitsystems geladen und erscheint erst dann im App-Wechsler.", + localManifestConflict: "{{message}}\nHinweis: Eine lokale App besitzt bereits diese manifest_id. Entfernen Sie sie zuerst aus objectstack.config.ts.", + localUnauthorized: "Melden Sie sich zuerst in diesem Laufzeitsystem an.", + localMarketplaceUnavailable: "Dieses Laufzeitsystem hat keine OS_CLOUD_URL konfiguriert.", + updateTo: "Aktualisieren → v{{version}}", + installedVersion: "Installiert v{{version}}", + deploying: "App wird bereitgestellt…", + deployed: "Die App ist bereitgestellt und einsatzbereit.", + openApp: "{{name}} öffnen", + deployTimeout: "Die Installation wurde erfasst, aber diese Umgebung stellt die App noch nicht bereit. Die Bereitstellung läuft möglicherweise noch – prüfen Sie es gleich erneut. Erscheint sie nie, wurde das Paket womöglich nicht geladen, enthält keine App, oder die App erfordert eine Berechtigung, die Ihnen fehlt.", + checkAgain: "Erneut prüfen", + // ADR-0131 D6 — the install modes a package's manifest permits, and + // what an unmanaged install answers (MarketplacePackagePage). + mode: { + label: "Installationsmodus", + managed: "Verwaltet", + managedHint: "Das Paket bleibt maßgeblich: Es wird als Code registriert, erhält seine Upgrades, und seine Elemente werden hier nicht bearbeitet.", + unmanaged: "Nicht verwaltete Kopie", + unmanagedHint: "Einmalig als eigene Metadaten in dieses Laufzeitsystem kopiert: Jedes Element ist hier bearbeitbar, und die Kopie erhält kein Upgrade.", + required: "Wählen Sie einen Installationsmodus, um dieses Paket zu installieren.", + }, + unmanagedSuccess: "v{{version}} von „{{name}}“ als eigene Metadaten in dieses Laufzeitsystem kopiert, in die Basis {{base}}. Die Kopie ist bearbeitbar und erhält kein Upgrade.", + unmanagedStrippedLocks: "Das Paket hat diese Elemente gesperrt. Die Kopie ist bearbeitbar, daher wurden ihre Sperren nicht kopiert:", + unmanagedIgnored: "Dieses Laufzeitsystem unterstützt keine nicht verwalteten Installationen, daher wurde „{{name}}“ verwaltet installiert (als Code registriert) statt kopiert. Aktualisieren Sie das Laufzeitsystem, deinstallieren Sie dann das Paket und installieren Sie es erneut als nicht verwaltete Kopie.", + }, + // objectui#12146 — the install dialog opened as an UPDATE of the copy + // this environment holds (MarketplacePackagePage). + update: { + dialogTitle: "{{name}} aktualisieren", + dialogDescCurrent: "Die in dieser Umgebung installierte Kopie aktualisieren ({{host}}).", + dialogDescPicker: "Wählen Sie die Umgebung, deren installierte Kopie Sie aktualisieren möchten. Sie müssen bei ObjectOS Cloud angemeldet sein.", + majorNotice: "Dies ist ein Update auf eine neue Hauptversion (v{{fromMajor}} → v{{toMajor}}), das inkompatible Änderungen enthalten kann.", + releaseNotes: "Versionshinweise zu v{{version}} lesen", + success: "Auf v{{version}} aktualisiert.", + deployTimeout: "Das Update wurde erfasst, aber diese Umgebung stellt v{{version}} noch nicht bereit. Die Bereitstellung läuft möglicherweise noch – prüfen Sie es gleich erneut.", + }, + // objectui#3546 — the MarketplacePage "Your organization" strip. + org: { + heading: "Ihre Organisation", + install: "Installieren", + installed: "{{name}} installiert", + installedBadge: "Installiert", + installing: "Installiere…", + }, + // ADR-0025 PD4 §3.5/§3.11 — the pre-install consent panel + // (PluginDisclosure.tsx). `runtime` is a CLOSED enum: spec + // PluginRuntimeSchema = z.enum(['node', 'sandbox', 'worker']). + disclosure: { + containsCode: "Dieses Paket enthält Code", + reviewed: "Geprüft und genehmigt", + unreviewed: "Noch nicht geprüft", + signed: "Signiert", + grantsIntro: "Dieses Paket fordert an:", + notEnforced: "Diese Liste wird bei der Installation erfasst und erneut bestätigt, wenn eine spätere Version mehr anfordert – die Laufzeit beschränkt das Paket jedoch noch nicht darauf.", + services: "Plattformdienste", + hooks: "Lifecycle-Hooks", + network: "Netzwerkzugriff", + fs: "Dateisystemzugriff", + noPermissions: "Fordert keine besonderen Berechtigungen an.", + acknowledge: "Ich habe verstanden, dass dieses Paket Code ausführt und die oben genannten Berechtigungen erhält.", + runtime: { + node: "Im Prozess · volles Vertrauen", + sandbox: "Sandbox-isoliert", + worker: "Außerhalb des Prozesses", + }, + }, + uninstall: { + successInDetail: "{{manifestId}} und das zugehörige zwischengespeicherte Manifest entfernt: Die App ist aus dem laufenden Kernel entfernt, samt ihren Berechtigungssets und deren Zuweisungen. Datensätze in ihren Objekten bleiben in der Datenbank erhalten.", + }, + accessDenied: { + title: "App-Marktplatz nur für Administratoren", + description: "Sie haben keine Berechtigung, Apps in dieser Umgebung zu installieren.", + }, + category: { + crm: "CRM", + erp: "ERP", + hr: "Personalwesen", + finance: "Finanzen", + project: "Projektmanagement", + collaboration: "Zusammenarbeit", + analytics: "Analyse", + integration: "Integration", + automation: "Automatisierung", + ai: "KI", + security: "Sicherheit", + "developer-tools": "Entwicklertools", + "ui-theme": "UI-Thema", + storage: "Speicher", + other: "Sonstiges", + }, + relativeTime: { + today: "heute", + daysAgo: "vor {{count}}d", + monthsAgo: "vor {{count}} Monaten", + monthsAgo_one: "vor {{count}} Monat", + monthsAgo_other: "vor {{count}} Monaten", + yearsAgo: "vor {{count}} Jahren", + yearsAgo_one: "vor {{count}} Jahr", + yearsAgo_other: "vor {{count}} Jahren", + }, + }, +}; + +export default de; diff --git a/ui/packages/i18n/src/namespaces/marketplace/en.ts b/ui/packages/i18n/src/namespaces/marketplace/en.ts new file mode 100644 index 00000000000..b76d5bfc1c1 --- /dev/null +++ b/ui/packages/i18n/src/namespaces/marketplace/en.ts @@ -0,0 +1,238 @@ +/** + * English (en) — the lazy `marketplace` namespace's catalogue. + * + * A PARTIAL pack: the same shape as `../../locales/en.ts`, holding only + * the `marketplace` group, which `./index.ts` deep-merges into an i18next + * instance when a marketplace page mounts. `./index.ts` says what the + * namespace is, why it is lazy, and which gates read this file. + * + * ⛔ Keep the declaration `const en = { … }` and `export default en`: + * the i18n gates under `ui/scripts/` read this file by AST under that name. + */ +const en = { + marketplace: { + title: 'App Marketplace', + subtitle: 'Browse approved apps published to the ObjectStack catalog. Click an app to view details and install it into one of your environments.', + searchPlaceholder: 'Search apps by name or app ID…', + searchAria: 'Search marketplace apps', + installed: 'Installed', + installedCount: 'Installed ({{count}})', + refresh: 'Refresh', + all: 'All', + noApprovedYet: 'No apps have been approved for the marketplace yet.', + noMatchFilters: 'No apps match your filters.', + noDescription: 'No description provided.', + back: 'Back to marketplace', + browseLink: 'Browse the marketplace →', + versionBadge: 'v{{version}}', + installedBadge: 'Installed v{{version}}', + load: { + failed: 'Failed to load marketplace', + failedHintConfigured: 'This runtime reaches the marketplace through the control plane at {{url}}. Check that it is online and reachable from here.', + failedHintSameOrigin: 'The marketplace catalog is reached through this runtime. Check that the runtime is online and can reach the catalog.', + loading: 'Loading the marketplace catalog…', + slow: 'Still loading — the marketplace catalog is slow to answer.', + retry: 'Retry', + unreachable: 'The marketplace catalog could not be reached.', + answeredError: 'The marketplace catalog answered with an error (HTTP {{status}}).', + packageFailed: 'Failed to load package', + notFound: 'Not found.', + }, + disabled: { + title: 'App Marketplace is turned off', + description: 'This runtime has no marketplace configured, so there is nothing to browse or install from here.', + hint: 'That is the expected state when the runtime is started with <code>OS_CLOUD_URL=off</code> (or <code>none</code>, <code>local</code>, <code>disabled</code>). To turn the marketplace on, point <code>OS_CLOUD_URL</code> at a control plane and restart the runtime.', + }, + detail: { + homepage: 'Homepage', + installedV: 'Installed · v{{version}}', + about: 'About', + noReadme: 'No readme provided.', + versions: 'Versions', + noApprovedVersions: 'No approved versions.', + prerelease: 'pre', + moreOptions: 'More install options', + uninstallFromRuntime: 'Uninstall from this runtime', + addSampleData: 'Add sample data', + reseedAgain: 'Re-seed sample data', + purgeSampleData: 'Purge sample data', + purgeConfirm: 'Delete all sample records seeded by this package? User-added records will NOT be touched.', + purgeSuccess: 'Removed {{count}} sample record(s).', + purgeSuccess_one: 'Removed {{count}} sample record.', + purgeSuccess_other: 'Removed {{count}} sample records.', + purgeNoData: 'No sample records found to purge.', + // objectstack#30323 — the install note's sample-data count, its clear and the app banner. + sampleDataLoaded: 'This install loaded {{count}} sample record(s) so you can see the app working. Clear them when you start using it for real.', + sampleDataLoaded_one: 'This install loaded {{count}} sample record so you can see the app working. Clear it when you start using the app for real.', + sampleDataLoaded_other: 'This install loaded {{count}} sample records so you can see the app working. Clear them when you start using it for real.', + sampleDataReloaded: 'Installing again loads the sample data again, including any you had cleared.', + purgeConfirmAtMost: 'Up to {{count}} sample record(s) will be deleted.', + purgeConfirmAtMost_one: 'Up to {{count}} sample record will be deleted.', + purgeConfirmAtMost_other: 'Up to {{count}} sample records will be deleted.', + sampleDataBannerTitle: '"{{name}}" has sample data.', + sampleDataBannerMessage: 'It is there so you can see the app working. Clear it when you start using the app for real.', + reseedQueued: 'Sample data will be re-seeded on next environment access.', + reseedLocalSuccess: 'Re-seeded sample data: {{inserted}} inserted, {{updated}} updated.', + reseedPartialErrors: '({{count}} record(s) failed to write)', + reseedPartialErrors_one: '({{count}} record failed to write)', + reseedPartialErrors_other: '({{count}} records failed to write)', + sampleDataKernelUnavailable: 'This control plane has no environment kernel, so sample data cannot be re-seeded or purged from here. Do it from the environment\'s own runtime.', + updateAvailable: 'Update available', + }, + action: { + install: 'Install', + reinstall: 'Reinstall', + working: 'Working…', + installToCloud: 'Install to cloud…', + installed: 'Installed', + installing: 'Installing…', + updating: 'Updating…', + close: 'Close', + dismiss: 'Dismiss', + openOnCloud: 'Open on cloud', + backHome: 'Back to home', + updateTo: 'Update', + }, + install: { + dialogTitle: 'Install {{name}}', + dialogDescCurrent: 'Install into this environment ({{host}}).', + dialogDescPicker: 'Choose an environment to install this app into. You need to be signed into ObjectOS Cloud.', + environment: 'Environment', + environmentPlaceholder: 'Pick an environment', + includeSampleData: 'Include sample data', + noEnvs: 'No environments found in your active organization.', + noPermission: 'You do not have permission to install apps in any environment. Only organization owners and admins can install — ask your organization admin.', + signInFirst: 'You need to sign into ObjectOS Cloud first. Click "Open on cloud" below.', + success: 'Installed successfully. Open the environment to see the new app.', + localSuccess: 'Installed v{{version}} to this runtime. "{{name}}" is now in the app switcher.', + // objectui#12098 — the install answered `hotLoaded: false`: installed, and + // the running kernel did not register it (objectstack#22695), so nothing + // of it is served before the next restart. + localLoadsAtRestart: 'Installed v{{version}} to this runtime. "{{name}}" loads when the runtime next restarts; it does not appear in the app switcher until then.', + localManifestConflict: '{{message}}\nTip: a local app already owns this manifest_id. Remove it from objectstack.config.ts first.', + localUnauthorized: 'Sign in to this runtime first, then try again.', + localMarketplaceUnavailable: 'This runtime has no OS_CLOUD_URL configured, so the marketplace catalog is unreachable.', + updateTo: 'Update → v{{version}}', + installedVersion: 'Installed v{{version}}', + // objectui#12087 — an install into THIS environment, from its answer + // until the runtime serves the package's app (MarketplacePackagePage). + deploying: 'Deploying the app…', + deployed: 'The app is deployed and ready.', + openApp: 'Open {{name}}', + deployTimeout: 'The install was recorded, but this environment is not serving the app yet. It may still be deploying — check again shortly. If it never appears, the package may not have loaded, may include no app, or its app may need a permission you do not hold.', + checkAgain: 'Check again', + // ADR-0131 D6 — the install modes a package's manifest permits, and + // what an unmanaged install answers (MarketplacePackagePage). + mode: { + label: 'Install mode', + managed: 'Managed', + managedHint: 'The package stays the authority: it is registered as code, receives its upgrades, and its items are not edited here.', + unmanaged: 'Unmanaged copy', + unmanagedHint: 'Copied once into this runtime as its own metadata: every item is editable here, and the copy receives no upgrade.', + required: 'Choose an install mode to install this package.', + }, + unmanagedSuccess: 'Copied v{{version}} of "{{name}}" into this runtime as its own metadata, in the base {{base}}. The copy is editable and receives no upgrade.', + unmanagedStrippedLocks: 'The package locked these items. The copy is editable, so their locks were not copied:', + unmanagedIgnored: 'This runtime does not support unmanaged installs, so "{{name}}" was installed managed (registered as code) instead of copied. Upgrade the runtime, then uninstall the package and install it again as an unmanaged copy.', + }, + // objectui#12146 — the install dialog opened as an UPDATE of the copy + // this environment holds (MarketplacePackagePage). + update: { + dialogTitle: 'Update {{name}}', + dialogDescCurrent: 'Update the copy installed in this environment ({{host}}).', + dialogDescPicker: 'Choose the environment whose installed copy to update. You need to be signed into ObjectOS Cloud.', + majorNotice: 'This is a major version update (v{{fromMajor}} → v{{toMajor}}), which may include breaking changes.', + releaseNotes: 'Read the v{{version}} release notes', + success: 'Updated to v{{version}}.', + deployTimeout: 'The update was recorded, but this environment is not serving v{{version}} yet. It may still be deploying — check again shortly.', + }, + // objectui#3546 — the MarketplacePage "Your organization" strip. + org: { + heading: 'Your organization', + install: 'Install', + installed: 'Installed {{name}}', + installedBadge: 'Installed', + installing: 'Installing…', + }, + // ADR-0025 PD4 §3.5/§3.11 — the pre-install consent panel + // (PluginDisclosure.tsx). `runtime` is a CLOSED enum: spec + // PluginRuntimeSchema = z.enum(['node', 'sandbox', 'worker']). + disclosure: { + containsCode: 'This package contains code', + reviewed: 'Reviewed & approved', + unreviewed: 'Not yet reviewed', + signed: 'Signed', + grantsIntro: 'This package requests:', + notEnforced: 'Recorded at install, and re-confirmed if a later version asks for more — but the runtime does not yet restrict the package to this list.', + services: 'Platform services', + hooks: 'Lifecycle hooks', + network: 'Network access', + fs: 'Filesystem access', + noPermissions: 'Requests no special permissions.', + acknowledge: 'I understand this package runs code and grants the permissions above.', + runtime: { + node: 'In-process · full trust', + sandbox: 'Sandboxed', + worker: 'Out-of-process', + }, + }, + // ADR-0090 D5/D9 — a package's isDefault permission set is an + // install-time suggestion to bind it to the everyone/guest position; + // the admin confirms here, the server never auto-binds. + suggestedBindings: { + promptEveryone: 'This app suggests granting "{{set}}" to all signed-in users (the Everyone position).', + promptGuest: 'This app suggests granting "{{set}}" to unauthenticated visitors (the Guest position).', + confirm: 'Grant', + confirming: 'Granting…', + dismiss: 'Dismiss', + confirmedToast: '"{{set}}" is now granted to {{anchor}}.', + dismissedToast: 'Suggestion for "{{set}}" dismissed.', + guestUnsupported: 'Cannot be granted here: the Guest position takes no sets from accepted suggestions.', + acceptedState: '"{{set}}" was accepted for all signed-in users (the Everyone position).', + notInForceState: '"{{set}}" was accepted but is not in force yet: the Everyone position did not change on this request. The server log says why.', + dismissedState: '"{{set}}" is not granted: the suggestion was dismissed or revoked.', + revoke: 'Revoke', + revoking: 'Revoking…', + revokedToast: 'Acceptance of "{{set}}" revoked.', + reopen: 'Reconsider', + reopening: 'Reopening…', + reopenedToast: 'Suggestion for "{{set}}" reopened: nothing is granted until it is accepted.', + }, + uninstall: { + successInDetail: 'Removed {{manifestId}} and its cached manifest: the app has left the running kernel, with its permission sets and their grants. Records stored in its objects stay in the database.', + }, + accessDenied: { + title: 'App Marketplace is admin-only', + description: 'You don\'t have permission to install apps in this environment. Ask an owner or admin of this organization for access.', + }, + category: { + crm: 'CRM', + erp: 'ERP', + hr: 'Human Resources', + finance: 'Finance', + project: 'Project Management', + collaboration: 'Collaboration', + analytics: 'Analytics', + integration: 'Integration', + automation: 'Automation', + ai: 'AI', + security: 'Security', + 'developer-tools': 'Developer Tools', + 'ui-theme': 'UI Theme', + storage: 'Storage', + other: 'Other', + }, + relativeTime: { + today: 'today', + daysAgo: '{{count}}d ago', + monthsAgo: '{{count}}mo ago', + monthsAgo_one: '{{count}}mo ago', + monthsAgo_other: '{{count}}mo ago', + yearsAgo: '{{count}}y ago', + yearsAgo_one: '{{count}}y ago', + yearsAgo_other: '{{count}}y ago', + }, + }, +} as const; + +export default en; diff --git a/ui/packages/i18n/src/namespaces/marketplace/es.ts b/ui/packages/i18n/src/namespaces/marketplace/es.ts new file mode 100644 index 00000000000..5bdfd35510e --- /dev/null +++ b/ui/packages/i18n/src/namespaces/marketplace/es.ts @@ -0,0 +1,236 @@ +/** + * Spanish (es) — the lazy `marketplace` namespace's catalogue. + * + * A PARTIAL pack: the same shape as `../../locales/es.ts`, holding only + * the `marketplace` group, which `./index.ts` deep-merges into an i18next + * instance when a marketplace page mounts. `./index.ts` says what the + * namespace is, why it is lazy, and which gates read this file. + * + * ⛔ Keep the declaration `const es = { … }` and `export default es`: + * the i18n gates under `ui/scripts/` read this file by AST under that name. + */ +const es = { + marketplace: { + suggestedBindings: { + promptEveryone: "Esta aplicación sugiere conceder «{{set}}» a todos los usuarios con sesión iniciada (la posición Everyone).", + promptGuest: "Esta aplicación sugiere conceder «{{set}}» a los visitantes no autenticados (la posición Guest).", + confirm: "Conceder", + confirming: "Concediendo…", + dismiss: "Descartar", + confirmedToast: "«{{set}}» ya está concedido a {{anchor}}.", + dismissedToast: "Sugerencia para «{{set}}» descartada.", + guestUnsupported: "No se puede conceder aquí: los conjuntos de la posición Guest no se derivan de sugerencias aceptadas.", + acceptedState: "«{{set}}» se aceptó para todos los usuarios con sesión iniciada (la posición Everyone).", + notInForceState: "«{{set}}» se aceptó, pero aún no está en vigor: la posición Everyone no cambió con esta solicitud. El registro del servidor indica el motivo.", + dismissedState: "«{{set}}» no está concedido: la sugerencia se descartó o se revocó.", + revoke: "Revocar", + revoking: "Revocando…", + revokedToast: "Aceptación de «{{set}}» revocada.", + reopen: "Reconsiderar", + reopening: "Reabriendo…", + reopenedToast: "Sugerencia para «{{set}}» reabierta: no se concede nada hasta que se acepte.", + }, + title: "Marketplace de aplicaciones", + subtitle: "Explore aplicaciones aprobadas publicadas en el catálogo de ObjectStack. Haga clic en una aplicación para ver detalles e instalarla en uno de sus entornos.", + searchPlaceholder: 'Buscar aplicaciones por nombre o ID de aplicación…', + searchAria: "Buscar aplicaciones del marketplace", + installed: "Instalado", + installedCount: "Instaladas ({{count}})", + refresh: "Actualizar", + all: "Todas", + noApprovedYet: "Aún no hay aplicaciones aprobadas para el marketplace.", + noMatchFilters: "Ninguna aplicación coincide con sus filtros.", + noDescription: "Sin descripción disponible.", + back: "Volver al marketplace", + browseLink: "Explorar marketplace →", + versionBadge: "v{{version}}", + installedBadge: "Instalado v{{version}}", + load: { + failed: "No se pudo cargar el marketplace", + failedHintConfigured: "Este runtime accede al marketplace a través del plano de control en {{url}}. Compruebe que esté en línea y sea accesible desde aquí.", + failedHintSameOrigin: "Al catálogo del marketplace se accede a través de este runtime. Compruebe que el runtime esté en línea y pueda acceder al catálogo.", + loading: "Cargando el catálogo del marketplace…", + slow: "Sigue cargando — el catálogo del marketplace tarda en responder.", + retry: "Reintentar", + unreachable: "No se pudo acceder al catálogo del marketplace.", + answeredError: "El catálogo del marketplace respondió con un error (HTTP {{status}}).", + packageFailed: "No se pudo cargar el paquete", + notFound: "No encontrado.", + }, + disabled: { + title: "El marketplace de aplicaciones está desactivado", + description: "Este runtime no tiene ningún marketplace configurado, por lo que no hay nada que explorar ni instalar desde aquí.", + hint: "Ese es el estado esperado cuando el runtime se inicia con <code>OS_CLOUD_URL=off</code> (o <code>none</code>, <code>local</code>, <code>disabled</code>). Para activar el marketplace, apunte <code>OS_CLOUD_URL</code> a un plano de control y reinicie el runtime.", + }, + detail: { + homepage: "Página de inicio", + installedV: "Instalado · v{{version}}", + about: "Acerca de", + noReadme: "Sin readme disponible.", + versions: "Versiones", + noApprovedVersions: "Sin versiones aprobadas.", + prerelease: "Prelanzamiento", + moreOptions: "Más opciones de instalación", + uninstallFromRuntime: "Desinstalar de este runtime", + addSampleData: 'Agregar datos de ejemplo', + reseedAgain: 'Volver a cargar datos de ejemplo', + purgeSampleData: 'Eliminar datos de ejemplo', + purgeConfirm: '¿Eliminar todos los registros de ejemplo creados por este paquete? Los registros añadidos por el usuario NO se verán afectados.', + purgeSuccess: 'Se eliminaron {{count}} registro(s) de ejemplo.', + purgeSuccess_one: 'Se eliminó {{count}} registro de ejemplo.', + purgeSuccess_many: 'Se eliminaron {{count}} de registros de ejemplo.', + purgeSuccess_other: 'Se eliminaron {{count}} registros de ejemplo.', + purgeNoData: 'No se encontraron registros de ejemplo para eliminar.', + // objectstack#30323 — the install note's sample-data count, its clear and the app banner. + sampleDataLoaded: 'Esta instalación cargó {{count}} registro(s) de ejemplo para que vea la aplicación en funcionamiento. Elimínelos cuando empiece a usarla de verdad.', + sampleDataLoaded_one: 'Esta instalación cargó {{count}} registro de ejemplo para que vea la aplicación en funcionamiento. Elimínelo cuando empiece a usarla de verdad.', + sampleDataLoaded_many: 'Esta instalación cargó {{count}} de registros de ejemplo para que vea la aplicación en funcionamiento. Elimínelos cuando empiece a usarla de verdad.', + sampleDataLoaded_other: 'Esta instalación cargó {{count}} registros de ejemplo para que vea la aplicación en funcionamiento. Elimínelos cuando empiece a usarla de verdad.', + sampleDataReloaded: 'Instalar de nuevo vuelve a cargar los datos de ejemplo, incluidos los que haya eliminado.', + purgeConfirmAtMost: 'Se eliminarán como máximo {{count}} registro(s) de ejemplo.', + purgeConfirmAtMost_one: 'Se eliminará como máximo {{count}} registro de ejemplo.', + purgeConfirmAtMost_many: 'Se eliminarán como máximo {{count}} de registros de ejemplo.', + purgeConfirmAtMost_other: 'Se eliminarán como máximo {{count}} registros de ejemplo.', + sampleDataBannerTitle: '"{{name}}" tiene datos de ejemplo.', + sampleDataBannerMessage: 'Están ahí para que vea la aplicación en funcionamiento. Elimínelos cuando empiece a usarla de verdad.', + reseedQueued: 'Los datos de ejemplo se volverán a cargar en el próximo acceso al entorno.', + reseedLocalSuccess: 'Datos de ejemplo recargados: {{inserted}} insertados, {{updated}} actualizados.', + reseedPartialErrors: '({{count}} registro(s) no se pudieron escribir)', + reseedPartialErrors_one: '({{count}} registro no se pudo escribir)', + reseedPartialErrors_many: '({{count}} de registros no se pudieron escribir)', + reseedPartialErrors_other: '({{count}} registros no se pudieron escribir)', + sampleDataKernelUnavailable: 'Este plano de control no tiene un kernel de entorno, por lo que los datos de ejemplo no se pueden volver a cargar ni eliminar desde aquí. Hágalo desde el propio runtime del entorno.', + updateAvailable: 'Actualización disponible', + }, + action: { + installed: "Instalada", + install: "Instalar", + reinstall: "Reinstalar", + working: "Procesando…", + installToCloud: "Instalar en la nube…", + installing: "Instalando…", + updating: "Actualizando…", + close: "Cerrar", + dismiss: "Descartar", + openOnCloud: "Abrir en la nube", + backHome: "Volver al inicio", + updateTo: "Actualizar", + }, + install: { + dialogTitle: "Instalar {{name}}", + dialogDescCurrent: "Instalar en este entorno ({{host}}).", + dialogDescPicker: "Elija un entorno donde instalar esta aplicación. Debe haber iniciado sesión en ObjectOS Cloud.", + environment: "Entorno", + environmentPlaceholder: "Seleccionar entorno", + includeSampleData: "Incluir datos de muestra", + noEnvs: "No se encontraron entornos en su organización activa.", + noPermission: "No tiene permiso para instalar aplicaciones en un entorno. Solo los propietarios y administradores de la organización pueden instalar; pida ayuda al administrador de su organización.", + signInFirst: "Primero debe iniciar sesión en ObjectOS Cloud.", + success: "Instalado correctamente. Abra el entorno para ver la nueva aplicación.", + localSuccess: 'v{{version}} instalado en este runtime. "{{name}}" ya está en el selector de aplicaciones.', + localLoadsAtRestart: "v{{version}} instalado en este runtime. \"{{name}}\" se cargará en el próximo reinicio del runtime y no aparecerá en el selector de aplicaciones hasta entonces.", + localManifestConflict: "{{message}}\nNota: Una aplicación local ya tiene este manifest_id. Primero elimínela de objectstack.config.ts.", + localUnauthorized: "Primero inicie sesión en este runtime.", + localMarketplaceUnavailable: "Este runtime no tiene configurado OS_CLOUD_URL.", + updateTo: "Actualizar → v{{version}}", + installedVersion: "Instalado v{{version}}", + deploying: "Desplegando la aplicación…", + deployed: "La aplicación está desplegada y lista.", + openApp: "Abrir {{name}}", + deployTimeout: "La instalación se registró, pero este entorno aún no ofrece la aplicación. Puede que siga desplegándose; vuelve a comprobarlo en breve. Si nunca aparece, es posible que el paquete no se haya cargado, que no incluya ninguna aplicación o que su aplicación requiera un permiso que no tienes.", + checkAgain: "Comprobar de nuevo", + // ADR-0131 D6 — the install modes a package's manifest permits, and + // what an unmanaged install answers (MarketplacePackagePage). + mode: { + label: "Modo de instalación", + managed: "Administrado", + managedHint: "El paquete sigue siendo la autoridad: se registra como código, recibe sus actualizaciones y sus elementos no se editan aquí.", + unmanaged: "Copia no administrada", + unmanagedHint: "Se copia una sola vez en este runtime como sus propios metadatos: cada elemento se puede editar aquí y la copia no recibe actualizaciones.", + required: "Elige un modo de instalación para instalar este paquete.", + }, + unmanagedSuccess: "v{{version}} de \"{{name}}\" copiado en este runtime como sus propios metadatos, en la base {{base}}. La copia se puede editar y no recibe actualizaciones.", + unmanagedStrippedLocks: "El paquete bloqueaba estos elementos. La copia se puede editar, así que sus bloqueos no se copiaron:", + unmanagedIgnored: "Este runtime no admite instalaciones no administradas, así que \"{{name}}\" se instaló como administrado (registrado como código) en lugar de copiarse. Actualiza el runtime, luego desinstala el paquete y vuelve a instalarlo como copia no administrada.", + }, + // objectui#12146 — the install dialog opened as an UPDATE of the copy + // this environment holds (MarketplacePackagePage). + update: { + dialogTitle: "Actualizar {{name}}", + dialogDescCurrent: "Actualizar la copia instalada en este entorno ({{host}}).", + dialogDescPicker: "Elija el entorno cuya copia instalada desea actualizar. Debe haber iniciado sesión en ObjectOS Cloud.", + majorNotice: "Esta es una actualización de versión principal (v{{fromMajor}} → v{{toMajor}}), que puede incluir cambios incompatibles.", + releaseNotes: "Leer las notas de la versión v{{version}}", + success: "Actualizado a la v{{version}}.", + deployTimeout: "La actualización se registró, pero este entorno aún no ofrece la v{{version}}. Puede que siga desplegándose; vuelva a comprobarlo en breve.", + }, + // objectui#3546 — the MarketplacePage "Your organization" strip. + org: { + heading: "Su organización", + install: "Instalar", + installed: "{{name}} instalado", + installedBadge: "Instalado", + installing: "Instalando…", + }, + // ADR-0025 PD4 §3.5/§3.11 — the pre-install consent panel + // (PluginDisclosure.tsx). `runtime` is a CLOSED enum: spec + // PluginRuntimeSchema = z.enum(['node', 'sandbox', 'worker']). + disclosure: { + containsCode: "Este paquete contiene código", + reviewed: "Revisado y aprobado", + unreviewed: "Sin revisar todavía", + signed: "Firmado", + grantsIntro: "Este paquete solicita:", + notEnforced: "Esta lista se registra al instalar y se vuelve a confirmar si una versión posterior solicita más, pero el runtime aún no restringe el paquete a ella.", + services: "Servicios de la plataforma", + hooks: "Hooks de ciclo de vida", + network: "Acceso a la red", + fs: "Acceso al sistema de archivos", + noPermissions: "No solicita permisos especiales.", + acknowledge: "Entiendo que este paquete ejecuta código y recibe los permisos anteriores.", + runtime: { + node: "En el proceso · confianza total", + sandbox: "En sandbox", + worker: "Fuera del proceso", + }, + }, + uninstall: { + successInDetail: "{{manifestId}} y su manifiesto en caché eliminados: la aplicación se retiró del kernel en ejecución, junto con sus conjuntos de permisos y sus asignaciones. Los registros almacenados en sus objetos permanecen en la base de datos.", + }, + accessDenied: { + title: "Marketplace solo para administradores", + description: "No tiene permiso para instalar aplicaciones en este entorno.", + }, + category: { + crm: "CRM", + erp: "ERP", + hr: "RRHH", + finance: "Finanzas", + project: "Gestión de proyectos", + collaboration: "Colaboración", + analytics: "Analítica", + integration: "Integración", + automation: "Automatización", + ai: "IA", + security: "Seguridad", + "developer-tools": "Herramientas de desarrollador", + "ui-theme": "Tema de UI", + storage: "Almacenamiento", + other: "Otro", + }, + relativeTime: { + today: "hoy", + daysAgo: "hace {{count}}d", + monthsAgo: "hace {{count}} meses", + monthsAgo_one: "hace {{count}} mes", + monthsAgo_many: "hace {{count}} meses", + monthsAgo_other: "hace {{count}} meses", + yearsAgo: "hace {{count}} años", + yearsAgo_one: "hace {{count}} año", + yearsAgo_many: "hace {{count}} años", + yearsAgo_other: "hace {{count}} años", + }, + }, +}; + +export default es; diff --git a/ui/packages/i18n/src/namespaces/marketplace/fr.ts b/ui/packages/i18n/src/namespaces/marketplace/fr.ts new file mode 100644 index 00000000000..2113f009a43 --- /dev/null +++ b/ui/packages/i18n/src/namespaces/marketplace/fr.ts @@ -0,0 +1,236 @@ +/** + * French (fr) — the lazy `marketplace` namespace's catalogue. + * + * A PARTIAL pack: the same shape as `../../locales/fr.ts`, holding only + * the `marketplace` group, which `./index.ts` deep-merges into an i18next + * instance when a marketplace page mounts. `./index.ts` says what the + * namespace is, why it is lazy, and which gates read this file. + * + * ⛔ Keep the declaration `const fr = { … }` and `export default fr`: + * the i18n gates under `ui/scripts/` read this file by AST under that name. + */ +const fr = { + marketplace: { + suggestedBindings: { + promptEveryone: "Cette application suggère d'accorder « {{set}} » à tous les utilisateurs connectés (position Everyone).", + promptGuest: "Cette application suggère d'accorder « {{set}} » aux visiteurs non authentifiés (position Guest).", + confirm: "Accorder", + confirming: "Attribution…", + dismiss: "Ignorer", + confirmedToast: "« {{set}} » est désormais accordé à {{anchor}}.", + dismissedToast: "Suggestion pour « {{set}} » ignorée.", + guestUnsupported: "Impossible de l'accorder ici : les ensembles de la position Guest ne sont pas dérivés des suggestions acceptées.", + acceptedState: "« {{set}} » a été accepté pour tous les utilisateurs connectés (position Everyone).", + notInForceState: "« {{set}} » a été accepté, mais n'est pas encore en vigueur : la position Everyone n'a pas changé lors de cette requête. Le journal du serveur indique pourquoi.", + dismissedState: "« {{set}} » n'est pas accordé : la suggestion a été ignorée ou révoquée.", + revoke: "Révoquer", + revoking: "Révocation…", + revokedToast: "Acceptation de « {{set}} » révoquée.", + reopen: "Reconsidérer", + reopening: "Réouverture…", + reopenedToast: "Suggestion pour « {{set}} » rouverte : rien n'est accordé tant qu'elle n'est pas acceptée.", + }, + title: "Marketplace d'applications", + subtitle: "Parcourez les applications approuvées publiées dans le catalogue ObjectStack. Cliquez sur une application pour voir les détails et l'installer dans l'un de vos environnements.", + searchPlaceholder: "Rechercher des applications par nom ou ID d'application…", + searchAria: "Rechercher des applications du marketplace", + installed: "Installé", + installedCount: "Installées ({{count}})", + refresh: "Actualiser", + all: "Toutes", + noApprovedYet: "Aucune application approuvée pour le marketplace pour l'instant.", + noMatchFilters: "Aucune application ne correspond à vos filtres.", + noDescription: "Aucune description disponible.", + back: "Retour au marketplace", + browseLink: "Parcourir le marketplace →", + versionBadge: "v{{version}}", + installedBadge: "Installé v{{version}}", + load: { + failed: "Impossible de charger le marketplace", + failedHintConfigured: "Ce runtime accède à la place de marché via le plan de contrôle situé à {{url}}. Vérifiez qu'il est en ligne et joignable depuis ici.", + failedHintSameOrigin: "Le catalogue de la place de marché est accessible via ce runtime. Vérifiez que le runtime est en ligne et qu'il peut joindre le catalogue.", + loading: "Chargement du catalogue de la place de marché…", + slow: "Chargement en cours — le catalogue de la place de marché tarde à répondre.", + retry: "Réessayer", + unreachable: "Le catalogue de la place de marché est injoignable.", + answeredError: "Le catalogue de la place de marché a répondu par une erreur (HTTP {{status}}).", + packageFailed: "Impossible de charger le paquet", + notFound: "Introuvable.", + }, + disabled: { + title: "La place de marché des applications est désactivée", + description: "Aucune place de marché n'est configurée pour ce runtime : il n'y a donc rien à parcourir ni à installer ici.", + hint: "C'est l'état attendu lorsque le runtime démarre avec <code>OS_CLOUD_URL=off</code> (ou <code>none</code>, <code>local</code>, <code>disabled</code>). Pour activer la place de marché, faites pointer <code>OS_CLOUD_URL</code> vers un plan de contrôle puis redémarrez le runtime.", + }, + detail: { + homepage: "Page d'accueil", + installedV: "Installé · v{{version}}", + about: "À propos", + noReadme: "Aucun readme disponible.", + versions: "Versions", + noApprovedVersions: "Aucune version approuvée.", + prerelease: "Prélancement", + moreOptions: "Plus d'options d'installation", + uninstallFromRuntime: "Désinstaller de ce runtime", + addSampleData: 'Ajouter des données d\'exemple', + reseedAgain: 'Recharger les données d\'exemple', + purgeSampleData: 'Supprimer les données d\'exemple', + purgeConfirm: 'Supprimer tous les enregistrements d\'exemple créés par ce paquet ? Les enregistrements ajoutés par l\'utilisateur ne seront PAS touchés.', + purgeSuccess: '{{count}} enregistrement(s) d\'exemple supprimé(s).', + purgeSuccess_one: "{{count}} enregistrement d'exemple supprimé.", + purgeSuccess_many: "{{count}} d'enregistrements d'exemple supprimés.", + purgeSuccess_other: "{{count}} enregistrements d'exemple supprimés.", + purgeNoData: 'Aucun enregistrement d\'exemple à supprimer.', + // objectstack#30323 — the install note's sample-data count, its clear and the app banner. + sampleDataLoaded: 'Cette installation a chargé {{count}} enregistrement(s) d\'exemple pour que vous voyiez l\'application fonctionner. Supprimez-les quand vous commencez à l\'utiliser pour de bon.', + sampleDataLoaded_one: 'Cette installation a chargé {{count}} enregistrement d\'exemple pour que vous voyiez l\'application fonctionner. Supprimez-le quand vous commencez à l\'utiliser pour de bon.', + sampleDataLoaded_many: 'Cette installation a chargé {{count}} d\'enregistrements d\'exemple pour que vous voyiez l\'application fonctionner. Supprimez-les quand vous commencez à l\'utiliser pour de bon.', + sampleDataLoaded_other: 'Cette installation a chargé {{count}} enregistrements d\'exemple pour que vous voyiez l\'application fonctionner. Supprimez-les quand vous commencez à l\'utiliser pour de bon.', + sampleDataReloaded: 'Réinstaller recharge les données d\'exemple, y compris celles que vous avez supprimées.', + purgeConfirmAtMost: 'Au plus {{count}} enregistrement(s) d\'exemple seront supprimés.', + purgeConfirmAtMost_one: 'Au plus {{count}} enregistrement d\'exemple sera supprimé.', + purgeConfirmAtMost_many: 'Au plus {{count}} d\'enregistrements d\'exemple seront supprimés.', + purgeConfirmAtMost_other: 'Au plus {{count}} enregistrements d\'exemple seront supprimés.', + sampleDataBannerTitle: '« {{name}} » contient des données d\'exemple.', + sampleDataBannerMessage: 'Elles sont là pour que vous voyiez l\'application fonctionner. Supprimez-les quand vous commencez à l\'utiliser pour de bon.', + reseedQueued: 'Les données d\'exemple seront rechargées au prochain accès à l\'environnement.', + reseedLocalSuccess: 'Données d\'exemple rechargées : {{inserted}} ajoutés, {{updated}} mis à jour.', + reseedPartialErrors: '({{count}} enregistrement(s) n\'ont pas pu être écrits)', + reseedPartialErrors_one: "({{count}} enregistrement n'a pas pu être écrit)", + reseedPartialErrors_many: "({{count}} d'enregistrements n'ont pas pu être écrits)", + reseedPartialErrors_other: "({{count}} enregistrements n'ont pas pu être écrits)", + sampleDataKernelUnavailable: 'Ce plan de contrôle n\'a pas de noyau d\'environnement : les données d\'exemple ne peuvent donc être ni rechargées ni supprimées ici. Faites-le depuis le runtime propre à l\'environnement.', + updateAvailable: 'Mise à jour disponible', + }, + action: { + installed: "Installé", + install: "Installer", + reinstall: "Réinstaller", + working: "Traitement…", + installToCloud: "Installer dans le cloud…", + installing: "Installation…", + updating: "Mise à jour…", + close: "Fermer", + dismiss: "Fermer", + openOnCloud: "Ouvrir dans le cloud", + backHome: "Retour à l'accueil", + updateTo: "Mettre à jour", + }, + install: { + dialogTitle: "Installer {{name}}", + dialogDescCurrent: "Installer dans cet environnement ({{host}}).", + dialogDescPicker: "Choisissez un environnement où installer cette application. Vous devez être connecté à ObjectOS Cloud.", + environment: "Environnement", + environmentPlaceholder: "Sélectionner un environnement", + includeSampleData: "Inclure des données d'exemple", + noEnvs: "Aucun environnement trouvé dans votre organisation active.", + noPermission: "Vous n'avez pas la permission d'installer des applications dans un environnement. Seuls les propriétaires et administrateurs de l'organisation peuvent installer — contactez l'administrateur de votre organisation.", + signInFirst: "Vous devez d'abord vous connecter à ObjectOS Cloud.", + success: "Installé avec succès. Ouvrez l'environnement pour voir la nouvelle application.", + localSuccess: 'v{{version}} installé dans ce runtime. « {{name}} » est maintenant dans le sélecteur d\'applications.', + localLoadsAtRestart: "v{{version}} installé dans ce runtime. \"{{name}}\" sera chargé au prochain redémarrage du runtime et n'apparaîtra pas dans le sélecteur d'applications d'ici là.", + localManifestConflict: "{{message}}\nRemarque : Une application locale possède déjà ce manifest_id. Supprimez-la d'abord de objectstack.config.ts.", + localUnauthorized: "Connectez-vous d'abord à ce runtime.", + localMarketplaceUnavailable: "Ce runtime n'a pas de OS_CLOUD_URL configuré.", + updateTo: "Mettre à jour → v{{version}}", + installedVersion: "Installé v{{version}}", + deploying: "Déploiement de l'application…", + deployed: "L'application est déployée et prête.", + openApp: "Ouvrir {{name}}", + deployTimeout: "L'installation a été enregistrée, mais cet environnement ne sert pas encore l'application. Le déploiement est peut-être encore en cours : vérifiez à nouveau dans un instant. Si elle n'apparaît jamais, le paquet n'a peut-être pas été chargé, ne contient aucune application, ou son application exige une autorisation que vous n'avez pas.", + checkAgain: "Vérifier à nouveau", + // ADR-0131 D6 — the install modes a package's manifest permits, and + // what an unmanaged install answers (MarketplacePackagePage). + mode: { + label: "Mode d'installation", + managed: "Géré", + managedHint: "Le paquet reste l'autorité : il est enregistré comme code, reçoit ses mises à niveau, et ses éléments ne sont pas modifiés ici.", + unmanaged: "Copie non gérée", + unmanagedHint: "Copié une seule fois dans ce runtime comme ses propres métadonnées : chaque élément est modifiable ici, et la copie ne reçoit aucune mise à niveau.", + required: "Choisissez un mode d'installation pour installer ce paquet.", + }, + unmanagedSuccess: "v{{version}} de \"{{name}}\" copié dans ce runtime comme ses propres métadonnées, dans la base {{base}}. La copie est modifiable et ne reçoit aucune mise à niveau.", + unmanagedStrippedLocks: "Le paquet verrouillait ces éléments. La copie est modifiable, leurs verrous n'ont donc pas été copiés :", + unmanagedIgnored: "Ce runtime ne prend pas en charge les installations non gérées : \"{{name}}\" a donc été installé en mode géré (enregistré comme code) au lieu d'être copié. Mettez à niveau le runtime, puis désinstallez le paquet et réinstallez-le comme copie non gérée.", + }, + // objectui#12146 — the install dialog opened as an UPDATE of the copy + // this environment holds (MarketplacePackagePage). + update: { + dialogTitle: "Mettre à jour {{name}}", + dialogDescCurrent: "Mettre à jour la copie installée dans cet environnement ({{host}}).", + dialogDescPicker: "Choisissez l'environnement dont la copie installée doit être mise à jour. Vous devez être connecté à ObjectOS Cloud.", + majorNotice: "Il s'agit d'une mise à jour de version majeure (v{{fromMajor}} → v{{toMajor}}), qui peut comporter des changements incompatibles.", + releaseNotes: "Lire les notes de version de la v{{version}}", + success: "Mis à jour vers la v{{version}}.", + deployTimeout: "La mise à jour a été enregistrée, mais cet environnement ne sert pas encore la v{{version}}. Le déploiement est peut-être encore en cours : vérifiez à nouveau dans un instant.", + }, + // objectui#3546 — the MarketplacePage "Your organization" strip. + org: { + heading: "Votre organisation", + install: "Installer", + installed: "{{name}} installé", + installedBadge: "Installé", + installing: "Installation…", + }, + // ADR-0025 PD4 §3.5/§3.11 — the pre-install consent panel + // (PluginDisclosure.tsx). `runtime` is a CLOSED enum: spec + // PluginRuntimeSchema = z.enum(['node', 'sandbox', 'worker']). + disclosure: { + containsCode: "Ce paquet contient du code", + reviewed: "Examiné et approuvé", + unreviewed: "Pas encore examiné", + signed: "Signé", + grantsIntro: "Ce paquet demande :", + notEnforced: "Cette liste est enregistrée à l'installation et reconfirmée si une version ultérieure en demande davantage, mais l'exécution ne restreint pas encore le paquet à cette liste.", + services: "Services de la plateforme", + hooks: "Hooks de cycle de vie", + network: "Accès réseau", + fs: "Accès au système de fichiers", + noPermissions: "Ne demande aucune autorisation particulière.", + acknowledge: "Je comprends que ce paquet exécute du code et obtient les autorisations ci-dessus.", + runtime: { + node: "Dans le processus · confiance totale", + sandbox: "Bac à sable", + worker: "Hors processus", + }, + }, + uninstall: { + successInDetail: "{{manifestId}} et son manifeste en cache supprimés : l'application a été retirée du noyau en cours d'exécution, avec ses jeux d'autorisations et leurs attributions. Les enregistrements stockés dans ses objets restent dans la base de données.", + }, + accessDenied: { + title: "Marketplace réservé aux administrateurs", + description: "Vous n'avez pas la permission d'installer des applications dans cet environnement.", + }, + category: { + crm: "CRM", + erp: "ERP", + hr: "RH", + finance: "Finance", + project: "Gestion de projet", + collaboration: "Collaboration", + analytics: "Analytique", + integration: "Intégration", + automation: "Automatisation", + ai: "IA", + security: "Sécurité", + "developer-tools": "Outils développeur", + "ui-theme": "Thème UI", + storage: "Stockage", + other: "Autre", + }, + relativeTime: { + today: "aujourd'hui", + daysAgo: "il y a {{count}}j", + monthsAgo: "il y a {{count}} mois", + monthsAgo_one: "il y a {{count}} mois", + monthsAgo_many: "il y a {{count}} mois", + monthsAgo_other: "il y a {{count}} mois", + yearsAgo: "il y a {{count}} ans", + yearsAgo_one: "il y a {{count}} an", + yearsAgo_many: "il y a {{count}} ans", + yearsAgo_other: "il y a {{count}} ans", + }, + }, +}; + +export default fr; diff --git a/ui/packages/i18n/src/namespaces/marketplace/index.ts b/ui/packages/i18n/src/namespaces/marketplace/index.ts new file mode 100644 index 00000000000..8e6ae29efae --- /dev/null +++ b/ui/packages/i18n/src/namespaces/marketplace/index.ts @@ -0,0 +1,355 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * @object-ui/i18n — the `marketplace` namespace: the marketplace pages' strings. + * + * ## What this is + * + * The `marketplace.*` group of the ten language packs, moved out of them into + * ten PARTIAL packs — `./<code>.ts`, each the same shape as + * `../../locales/<code>.ts` with only the `marketplace` group in it — that are + * fetched when a marketplace page mounts and deep-merged into the page's + * i18next instance. The call sites do not change: they still ask + * `t('marketplace.…')` of the default `translation` namespace, and the keys + * are there by the time they render. + * + * ## Why it is lazy + * + * `locales/en.ts` is the console's one EAGER catalogue (`../../locales/registry.ts` + * says why), and `ui/scripts/check-eager-closure-budget.mjs` holds its chunk, + * `i18n-locale-en`, under a ceiling of its own. The marketplace group was + * about 4 KB of that chunk gzipped, paid on every console page load for two + * pages that are lazy routes. Moved here it is paid by the visitor who opens + * them, in the language they read. + * + * ## What stayed in the packs + * + * Studio's packages page — outside the marketplace route — draws marketplace + * installs with the same app-shell components the marketplace pages use: + * `MarketplaceRowActions` / `MarketplaceNotLoaded` for a row's Not loaded + * badge and reason and its Uninstall action, confirmation and result, and + * `useDependencyMissingText` / `useProtocolCheckSkippedText` for an import's + * refusal and protocol note. The keys those read are still pack keys, resident + * wherever that page can open: `marketplace.installedAdditiveNote`, + * `marketplace.notLoaded.*`, `marketplace.action.{uninstall,uninstalling,details}`, + * four `marketplace.uninstall.*` leaves, and + * `marketplace.install.{dependencyMissing,protocolCheckSkipped}.*`. ⛔ A key + * that a page outside the marketplace route reads belongs in the pack, not + * here; a key here is only reachable under {@link useMarketplaceNamespace}. + * App-shell's `marketplaceNamespaceBoundary` test walks what those files read + * and fails on a key that is only here. + * + * ## Residency, and why every catalogue is lazy + * + * Unlike the packs (and unlike the `studio` namespace) nothing here is + * resident: the pages that read these strings are themselves fetched on + * demand, so there is no first frame that needs one synchronously. All ten + * catalogues are one literal `() => import()` each — its own chunk + * (`apps/console/vite.config.ts` names them `i18n-ns-marketplace-<code>`). + * ⛔ Never collapse the loaders into a template-literal `import()`; the + * registry's header says why. + * + * The one exception is a host that made every catalogue resident on purpose: + * `registerBuiltInLocales()` (the all-ten door, `@object-ui/i18n/locales`) + * registers each language COMPLETE, namespace included, so a catalogue the + * registry already holds is used as it stands and never fetched. + * + * A page waits for TWO catalogues: the one that serves its language, and + * `en`. `en` is what the packs' `fallbackLng: 'en'` falls back to, so loading + * it keeps the fallback the marketplace strings had while they were pack keys + * — a key a translation lacks renders in English, never as its raw name. + * + * ## Which gates read it + * + * Every i18n gate that reads pack keys reads these files as part of the + * packs: `ui/scripts/check-i18n-call-site-keys.mjs` (`collectEnKeys`, so the + * dead-keys report too) and `ui/scripts/check-i18n-en-drift.mjs` read + * `./<code>.ts` beside `../../locales/<code>.ts` and refuse a key defined in + * both. `../../locales/index.ts` (the all-ten door) hands out each language's + * pack merged with its catalogue here, so `all-locales-key-parity.test.ts` + * and every other suite that walks `builtInLocales` still walks these keys. + * `../../__tests__/marketplace-namespace.test.ts` holds this module's own + * shape: ten literal loaders, no static catalogue import, no key in both + * places. + */ +import { useCallback, useContext, useEffect, useSyncExternalStore } from 'react'; +import type { i18n as I18nInstance } from 'i18next'; +import { I18nContext, getI18n } from 'react-i18next'; +import { DEFAULT_BUILT_IN_LANGUAGE, getLoadedBuiltInLocales, matchLanguageTag } from '../../locales/registry.js'; + +/** One language's catalogue: a partial pack holding the `marketplace` group. */ +export type MarketplaceCatalogue = Readonly<Record<string, unknown>>; + +/** The namespace's name — and the pack group its catalogues hold. */ +export const MARKETPLACE_NAMESPACE = 'marketplace'; + +/** + * A key every catalogue defines and no pack does: whether an instance can + * render the namespace in a language is whether it holds this key there. + * `marketplace-namespace.test.ts` holds both halves of that. + */ +export const MARKETPLACE_NAMESPACE_PROBE_KEY = 'marketplace.title'; + +/** + * The ten catalogues, each fetched on demand — one literal specifier each, so + * each one is a chunk of its own. ⛔ Read the header before touching the + * spelling. + */ +const MARKETPLACE_LOADERS: Readonly<Record<string, () => Promise<{ default: MarketplaceCatalogue }>>> = + Object.freeze({ + en: () => import('./en.js'), + zh: () => import('./zh.js'), + ja: () => import('./ja.js'), + ko: () => import('./ko.js'), + de: () => import('./de.js'), + fr: () => import('./fr.js'), + es: () => import('./es.js'), + pt: () => import('./pt.js'), + ru: () => import('./ru.js'), + ar: () => import('./ar.js'), + }); + +/** Every language this namespace ships a catalogue for. */ +export const MARKETPLACE_LANGUAGE_CODES: readonly string[] = Object.freeze(Object.keys(MARKETPLACE_LOADERS)); + +const own = (record: object, key: string): boolean => + Object.prototype.hasOwnProperty.call(record, key); + +const hasMarketplaceCatalogue = (code: string): boolean => own(MARKETPLACE_LOADERS, code); + +/** + * The code of the catalogue that serves `lang` — exact tag, then base language + * (`zh-CN` → `zh`), the order the packs resolve in — or `null` when this + * namespace has none for it, and `lang` is served by the `en` fallback. + */ +export function resolveMarketplaceLanguage(lang: string | undefined): string | null { + return matchLanguageTag(lang ?? '', hasMarketplaceCatalogue); +} + +const loaded: Record<string, MarketplaceCatalogue> = {}; +const inFlight = new Map<string, Promise<MarketplaceCatalogue>>(); +const failed = new Map<string, unknown>(); +const listeners = new Set<() => void>(); +let version = 0; + +function announce(): void { + version += 1; + for (const listener of [...listeners]) listener(); +} + +/** + * The catalogue `code` names when it needs no fetch: one this module fetched, + * or the namespace group of the COMPLETE catalogue the registry holds once a + * host called `registerBuiltInLocales()` (see the header). + */ +function residentCatalogue(code: string): MarketplaceCatalogue | undefined { + if (own(loaded, code)) return loaded[code]; + if (!hasMarketplaceCatalogue(code)) return undefined; + const registered = getLoadedBuiltInLocales()[code] as Record<string, unknown> | undefined; + const group = registered?.[MARKETPLACE_NAMESPACE] as Record<string, unknown> | undefined; + const probe = MARKETPLACE_NAMESPACE_PROBE_KEY.slice(MARKETPLACE_NAMESPACE.length + 1); + if (typeof group?.[probe] !== 'string') return undefined; + // Kept, so every later answer for `code` is this same object. + loaded[code] = Object.freeze({ [MARKETPLACE_NAMESPACE]: group }); + return loaded[code]; +} + +/** Whether the catalogue `code` names is resident (no fetch needed). */ +export function isMarketplaceCatalogueLoaded(code: string): boolean { + return residentCatalogue(code) !== undefined; +} + +/** + * Fetch the catalogue `code` names and resolve it. Memoised: a resident + * catalogue resolves without a second import, and concurrent callers share one + * in-flight promise. ⛔ A rejected load is not memoised: it is recorded, so a + * page can say it failed, and the next explicit request fetches again. + */ +export function loadMarketplaceCatalogue(code: string): Promise<MarketplaceCatalogue> { + if (!hasMarketplaceCatalogue(code)) { + return Promise.reject(new Error(`The marketplace namespace ships no \`${code}\` catalogue.`)); + } + const resident = residentCatalogue(code); + if (resident) return Promise.resolve(resident); + const existing = inFlight.get(code); + if (existing) return existing; + failed.delete(code); + // The in-flight entry goes BEFORE the announcement: a listener re-reads the + // state at once, and must not find a settled request still marked in flight. + const pending = MARKETPLACE_LOADERS[code]().then( + (mod) => { + inFlight.delete(code); + loaded[code] = mod.default; + announce(); + return mod.default; + }, + (err: unknown) => { + inFlight.delete(code); + failed.set(code, err); + announce(); + throw err; + }, + ); + inFlight.set(code, pending); + return pending; +} + +/** + * The `[language key, catalogue code]` pairs an instance rendering `lang` needs: + * the catalogue that serves `lang` (stored under its code, which is on i18next's + * resolution path for every tag it serves), and `en`, the packs' fallback. + */ +function catalogueTargets(lang: string): Array<readonly [string, string]> { + const serving = resolveMarketplaceLanguage(lang); + const targets: Array<readonly [string, string]> = [[DEFAULT_BUILT_IN_LANGUAGE, DEFAULT_BUILT_IN_LANGUAGE]]; + if (serving !== null && serving !== DEFAULT_BUILT_IN_LANGUAGE) targets.unshift([serving, serving]); + return targets; +} + +/** + * Whether `instance` still holds the namespace in `lng`, without leaning on a + * fallback language — i18next's own answer, so a bundle removed since it was + * installed is installed again. + */ +function holdsNamespace(instance: I18nInstance, lng: string): boolean { + return instance.exists(MARKETPLACE_NAMESPACE_PROBE_KEY, { lng, fallbackLng: false }); +} + +/** + * The language keys each instance has been given its catalogue under. Kept + * per instance rather than read off the instance's keys: a host whose own + * `resources` spell one `marketplace.*` key in a language has not supplied + * that language's catalogue, and must still get the rest of it. + */ +const installed = new WeakMap<I18nInstance, Set<string>>(); + +/** The writes {@link installResident} makes are SILENT: see there. */ +const SILENT_WRITE = Object.freeze({ silent: true }); + +/** + * Merge every catalogue `lang` needs that is resident but not yet in + * `instance`. Deep, and ⛔ never overwriting: a host's own `resources` for a + * `marketplace.*` key win over the built-in text, the precedence the packs keep. + * + * Silent, because it runs while the gate renders, before anything below it + * has read a marketplace key: nothing on screen needs telling, and an `added` + * event would re-render every translated component in the page for nothing. + * + * @returns whether the instance now holds every catalogue `lang` needs. + */ +function installResident(instance: I18nInstance, lang: string): boolean { + let given = installed.get(instance); + if (!given) { + given = new Set(); + installed.set(instance, given); + } + let complete = true; + for (const [target, code] of catalogueTargets(lang)) { + if (given.has(target) && holdsNamespace(instance, target)) continue; + const catalogue = residentCatalogue(code); + if (!catalogue) { + complete = false; + continue; + } + instance.addResourceBundle(target, 'translation', catalogue, true, false, SILENT_WRITE); + given.add(target); + } + return complete; +} + +/** What {@link useMarketplaceNamespace} answers. */ +export interface MarketplaceNamespaceState { + /** + * `ready` — every `marketplace.*` key renders in the current language; + * `loading` — a catalogue is still on its way; `failed` — a catalogue could + * not be fetched, and nothing will try again until {@link retry} is called. + */ + status: 'ready' | 'loading' | 'failed'; + /** Fetch the catalogues that failed again. */ + retry: () => void; +} + +function subscribe(listener: () => void): () => void { + listeners.add(listener); + return () => { + listeners.delete(listener); + }; +} + +const readVersion = (): number => version; + +/** + * React hook: whether the page's i18next instance can render the + * `marketplace.*` strings in its current language, starting the fetch when it + * cannot. + * + * A marketplace page renders nothing that reads a `marketplace.*` key until + * this answers `ready` (app-shell's `MarketplaceNamespaceGate` is that page + * frame), so a visitor sees a loading state, never an English sentence in a + * Chinese page nor a raw key. On a language switch it answers `loading` again + * until the new language's catalogue has arrived. + * + * With no i18next instance at all (a component rendered without a provider) + * there is no store to fill and the translator answers with keys or defaults + * anyway, so it answers `ready`. + */ +export function useMarketplaceNamespace(): MarketplaceNamespaceState { + // The instance react-i18next hands `t()`: the provider's, else the one + // `initReactI18next` installed. Read directly rather than through + // `useTranslation`, which warns (NO_I18NEXT_INSTANCE) where there is none — + // a host or a test that renders a marketplace component bare. + const instance = (useContext(I18nContext)?.i18n ?? getI18n()) as I18nInstance | undefined; + // Re-render when a catalogue lands or fails — the store write this hook + // makes is silent, so react-i18next's own subscription never hears of it. + useSyncExternalStore(subscribe, readVersion, readVersion); + // …and when the instance switches language. + const subscribeLanguage = useCallback( + (listener: () => void) => { + if (typeof instance?.on !== 'function') return () => undefined; + instance.on('languageChanged', listener); + return () => { + instance.off('languageChanged', listener); + }; + }, + [instance], + ); + const readLanguage = (): string => instance?.language || DEFAULT_BUILT_IN_LANGUAGE; + const lang = useSyncExternalStore(subscribeLanguage, readLanguage, readLanguage); + + const retry = useCallback(() => { + let started = false; + for (const [, code] of catalogueTargets(lang)) { + if (!failed.has(code) || inFlight.has(code)) continue; + loadMarketplaceCatalogue(code).catch(() => undefined); + started = true; + } + // The failure is cleared and the request is under way, so the frame shows + // its loading state again instead of the failure it is retrying. Never + // called while rendering (an effect or a click), so announcing is safe. + if (started) announce(); + }, [lang]); + // A page opened (or switched to a language) after an earlier failure tries + // again, once; otherwise a failure stays a failure until the visitor asks + // again, so a chunk that cannot be fetched is never re-requested in a loop. + useEffect(() => { + retry(); + }, [retry]); + + if (typeof instance?.exists !== 'function' || typeof instance.addResourceBundle !== 'function') { + return { status: 'ready', retry }; + } + if (installResident(instance, lang)) return { status: 'ready', retry }; + + let status: MarketplaceNamespaceState['status'] = 'loading'; + for (const [, code] of catalogueTargets(lang)) { + if (residentCatalogue(code)) continue; + if (failed.has(code) && !inFlight.has(code)) { + status = 'failed'; + continue; + } + // Started while rendering, as `useStudioCatalogue` starts its fetch: + // memoised, so a re-render joins the request already in flight. + loadMarketplaceCatalogue(code).catch(() => undefined); + } + return { status, retry }; +} diff --git a/ui/packages/i18n/src/namespaces/marketplace/ja.ts b/ui/packages/i18n/src/namespaces/marketplace/ja.ts new file mode 100644 index 00000000000..9a310620f7d --- /dev/null +++ b/ui/packages/i18n/src/namespaces/marketplace/ja.ts @@ -0,0 +1,230 @@ +/** + * Japanese (ja) — the lazy `marketplace` namespace's catalogue. + * + * A PARTIAL pack: the same shape as `../../locales/ja.ts`, holding only + * the `marketplace` group, which `./index.ts` deep-merges into an i18next + * instance when a marketplace page mounts. `./index.ts` says what the + * namespace is, why it is lazy, and which gates read this file. + * + * ⛔ Keep the declaration `const ja = { … }` and `export default ja`: + * the i18n gates under `ui/scripts/` read this file by AST under that name. + */ +const ja = { + marketplace: { + suggestedBindings: { + promptEveryone: "このアプリは、サインイン済みの全ユーザー(Everyone ポジション)に「{{set}}」を付与することを推奨しています。", + promptGuest: "このアプリは、未認証の訪問者(Guest ポジション)に「{{set}}」を付与することを推奨しています。", + confirm: "付与", + confirming: "付与しています…", + dismiss: "閉じる", + confirmedToast: "「{{set}}」を {{anchor}} に付与しました。", + dismissedToast: "「{{set}}」の提案を閉じました。", + guestUnsupported: "ここでは付与できません。Guest ポジションのセットは承認された提案から導出されません。", + acceptedState: "「{{set}}」は、サインイン済みの全ユーザー(Everyone ポジション)向けに承認されています。", + notInForceState: "「{{set}}」は承認されましたが、まだ有効ではありません。このリクエストでは Everyone ポジションは変更されませんでした。理由はサーバーログに記録されています。", + dismissedState: "「{{set}}」は付与されていません。提案は閉じられたか、取り消されました。", + revoke: "取り消す", + revoking: "取り消しています…", + revokedToast: "「{{set}}」の承認を取り消しました。", + reopen: "再検討", + reopening: "再オープン中…", + reopenedToast: "「{{set}}」の提案を再オープンしました。承認されるまで何も付与されません。", + }, + title: "アプリマーケットプレイス", + subtitle: "ObjectStackカタログに公開された承認済みアプリを参照します。アプリをクリックして詳細を表示し、環境にインストールしてください。", + searchPlaceholder: '名前またはアプリIDでアプリを検索…', + searchAria: "マーケットプレイスアプリを検索", + installed: "インストール済み", + installedCount: "インストール済み({{count}})", + refresh: "更新", + all: "すべて", + noApprovedYet: "マーケットプレイスにはまだ承認されたアプリがありません。", + noMatchFilters: "フィルターに一致するアプリがありません。", + noDescription: "説明がありません。", + back: "マーケットプレイスに戻る", + browseLink: "マーケットプレイスを参照 →", + versionBadge: "v{{version}}", + installedBadge: "インストール済み v{{version}}", + load: { + failed: "マーケットプレイスの読み込みに失敗しました", + failedHintConfigured: "このランタイムは {{url}} のコントロールプレーン経由でマーケットプレイスに接続します。オンラインであること、ここから到達できることを確認してください。", + failedHintSameOrigin: "マーケットプレイスのカタログには、このランタイムを経由してアクセスします。ランタイムがオンラインで、カタログに到達できることを確認してください。", + loading: "マーケットプレイスのカタログを読み込んでいます…", + slow: "まだ読み込み中です — マーケットプレイスのカタログの応答が遅れています。", + retry: "再試行", + unreachable: "マーケットプレイスのカタログに到達できませんでした。", + answeredError: "マーケットプレイスのカタログがエラーを返しました(HTTP {{status}})。", + packageFailed: "パッケージの読み込みに失敗しました", + notFound: "見つかりません。", + }, + disabled: { + title: "アプリマーケットプレイスは無効です", + description: "このランタイムにはマーケットプレイスが設定されていないため、ここから閲覧またはインストールできるものはありません。", + hint: "ランタイムを <code>OS_CLOUD_URL=off</code>(または <code>none</code>、<code>local</code>、<code>disabled</code>)で起動した場合、これは想定どおりの状態です。マーケットプレイスを有効にするには、<code>OS_CLOUD_URL</code> をコントロールプレーンに向けてランタイムを再起動してください。", + }, + detail: { + homepage: "ホームページ", + installedV: "インストール済み · v{{version}}", + about: "概要", + noReadme: "READMEがありません。", + versions: "バージョン", + noApprovedVersions: "承認済みバージョンがありません。", + prerelease: "プレ", + moreOptions: "その他のインストールオプション", + uninstallFromRuntime: "このランタイムからアンインストール", + addSampleData: 'サンプルデータを追加', + reseedAgain: 'サンプルデータを再投入', + purgeSampleData: 'サンプルデータを削除', + purgeConfirm: 'このパッケージが投入したサンプルレコードをすべて削除しますか?ユーザーが追加したレコードは削除されません。', + purgeSuccess: '{{count}} 件のサンプルレコードを削除しました。', + purgeSuccess_one: '{{count}} 件のサンプルレコードを削除しました。', + purgeSuccess_other: '{{count}} 件のサンプルレコードを削除しました。', + purgeNoData: '削除するサンプルレコードがありません。', + // objectstack#30323 — the install note's sample-data count, its clear and the app banner. + sampleDataLoaded: 'このインストールで {{count}} 件のサンプルレコードを読み込みました。アプリの動作を確認できます。本番で使い始める前に削除できます。', + sampleDataLoaded_one: 'このインストールで {{count}} 件のサンプルレコードを読み込みました。アプリの動作を確認できます。本番で使い始める前に削除できます。', + sampleDataLoaded_other: 'このインストールで {{count}} 件のサンプルレコードを読み込みました。アプリの動作を確認できます。本番で使い始める前に削除できます。', + sampleDataReloaded: 'もう一度インストールすると、削除したものも含めてサンプルデータが再び読み込まれます。', + purgeConfirmAtMost: '削除されるサンプルレコードは最大 {{count}} 件です。', + purgeConfirmAtMost_one: '削除されるサンプルレコードは最大 {{count}} 件です。', + purgeConfirmAtMost_other: '削除されるサンプルレコードは最大 {{count}} 件です。', + sampleDataBannerTitle: '「{{name}}」にはサンプルデータがあります。', + sampleDataBannerMessage: 'アプリの動作を確認するためのデータです。本番で使い始める前に削除できます。', + reseedQueued: 'サンプルデータは次回の環境アクセス時に再投入されます。', + reseedLocalSuccess: 'サンプルデータを再投入しました:{{inserted}} 件追加、{{updated}} 件更新。', + reseedPartialErrors: '({{count}} 件のレコードの書き込みに失敗しました)', + reseedPartialErrors_one: '({{count}} 件のレコードの書き込みに失敗しました)', + reseedPartialErrors_other: '({{count}} 件のレコードの書き込みに失敗しました)', + sampleDataKernelUnavailable: 'このコントロールプレーンには環境カーネルがないため、ここではサンプルデータを再投入・削除できません。環境自身のランタイムから実行してください。', + updateAvailable: 'アップデートあり', + }, + action: { + installed: "インストール済み", + install: "インストール", + reinstall: "再インストール", + working: "処理中…", + installToCloud: "クラウドにインストール…", + installing: "インストール中…", + updating: "更新中…", + close: "閉じる", + dismiss: "閉じる", + openOnCloud: "クラウドで開く", + backHome: "ホームに戻る", + updateTo: "更新", + }, + install: { + dialogTitle: "{{name}} をインストール", + dialogDescCurrent: "この環境({{host}})にインストールします。", + dialogDescPicker: "このアプリをインストールする環境を選択します。ObjectOS Cloudにサインインする必要があります。", + environment: "環境", + environmentPlaceholder: "環境を選択", + includeSampleData: "サンプルデータを含む", + noEnvs: "アクティブな組織に環境が見つかりません。", + noPermission: "どの環境にもアプリをインストールする権限がありません。組織のオーナーと管理者のみインストールできます。組織の管理者にお問い合わせください。", + signInFirst: "最初にObjectOS Cloudにサインインしてください。", + success: "インストールが完了しました。環境を開いて新しいアプリを確認してください。", + localSuccess: 'このランタイムに v{{version}} をインストールしました。「{{name}}」はアプリスイッチャーに表示されています。', + localLoadsAtRestart: "このランタイムに v{{version}} をインストールしました。「{{name}}」はランタイムの次回再起動時に読み込まれ、それまではアプリスイッチャーに表示されません。", + localManifestConflict: "{{message}}\nヒント:ローカルアプリがすでにこのmanifest_idを使用しています。最初にobjectstack.config.tsから削除してください。", + localUnauthorized: "最初にこのランタイムにサインインしてから再試行してください。", + localMarketplaceUnavailable: "このランタイムにはOS_CLOUD_URLが設定されていないため、マーケットプレイスカタログにアクセスできません。", + updateTo: "更新 → v{{version}}", + installedVersion: "インストール済み v{{version}}", + deploying: "アプリをデプロイしています…", + deployed: "アプリのデプロイが完了しました。", + openApp: "{{name}} を開く", + deployTimeout: "インストールは記録されましたが、この環境ではまだアプリが提供されていません。デプロイ中の可能性があります。しばらくしてから再確認してください。表示されない場合は、パッケージが読み込まれていない、アプリが含まれていない、またはアプリにあなたが持っていない権限が必要な可能性があります。", + checkAgain: "再確認", + // ADR-0131 D6 — the install modes a package's manifest permits, and + // what an unmanaged install answers (MarketplacePackagePage). + mode: { + label: "インストール方式", + managed: "マネージド", + managedHint: "パッケージが管理元のままです。コードとして登録され、アップグレードを受け取り、その項目はここでは編集しません。", + unmanaged: "アンマネージドコピー", + unmanagedHint: "このランタイム自身のメタデータとして一度だけコピーされます。すべての項目をここで編集でき、コピーはアップグレードを受け取りません。", + required: "このパッケージをインストールするには、インストール方式を選択してください。", + }, + unmanagedSuccess: "「{{name}}」の v{{version}} を、このランタイム自身のメタデータとしてベース {{base}} にコピーしました。コピーは編集でき、アップグレードは受け取りません。", + unmanagedStrippedLocks: "パッケージはこれらの項目をロックしていました。コピーは編集可能なため、ロックはコピーされていません:", + unmanagedIgnored: "このランタイムはアンマネージドインストールに対応していないため、「{{name}}」はコピーではなくマネージド(コードとして登録)でインストールされました。ランタイムをアップグレードしてから、パッケージをアンインストールし、アンマネージドコピーとして再度インストールしてください。", + }, + // objectui#12146 — the install dialog opened as an UPDATE of the copy + // this environment holds (MarketplacePackagePage). + update: { + dialogTitle: "{{name}} を更新", + dialogDescCurrent: "この環境({{host}})にインストール済みのアプリを更新します。", + dialogDescPicker: "インストール済みのアプリを更新する環境を選択します。ObjectOS Cloudにサインインする必要があります。", + majorNotice: "メジャーバージョンの更新です(v{{fromMajor}} → v{{toMajor}})。互換性のない変更が含まれる可能性があります。", + releaseNotes: "v{{version}} のリリースノートを読む", + success: "v{{version}} に更新しました。", + deployTimeout: "更新は記録されましたが、この環境ではまだ v{{version}} が提供されていません。デプロイ中の可能性があります。しばらくしてから再確認してください。", + }, + // objectui#3546 — the MarketplacePage "Your organization" strip. + org: { + heading: "組織", + install: "インストール", + installed: "{{name}} をインストールしました", + installedBadge: "インストール済み", + installing: "インストール中…", + }, + // ADR-0025 PD4 §3.5/§3.11 — the pre-install consent panel + // (PluginDisclosure.tsx). `runtime` is a CLOSED enum: spec + // PluginRuntimeSchema = z.enum(['node', 'sandbox', 'worker']). + disclosure: { + containsCode: "このパッケージにはコードが含まれています", + reviewed: "審査・承認済み", + unreviewed: "未審査", + signed: "署名済み", + grantsIntro: "このパッケージは次の権限を要求します:", + notEnforced: "この一覧はインストール時に記録され、新しいバージョンがより多くを要求した場合は再確認されます。ただしランタイムはまだ、このパッケージをこの一覧に制限しません。", + services: "プラットフォームサービス", + hooks: "ライフサイクルフック", + network: "ネットワークアクセス", + fs: "ファイルシステムアクセス", + noPermissions: "特別な権限は要求しません。", + acknowledge: "このパッケージがコードを実行し、上記の権限を取得することを理解しました。", + runtime: { + node: "プロセス内 · 完全信頼", + sandbox: "サンドボックス", + worker: "プロセス外", + }, + }, + uninstall: { + successInDetail: "{{manifestId}} とそのキャッシュされたマニフェストを削除しました。アプリは実行中のカーネルから削除され、その権限セットと権限セットの付与も削除されました。オブジェクトに保存されたレコードはデータベースに残ります。", + }, + accessDenied: { + title: "アプリマーケットプレイスは管理者専用です", + description: "この環境にアプリをインストールする権限がありません。アクセスのために組織のオーナーまたは管理者に連絡してください。", + }, + category: { + crm: "CRM", + erp: "ERP", + hr: "人事", + finance: "財務", + project: "プロジェクト管理", + collaboration: "コラボレーション", + analytics: "分析", + integration: "統合", + automation: "自動化", + ai: "AI", + security: "セキュリティ", + "developer-tools": "開発者ツール", + "ui-theme": "UIテーマ", + storage: "ストレージ", + other: "その他", + }, + relativeTime: { + today: "今日", + daysAgo: "{{count}}日前", + monthsAgo: "{{count}}ヶ月前", + monthsAgo_one: "{{count}}ヶ月前", + monthsAgo_other: "{{count}}ヶ月前", + yearsAgo: "{{count}}年前", + yearsAgo_one: "{{count}}年前", + yearsAgo_other: "{{count}}年前", + }, + }, +}; + +export default ja; diff --git a/ui/packages/i18n/src/namespaces/marketplace/ko.ts b/ui/packages/i18n/src/namespaces/marketplace/ko.ts new file mode 100644 index 00000000000..78aed79f4e5 --- /dev/null +++ b/ui/packages/i18n/src/namespaces/marketplace/ko.ts @@ -0,0 +1,230 @@ +/** + * Korean (ko) — the lazy `marketplace` namespace's catalogue. + * + * A PARTIAL pack: the same shape as `../../locales/ko.ts`, holding only + * the `marketplace` group, which `./index.ts` deep-merges into an i18next + * instance when a marketplace page mounts. `./index.ts` says what the + * namespace is, why it is lazy, and which gates read this file. + * + * ⛔ Keep the declaration `const ko = { … }` and `export default ko`: + * the i18n gates under `ui/scripts/` read this file by AST under that name. + */ +const ko = { + marketplace: { + suggestedBindings: { + promptEveryone: "이 앱은 로그인한 모든 사용자(Everyone 위치)에게 \"{{set}}\"을(를) 부여할 것을 제안합니다.", + promptGuest: "이 앱은 인증되지 않은 방문자(Guest 위치)에게 \"{{set}}\"을(를) 부여할 것을 제안합니다.", + confirm: "부여", + confirming: "부여하는 중…", + dismiss: "닫기", + confirmedToast: "\"{{set}}\"이(가) 이제 {{anchor}}에 부여되었습니다.", + dismissedToast: "\"{{set}}\" 제안을 닫았습니다.", + guestUnsupported: "여기서는 부여할 수 없습니다. Guest 위치의 세트는 승인된 제안에서 파생되지 않습니다.", + acceptedState: "\"{{set}}\"이(가) 로그인한 모든 사용자(Everyone 위치)에 대해 승인되었습니다.", + notInForceState: "\"{{set}}\"이(가) 승인되었지만 아직 적용되지 않았습니다. 이 요청으로 Everyone 위치가 변경되지 않았습니다. 이유는 서버 로그에 기록되어 있습니다.", + dismissedState: "\"{{set}}\"은(는) 부여되지 않았습니다. 제안이 닫혔거나 취소되었습니다.", + revoke: "취소", + revoking: "취소하는 중…", + revokedToast: "\"{{set}}\" 승인을 취소했습니다.", + reopen: "다시 검토", + reopening: "다시 여는 중…", + reopenedToast: "\"{{set}}\" 제안을 다시 열었습니다. 승인될 때까지 아무것도 부여되지 않습니다.", + }, + title: "앱 마켓플레이스", + subtitle: "ObjectStack 카탈로그에 게시된 승인된 앱을 탐색하세요. 앱을 클릭하여 세부 정보를 보고 환경 중 하나에 설치하세요.", + searchPlaceholder: '이름 또는 앱 ID로 앱 검색…', + searchAria: "마켓플레이스 앱 검색", + installed: "설치됨", + installedCount: "설치됨 ({{count}})", + refresh: "새로고침", + all: "전체", + noApprovedYet: "아직 마켓플레이스에 승인된 앱이 없습니다.", + noMatchFilters: "필터와 일치하는 앱이 없습니다.", + noDescription: "사용 가능한 설명이 없습니다.", + back: "마켓플레이스로 돌아가기", + browseLink: "마켓플레이스 탐색 →", + versionBadge: "v{{version}}", + installedBadge: "v{{version}} 설치됨", + load: { + failed: "마켓플레이스를 로드할 수 없습니다", + failedHintConfigured: "이 런타임은 {{url}} 의 컨트롤 플레인을 통해 마켓플레이스에 접속합니다. 해당 주소가 온라인이고 여기에서 접근 가능한지 확인하세요.", + failedHintSameOrigin: "마켓플레이스 카탈로그는 이 런타임을 통해 접근합니다. 런타임이 온라인이고 카탈로그에 접근할 수 있는지 확인하세요.", + loading: "마켓플레이스 카탈로그를 불러오는 중…", + slow: "아직 불러오는 중입니다 — 마켓플레이스 카탈로그의 응답이 느립니다.", + retry: "다시 시도", + unreachable: "마켓플레이스 카탈로그에 연결할 수 없습니다.", + answeredError: "마켓플레이스 카탈로그가 오류로 응답했습니다(HTTP {{status}}).", + packageFailed: "패키지를 로드할 수 없습니다", + notFound: "찾을 수 없습니다.", + }, + disabled: { + title: "앱 마켓플레이스가 꺼져 있습니다", + description: "이 런타임에는 마켓플레이스가 구성되어 있지 않아 여기에서 둘러보거나 설치할 항목이 없습니다.", + hint: "런타임을 <code>OS_CLOUD_URL=off</code>(또는 <code>none</code>, <code>local</code>, <code>disabled</code>)로 시작하면 이것이 정상 상태입니다. 마켓플레이스를 켜려면 <code>OS_CLOUD_URL</code>을 컨트롤 플레인으로 지정하고 런타임을 다시 시작하세요.", + }, + detail: { + homepage: "홈페이지", + installedV: "설치됨 · v{{version}}", + about: "정보", + noReadme: "사용 가능한 README 없음.", + versions: "버전", + noApprovedVersions: "승인된 버전 없음.", + prerelease: "프리릴리즈", + moreOptions: "추가 설치 옵션", + uninstallFromRuntime: "이 런타임에서 제거", + addSampleData: '샘플 데이터 추가', + reseedAgain: '샘플 데이터 다시 적재', + purgeSampleData: '샘플 데이터 삭제', + purgeConfirm: '이 패키지가 생성한 모든 샘플 레코드를 삭제하시겠습니까? 사용자가 추가한 레코드는 삭제되지 않습니다.', + purgeSuccess: '샘플 레코드 {{count}}개를 삭제했습니다.', + purgeSuccess_one: '샘플 레코드 {{count}}개를 삭제했습니다.', + purgeSuccess_other: '샘플 레코드 {{count}}개를 삭제했습니다.', + purgeNoData: '삭제할 샘플 레코드가 없습니다.', + // objectstack#30323 — the install note's sample-data count, its clear and the app banner. + sampleDataLoaded: '이번 설치로 샘플 레코드 {{count}}개를 불러왔습니다. 앱이 동작하는 모습을 확인할 수 있습니다. 실제로 사용하기 전에 삭제할 수 있습니다.', + sampleDataLoaded_one: '이번 설치로 샘플 레코드 {{count}}개를 불러왔습니다. 앱이 동작하는 모습을 확인할 수 있습니다. 실제로 사용하기 전에 삭제할 수 있습니다.', + sampleDataLoaded_other: '이번 설치로 샘플 레코드 {{count}}개를 불러왔습니다. 앱이 동작하는 모습을 확인할 수 있습니다. 실제로 사용하기 전에 삭제할 수 있습니다.', + sampleDataReloaded: '다시 설치하면 삭제한 것을 포함해 샘플 데이터를 다시 불러옵니다.', + purgeConfirmAtMost: '최대 {{count}}개의 샘플 레코드가 삭제됩니다.', + purgeConfirmAtMost_one: '최대 {{count}}개의 샘플 레코드가 삭제됩니다.', + purgeConfirmAtMost_other: '최대 {{count}}개의 샘플 레코드가 삭제됩니다.', + sampleDataBannerTitle: '"{{name}}"에 샘플 데이터가 있습니다.', + sampleDataBannerMessage: '앱이 동작하는 모습을 확인하기 위한 데이터입니다. 실제로 사용하기 전에 삭제할 수 있습니다.', + reseedQueued: '샘플 데이터는 다음 환경 접속 시 다시 적재됩니다.', + reseedLocalSuccess: '샘플 데이터를 다시 적재했습니다: {{inserted}}개 추가, {{updated}}개 업데이트.', + reseedPartialErrors: '({{count}}개 레코드 쓰기 실패)', + reseedPartialErrors_one: '({{count}}개 레코드 쓰기 실패)', + reseedPartialErrors_other: '({{count}}개 레코드 쓰기 실패)', + sampleDataKernelUnavailable: '이 컨트롤 플레인에는 환경 커널이 없어 여기서는 샘플 데이터를 다시 적재하거나 삭제할 수 없습니다. 해당 환경 자체의 런타임에서 실행하세요.', + updateAvailable: '업데이트 가능', + }, + action: { + installed: "설치됨", + install: "설치", + reinstall: "재설치", + working: "처리 중…", + installToCloud: "클라우드에 설치…", + installing: "설치 중…", + updating: "업데이트 중…", + close: "닫기", + dismiss: "닫기", + openOnCloud: "클라우드에서 열기", + backHome: "홈으로 돌아가기", + updateTo: "업데이트", + }, + install: { + dialogTitle: "{{name}} 설치", + dialogDescCurrent: "이 환경에 설치 ({{host}}).", + dialogDescPicker: "이 앱을 설치할 환경을 선택하세요. ObjectOS Cloud에 로그인되어 있어야 합니다.", + environment: "환경", + environmentPlaceholder: "환경 선택", + includeSampleData: "샘플 데이터 포함", + noEnvs: "활성 조직에서 환경을 찾을 수 없습니다.", + noPermission: "환경에 앱을 설치할 권한이 없습니다. 조직 소유자 및 관리자만 설치할 수 있습니다 — 조직 관리자에게 문의하세요.", + signInFirst: "먼저 ObjectOS Cloud에 로그인해야 합니다.", + success: "설치 성공. 환경을 열어 새 앱을 확인하세요.", + localSuccess: '이 런타임에 v{{version}}을(를) 설치했습니다. 이제 앱 전환기에 "{{name}}"이(가) 있습니다.', + localLoadsAtRestart: "이 런타임에 v{{version}} 설치됨. \"{{name}}\"은(는) 런타임이 다음에 다시 시작될 때 로드되며, 그때까지는 앱 전환기에 표시되지 않습니다.", + localManifestConflict: "{{message}}\n참고: 로컬 앱이 이미 이 manifest_id를 가지고 있습니다. 먼저 objectstack.config.ts에서 제거하세요.", + localUnauthorized: "먼저 이 런타임에 로그인하세요.", + localMarketplaceUnavailable: "이 런타임에 OS_CLOUD_URL이 구성되지 않았습니다.", + updateTo: "업데이트 → v{{version}}", + installedVersion: "v{{version}} 설치됨", + deploying: "앱을 배포하는 중…", + deployed: "앱이 배포되어 사용할 준비가 되었습니다.", + openApp: "{{name}} 열기", + deployTimeout: "설치는 기록되었지만 이 환경에서 아직 앱을 제공하지 않습니다. 아직 배포 중일 수 있으니 잠시 후 다시 확인하세요. 계속 나타나지 않으면 패키지가 로드되지 않았거나, 앱이 포함되어 있지 않거나, 앱에 사용자에게 없는 권한이 필요할 수 있습니다.", + checkAgain: "다시 확인", + // ADR-0131 D6 — the install modes a package's manifest permits, and + // what an unmanaged install answers (MarketplacePackagePage). + mode: { + label: "설치 방식", + managed: "관리형", + managedHint: "패키지가 계속 원본으로 남습니다. 코드로 등록되고 업그레이드를 받으며, 항목은 여기에서 편집하지 않습니다.", + unmanaged: "비관리형 사본", + unmanagedHint: "이 런타임의 자체 메타데이터로 한 번 복사됩니다. 모든 항목을 여기에서 편집할 수 있으며, 사본은 업그레이드를 받지 않습니다.", + required: "이 패키지를 설치하려면 설치 방식을 선택하세요.", + }, + unmanagedSuccess: "\"{{name}}\" v{{version}}을(를) 이 런타임의 자체 메타데이터로 베이스 {{base}}에 복사했습니다. 사본은 편집할 수 있으며 업그레이드를 받지 않습니다.", + unmanagedStrippedLocks: "패키지가 이 항목들을 잠갔습니다. 사본은 편집할 수 있으므로 잠금은 복사되지 않았습니다:", + unmanagedIgnored: "이 런타임은 비관리형 설치를 지원하지 않으므로 \"{{name}}\"이(가) 복사되지 않고 관리형(코드로 등록)으로 설치되었습니다. 런타임을 업그레이드한 다음 패키지를 제거하고 비관리형 사본으로 다시 설치하세요.", + }, + // objectui#12146 — the install dialog opened as an UPDATE of the copy + // this environment holds (MarketplacePackagePage). + update: { + dialogTitle: "{{name}} 업데이트", + dialogDescCurrent: "이 환경({{host}})에 설치된 앱을 업데이트합니다.", + dialogDescPicker: "설치된 앱을 업데이트할 환경을 선택하세요. ObjectOS Cloud에 로그인되어 있어야 합니다.", + majorNotice: "메이저 버전 업데이트입니다(v{{fromMajor}} → v{{toMajor}}). 호환되지 않는 변경이 포함될 수 있습니다.", + releaseNotes: "v{{version}} 릴리스 노트 읽기", + success: "v{{version}}(으)로 업데이트했습니다.", + deployTimeout: "업데이트는 기록되었지만 이 환경에서 아직 v{{version}}을(를) 제공하지 않습니다. 아직 배포 중일 수 있으니 잠시 후 다시 확인하세요.", + }, + // objectui#3546 — the MarketplacePage "Your organization" strip. + org: { + heading: "조직", + install: "설치", + installed: "{{name}} 설치됨", + installedBadge: "설치됨", + installing: "설치 중…", + }, + // ADR-0025 PD4 §3.5/§3.11 — the pre-install consent panel + // (PluginDisclosure.tsx). `runtime` is a CLOSED enum: spec + // PluginRuntimeSchema = z.enum(['node', 'sandbox', 'worker']). + disclosure: { + containsCode: "이 패키지에는 코드가 포함되어 있습니다", + reviewed: "검토 및 승인됨", + unreviewed: "아직 검토되지 않음", + signed: "서명됨", + grantsIntro: "이 패키지가 요청하는 권한:", + notEnforced: "이 목록은 설치 시 기록되며 이후 버전이 더 많은 권한을 요청하면 다시 확인합니다. 다만 런타임은 아직 패키지를 이 목록으로 제한하지 않습니다.", + services: "플랫폼 서비스", + hooks: "라이프사이클 후크", + network: "네트워크 액세스", + fs: "파일 시스템 액세스", + noPermissions: "특별한 권한을 요청하지 않습니다.", + acknowledge: "이 패키지가 코드를 실행하고 위 권한을 부여받는다는 점을 이해합니다.", + runtime: { + node: "프로세스 내 · 완전 신뢰", + sandbox: "샌드박스", + worker: "프로세스 외부", + }, + }, + uninstall: { + successInDetail: "{{manifestId}} 및 캐시된 매니페스트 제거됨. 앱이 실행 중인 커널에서 제거되었으며, 권한 세트와 그 부여 내역도 함께 제거되었습니다. 앱의 객체에 저장된 레코드는 데이터베이스에 남습니다.", + }, + accessDenied: { + title: "관리자 전용 앱 마켓플레이스", + description: "이 환경에 앱을 설치할 권한이 없습니다.", + }, + category: { + crm: "CRM", + erp: "ERP", + hr: "인사", + finance: "재무", + project: "프로젝트 관리", + collaboration: "협업", + analytics: "분석", + integration: "통합", + automation: "자동화", + ai: "AI", + security: "보안", + "developer-tools": "개발자 도구", + "ui-theme": "UI 테마", + storage: "스토리지", + other: "기타", + }, + relativeTime: { + today: "오늘", + daysAgo: "{{count}}일 전", + monthsAgo: "{{count}}개월 전", + monthsAgo_one: "{{count}}개월 전", + monthsAgo_other: "{{count}}개월 전", + yearsAgo: "{{count}}년 전", + yearsAgo_one: "{{count}}년 전", + yearsAgo_other: "{{count}}년 전", + }, + }, +}; + +export default ko; diff --git a/ui/packages/i18n/src/namespaces/marketplace/pt.ts b/ui/packages/i18n/src/namespaces/marketplace/pt.ts new file mode 100644 index 00000000000..972a70a2d16 --- /dev/null +++ b/ui/packages/i18n/src/namespaces/marketplace/pt.ts @@ -0,0 +1,236 @@ +/** + * Portuguese (pt) — the lazy `marketplace` namespace's catalogue. + * + * A PARTIAL pack: the same shape as `../../locales/pt.ts`, holding only + * the `marketplace` group, which `./index.ts` deep-merges into an i18next + * instance when a marketplace page mounts. `./index.ts` says what the + * namespace is, why it is lazy, and which gates read this file. + * + * ⛔ Keep the declaration `const pt = { … }` and `export default pt`: + * the i18n gates under `ui/scripts/` read this file by AST under that name. + */ +const pt = { + marketplace: { + suggestedBindings: { + promptEveryone: "Este app sugere conceder \"{{set}}\" a todos os usuários autenticados (a posição Everyone).", + promptGuest: "Este app sugere conceder \"{{set}}\" a visitantes não autenticados (a posição Guest).", + confirm: "Conceder", + confirming: "Concedendo…", + dismiss: "Dispensar", + confirmedToast: "\"{{set}}\" agora está concedido a {{anchor}}.", + dismissedToast: "Sugestão de \"{{set}}\" dispensada.", + guestUnsupported: "Não é possível conceder aqui: os conjuntos da posição Guest não são derivados de sugestões aceitas.", + acceptedState: "\"{{set}}\" foi aceito para todos os usuários autenticados (a posição Everyone).", + notInForceState: "\"{{set}}\" foi aceito, mas ainda não está em vigor: a posição Everyone não mudou nesta solicitação. O log do servidor informa o motivo.", + dismissedState: "\"{{set}}\" não está concedido: a sugestão foi dispensada ou revogada.", + revoke: "Revogar", + revoking: "Revogando…", + revokedToast: "Aceitação de \"{{set}}\" revogada.", + reopen: "Reconsiderar", + reopening: "Reabrindo…", + reopenedToast: "Sugestão para \"{{set}}\" reaberta: nada é concedido até que seja aceita.", + }, + title: "Marketplace de aplicativos", + subtitle: "Explore aplicativos aprovados publicados no catálogo ObjectStack. Clique em um aplicativo para ver os detalhes e instalá-lo em um de seus ambientes.", + searchPlaceholder: 'Pesquisar aplicativos por nome ou ID do aplicativo…', + searchAria: "Pesquisar aplicativos do marketplace", + installed: "Instalado", + installedCount: "Instalados ({{count}})", + refresh: "Atualizar", + all: "Todos", + noApprovedYet: "Nenhum aplicativo aprovado para o marketplace ainda.", + noMatchFilters: "Nenhum aplicativo corresponde aos seus filtros.", + noDescription: "Sem descrição disponível.", + back: "Voltar ao marketplace", + browseLink: "Explorar marketplace →", + versionBadge: "v{{version}}", + installedBadge: "Instalado v{{version}}", + load: { + failed: "Não foi possível carregar o marketplace", + failedHintConfigured: "Este runtime acessa o marketplace através do plano de controle em {{url}}. Verifique se ele está online e acessível a partir daqui.", + failedHintSameOrigin: "O catálogo do marketplace é acessado através deste runtime. Verifique se o runtime está online e consegue acessar o catálogo.", + loading: "Carregando o catálogo do marketplace…", + slow: "Ainda carregando — o catálogo do marketplace está demorando para responder.", + retry: "Tentar novamente", + unreachable: "Não foi possível acessar o catálogo do marketplace.", + answeredError: "O catálogo do marketplace respondeu com um erro (HTTP {{status}}).", + packageFailed: "Não foi possível carregar o pacote", + notFound: "Não encontrado.", + }, + disabled: { + title: "O marketplace de aplicativos está desativado", + description: "Este runtime não tem marketplace configurado, portanto não há nada para explorar ou instalar aqui.", + hint: "Esse é o estado esperado quando o runtime é iniciado com <code>OS_CLOUD_URL=off</code> (ou <code>none</code>, <code>local</code>, <code>disabled</code>). Para ativar o marketplace, aponte <code>OS_CLOUD_URL</code> para um plano de controle e reinicie o runtime.", + }, + detail: { + homepage: "Página inicial", + installedV: "Instalado · v{{version}}", + about: "Sobre", + noReadme: "Sem readme disponível.", + versions: "Versões", + noApprovedVersions: "Sem versões aprovadas.", + prerelease: "Pré-lançamento", + moreOptions: "Mais opções de instalação", + uninstallFromRuntime: "Desinstalar deste runtime", + addSampleData: 'Adicionar dados de exemplo', + reseedAgain: 'Recarregar dados de exemplo', + purgeSampleData: 'Remover dados de exemplo', + purgeConfirm: 'Excluir todos os registros de exemplo criados por este pacote? Os registros adicionados pelo usuário NÃO serão afetados.', + purgeSuccess: '{{count}} registro(s) de exemplo removido(s).', + purgeSuccess_one: '{{count}} registro de exemplo removido.', + purgeSuccess_many: '{{count}} de registros de exemplo removidos.', + purgeSuccess_other: '{{count}} registros de exemplo removidos.', + purgeNoData: 'Nenhum registro de exemplo encontrado para remover.', + // objectstack#30323 — the install note's sample-data count, its clear and the app banner. + sampleDataLoaded: 'Esta instalação carregou {{count}} registro(s) de exemplo para você ver o aplicativo funcionando. Remova-os quando começar a usá-lo de verdade.', + sampleDataLoaded_one: 'Esta instalação carregou {{count}} registro de exemplo para você ver o aplicativo funcionando. Remova-o quando começar a usá-lo de verdade.', + sampleDataLoaded_many: 'Esta instalação carregou {{count}} de registros de exemplo para você ver o aplicativo funcionando. Remova-os quando começar a usá-lo de verdade.', + sampleDataLoaded_other: 'Esta instalação carregou {{count}} registros de exemplo para você ver o aplicativo funcionando. Remova-os quando começar a usá-lo de verdade.', + sampleDataReloaded: 'Instalar de novo carrega os dados de exemplo outra vez, inclusive os que você removeu.', + purgeConfirmAtMost: 'No máximo {{count}} registro(s) de exemplo serão excluídos.', + purgeConfirmAtMost_one: 'No máximo {{count}} registro de exemplo será excluído.', + purgeConfirmAtMost_many: 'No máximo {{count}} de registros de exemplo serão excluídos.', + purgeConfirmAtMost_other: 'No máximo {{count}} registros de exemplo serão excluídos.', + sampleDataBannerTitle: '"{{name}}" tem dados de exemplo.', + sampleDataBannerMessage: 'Eles estão lá para você ver o aplicativo funcionando. Remova-os quando começar a usá-lo de verdade.', + reseedQueued: 'Os dados de exemplo serão recarregados no próximo acesso ao ambiente.', + reseedLocalSuccess: 'Dados de exemplo recarregados: {{inserted}} inseridos, {{updated}} atualizados.', + reseedPartialErrors: '({{count}} registro(s) não puderam ser gravados)', + reseedPartialErrors_one: '({{count}} registro não pôde ser gravado)', + reseedPartialErrors_many: '({{count}} de registros não puderam ser gravados)', + reseedPartialErrors_other: '({{count}} registros não puderam ser gravados)', + sampleDataKernelUnavailable: 'Este plano de controle não tem um kernel de ambiente, portanto os dados de exemplo não podem ser recarregados nem removidos aqui. Faça isso no runtime do próprio ambiente.', + updateAvailable: 'Atualização disponível', + }, + action: { + installed: "Instalado", + install: "Instalar", + reinstall: "Reinstalar", + working: "Processando…", + installToCloud: "Instalar na nuvem…", + installing: "Instalando…", + updating: "Atualizando…", + close: "Fechar", + dismiss: "Fechar", + openOnCloud: "Abrir na nuvem", + backHome: "Voltar ao início", + updateTo: "Atualizar", + }, + install: { + dialogTitle: "Instalar {{name}}", + dialogDescCurrent: "Instalar neste ambiente ({{host}}).", + dialogDescPicker: "Escolha um ambiente para instalar este aplicativo. Você deve estar conectado ao ObjectOS Cloud.", + environment: "Ambiente", + environmentPlaceholder: "Selecionar ambiente", + includeSampleData: "Incluir dados de amostra", + noEnvs: "Nenhum ambiente encontrado em sua organização ativa.", + noPermission: "Você não tem permissão para instalar aplicativos em um ambiente. Somente proprietários e administradores da organização podem instalar — fale com o administrador da sua organização.", + signInFirst: "Você deve primeiro fazer login no ObjectOS Cloud.", + success: "Instalado com sucesso. Abra o ambiente para ver o novo aplicativo.", + localSuccess: 'v{{version}} instalado neste runtime. "{{name}}" já está no seletor de aplicativos.', + localLoadsAtRestart: "v{{version}} instalado neste runtime. \"{{name}}\" será carregado na próxima reinicialização do runtime e não aparecerá no seletor de aplicativos até lá.", + localManifestConflict: "{{message}}\nNota: Um aplicativo local já possui este manifest_id. Remova-o primeiro de objectstack.config.ts.", + localUnauthorized: "Faça login neste runtime primeiro.", + localMarketplaceUnavailable: "Este runtime não tem OS_CLOUD_URL configurado.", + updateTo: "Atualizar → v{{version}}", + installedVersion: "Instalado v{{version}}", + deploying: "Implantando o aplicativo…", + deployed: "O aplicativo foi implantado e está pronto.", + openApp: "Abrir {{name}}", + deployTimeout: "A instalação foi registrada, mas este ambiente ainda não está servindo o aplicativo. Ele pode ainda estar sendo implantado — verifique novamente em instantes. Se nunca aparecer, o pacote pode não ter sido carregado, pode não incluir nenhum aplicativo ou o aplicativo pode exigir uma permissão que você não tem.", + checkAgain: "Verificar novamente", + // ADR-0131 D6 — the install modes a package's manifest permits, and + // what an unmanaged install answers (MarketplacePackagePage). + mode: { + label: "Modo de instalação", + managed: "Gerenciado", + managedHint: "O pacote continua sendo a autoridade: é registrado como código, recebe suas atualizações e seus itens não são editados aqui.", + unmanaged: "Cópia não gerenciada", + unmanagedHint: "Copiado uma única vez para este runtime como seus próprios metadados: cada item pode ser editado aqui, e a cópia não recebe atualizações.", + required: "Escolha um modo de instalação para instalar este pacote.", + }, + unmanagedSuccess: "v{{version}} de \"{{name}}\" copiado para este runtime como seus próprios metadados, na base {{base}}. A cópia pode ser editada e não recebe atualizações.", + unmanagedStrippedLocks: "O pacote bloqueava estes itens. A cópia pode ser editada, então seus bloqueios não foram copiados:", + unmanagedIgnored: "Este runtime não oferece suporte a instalações não gerenciadas, então \"{{name}}\" foi instalado como gerenciado (registrado como código) em vez de copiado. Atualize o runtime, depois desinstale o pacote e instale-o novamente como cópia não gerenciada.", + }, + // objectui#12146 — the install dialog opened as an UPDATE of the copy + // this environment holds (MarketplacePackagePage). + update: { + dialogTitle: "Atualizar {{name}}", + dialogDescCurrent: "Atualizar a cópia instalada neste ambiente ({{host}}).", + dialogDescPicker: "Escolha o ambiente cuja cópia instalada deseja atualizar. Você deve estar conectado ao ObjectOS Cloud.", + majorNotice: "Esta é uma atualização de versão principal (v{{fromMajor}} → v{{toMajor}}), que pode incluir alterações incompatíveis.", + releaseNotes: "Ler as notas da versão v{{version}}", + success: "Atualizado para a v{{version}}.", + deployTimeout: "A atualização foi registrada, mas este ambiente ainda não está servindo a v{{version}}. Ela pode ainda estar sendo implantada — verifique novamente em instantes.", + }, + // objectui#3546 — the MarketplacePage "Your organization" strip. + org: { + heading: "Sua organização", + install: "Instalar", + installed: "{{name}} instalado", + installedBadge: "Instalado", + installing: "Instalando…", + }, + // ADR-0025 PD4 §3.5/§3.11 — the pre-install consent panel + // (PluginDisclosure.tsx). `runtime` is a CLOSED enum: spec + // PluginRuntimeSchema = z.enum(['node', 'sandbox', 'worker']). + disclosure: { + containsCode: "Este pacote contém código", + reviewed: "Revisado e aprovado", + unreviewed: "Ainda não revisado", + signed: "Assinado", + grantsIntro: "Este pacote solicita:", + notEnforced: "Esta lista é registada na instalação e reconfirmada se uma versão posterior pedir mais, mas o runtime ainda não restringe o pacote a ela.", + services: "Serviços da plataforma", + hooks: "Hooks de ciclo de vida", + network: "Acesso à rede", + fs: "Acesso ao sistema de arquivos", + noPermissions: "Não solicita permissões especiais.", + acknowledge: "Entendo que este pacote executa código e recebe as permissões acima.", + runtime: { + node: "No processo · confiança total", + sandbox: "Em sandbox", + worker: "Fora do processo", + }, + }, + uninstall: { + successInDetail: "{{manifestId}} e seu manifesto em cache removidos: o aplicativo saiu do kernel em execução, junto com seus conjuntos de permissões e as respectivas atribuições. Os registros armazenados em seus objetos permanecem no banco de dados.", + }, + accessDenied: { + title: "Marketplace somente para administradores", + description: "Você não tem permissão para instalar aplicativos neste ambiente.", + }, + category: { + crm: "CRM", + erp: "ERP", + hr: "RH", + finance: "Finanças", + project: "Gestão de projetos", + collaboration: "Colaboração", + analytics: "Análise", + integration: "Integração", + automation: "Automação", + ai: "IA", + security: "Segurança", + "developer-tools": "Ferramentas de desenvolvedor", + "ui-theme": "Tema de UI", + storage: "Armazenamento", + other: "Outro", + }, + relativeTime: { + today: "hoje", + daysAgo: "há {{count}}d", + monthsAgo: "há {{count}} meses", + monthsAgo_one: "há {{count}} mês", + monthsAgo_many: "há {{count}} meses", + monthsAgo_other: "há {{count}} meses", + yearsAgo: "há {{count}} anos", + yearsAgo_one: "há {{count}} ano", + yearsAgo_many: "há {{count}} anos", + yearsAgo_other: "há {{count}} anos", + }, + }, +}; + +export default pt; diff --git a/ui/packages/i18n/src/namespaces/marketplace/ru.ts b/ui/packages/i18n/src/namespaces/marketplace/ru.ts new file mode 100644 index 00000000000..d5a78ae1ffc --- /dev/null +++ b/ui/packages/i18n/src/namespaces/marketplace/ru.ts @@ -0,0 +1,242 @@ +/** + * Russian (ru) — the lazy `marketplace` namespace's catalogue. + * + * A PARTIAL pack: the same shape as `../../locales/ru.ts`, holding only + * the `marketplace` group, which `./index.ts` deep-merges into an i18next + * instance when a marketplace page mounts. `./index.ts` says what the + * namespace is, why it is lazy, and which gates read this file. + * + * ⛔ Keep the declaration `const ru = { … }` and `export default ru`: + * the i18n gates under `ui/scripts/` read this file by AST under that name. + */ +const ru = { + marketplace: { + suggestedBindings: { + promptEveryone: "Приложение предлагает предоставить «{{set}}» всем вошедшим пользователям (позиция Everyone).", + promptGuest: "Приложение предлагает предоставить «{{set}}» неаутентифицированным посетителям (позиция Guest).", + confirm: "Предоставить", + confirming: "Предоставление…", + dismiss: "Закрыть", + confirmedToast: "«{{set}}» теперь предоставлен: {{anchor}}.", + dismissedToast: "Предложение по «{{set}}» закрыто.", + guestUnsupported: "Здесь предоставить нельзя: наборы позиции Guest не выводятся из принятых предложений.", + acceptedState: "«{{set}}» принят для всех вошедших пользователей (позиция Everyone).", + notInForceState: "«{{set}}» принят, но ещё не действует: этот запрос не изменил позицию Everyone. Причина указана в журнале сервера.", + dismissedState: "«{{set}}» не предоставлен: предложение закрыто или отозвано.", + revoke: "Отозвать", + revoking: "Отзыв…", + revokedToast: "Принятие «{{set}}» отозвано.", + reopen: "Пересмотреть", + reopening: "Открывается снова…", + reopenedToast: "Предложение для «{{set}}» снова открыто: ничего не предоставляется, пока оно не принято.", + }, + title: "Маркетплейс приложений", + subtitle: "Просматривайте одобренные приложения из каталога ObjectStack. Нажмите на приложение, чтобы увидеть подробности и установить его.", + searchPlaceholder: 'Поиск приложений по имени или ID приложения…', + searchAria: "Поиск в маркетплейсе", + installed: "Установлено", + installedCount: "Установлено ({{count}})", + refresh: "Обновить", + all: "Все", + noApprovedYet: "Пока нет одобренных приложений.", + noMatchFilters: "Нет приложений, соответствующих фильтрам.", + noDescription: "Описание отсутствует.", + back: "Назад в маркетплейс", + browseLink: "Маркетплейс →", + versionBadge: "v{{version}}", + installedBadge: "Установлено v{{version}}", + load: { + failed: "Не удалось загрузить маркетплейс", + failedHintConfigured: "Этот рантайм обращается к маркетплейсу через control plane по адресу {{url}}. Убедитесь, что он доступен и достижим отсюда.", + failedHintSameOrigin: "Каталог маркетплейса доступен через этот рантайм. Убедитесь, что рантайм онлайн и имеет доступ к каталогу.", + loading: "Загрузка каталога маркетплейса…", + slow: "Всё ещё загружается — каталог маркетплейса медленно отвечает.", + retry: "Повторить", + unreachable: "Не удалось связаться с каталогом маркетплейса.", + answeredError: "Каталог маркетплейса ответил ошибкой (HTTP {{status}}).", + packageFailed: "Не удалось загрузить пакет", + notFound: "Не найдено.", + }, + disabled: { + title: "Маркетплейс приложений отключён", + description: "Для этого рантайма не настроен маркетплейс, поэтому здесь нечего просматривать или устанавливать.", + hint: "Это ожидаемое состояние, когда рантайм запущен с <code>OS_CLOUD_URL=off</code> (или <code>none</code>, <code>local</code>, <code>disabled</code>). Чтобы включить маркетплейс, укажите в <code>OS_CLOUD_URL</code> адрес control plane и перезапустите рантайм.", + }, + detail: { + homepage: "Сайт", + installedV: "Установлено · v{{version}}", + about: "О приложении", + noReadme: "README отсутствует.", + versions: "Версии", + noApprovedVersions: "Нет одобренных версий.", + prerelease: "Пред-релиз", + moreOptions: "Другие варианты установки", + uninstallFromRuntime: "Удалить из рантайма", + addSampleData: 'Добавить демоданные', + reseedAgain: 'Перезагрузить демоданные', + purgeSampleData: 'Удалить демоданные', + purgeConfirm: 'Удалить все демонстрационные записи, созданные этим пакетом? Записи, добавленные пользователем, НЕ будут затронуты.', + purgeSuccess: 'Удалено демонстрационных записей: {{count}}.', + purgeSuccess_one: 'Удалено демонстрационных записей: {{count}}.', + purgeSuccess_few: 'Удалено демонстрационных записей: {{count}}.', + purgeSuccess_many: 'Удалено демонстрационных записей: {{count}}.', + purgeSuccess_other: 'Удалено демонстрационных записей: {{count}}.', + purgeNoData: 'Демонстрационные записи для удаления не найдены.', + // objectstack#30323 — the install note's sample-data count, its clear and the app banner. + sampleDataLoaded: 'Эта установка загрузила демонстрационных записей: {{count}}, чтобы вы увидели приложение в работе. Удалите их, когда начнёте работать по-настоящему.', + sampleDataLoaded_one: 'Эта установка загрузила демонстрационных записей: {{count}}, чтобы вы увидели приложение в работе. Удалите их, когда начнёте работать по-настоящему.', + sampleDataLoaded_few: 'Эта установка загрузила демонстрационных записей: {{count}}, чтобы вы увидели приложение в работе. Удалите их, когда начнёте работать по-настоящему.', + sampleDataLoaded_many: 'Эта установка загрузила демонстрационных записей: {{count}}, чтобы вы увидели приложение в работе. Удалите их, когда начнёте работать по-настоящему.', + sampleDataLoaded_other: 'Эта установка загрузила демонстрационных записей: {{count}}, чтобы вы увидели приложение в работе. Удалите их, когда начнёте работать по-настоящему.', + sampleDataReloaded: 'Повторная установка снова загружает демоданные, включая удалённые.', + purgeConfirmAtMost: 'Будет удалено не более {{count}} демонстрационных записей.', + purgeConfirmAtMost_one: 'Будет удалено не более {{count}} демонстрационной записи.', + purgeConfirmAtMost_few: 'Будет удалено не более {{count}} демонстрационных записей.', + purgeConfirmAtMost_many: 'Будет удалено не более {{count}} демонстрационных записей.', + purgeConfirmAtMost_other: 'Будет удалено не более {{count}} демонстрационных записей.', + sampleDataBannerTitle: 'В «{{name}}» есть демоданные.', + sampleDataBannerMessage: 'Они нужны, чтобы вы увидели приложение в работе. Удалите их, когда начнёте работать по-настоящему.', + reseedQueued: 'Демоданные будут перезагружены при следующем доступе к среде.', + reseedLocalSuccess: 'Демоданные перезагружены: добавлено {{inserted}}, обновлено {{updated}}.', + reseedPartialErrors: '(не удалось записать записей: {{count}})', + reseedPartialErrors_one: '(не удалось записать записей: {{count}})', + reseedPartialErrors_few: '(не удалось записать записей: {{count}})', + reseedPartialErrors_many: '(не удалось записать записей: {{count}})', + reseedPartialErrors_other: '(не удалось записать записей: {{count}})', + sampleDataKernelUnavailable: 'У этой control plane нет ядра среды, поэтому здесь нельзя перезагрузить или удалить демоданные. Выполните это действие в собственном рантайме среды.', + updateAvailable: 'Доступно обновление', + }, + action: { + installed: "Установлено", + install: "Установить", + reinstall: "Переустановить", + working: "Обработка…", + installToCloud: "Установить в облако…", + installing: "Установка…", + updating: "Обновление…", + close: "Закрыть", + dismiss: "Закрыть", + openOnCloud: "Открыть в облаке", + backHome: "На главную", + updateTo: "Обновить", + }, + install: { + dialogTitle: "Установить {{name}}", + dialogDescCurrent: "Установить в эту среду ({{host}}).", + dialogDescPicker: "Выберите среду для установки. Требуется вход в ObjectOS Cloud.", + environment: "Среда", + environmentPlaceholder: "Выбрать среду", + includeSampleData: "Включить образцы данных", + noEnvs: "В активной организации нет сред.", + noPermission: "Нет прав на установку. Только владельцы и администраторы организации могут устанавливать — обратитесь к администратору вашей организации.", + signInFirst: "Сначала войдите в ObjectOS Cloud.", + success: "Установлено. Откройте среду, чтобы увидеть новое приложение.", + localSuccess: 'v{{version}} установлено в эту среду выполнения. «{{name}}» теперь есть в переключателе приложений.', + localLoadsAtRestart: "v{{version}} установлено. \"{{name}}\" загрузится при следующем перезапуске среды выполнения и до этого не появится в переключателе.", + localManifestConflict: "{{message}}\nПримечание: Локальное приложение уже имеет этот manifest_id. Сначала удалите его из objectstack.config.ts.", + localUnauthorized: "Сначала войдите в этот рантайм.", + localMarketplaceUnavailable: "OS_CLOUD_URL не настроен.", + updateTo: "Обновить → v{{version}}", + installedVersion: "Установлено v{{version}}", + deploying: "Развёртывание приложения…", + deployed: "Приложение развёрнуто и готово к работе.", + openApp: "Открыть {{name}}", + deployTimeout: "Установка записана, но эта среда пока не предоставляет приложение. Возможно, развёртывание ещё идёт — проверьте снова чуть позже. Если оно так и не появится, пакет мог не загрузиться, не содержать приложения или требовать разрешение, которого у вас нет.", + checkAgain: "Проверить снова", + // ADR-0131 D6 — the install modes a package's manifest permits, and + // what an unmanaged install answers (MarketplacePackagePage). + mode: { + label: "Способ установки", + managed: "Управляемая", + managedHint: "Пакет остаётся источником истины: он регистрируется как код, получает свои обновления, а его элементы здесь не редактируются.", + unmanaged: "Неуправляемая копия", + unmanagedHint: "Однократно копируется в эту среду выполнения как её собственные метаданные: каждый элемент здесь можно редактировать, а копия не получает обновлений.", + required: "Выберите способ установки, чтобы установить этот пакет.", + }, + unmanagedSuccess: "v{{version}} пакета \"{{name}}\" скопирована в эту среду выполнения как её собственные метаданные, в базу {{base}}. Копию можно редактировать, обновлений она не получает.", + unmanagedStrippedLocks: "Пакет блокировал эти элементы. Копию можно редактировать, поэтому их блокировки не скопированы:", + unmanagedIgnored: "Эта среда выполнения не поддерживает неуправляемую установку, поэтому \"{{name}}\" установлен как управляемый (зарегистрирован как код), а не скопирован. Обновите среду выполнения, затем удалите пакет и установите его снова как неуправляемую копию.", + }, + // objectui#12146 — the install dialog opened as an UPDATE of the copy + // this environment holds (MarketplacePackagePage). + update: { + dialogTitle: "Обновить {{name}}", + dialogDescCurrent: "Обновить копию, установленную в этой среде ({{host}}).", + dialogDescPicker: "Выберите среду, в которой нужно обновить установленную копию. Требуется вход в ObjectOS Cloud.", + majorNotice: "Это обновление основной версии (v{{fromMajor}} → v{{toMajor}}), оно может содержать несовместимые изменения.", + releaseNotes: "Прочитать примечания к выпуску v{{version}}", + success: "Обновлено до v{{version}}.", + deployTimeout: "Обновление записано, но эта среда пока не предоставляет v{{version}}. Возможно, развёртывание ещё идёт — проверьте снова чуть позже.", + }, + // objectui#3546 — the MarketplacePage "Your organization" strip. + org: { + heading: "Ваша организация", + install: "Установить", + installed: "Установлено {{name}}", + installedBadge: "Установлено", + installing: "Установка…", + }, + // ADR-0025 PD4 §3.5/§3.11 — the pre-install consent panel + // (PluginDisclosure.tsx). `runtime` is a CLOSED enum: spec + // PluginRuntimeSchema = z.enum(['node', 'sandbox', 'worker']). + disclosure: { + containsCode: "Этот пакет содержит код", + reviewed: "Проверено и одобрено", + unreviewed: "Ещё не проверено", + signed: "Подписано", + grantsIntro: "Пакет запрашивает:", + notEnforced: "Список записывается при установке и подтверждается заново, если более поздняя версия запросит больше, но среда выполнения пока не ограничивает пакет этим списком.", + services: "Сервисы платформы", + hooks: "Хуки жизненного цикла", + network: "Доступ к сети", + fs: "Доступ к файловой системе", + noPermissions: "Не запрашивает особых разрешений.", + acknowledge: "Я понимаю, что этот пакет выполняет код и получает указанные выше разрешения.", + runtime: { + node: "В процессе · полное доверие", + sandbox: "В песочнице", + worker: "Вне процесса", + }, + }, + uninstall: { + successInDetail: "{{manifestId}} и его кэшированный манифест удалены: приложение убрано из работающего ядра вместе с его наборами разрешений и их назначениями. Записи, хранящиеся в его объектах, остаются в базе данных.", + }, + accessDenied: { + title: "Только для администраторов", + description: "Нет прав на установку приложений.", + }, + category: { + crm: "CRM", + erp: "ERP", + hr: "HR", + finance: "Финансы", + project: "Управление проектами", + collaboration: "Совместная работа", + analytics: "Аналитика", + integration: "Интеграция", + automation: "Автоматизация", + ai: "ИИ", + security: "Безопасность", + "developer-tools": "Инструменты разработчика", + "ui-theme": "Тема UI", + storage: "Хранилище", + other: "Другое", + }, + relativeTime: { + today: "сегодня", + daysAgo: "{{count}} д назад", + monthsAgo: "{{count}} мес назад", + monthsAgo_one: "{{count}} мес назад", + monthsAgo_few: "{{count}} мес назад", + monthsAgo_many: "{{count}} мес назад", + monthsAgo_other: "{{count}} мес назад", + yearsAgo: "{{count}} лет назад", + yearsAgo_one: "{{count}} год назад", + yearsAgo_few: "{{count}} года назад", + yearsAgo_many: "{{count}} лет назад", + yearsAgo_other: "{{count}} года назад", + }, + }, +}; + +export default ru; diff --git a/ui/packages/i18n/src/namespaces/marketplace/zh.ts b/ui/packages/i18n/src/namespaces/marketplace/zh.ts new file mode 100644 index 00000000000..11ceae4fcb4 --- /dev/null +++ b/ui/packages/i18n/src/namespaces/marketplace/zh.ts @@ -0,0 +1,232 @@ +/** + * Chinese (zh) — the lazy `marketplace` namespace's catalogue. + * + * A PARTIAL pack: the same shape as `../../locales/zh.ts`, holding only + * the `marketplace` group, which `./index.ts` deep-merges into an i18next + * instance when a marketplace page mounts. `./index.ts` says what the + * namespace is, why it is lazy, and which gates read this file. + * + * ⛔ Keep the declaration `const zh = { … }` and `export default zh`: + * the i18n gates under `ui/scripts/` read this file by AST under that name. + */ +const zh = { + marketplace: { + title: '应用市场', + subtitle: '浏览已通过审核、发布到 ObjectStack 目录中的应用。点击应用查看详情并安装到你的某个环境中。', + searchPlaceholder: '按名称或标识搜索应用…', + searchAria: '搜索市场应用', + installed: '已安装', + installedCount: '已安装({{count}})', + refresh: '刷新', + all: '全部', + noApprovedYet: '应用市场暂无已通过审核的应用。', + noMatchFilters: '没有应用匹配当前筛选条件。', + noDescription: '未提供描述。', + back: '返回应用市场', + browseLink: '去应用市场看看 →', + versionBadge: 'v{{version}}', + installedBadge: '已安装 v{{version}}', + load: { + failed: '应用市场加载失败', + failedHintConfigured: '本运行时通过 {{url}} 上的控制面访问应用市场。请检查该地址是否在线、能否从本运行时访问。', + failedHintSameOrigin: '应用市场目录通过本运行时访问。请检查运行时是否在线、能否访问该目录。', + loading: '正在加载应用市场目录…', + slow: '仍在加载 —— 应用市场目录响应缓慢。', + retry: '重试', + unreachable: '无法访问应用市场目录。', + answeredError: '应用市场目录返回了错误(HTTP {{status}})。', + packageFailed: '应用加载失败', + notFound: '未找到。', + }, + disabled: { + title: '应用市场已关闭', + description: '本运行时未配置应用市场,因此这里没有可浏览或安装的内容。', + hint: '当运行时以 <code>OS_CLOUD_URL=off</code>(或 <code>none</code>、<code>local</code>、<code>disabled</code>)启动时,这就是预期状态。如需开启应用市场,请把 <code>OS_CLOUD_URL</code> 指向一个控制面并重启运行时。', + }, + detail: { + homepage: '主页', + installedV: '已安装 · v{{version}}', + about: '简介', + noReadme: '未提供 README。', + versions: '版本', + noApprovedVersions: '暂无已审核版本。', + prerelease: '预发布', + moreOptions: '更多安装选项', + uninstallFromRuntime: '从本运行时卸载', + addSampleData: '添加示例数据', + reseedAgain: '重新加载示例数据', + purgeSampleData: '清除示例数据', + purgeConfirm: '删除此应用预置的所有示例记录?用户自行添加的记录不会被删除。', + purgeSuccess: '已删除 {{count}} 条示例记录。', + purgeSuccess_one: '已删除 {{count}} 条示例记录。', + purgeSuccess_other: '已删除 {{count}} 条示例记录。', + purgeNoData: '没有可清除的示例记录。', + // objectstack#30323 — the install note's sample-data count, its clear and the app banner. + sampleDataLoaded: '本次安装载入了 {{count}} 条示例记录,方便查看应用效果。正式使用前可以清除。', + sampleDataLoaded_one: '本次安装载入了 {{count}} 条示例记录,方便查看应用效果。正式使用前可以清除。', + sampleDataLoaded_other: '本次安装载入了 {{count}} 条示例记录,方便查看应用效果。正式使用前可以清除。', + sampleDataReloaded: '再次安装会重新载入示例数据,包括已清除的示例数据。', + purgeConfirmAtMost: '最多删除 {{count}} 条示例记录。', + purgeConfirmAtMost_one: '最多删除 {{count}} 条示例记录。', + purgeConfirmAtMost_other: '最多删除 {{count}} 条示例记录。', + sampleDataBannerTitle: '"{{name}}" 中有示例数据。', + sampleDataBannerMessage: '这些数据用于查看应用效果,正式使用前可以清除。', + reseedQueued: '示例数据将在下次访问该环境时重新加载。', + reseedLocalSuccess: '已重新加载示例数据:新增 {{inserted}} 条,更新 {{updated}} 条。', + reseedPartialErrors: '(有 {{count}} 条记录写入失败)', + reseedPartialErrors_one: '(有 {{count}} 条记录写入失败)', + reseedPartialErrors_other: '(有 {{count}} 条记录写入失败)', + sampleDataKernelUnavailable: '此控制面没有环境内核,因此无法在这里重新加载或清除示例数据。请在该环境自己的运行时中操作。', + updateAvailable: '有可用更新', + }, + action: { + install: '安装', + reinstall: '重新安装', + working: '处理中…', + installToCloud: '安装到云端…', + installed: '已安装', + installing: '安装中…', + updating: '更新中…', + close: '关闭', + dismiss: '关闭', + openOnCloud: '在云端打开', + backHome: '返回首页', + updateTo: '更新', + }, + install: { + dialogTitle: '安装 {{name}}', + dialogDescCurrent: '安装到当前环境({{host}})。', + dialogDescPicker: '选择要将此应用安装到的环境。需要先登录 ObjectOS Cloud。', + environment: '环境', + environmentPlaceholder: '选择一个环境', + includeSampleData: '包含示例数据', + noEnvs: '当前组织没有可用的环境。', + noPermission: '你没有权限在任何环境中安装应用。仅组织所有者和管理员可以安装 — 请联系你的组织管理员。', + signInFirst: '请先登录 ObjectOS Cloud,然后点击下方"在云端打开"。', + success: '安装成功。打开环境即可看到新应用。', + localSuccess: '已将 v{{version}} 安装到本运行时。"{{name}}" 已出现在应用切换器中。', + localLoadsAtRestart: '已将 v{{version}} 安装到本运行时。"{{name}}" 将在运行时下次重启时加载,在此之前不会出现在应用切换器中。', + localManifestConflict: '{{message}}\n提示:本地已有应用占用此 manifest_id,请先从 objectstack.config.ts 中移除。', + localUnauthorized: '请先登录本运行时再试。', + localMarketplaceUnavailable: '本运行时未配置 OS_CLOUD_URL,无法访问应用市场目录。', + updateTo: '更新 → v{{version}}', + installedVersion: '已安装 v{{version}}', + deploying: '正在部署应用…', + deployed: '应用已部署完成。', + openApp: '打开 {{name}}', + deployTimeout: '安装已记录,但当前环境尚未提供该应用。它可能仍在部署中,请稍后再次检查。如果一直没有出现,可能是软件包未能加载、不包含应用,或其应用需要你尚未拥有的权限。', + checkAgain: '再次检查', + // ADR-0131 D6 — the install modes a package's manifest permits, and + // what an unmanaged install answers (MarketplacePackagePage). + mode: { + label: '安装方式', + managed: '托管', + managedHint: '软件包保持权威:以代码形式注册并接收其升级,其中的条目不在此处编辑。', + unmanaged: '非托管副本', + unmanagedHint: '一次性复制到本运行时,成为它自己的元数据:每个条目都可在此编辑,副本不再接收升级。', + required: '请先选择安装方式,再安装此软件包。', + }, + unmanagedSuccess: '已将 "{{name}}" 的 v{{version}} 复制到本运行时,成为它自己的元数据,位于基础包 {{base}} 中。副本可编辑,且不再接收升级。', + unmanagedStrippedLocks: '软件包锁定了以下条目。副本可编辑,因此这些锁未被复制:', + unmanagedIgnored: '本运行时不支持非托管安装,因此 "{{name}}" 以托管方式安装(以代码形式注册),而不是被复制。请先升级运行时,然后卸载该软件包,再以非托管副本重新安装。', + }, + // objectui#12146 — the install dialog opened as an UPDATE of the copy + // this environment holds (MarketplacePackagePage). + update: { + dialogTitle: '更新 {{name}}', + dialogDescCurrent: '更新当前环境({{host}})中已安装的应用。', + dialogDescPicker: '选择要更新已安装应用的环境。需要先登录 ObjectOS Cloud。', + majorNotice: '这是一次主版本更新(v{{fromMajor}} → v{{toMajor}}),可能包含不兼容的变更。', + releaseNotes: '查看 v{{version}} 的版本说明', + success: '已更新到 v{{version}}。', + deployTimeout: '更新已记录,但当前环境尚未提供 v{{version}}。可能仍在部署中,请稍后再次检查。', + }, + // objectui#3546 — the MarketplacePage "Your organization" strip. + org: { + heading: '你的组织', + install: '安装', + installed: '已安装 {{name}}', + installedBadge: '已安装', + installing: '安装中…', + }, + // ADR-0025 PD4 §3.5/§3.11 — the pre-install consent panel + // (PluginDisclosure.tsx). `runtime` is a CLOSED enum: spec + // PluginRuntimeSchema = z.enum(['node', 'sandbox', 'worker']). + disclosure: { + containsCode: '此软件包包含代码', + reviewed: '已审核并批准', + unreviewed: '尚未审核', + signed: '已签名', + grantsIntro: '此软件包请求以下权限:', + notEnforced: '安装时会记录这份清单,后续版本要求更多权限时会再次征求同意;但运行时目前还不会把该软件包限制在此清单内。', + services: '平台服务', + hooks: '生命周期钩子', + network: '网络访问', + fs: '文件系统访问', + noPermissions: '不请求任何特殊权限。', + acknowledge: '我了解此软件包会运行代码,并将获得上述权限。', + runtime: { + node: '进程内 · 完全信任', + sandbox: '沙箱隔离', + worker: '进程外', + }, + }, + // ADR-0090 D5/D9 — 安装时建议:包声明 isDefault 的权限集建议绑定到 + // everyone/guest 岗位,由管理员在此确认,服务端绝不自动绑定。 + suggestedBindings: { + promptEveryone: '此应用建议将「{{set}}」授予所有已登录用户(Everyone 岗位)。', + promptGuest: '此应用建议将「{{set}}」授予未登录访客(Guest 岗位)。', + confirm: '授予', + confirming: '授予中…', + dismiss: '忽略', + confirmedToast: '「{{set}}」已授予 {{anchor}}。', + dismissedToast: '已忽略「{{set}}」的建议。', + guestUnsupported: '此处无法授予:Guest 岗位的权限集不由已接受的建议派生。', + acceptedState: '「{{set}}」已为所有已登录用户(Everyone 岗位)接受。', + notInForceState: '「{{set}}」已接受,但尚未生效:本次请求未改变 Everyone 岗位。原因见服务器日志。', + dismissedState: '「{{set}}」未授予:该建议已被忽略或撤销。', + revoke: '撤销', + revoking: '撤销中…', + revokedToast: '已撤销对「{{set}}」的接受。', + reopen: '重新考虑', + reopening: '重新打开中…', + reopenedToast: '已重新打开「{{set}}」的建议:接受之前不授予任何权限。', + }, + uninstall: { + successInDetail: '已移除 {{manifestId}} 及其清单缓存:该应用已从运行中的内核中移除,其权限集及相应授权也已一并移除。其对象中已存储的记录仍保留在数据库中。', + }, + accessDenied: { + title: '应用市场仅限管理员使用', + description: '你没有权限在本环境中安装应用。请联系组织的所有者或管理员申请权限。', + }, + category: { + crm: '客户关系', + erp: '企业资源', + hr: '人力资源', + finance: '财务', + project: '项目管理', + collaboration: '协作', + analytics: '分析', + integration: '集成', + automation: '自动化', + ai: '人工智能', + security: '安全', + 'developer-tools': '开发者工具', + 'ui-theme': '界面主题', + storage: '存储', + other: '其他', + }, + relativeTime: { + today: '今天', + daysAgo: '{{count}} 天前', + monthsAgo: '{{count}} 个月前', + monthsAgo_one: '{{count}} 个月前', + monthsAgo_other: '{{count}} 个月前', + yearsAgo: '{{count}} 年前', + yearsAgo_one: '{{count}} 年前', + yearsAgo_other: '{{count}} 年前', + }, + }, +} as const; + +export default zh; diff --git a/ui/scripts/__tests__/check-i18n-call-site-keys.test.ts b/ui/scripts/__tests__/check-i18n-call-site-keys.test.ts index 55eda0eec34..dfcf511bf1c 100644 --- a/ui/scripts/__tests__/check-i18n-call-site-keys.test.ts +++ b/ui/scripts/__tests__/check-i18n-call-site-keys.test.ts @@ -16,6 +16,7 @@ import { HAND_ROLLED_TABLES, holesOf, PACK_HOOK, + PACK_NAMESPACE_DIRS, readVocabulary, RESERVED_OPTION_NAMES, unresolvableSpellings, @@ -95,10 +96,27 @@ const tempRoots: string[] = []; * AST walk reports only workspace-package specifiers and would not have * caught a relative one. Computing the path keeps the premise true instead of stepping * around the pin that guards it. + * + * The gate's `en` is the pack PLUS the `en` partial pack of every pack-shaped + * namespace (`PACK_NAMESPACE_DIRS`, objectstack#30588), so the module-evaluated + * side is the same union, merged the way the runtime merges it. */ +const evaluatedPack = async (rel: string): Promise<Record<string, unknown>> => + (await import(pathToFileURL(path.join(repoRoot, rel)).href)).default; +const mergedPacks = (into: Record<string, unknown>, from: Record<string, unknown>): Record<string, unknown> => { + const out: Record<string, unknown> = { ...into }; + for (const [key, value] of Object.entries(from)) { + const existing = out[key]; + out[key] = + typeof existing === 'object' && existing !== null && typeof value === 'object' && value !== null + ? mergedPacks(existing as Record<string, unknown>, value as Record<string, unknown>) + : value; + } + return out; +}; const realEn: unknown = ( - await import(pathToFileURL(path.join(repoRoot, 'packages/i18n/src/locales/en.ts')).href) -).default; + await Promise.all(PACK_NAMESPACE_DIRS.map((dir) => evaluatedPack(`${dir}en.ts`))) +).reduce(mergedPacks, await evaluatedPack('packages/i18n/src/locales/en.ts')); /** * The workspace specifier that appears inside the FIXTURE SOURCES below — text @@ -234,6 +252,56 @@ describe('the parsed en key set equals the module vitest actually evaluates', () }); }); +describe('the pack-shaped namespaces are part of the en pack (objectstack#30588)', () => { + const NS_EN = `${PACK_NAMESPACE_DIRS[0]}en.ts`; + + it('reads a key the namespace defines as a pack key', () => { + const root = repoWith({ + 'packages/i18n/src/locales/en.ts': EN_FIXTURE, + [NS_EN]: "const en = { shop: { title: 'Shop', install: { go: 'Install' } } } as const;\nexport default en;\n", + }); + const keys = collectEnKeys(root); + expect(keys.leaves.has('shop.title')).toBe(true); + expect(keys.values.get('shop.install.go')).toBe('Install'); + expect(keys.branches.has('shop')).toBe(true); + expect(keys.leaves.has('common.save')).toBe(true); + }); + + it('merges a group both files hold, key by key', () => { + const root = repoWith({ + 'packages/i18n/src/locales/en.ts': "const en = { shop: { kept: 'Kept' } } as const;\nexport default en;\n", + [NS_EN]: "const en = { shop: { moved: 'Moved' } } as const;\nexport default en;\n", + }); + const keys = collectEnKeys(root); + expect([...keys.leaves].sort()).toEqual(['shop.kept', 'shop.moved']); + }); + + it('refuses a key defined in both homes', () => { + const root = repoWith({ + 'packages/i18n/src/locales/en.ts': "const en = { shop: { title: 'A' } } as const;\nexport default en;\n", + [NS_EN]: "const en = { shop: { title: 'B' } } as const;\nexport default en;\n", + }); + expect(() => collectEnKeys(root)).toThrow(/`shop\.title` is defined in/); + }); + + it('refuses a key in one home that is a group in the other', () => { + const root = repoWith({ + 'packages/i18n/src/locales/en.ts': "const en = { shop: { title: 'A' } } as const;\nexport default en;\n", + [NS_EN]: "const en = { shop: { title: { short: 'B' } } } as const;\nexport default en;\n", + }); + expect(() => collectEnKeys(root)).toThrow(/`shop\.title` is a group in/); + }); + + it('reports a call site asking for a namespace key the namespace no longer defines', () => { + const root = repoWith({ + 'packages/i18n/src/locales/en.ts': EN_FIXTURE, + 'packages/x/src/page.tsx': `import { useObjectTranslation } from '${I18N_PKG}';\nexport function P() { const { t } = useObjectTranslation(); return t('shop.title'); }\n`, + }); + // fixture-address: the finding the gate reports for the fixture page written just above + expect(findingsOf(root, 'missing-key')).toEqual(['shop.title@packages/x/src/page.tsx:2']); + }); +}); + describe('the file walk', () => { it('reads sources but not tests, type declarations or build output', () => { const root = repoWith({ diff --git a/ui/scripts/__tests__/check-i18n-en-drift.test.ts b/ui/scripts/__tests__/check-i18n-en-drift.test.ts index 0fa24e2cfae..92ca0489356 100644 --- a/ui/scripts/__tests__/check-i18n-en-drift.test.ts +++ b/ui/scripts/__tests__/check-i18n-en-drift.test.ts @@ -14,6 +14,7 @@ import { ISSUE_PATTERN, LEDGER_PATH, LOCALES, + namespacePackPaths, packPath, parsePack, readLedger, @@ -448,6 +449,58 @@ describe('the objectui#3625 commit, reconstructed in a real git repo', () => { }); }); +// -- the pack-shaped namespaces (objectstack#30588) --------------------------- + +describe('a language is its pack plus its namespace partial packs', () => { + const NS = namespacePackPaths('en')[0].replace(/en\.ts$/, ''); + const nsFiles = (trees: Record<string, Nested>): Record<string, string> => + Object.fromEntries(LOCALES.map((lang) => [`${NS}${lang}.ts`, packSource(lang, trees[lang])])); + + it('names one partial pack per language per namespace', () => { + expect(namespacePackPaths('zh')).toEqual(['packages/i18n/src/namespaces/marketplace/zh.ts']); + }); + + it('a group MOVED from the pack to its namespace is no drift and no key-set change', () => { + const before = packFiles(treesFor((lang) => ({ common: { save: `save-${lang}` }, shop: { title: `shop-${lang}` } }))); + const after = { + ...packFiles(treesFor((lang) => ({ common: { save: `save-${lang}` } }))), + ...nsFiles(treesFor((lang) => ({ shop: { title: `shop-${lang}` } }))), + }; + const { root, shas } = repoWithCommits([before, after]); + const { code, output } = runGate(root, [...FIXTURE_FLOOR, '--base', shas[0], '--head', shas[1]]); + expect(code).toBe(0); + expect(output).toContain('0 en value(s) changed (0 key(s) added, 0 removed'); + }); + + it('an en value changed INSIDE the namespace is drift the nine must follow', () => { + const base = treesFor((lang) => ({ shop: { title: `shop-${lang}` } })); + const before = { ...packFiles(treesFor(() => ({ common: { save: 's' } }))), ...nsFiles(base) }; + const after = { + ...packFiles(treesFor(() => ({ common: { save: 's' } }))), + ...nsFiles({ ...base, en: { shop: { title: 'Shop, renamed' } } }), + }; + const { root, shas } = repoWithCommits([before, after]); + const { code, output } = runGateExpectingFailure(root, [ + ...FIXTURE_FLOOR, + '--base', + shas[0], + '--head', + shas[1], + ]); + expect(code).toBe(1); + expect(output).toContain('shop.title'); + }); + + it('refuses a key defined both in a pack and in its namespace', () => { + const files = { + ...packFiles(treesFor((lang) => ({ shop: { title: `pack-${lang}` } }))), + ...nsFiles(treesFor((lang) => ({ shop: { title: `ns-${lang}` } }))), + }; + const { root } = repoWithCommits([files]); + expect(() => readPacks(root, null)).toThrow(/shop\.title is defined both in/); + }); +}); + // -- the waiver ledger -------------------------------------------------------- /** The reconstructed drift plus a ledger, as a two-commit repo the CLI can read. */ @@ -606,7 +659,10 @@ describe('the shipped ledger', () => { }); it('has no entry that the current en pack contradicts', () => { - const enAfter = parsePack(fs.readFileSync(path.join(repoRoot, packPath('en')), 'utf8'), 'en.ts'); + // The gate's own `en` population — the pack and its namespaces' partial + // packs (objectstack#30588) — so a waived key that moved to a namespace is + // still read where it now lives. + const enAfter = readPacks(repoRoot, null).get('en')!; expect(checkLedger(ledger, enAfter)).toEqual([]); for (const entry of Object.values(ledger.waivers) as Array<{ issue: string }>) { expect(entry.issue).toMatch(ISSUE_PATTERN); diff --git a/ui/scripts/check-eager-closure-budget.mjs b/ui/scripts/check-eager-closure-budget.mjs index cae52a3fc13..03dcc39716f 100644 --- a/ui/scripts/check-eager-closure-budget.mjs +++ b/ui/scripts/check-eager-closure-budget.mjs @@ -2226,6 +2226,16 @@ export const PER_CHUNK_GZIP_CEILINGS = Object.freeze({ // // ⚠️ RAISED ×1.20 by objectui#12653 (2026-10-16), the maintainer's 20% // ruling, from 50,000 (the entry on MAX_EAGER_CLOSURE_GZIP_BYTES). + // + // READ, ⛔ not moved, by objectstack#30588 (2026-11-05): the pack's + // `marketplace.*` group (bar the keys Studio's packages page reads) left + // the ten packs for the lazy `marketplace` namespace + // (`packages/i18n/src/namespaces/marketplace/`). On two console builds + // (`CI=true vite build`, `apps/console/dist/eager-closure.json`) at + // `bdbaf80c` and at that card's head this chunk read 59,951 -> 57,010 + // gzipped bytes, headroom 49 -> 2,990. The eager closure read 2,204,342 -> + // 2,201,440 (-2,902), with no chunk added and none but this one moving by + // more than 40 bytes. 'i18n-locale-en': 60_000, // Raised by the maintainer ruling of 2026-09-08, ⛔ not by a measurement here: // `main` had been red on this line since `f76f43628`. The bytes that put it diff --git a/ui/scripts/check-i18n-call-site-keys.mjs b/ui/scripts/check-i18n-call-site-keys.mjs index 17aa5769ed6..b52a5cd77c4 100644 --- a/ui/scripts/check-i18n-call-site-keys.mjs +++ b/ui/scripts/check-i18n-call-site-keys.mjs @@ -1315,7 +1315,25 @@ function staticTextSegments(node, source) { } /** - * Dotted leaf paths of `packages/i18n/src/locales/en.ts`, read from its AST, + * The pack-shaped lazy namespaces (objectstack#30588): directories holding one + * PARTIAL pack per language — the same shape as `packages/i18n/src/locales/<code>.ts`, + * holding only the groups that moved out of the packs so the console's eager + * `en` chunk stops carrying them. Their keys are pack keys to every reader (a + * call site asks `t('marketplace.…')` of the default namespace, and the page + * merges the partial pack in before it renders), so every gate that reads pack + * keys reads `<dir>en.ts` as part of the `en` pack. ⛔ A directory added here + * is a directory of partial packs; the `studio` namespace is NOT one (it is a + * `{token}` string table of its own, read by `collectDesignerKeys` in + * `check-i18n-dead-keys.mjs`). + */ +export const PACK_NAMESPACE_DIRS = Object.freeze(['packages/i18n/src/namespaces/marketplace/']); + +/** Every directory a pack key is DEFINED in: the packs, then the namespaces. */ +export const PACK_DEFINITION_DIRS = Object.freeze(['packages/i18n/src/locales/', ...PACK_NAMESPACE_DIRS]); + +/** + * Dotted leaf paths of `packages/i18n/src/locales/en.ts` and of the `en` partial + * pack of every {@link PACK_NAMESPACE_DIRS} directory, read from their ASTs, * plus the leaf VALUES — the strings the app actually renders. * * Parsed rather than imported so the gate needs no build step and no TS loader. @@ -1327,62 +1345,84 @@ function staticTextSegments(node, source) { * `leaves` either way: the key rules judge existence and need no value, and the * value rule declines to judge what it could not read rather than guessing. * + * A leaf defined in two of the files — or a leaf in one that is a group in + * another — THROWS: two homes for one key cannot both be what renders. + * * @returns {{ leaves: Set<string>, branches: Set<string>, values: Map<string, string> }} */ export function collectEnKeys(root) { - const file = join(root, 'packages/i18n/src/locales/en.ts'); - const source = ts.createSourceFile(file, readFileSync(file, 'utf8'), ts.ScriptTarget.Latest, true); - - const unwrap = (node) => { - let n = node; - while (ts.isAsExpression(n) || ts.isParenthesizedExpression(n) || (ts.isSatisfiesExpression?.(n) ?? false)) n = n.expression; - return n; - }; - - let literal = null; - const findEn = (node) => { - if ( - ts.isVariableDeclaration(node) && - ts.isIdentifier(node.name) && - node.name.text === 'en' && - node.initializer - ) { - const init = unwrap(node.initializer); - if (ts.isObjectLiteralExpression(init)) literal = init; - } - ts.forEachChild(node, findEn); - }; - findEn(source); - if (!literal) throw new Error(`cannot find the \`const en = { … }\` object literal in ${file}`); - const leaves = new Set(); const branches = new Set(); const values = new Map(); - const walk = (object, prefix) => { - for (const prop of object.properties) { - if (!ts.isPropertyAssignment(prop)) { - throw new Error(`unsupported property form in ${file} at ${prefix || '<root>'}: ${ts.SyntaxKind[prop.kind]}`); - } - const name = - ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name) || ts.isNumericLiteral(prop.name) - ? prop.name.text - : null; - if (name === null) { - throw new Error(`unsupported key form in ${file} at ${prefix || '<root>'}`); + const homeOf = new Map(); + // The pack is required; a namespace's `en` file is read when it exists. A + // missing one cannot hide anything: its keys simply are not defined, so every + // call site that asks for one is reported as asking for a missing key. + const files = [ + join(root, 'packages/i18n/src/locales/en.ts'), + ...PACK_NAMESPACE_DIRS.map((dir) => join(root, dir, 'en.ts')).filter((file) => existsSync(file)), + ]; + for (const file of files) { + const source = ts.createSourceFile(file, readFileSync(file, 'utf8'), ts.ScriptTarget.Latest, true); + + const unwrap = (node) => { + let n = node; + while (ts.isAsExpression(n) || ts.isParenthesizedExpression(n) || (ts.isSatisfiesExpression?.(n) ?? false)) n = n.expression; + return n; + }; + + let literal = null; + const findEn = (node) => { + if ( + ts.isVariableDeclaration(node) && + ts.isIdentifier(node.name) && + node.name.text === 'en' && + node.initializer + ) { + const init = unwrap(node.initializer); + if (ts.isObjectLiteralExpression(init)) literal = init; } - const path = prefix ? `${prefix}.${name}` : name; - const value = unwrap(prop.initializer); - if (ts.isObjectLiteralExpression(value)) { - branches.add(path); - walk(value, path); - } else { - leaves.add(path); - const text = staticString(value, source); - if (text !== null) values.set(path, text); + ts.forEachChild(node, findEn); + }; + findEn(source); + if (!literal) throw new Error(`cannot find the \`const en = { … }\` object literal in ${file}`); + + const walk = (object, prefix) => { + for (const prop of object.properties) { + if (!ts.isPropertyAssignment(prop)) { + throw new Error(`unsupported property form in ${file} at ${prefix || '<root>'}: ${ts.SyntaxKind[prop.kind]}`); + } + const name = + ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name) || ts.isNumericLiteral(prop.name) + ? prop.name.text + : null; + if (name === null) { + throw new Error(`unsupported key form in ${file} at ${prefix || '<root>'}`); + } + const path = prefix ? `${prefix}.${name}` : name; + const value = unwrap(prop.initializer); + if (ts.isObjectLiteralExpression(value)) { + if (leaves.has(path)) { + throw new Error(`\`${path}\` is a group in ${file} and a key in ${homeOf.get(path)}`); + } + branches.add(path); + walk(value, path); + } else { + if (leaves.has(path) || branches.has(path)) { + throw new Error( + `\`${path}\` is defined in ${file} and in ${homeOf.get(path) ?? 'another file as a group'} — ` + + 'a pack key lives in exactly one of the pack and its namespace', + ); + } + leaves.add(path); + homeOf.set(path, file); + const text = staticString(value, source); + if (text !== null) values.set(path, text); + } } - } - }; - walk(literal, ''); + }; + walk(literal, ''); + } return { leaves, branches, values }; } diff --git a/ui/scripts/check-i18n-dead-keys.mjs b/ui/scripts/check-i18n-dead-keys.mjs index 90ef65bf2c9..cf2af751527 100644 --- a/ui/scripts/check-i18n-dead-keys.mjs +++ b/ui/scripts/check-i18n-dead-keys.mjs @@ -465,7 +465,13 @@ import { tmpdir } from 'node:os'; import { dirname, join, relative, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { EXCLUDED_TRANSLATORS, analyze, collectEnKeys, collectSourceFiles } from './check-i18n-call-site-keys.mjs'; +import { + EXCLUDED_TRANSLATORS, + PACK_DEFINITION_DIRS, + analyze, + collectEnKeys, + collectSourceFiles, +} from './check-i18n-call-site-keys.mjs'; import { isEntrypoint } from './invoked-as.mjs'; const scriptDir = dirname(fileURLToPath(import.meta.url)); @@ -512,8 +518,9 @@ const TEXT_SWEEP_SKIP_DIRS = new Set([ */ const PACK_IMPORT_SPECIFIER = '@object-ui/i18n'; -/** The locale pack directory — every candidate's DEFINITION lives here, so a - * hit inside it is not evidence of a reference and must not count as one. */ +/** The locale pack directory — every candidate's DEFINITION lives here or in + * a pack-shaped namespace (`PACK_DEFINITION_DIRS`, objectstack#30588), so a + * hit inside one is not evidence of a reference and must not count as one. */ const LOCALES_DIR = 'packages/i18n/src/locales/'; /** Appended to a hit path that only the property-chain probe matched, so the @@ -624,7 +631,7 @@ function occursAtKeyBoundary(content, key) { /** * Whether the literal dotted string of each `key` in `keys` occurs anywhere in - * the repo outside `LOCALES_DIR`, OR — for keys of three or more segments — + * the repo outside `PACK_DEFINITION_DIRS`, OR — for keys of three or more segments — * its property chain does (`propertyChainProbe()`; header section "The * property-chain leg"). * @@ -641,7 +648,8 @@ function occursAtKeyBoundary(content, key) { * @param {object} [options] * @param {string | string[]} [options.definitionPrefix] Repo-relative path * prefix (or prefixes) whose hits are NOT evidence, because it is where the - * keys are defined. Defaults to `LOCALES_DIR` (the packs). The designer + * keys are defined. Defaults to `PACK_DEFINITION_DIRS` (the packs and the + * pack-shaped namespaces). The designer * corpus passes its table FILE and its namespace homes instead — the whole * table file, not just its two table literals, because that module's header * docblock quotes example keys in prose and a grep cannot tell prose from a @@ -659,7 +667,7 @@ function occursAtKeyBoundary(content, key) { * absent from the map, or mapped to `[]`, has no textual footprint at all. */ export function textFootprint(root, keys, options = {}) { - const { definitionPrefix = LOCALES_DIR, propertyChain = true, keyBoundary = true } = options; + const { definitionPrefix = PACK_DEFINITION_DIRS, propertyChain = true, keyBoundary = true } = options; const definitionPrefixes = [definitionPrefix].flat(); const result = new Map(keys.map((key) => [key, []])); if (keys.length === 0) return result; diff --git a/ui/scripts/check-i18n-en-drift.mjs b/ui/scripts/check-i18n-en-drift.mjs index 14bd7d04195..d90a36e22fd 100644 --- a/ui/scripts/check-i18n-en-drift.mjs +++ b/ui/scripts/check-i18n-en-drift.mjs @@ -159,6 +159,7 @@ import { execFileSync } from 'node:child_process'; import { existsSync, readFileSync } from 'node:fs'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; +import { PACK_NAMESPACE_DIRS } from './check-i18n-call-site-keys.mjs'; import { DESIGNER_TABLE, DESIGNER_TABLE_PAIRS, @@ -178,6 +179,15 @@ export const FOLLOWERS = LOCALES.filter((lang) => lang !== 'en'); /** Repo-relative path of a pack source. */ export const packPath = (lang) => `packages/i18n/src/locales/${lang}.ts`; +/** + * Repo-relative paths of a language's partial packs in the pack-shaped lazy + * namespaces (`PACK_NAMESPACE_DIRS`, objectstack#30588). Their keys are pack + * keys moved out of the pack, so a language's population is its pack PLUS + * these: a key that moved between the two keeps one value on each side, and + * the move itself is not drift. + */ +export const namespacePackPaths = (lang) => PACK_NAMESPACE_DIRS.map((dir) => `${dir}${lang}.ts`); + /** Where the waiver ledger lives, repo-relative. */ export const LEDGER_PATH = 'scripts/i18n-en-drift-baseline.json'; @@ -338,28 +348,56 @@ function gitQuiet(root, args) { } /** - * The ten packs at `ref`, or from the working tree when `ref` is null. + * The ten packs at `ref`, or from the working tree when `ref` is null — each + * one merged with its partial packs from `namespacePackPaths(lang)`. * * The working tree is the default "after" side on purpose: an agent editing * `en.ts` gets the answer before committing, not after pushing. * + * A partial pack is read when it exists at that side and skipped when it does + * not: a namespace absent at the base is history (its keys were still in the + * pack there), and one absent anywhere holds no key, so a key it took with it + * is a key-set change — `all-locales-key-parity`'s, never drift. A partial pack + * that exists and cannot be parsed still throws, and so does a key defined both + * in a pack and in its namespace: two homes for one key cannot both render. + * * @returns {Map<string, Map<string, string>>} lang -> (key -> value) */ export function readPacks(root, ref) { - const packs = new Map(); - for (const lang of LOCALES) { - const rel = packPath(lang); + const read = (rel, required) => { const label = ref === null ? rel : revPath(ref, rel); let text; if (ref === null) { const full = join(root, rel); - if (!existsSync(full)) throw new Error(`${rel} does not exist in the working tree`); + if (!existsSync(full)) { + if (!required) return null; + throw new Error(`${rel} does not exist in the working tree`); + } text = readFileSync(full, 'utf8'); } else { text = gitQuiet(root, ['show', revPath(ref, rel)]); - if (text === null) throw new Error(`cannot read ${label} — is ${ref} in this clone?`); + if (text === null) { + if (!required) return null; + throw new Error(`cannot read ${label} — is ${ref} in this clone?`); + } + } + return { values: parsePack(text, label), label }; + }; + const packs = new Map(); + for (const lang of LOCALES) { + const pack = read(packPath(lang), true); + const values = new Map(pack.values); + for (const rel of namespacePackPaths(lang)) { + const partial = read(rel, false); + if (partial === null) continue; + for (const [key, value] of partial.values) { + if (values.has(key)) { + throw new Error(`${key} is defined both in ${pack.label} and in ${partial.label}`); + } + values.set(key, value); + } } - packs.set(lang, parsePack(text, label)); + packs.set(lang, values); } return packs; }