diff --git a/.changeset/marketplace-preview-locale-keys-3546-slice5.md b/.changeset/marketplace-preview-locale-keys-3546-slice5.md new file mode 100644 index 000000000..f2fa4531c --- /dev/null +++ b/.changeset/marketplace-preview-locale-keys-3546-slice5.md @@ -0,0 +1,46 @@ +--- +"@object-ui/i18n": patch +--- + +Backfill the `marketplace` and `preview` namespaces' 37 missing locale keys plus the `marketplace.disclosure.runtime.` template-key family (objectui#3546, slice five) + +`scripts/check-i18n-call-site-keys.mjs` (objectui#3530) measured 37 keys that a +`t()` call site asks for and that **no locale pack defined** — 37 distinct keys at +37 call sites across five console components — plus one `missing-prefix` family +whose static head matched no `en` key at all, so every expansion missed. All 37 +carried an inline `t(key, { defaultValue: 'English' })`, which is exactly the +objectui#3517 class: English rendered correctly, and **all ten languages were +stuck on it** for months. Nothing here rendered a raw key — slice one (PR #3583) +held those sites. + +What that meant on the page for a `zh` (or `ja`, `de`, `ar`, …) user: the +marketplace's "Your organization" strip, its Install / Installing… / Installed +buttons and the version-update affordances were English; the whole ADR-0025 PD4 +**pre-install permission disclosure** was English — "This package contains code", +the trust-tier badge, "Reviewed & approved" / "Not yet reviewed" / "Signed", the +four permission group labels (platform services, lifecycle hooks, network, +filesystem) and the consent checkbox the user ticks to accept them; the ADR-0045 +unpublished-app banner and its publish toasts were English; and the entire +ADR-0067 build-history sheet — title, description, the per-commit labels, the +Revert button and both of its result toasts — was English. + +`marketplace.disclosure.runtime.` is repaired as an **enumeration, not a +wildcard**: its value surface is the closed trust-tier enum +(`PluginRuntimeSchema` = `z.enum(['node', 'sandbox', 'worker'])`, ADR-0025 §3.6), +so all three members are backfilled and the family leaves the ratchet's +`missingPrefixes` (3 → 2). A test reads the component's own fallback map and +fails if a fourth tier is ever added without a key — the job the prefix entry +used to do. + +Each `en` value is byte-identical to the inline `defaultValue` it replaces (36 of +36 literal sites; the 37th's `defaultValue` is a template literal whose +`${pkg.display_name}` becomes the `{{name}}` hole its call site already passes), +so no English string a user sees today changes. The nine translations follow each +pack's own neighbourhood — including two namespaces that legitimately take +**different** second persons in `zh` (`marketplace` 你, `preview` 您) — and reuse +an existing neighbour's translation wherever the `en` string already existed +verbatim, so one English string never renders as two different sentences in the +same language. + +No component changed: an AST sweep of the whole `marketplace.*`/`preview.*` +call-site surface found the slice's own dead-`||`-fallback count to be zero. diff --git a/packages/i18n/src/__tests__/auth-namespace-3546.test.tsx b/packages/i18n/src/__tests__/auth-namespace-3546.test.tsx index 4acb1bcde..96da0d1da 100644 --- a/packages/i18n/src/__tests__/auth-namespace-3546.test.tsx +++ b/packages/i18n/src/__tests__/auth-namespace-3546.test.tsx @@ -256,17 +256,18 @@ describe('objectui#3546 slice three — the auth / oauth / acceptInvitation name ); expect(stillBaselined).toEqual([]); // 163 before this slice, 54 removed — then slice four (console, 41 keys) took - // it to 68. The other namespaces' debt is not this slice's to spend, and this - // number is what catches a slice that overreaches; it moves once per slice, - // and only downwards. - expect(Object.keys(baseline.missingKeys).length).toBe(68); + // it to 68 and slice five (marketplace + preview, 37 keys) to 31. The other + // namespaces' debt is not this slice's to spend, and this number is what + // catches a slice that overreaches; it moves once per slice, and only + // downwards. + expect(Object.keys(baseline.missingKeys).length).toBe(31); // None of the template-key FAMILIES belonged to the auth family, so this slice // left all four. Slice four then took `console.ai.group.` (it is a `console` - // key), leaving three. This assertion is what stops a later slice from - // thinking one of the remaining three was already handled. + // key) and slice five `marketplace.disclosure.runtime.`, leaving two. This + // assertion is what stops a later slice from thinking one of the remaining + // two was already handled. expect(Object.keys(baseline.missingPrefixes).sort()).toEqual([ 'gantt.linkEnd.', - 'marketplace.disclosure.runtime.', 'organization.invitations.status.', ]); }); diff --git a/packages/i18n/src/__tests__/console-namespace-3546.test.tsx b/packages/i18n/src/__tests__/console-namespace-3546.test.tsx index 4d58a53cf..43aafc96d 100644 --- a/packages/i18n/src/__tests__/console-namespace-3546.test.tsx +++ b/packages/i18n/src/__tests__/console-namespace-3546.test.tsx @@ -321,14 +321,15 @@ describe('objectui#3546 slice four — the console namespace', () => { missingPrefixes: Record; }; expect(Object.keys(baseline.missingKeys).filter((k) => k.startsWith('console.'))).toEqual([]); - // 109 before this slice, 41 removed. The other namespaces' debt is not this - // slice's to spend; this number moves once per slice, and only downwards. - expect(Object.keys(baseline.missingKeys).length).toBe(68); - // The prefix family this slice handled is GONE from the ratchet, and the other - // three are untouched — none of them belongs to `console`. + // 109 before this slice, 41 removed — then slice five (marketplace + preview, + // 37 keys) took it to 31. The other namespaces' debt is not this slice's to + // spend; this number moves once per slice, and only downwards. + expect(Object.keys(baseline.missingKeys).length).toBe(31); + // The prefix family this slice handled is GONE from the ratchet, and the ones + // that remain are untouched — none of them belongs to `console`. Slice five + // then took `marketplace.disclosure.runtime.`, leaving two. expect(Object.keys(baseline.missingPrefixes).sort()).toEqual([ 'gantt.linkEnd.', - 'marketplace.disclosure.runtime.', 'organization.invitations.status.', ]); expect(Object.keys(baseline.missingPrefixes)).not.toContain('console.ai.group.'); diff --git a/packages/i18n/src/__tests__/marketplace-preview-namespace-3546.test.tsx b/packages/i18n/src/__tests__/marketplace-preview-namespace-3546.test.tsx new file mode 100644 index 000000000..f8c5b8988 --- /dev/null +++ b/packages/i18n/src/__tests__/marketplace-preview-namespace-3546.test.tsx @@ -0,0 +1,712 @@ +/** + * The `marketplace` and `preview` namespaces — objectui#3546 slice five — + * resolve **from the locale packs, with a provider mounted**. + * + * ## What was broken, precisely + * + * `scripts/check-i18n-call-site-keys.mjs` (#3530's gate) measured **37 distinct + * keys at 37 call sites** under `marketplace.*` / `preview.*` that a `t()` call + * site asks for and that NO pack defined, plus **one** `missing-prefix` family + * (`marketplace.disclosure.runtime.`, whose static head matched no `en` key at + * all so every expansion missed). Unlike slices two through four this slice has + * no multi-site key: the denominator is 1:1 — still measured, never counted by + * hand, because slice two predicted 90 and the truth was 93. + * + * All 37 carried an inline `t(key, { defaultValue: 'English' })`, so this is the + * milder objectui#3517 class: English rendered correctly and **all ten + * languages were stuck on it**. Nothing rendered a raw key here — slice one + * (PR #3583) held the sites that did — and an AST sweep of the whole + * `marketplace.*`/`preview.*` call-site surface (172 sites in 11 files, not just + * this slice's 37) found 16 dead `t(key) || 'English'` fallbacks, every one of + * them on a key that already resolves in `en` and NOT in this slice, so this + * slice touches no component. (They are filed; see the PR body.) + * + * Consequence for test design, same as slices two through four: `en` output was + * already correct before the change, so **an `en` assertion cannot discriminate + * before from after**. Every assertion that pins the fix is a non-`en` one; the + * `en` cases only prove the key is reachable through the real binding. + * + * ## The template-key family + * + * `marketplace.disclosure.runtime.` is the plugin trust tier badge in + * `PluginDisclosure.tsx:70`: + * + * t(`marketplace.disclosure.runtime.${version.runtime}`, { + * defaultValue: RUNTIME_FALLBACK[version.runtime] ?? version.runtime, + * }) + * + * Its value surface is a CLOSED enumeration — `PluginRuntimeSchema` in + * objectstack `packages/spec/src/kernel/manifest.zod.ts` is + * `z.enum(['node', 'sandbox', 'worker'])` (ADR-0025 §3.6) — so the repair is an + * enumeration of three members, not a wildcard, and the family leaves + * `missingPrefixes` (3 → 2). The spec lives in a sibling repo and cannot be read + * from here, so the in-repo authority this test reads is the component's own + * `RUNTIME_FALLBACK` map, which is what `defaultValue` reads; a fourth tier + * added to either side fails the test, which is the job the prefix entry used to + * do. + * + * ## Why a provider is mounted + * + * All five components behind these keys — `MarketplacePage`, + * `MarketplacePackagePage`, `PluginDisclosure`, `CommitTimeline` and + * `UnpublishedAppBar` — bind `t` from a bare `useObjectTranslation()`. None sits + * behind a `createSafeTranslation` defaults map, so there is no provider-less + * path to be green on: without `I18nProvider`, i18next is not the thing + * answering and the test would describe a binding the console never uses. + */ +import { describe, it, expect, beforeEach } from 'vitest'; +import { renderHook } from '@testing-library/react'; +import { existsSync, readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import React from 'react'; +import { I18nProvider, useObjectTranslation } from '../provider'; +import { builtInLocales } from '../locales/index'; + +/** The 40 keys this slice backfilled, grouped as the packs group them. */ +const KEYS = [ + // marketplace.action / marketplace.install — the package page's buttons (3) + 'marketplace.action.updateTo', + 'marketplace.install.updateTo', + 'marketplace.install.installedVersion', + // marketplace.org — MarketplacePage's "Your organization" strip (5) + 'marketplace.org.heading', + 'marketplace.org.install', + 'marketplace.org.installed', + 'marketplace.org.installedBadge', + 'marketplace.org.installing', + // marketplace.disclosure — the ADR-0025 PD4 pre-install consent panel (11) + 'marketplace.disclosure.containsCode', + 'marketplace.disclosure.reviewed', + 'marketplace.disclosure.unreviewed', + 'marketplace.disclosure.signed', + 'marketplace.disclosure.grantsIntro', + 'marketplace.disclosure.services', + 'marketplace.disclosure.hooks', + 'marketplace.disclosure.network', + 'marketplace.disclosure.fs', + 'marketplace.disclosure.noPermissions', + 'marketplace.disclosure.acknowledge', + // marketplace.disclosure.runtime — the template-key FAMILY, enumerated (3) + 'marketplace.disclosure.runtime.node', + 'marketplace.disclosure.runtime.sandbox', + 'marketplace.disclosure.runtime.worker', + // preview.unpublishedBar — the ADR-0045 banner (5) + 'preview.unpublishedBar.message', + 'preview.unpublishedBar.publish', + 'preview.unpublishedBar.publishing', + 'preview.unpublishedBar.published', + 'preview.unpublishedBar.publishFailed', + // preview.history — the ADR-0067 commit timeline (13) + 'preview.history.button', + 'preview.history.title', + 'preview.history.description', + 'preview.history.loadFailed', + 'preview.history.loading', + 'preview.history.empty', + 'preview.history.revertLabel', + 'preview.history.applyLabel', + 'preview.history.revert', + 'preview.history.items', + 'preview.history.revertAction', + 'preview.history.reverted', + 'preview.history.revertFailed', +] as const; + +/** The 37 the guard measured; the other 3 are the prefix family. */ +const MEASURED_KEYS = KEYS.filter((k) => !k.startsWith('marketplace.disclosure.runtime.')); + +const LANGS = Object.keys(builtInLocales); + +const DISCLOSURE = 'packages/app-shell/src/console/marketplace/PluginDisclosure.tsx'; +const PACKAGE_PAGE = 'packages/app-shell/src/console/marketplace/MarketplacePackagePage.tsx'; +const ORG_PAGE = 'packages/app-shell/src/console/marketplace/MarketplacePage.tsx'; + +const at = (pack: unknown, path: string): unknown => + path.split('.').reduce((n, k) => (n as Record | undefined)?.[k], pack); + +const wrapperFor = (lang: string) => + function Wrapper({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); + }; + +/** + * Read a component's source. `import.meta.url` is not a file: URL in the dom + * project, so resolve from the vitest root (which the invocation guard pins to + * the repo root) — and prove the read landed, or every assertion on it is + * vacuous. + */ +function sourceOf(rel: string): string { + const path = join(process.cwd(), rel); + expect(existsSync(path), `source not found at ${path}`).toBe(true); + return readFileSync(path, 'utf8'); +} + +beforeEach(() => { + // The provider persists the last language (objectstack#5406); without this a + // stale locale leaks into the `en` cases. + window.localStorage.clear(); +}); + +describe('objectui#3546 slice five — the marketplace and preview namespaces', () => { + it('covers all ten packs and all forty keys (guards the loops from emptying)', () => { + expect(LANGS).toHaveLength(10); + expect(KEYS).toHaveLength(40); + expect(new Set(KEYS).size).toBe(40); + // 37 measured missing keys + 3 that came from the prefix family. Splitting + // the two apart here is what stops a later reader reading "40" off the + // ratchet, which only ever held 37 + one prefix line. + expect(MEASURED_KEYS).toHaveLength(37); + const perArea = KEYS.reduce>((acc, k) => { + const area = k.split('.').slice(0, 2).join('.'); + acc[area] = (acc[area] ?? 0) + 1; + return acc; + }, {}); + expect(perArea).toEqual({ + 'marketplace.action': 1, + 'marketplace.install': 2, + 'marketplace.org': 5, + 'marketplace.disclosure': 14, + 'preview.unpublishedBar': 5, + 'preview.history': 13, + }); + }); + + it.each(LANGS)('%s defines every key in this slice as a non-empty string', (lang) => { + for (const key of KEYS) { + const value = at(builtInLocales[lang], key); + expect(typeof value, `${lang}.${key}`).toBe('string'); + expect((value as string).trim().length, `${lang}.${key} is empty`).toBeGreaterThan(0); + } + }); + + it('the nine non-en packs carry real translations, not the English strings', () => { + // The failure this catches is a backfill that copy-pastes `en` into the other + // nine packs: full key parity, ten packs green, nine languages still reading + // English. `all-locales-key-parity.test.ts` cannot see it — it compares key + // sets and placeholder shape, and never looks at what a value SAYS. + // + // Set equality against the EMPTY set, not a subset: unlike slice two (12 + // pairs), three (18) and four (1 — French "Assistant"), this slice has no + // legitimate cognate at all. 0 of 360 pairs is identical to `en`. A first + // identical pair fails here and must be justified on the line below, the way + // those slices justified theirs. + const identical: string[] = []; + for (const lang of LANGS.filter((l) => l !== 'en')) { + for (const key of KEYS) { + if (at(builtInLocales[lang], key) === at(builtInLocales.en, key)) identical.push(`${lang} :: ${key}`); + } + } + expect(identical.sort()).toEqual([]); + }); + + it('the empty cognate set is not vacuous — the loanwords are there and still differ', () => { + // An empty set-equality is green when the packs are green AND when they are + // empty (slice two's B1 lesson). The presence assertion above covers + // emptiness; this one covers the other way the set could be empty for the + // wrong reason — a pack that dodged every English-looking word. These four + // DO embed the loanword and are still not byte-identical to `en`, which is + // what shows the zero is a real zero and not an avoidance. + expect(at(builtInLocales.de, 'marketplace.disclosure.hooks')).toBe('Lifecycle-Hooks'); + expect(at(builtInLocales.fr, 'marketplace.disclosure.hooks')).toBe('Hooks de cycle de vie'); + expect(at(builtInLocales.es, 'marketplace.disclosure.runtime.sandbox')).toBe('En sandbox'); + expect(at(builtInLocales.pt, 'marketplace.disclosure.runtime.sandbox')).toBe('Em sandbox'); + for (const [lang, key] of [ + ['de', 'marketplace.disclosure.hooks'], + ['fr', 'marketplace.disclosure.hooks'], + ['es', 'marketplace.disclosure.runtime.sandbox'], + ['pt', 'marketplace.disclosure.runtime.sandbox'], + ] as const) { + expect(at(builtInLocales[lang], key)).not.toBe(at(builtInLocales.en, key)); + } + }); + + it('exactly three keys interpolate, and every pack carries the same holes', () => { + // Unlike slice four (where not one of the 46 strings took an option), three + // of these do, and the rest must NOT: a translator who invents `{{name}}` + // renders the braces to the user verbatim, and one who drops `{{version}}` + // from `install.updateTo` leaves the dropdown saying "Update →" with no + // version. `all-locales-key-parity` compares placeholder shape too — this + // states the intended shape by name so a wrong one is legible here. + // Deliberately two regexes: a `/g` one is stateful, and reusing it for + // `.test()` inside a `filter` silently skips every other match through + // `lastIndex`. (It did, on the first run of this file.) + const HOLES = /\{\{?\w+\}?\}/g; + const HAS_HOLE = /\{\{?\w+\}?\}/; + const INTERPOLATED: Record = { + 'marketplace.install.updateTo': '{{version}}', + 'marketplace.install.installedVersion': '{{version}}', + 'marketplace.org.installed': '{{name}}', + }; + expect(KEYS.filter((k) => HAS_HOLE.test(at(builtInLocales.en, k) as string)).sort()).toEqual( + Object.keys(INTERPOLATED).sort(), + ); + for (const lang of LANGS) { + for (const key of KEYS) { + const holes = ((at(builtInLocales[lang], key) as string).match(HOLES) ?? []).join(','); + expect(holes, `${lang}.${key}`).toBe(INTERPOLATED[key] ?? ''); + } + } + }); + + it('the ellipsis, middle-dot and arrow characters follow en in every pack', () => { + // The packs mirror `en`'s typographic bytes rather than picking their own. + // Three keys end in U+2026 (the two "…ing" progress labels and the history + // loader); one carries U+00B7 (following `marketplace.detail.installedV`, + // "Installed · v{{version}}"); one carries U+2192 (following + // `marketplace.browseLink`, which every pack including `ar` keeps as "→"). + const ellipsised = KEYS.filter((k) => (at(builtInLocales.en, k) as string).endsWith('…')); + expect(ellipsised.sort()).toEqual([ + 'marketplace.org.installing', + 'preview.history.loading', + 'preview.unpublishedBar.publishing', + ]); + for (const lang of LANGS) { + for (const key of ellipsised) { + const value = at(builtInLocales[lang], key) as string; + expect(value.endsWith('…'), `${lang}.${key} = ${value}`).toBe(true); + expect(value.includes('...'), `${lang}.${key} mixes "..." into an … string`).toBe(false); + } + expect( + (at(builtInLocales[lang], 'marketplace.disclosure.runtime.node') as string).includes(' · '), + `${lang} runtime.node lost the middle dot`, + ).toBe(true); + expect( + (at(builtInLocales[lang], 'marketplace.install.updateTo') as string).includes(' → '), + `${lang} install.updateTo lost the arrow`, + ).toBe(true); + } + }); + + it('the ja trailing colon follows that pack\'s predicate/noun split, not en', () => { + // The ja pack is not inconsistent here, it is conditioned: a value that ends + // in a PREDICATE takes the halfwidth ":" (10 of 12 — the five connectAgent + // bodies, oauth.consent.willAllow, preview.changes.loadFailed, …), and one + // that ends in a NOUN label takes the fullwidth ":" (4 of 5 — + // grid.import.mappingTemplate, auth.verifyEmail.sentTo, + // preview.changes.detailChangedKeys, …). + // + // Both of this slice's ja colons end in a predicate (`付与されます`, + // `読み込めませんでした`), so both are halfwidth. `loadFailed` additionally + // matches its same-name sibling `preview.changes.loadFailed` byte for byte; + // `grantsIntro` has no sibling and is decided by the split alone. Written + // down because the first draft of this file got `grantsIntro` wrong (it had + // the fullwidth form) and only the census caught it. + expect(at(builtInLocales.ja, 'marketplace.disclosure.grantsIntro')).toBe( + 'インストール時に、このパッケージには次の権限が付与されます:', + ); + expect(at(builtInLocales.ja, 'preview.history.loadFailed')).toBe('履歴を読み込めませんでした:'); + for (const key of ['marketplace.disclosure.grantsIntro', 'preview.history.loadFailed']) { + expect((at(builtInLocales.ja, key) as string).endsWith(':'), `ja ${key} used the fullwidth colon`).toBe(false); + } + // zh goes the other way on the same two strings: that pack writes fullwidth + // punctuation throughout (165 fullwidth commas, 13 fullwidth trailing colons + // against 6 halfwidth), and preview.changes.loadFailed zh is fullwidth too. + for (const key of ['marketplace.disclosure.grantsIntro', 'preview.history.loadFailed']) { + expect((at(builtInLocales.zh, key) as string).endsWith(':'), `zh ${key} lost the fullwidth colon`).toBe(true); + } + // ko has no fullwidth trailing colon anywhere in the pack (16 halfwidth, 0). + expect((at(builtInLocales.ko, 'marketplace.disclosure.grantsIntro') as string).endsWith(':')).toBe(true); + }); + + describe('the marketplace.disclosure.runtime. template-key family', () => { + it('is enumerated from the trust-tier enum, not guessed', () => { + // The family left `missingPrefixes` because all three members now exist. + // The canonical enum is objectstack spec `PluginRuntimeSchema` + // (z.enum(['node','sandbox','worker']), ADR-0025 §3.6) which is in another + // repo; the in-repo authority is the component's own fallback map, and a + // fourth tier on either side fails here — the job the prefix entry did. + const src = sourceOf(DISCLOSURE); + const block = src.match(/const RUNTIME_FALLBACK: Record = \{([^}]+)\}/); + expect(block, 'RUNTIME_FALLBACK not found — did the map move?').not.toBeNull(); + const members = [...block![1].matchAll(/(\w+):\s*'([^']*)'/g)].map((m) => m[1]); + expect(members.sort()).toEqual(['node', 'sandbox', 'worker']); + const packMembers = Object.keys(at(builtInLocales.en, 'marketplace.disclosure.runtime') as object); + expect(packMembers.sort()).toEqual(['node', 'sandbox', 'worker']); + // And the call site really is the template shape this family describes. + expect(src).toContain('t(`marketplace.disclosure.runtime.${version.runtime}`'); + }); + + it("each en value is byte-identical to the component's own fallback label", () => { + // `RUNTIME_FALLBACK` is the map i18next's `defaultValue` reads. The two + // paths must not diverge: with the pack present i18next answers, without it + // the map does, and a user must not be able to tell which one ran. + const block = sourceOf(DISCLOSURE).match( + /const RUNTIME_FALLBACK: Record = \{([^}]+)\}/, + ); + const labels = Object.fromEntries( + [...block![1].matchAll(/(\w+):\s*'([^']*)'/g)].map((m) => [m[1], m[2]]), + ); + expect(Object.keys(labels)).toHaveLength(3); + for (const [tier, label] of Object.entries(labels)) { + expect( + at(builtInLocales.en, `marketplace.disclosure.runtime.${tier}`), + `en marketplace.disclosure.runtime.${tier}`, + ).toBe(label); + } + }); + + it.each(['en', 'zh'])('%s renders every tier through the real template call', (lang) => { + const { result } = renderHook(() => useObjectTranslation(), { wrapper: wrapperFor(lang) }); + for (const tier of ['node', 'sandbox', 'worker']) { + const full = `marketplace.disclosure.runtime.${tier}`; + const value = result.current.t(full); + expect(value, `${lang} rendered the raw key for ${full}`).not.toBe(full); + expect(value, `${lang}.${full}`).toBe(at(builtInLocales[lang], full)); + } + }); + + it('zh tiers are Chinese — the half that was red before the backfill', () => { + const { result } = renderHook(() => useObjectTranslation(), { wrapper: wrapperFor('zh') }); + const { t } = result.current; + expect(t('marketplace.disclosure.runtime.node')).toBe('进程内 · 完全信任'); + expect(t('marketplace.disclosure.runtime.sandbox')).toBe('沙箱隔离'); + expect(t('marketplace.disclosure.runtime.worker')).toBe('进程外'); + }); + }); + + it("marketplace.org.installed's en value is the template literal's sentence, hole for hole", () => { + // The one call site whose `defaultValue` is not a static string: + // `` t('marketplace.org.installed', { defaultValue: `Installed ${pkg.display_name}`, name: pkg.display_name }) ``. + // A byte comparison is structurally impossible against a template literal, so + // the equivalence pinned instead is: the literal's static head plus the + // interpolated identifier === the pack's head plus i18next's hole, driven by + // the `name` option the site ALREADY passes. Anything else and the toast + // changes wording the moment the pack starts answering. + const src = sourceOf(ORG_PAGE); + const call = src.match( + /t\('marketplace\.org\.installed',\s*\{\s*defaultValue:\s*`([^`]*)`,\s*name:\s*([\w.]+)\s*\}\)/, + ); + expect(call, 'the marketplace.org.installed call site moved — recheck the mapping').not.toBeNull(); + const [, template, option] = call!; + expect(template).toBe('Installed ${pkg.display_name}'); + expect(option).toBe('pkg.display_name'); + expect(at(builtInLocales.en, 'marketplace.org.installed')).toBe('Installed {{name}}'); + }); + + it("marketplace.action.updateTo's `version` option is inert, and en does not pretend otherwise", () => { + // `MarketplacePackagePage.tsx:555` passes `version` next to a `defaultValue` + // that has no hole for it, so nothing has ever been interpolated there and + // the primary button reads a bare "Update". Its sibling + // `marketplace.install.updateTo` (the environment dropdown, same file) DOES + // render the version — the two were plainly meant to match. + // + // Deliberately NOT repaired by giving `en` a `{{version}}` hole: that would + // change a string the user sees today, on a call site whose intent is + // ambiguous, in a slice whose contract is that `en` equals the inline + // `defaultValue` byte for byte. Filed as a finding; this assertion is what + // makes the next reader see the choice instead of "fixing" it silently. + const src = sourceOf(PACKAGE_PAGE); + expect(src).toContain("t('marketplace.action.updateTo', { defaultValue: 'Update', version: latestVersion })"); + for (const lang of LANGS) { + expect(at(builtInLocales[lang], 'marketplace.action.updateTo')).not.toContain('{{'); + } + expect(at(builtInLocales.en, 'marketplace.install.updateTo')).toContain('{{version}}'); + }); + + it('the two revert labels collapse only in the packs with no letter case', () => { + // `en` distinguishes the row's KIND chip ('revert', lowercase) from the + // button ('Revert') by case alone. Packs with a bicameral script keep that + // contrast; `zh` and `ar` have no case and necessarily render one string for + // both. `ja`/`ko` have no case either and still contrast, because their words + // differ in kind (noun vs verb) rather than in case. Pinning the exact set + // stops a reviewer reading the zh/ar duplication as a copy-paste slip. + const collapsed = LANGS.filter( + (l) => at(builtInLocales[l], 'preview.history.revert') === at(builtInLocales[l], 'preview.history.revertAction'), + ); + expect(collapsed.sort()).toEqual(['ar', 'zh']); + expect(at(builtInLocales.ja, 'preview.history.revert')).toBe('取り消し'); + expect(at(builtInLocales.ja, 'preview.history.revertAction')).toBe('取り消す'); + expect(at(builtInLocales.ko, 'preview.history.revert')).toBe('되돌림'); + expect(at(builtInLocales.ko, 'preview.history.revertAction')).toBe('되돌리기'); + }); + + it('the register split between the two namespaces is deliberate, per pack neighbourhood', () => { + // "Same rule, different answer" (slice two's es ruling). Measured on the + // packs as they stood before this slice: + // zh marketplace uses 你 (3 strings, 0 with 您) — marketplace.subtitle; + // zh preview uses 您 (2 strings, 0 with 你) — preview.draftBar.*. + // es marketplace is unanimously usted (9 / 0). + // So the two namespaces get DIFFERENT second persons in zh, on purpose. The + // es `preview` neighbourhood is genuinely split before this slice + // (draftBar.message is tú while draftBar.messageClean / sampleDataBody are + // usted — filed as a finding), and the tie-break taken is usted, on the + // strength of home.pendingDrafts.published ("¡Publicado! Sus cambios…"), the + // structural twin of preview.unpublishedBar.published. + expect(at(builtInLocales.zh, 'marketplace.org.heading')).toBe('你的组织'); + expect(at(builtInLocales.zh, 'preview.unpublishedBar.message')).toContain('您的用户'); + expect(at(builtInLocales.zh, 'preview.unpublishedBar.published')).toContain('您的用户'); + expect(at(builtInLocales.es, 'marketplace.org.heading')).toBe('Su organización'); + expect(at(builtInLocales.es, 'preview.unpublishedBar.published')).toContain('sus usuarios'); + expect(at(builtInLocales.es, 'preview.unpublishedBar.message')).toContain('Publíquela'); + // ...and the neighbours that ruling was read off are still what it claims. + expect(at(builtInLocales.zh, 'marketplace.subtitle')).toContain('你的'); + expect(at(builtInLocales.zh, 'preview.draftBar.sampleDataBody')).toContain('您'); + expect(at(builtInLocales.es, 'home.pendingDrafts.published')).toBe('¡Publicado! Sus cambios están activos.'); + }); + + it('reused strings are the neighbours own translations, not second renderings', () => { + // Where this slice's `en` string already existed verbatim elsewhere, the + // existing row is reused rather than re-translated, so one English string + // never renders as two different sentences in the same language. These are + // the pairs; a drift on either side fails here. + const REUSED: Array<[newKey: string, neighbour: string]> = [ + ['marketplace.action.updateTo', 'form.update'], + ['marketplace.install.installedVersion', 'marketplace.installedBadge'], + ['marketplace.org.install', 'marketplace.action.install'], + ['marketplace.org.installedBadge', 'marketplace.installed'], + ['marketplace.org.installing', 'marketplace.action.installing'], + ['preview.unpublishedBar.publish', 'preview.draftBar.publish'], + ['preview.unpublishedBar.publishing', 'preview.draftBar.publishing'], + ['preview.unpublishedBar.publishFailed', 'home.pendingDrafts.publishFailed'], + ['preview.history.button', 'detail.history'], + ]; + for (const [newKey, neighbour] of REUSED) { + // the premise: the two really are the same English string + expect(at(builtInLocales.en, newKey), `en ${newKey} vs ${neighbour}`).toBe( + at(builtInLocales.en, neighbour), + ); + for (const lang of LANGS) { + expect(at(builtInLocales[lang], newKey), `${lang} ${newKey} diverged from ${neighbour}`).toBe( + at(builtInLocales[lang], neighbour), + ); + } + } + // The two rows deliberately NOT reused, and why: `marketplace.action.installed` + // carries the feminine/diacritised agreement with "aplicación" ("Instalada" / + // "مثبَّت") while the org badge labels a package, and + // `home.pendingDrafts.publishing` differs from the same-namespace + // `preview.draftBar.publishing` in ja/ko. + expect(at(builtInLocales.es, 'marketplace.action.installed')).toBe('Instalada'); + expect(at(builtInLocales.es, 'marketplace.org.installedBadge')).toBe('Instalado'); + expect(at(builtInLocales.ja, 'home.pendingDrafts.publishing')).not.toBe( + at(builtInLocales.ja, 'preview.unpublishedBar.publishing'), + ); + }); + + it("revertAction borrows the packs' undo VERB even though its en string differs", () => { + // A different and weaker claim than the table above, stated separately + // rather than smuggled into it: `en` says "Revert" where `grid.bulk.undo` + // says "Undo", so these are NOT the same English string and the reuse rule + // does not apply automatically. What is reused is each pack's existing verb + // for this user action (撤销 / 取り消す / 되돌리기 / Rückgängig / Annuler / + // Deshacer / Desfazer / Отменить / تراجع), because no pack has a distinct + // word for git-style "revert" and coining a second verb for the same button + // would teach the user two names for one thing. If `en` ever gains a real + // revert/undo distinction, this assertion is the place that has to be + // revisited — which is why it says so out loud. + expect(at(builtInLocales.en, 'preview.history.revertAction')).toBe('Revert'); + expect(at(builtInLocales.en, 'grid.bulk.undo')).toBe('Undo'); + for (const lang of LANGS.filter((l) => l !== 'en')) { + expect(at(builtInLocales[lang], 'preview.history.revertAction'), `${lang} revertAction`).toBe( + at(builtInLocales[lang], 'grid.bulk.undo'), + ); + } + }); + + it('preview.history.items reuses common.itemCount, the same count-plus-unit adjacency', () => { + // The call site renders `{c.itemCount} {t('preview.history.items')}` — the + // number comes from the component and the pack supplies only the unit. That + // is structurally identical to `common.itemCount` (`{{count}} items`), which + // this repo already translates in all ten packs, so the unit is taken from + // there rather than invented. + // + // This one was invented first, and wrongly: `de Element(e)` / `fr élément(s)` + // / `es elemento(s)` / `pt item(ns)` / `ru элемент(ов)`, copying the + // parenthesised plural marker those packs use elsewhere. Two things were wrong + // with it — `ru` uses that marker NOWHERE in 2832 values (it restructures, or + // abbreviates as in `fields.relativeDate.overdue`'s `{{count}} дн.`), and + // `item(ns)` does not even yield `itens` under pt's own append convention. + // The reason the neighbour was missed: the reuse table above matches + // BYTE-IDENTICAL `en` strings, and `common.itemCount`'s en is `{{count}} items`, + // not `item(s)` — so a neighbour expressing the same concept with different + // English is invisible to that search. Hence this assertion, by hand. + const UNIT: Record = { + zh: '项', + ja: '件', + de: 'Elemente', + fr: 'éléments', + es: 'elementos', + pt: 'itens', + ru: 'элементов', + ar: 'عناصر', + }; + for (const [lang, unit] of Object.entries(UNIT)) { + expect(at(builtInLocales[lang], 'preview.history.items'), `${lang} items`).toBe(unit); + // the premise: that unit really is what common.itemCount uses + expect(at(builtInLocales[lang], 'common.itemCount'), `${lang} common.itemCount`).toBe( + `{{count}} ${unit}`, + ); + } + // `ko` is the one deliberate departure. `common.itemCount` ko is + // `{{count}}개 항목` — the counter 개 binds to the numeral with no space — but + // this call site emits `{count}` + a space + the unit, so carrying 개 across + // would render `3 개 항목` with a space inside the number-counter unit. The + // counter is dropped and the bare noun kept, which reads correctly as `3 항목`. + expect(at(builtInLocales.ko, 'preview.history.items')).toBe('항목'); + expect(at(builtInLocales.ko, 'common.itemCount')).toBe('{{count}}개 항목'); + // And no pack reintroduced a parenthesised plural marker here. + for (const lang of LANGS.filter((l) => l !== 'en')) { + expect( + /\(\w{1,4}\)/.test(at(builtInLocales[lang], 'preview.history.items') as string), + `${lang} items reintroduced a "(s)" marker`, + ).toBe(false); + } + // en keeps the call site's own spelling, per this slice's byte-identity rule. + expect(at(builtInLocales.en, 'preview.history.items')).toBe('item(s)'); + }); + + it('the ratchet actually shrank — no marketplace/preview key is still baselined', () => { + // `scripts/i18n-call-site-key-baseline.json` fails the build both ways: an + // unfixed key missing from it, AND a fixed key still listed. Pinning the + // absence here means a revert of the packs cannot quietly restore the entries + // and go green again. + const baselinePath = join(process.cwd(), 'scripts/i18n-call-site-key-baseline.json'); + expect(existsSync(baselinePath), `baseline not found at ${baselinePath}`).toBe(true); + const baseline = JSON.parse(readFileSync(baselinePath, 'utf8')) as { + missingKeys: Record; + missingPrefixes: Record; + }; + expect( + Object.keys(baseline.missingKeys).filter( + (k) => k.startsWith('marketplace.') || k.startsWith('preview.'), + ), + ).toEqual([]); + // 68 before this slice, 37 removed. The other namespaces' debt is not this + // slice's to spend; this number moves once per slice, and only downwards. + expect(Object.keys(baseline.missingKeys).length).toBe(31); + // The prefix family this slice handled is GONE from the ratchet, and the two + // that remain are untouched — neither belongs to these namespaces. + expect(Object.keys(baseline.missingPrefixes).sort()).toEqual([ + 'gantt.linkEnd.', + 'organization.invitations.status.', + ]); + expect(Object.keys(baseline.missingPrefixes)).not.toContain('marketplace.disclosure.runtime.'); + }); + + describe('through the real binding — bare useObjectTranslation, provider mounted', () => { + /** One key per component that owns a group of these keys. */ + const SAMPLE: Array<[key: string, owner: string]> = [ + ['marketplace.org.heading', 'MarketplacePage (org strip)'], + ['marketplace.action.updateTo', 'MarketplacePackagePage (primary button)'], + ['marketplace.disclosure.containsCode', 'PluginDisclosure'], + ['marketplace.disclosure.grantsIntro', 'PluginDisclosure (permission groups)'], + ['preview.unpublishedBar.message', 'UnpublishedAppBar'], + ['preview.history.title', 'CommitTimeline'], + ]; + + it('all five owning components bind t from a bare useObjectTranslation', () => { + // The premise of mounting a provider at all. If one of them ever moves + // behind a `createSafeTranslation` defaults map, the map would answer in + // provider-less tests and this suite would stop describing the console's + // path — so assert the binding, do not assume it. + for (const rel of [ + ORG_PAGE, + PACKAGE_PAGE, + DISCLOSURE, + 'packages/app-shell/src/preview/CommitTimeline.tsx', + 'packages/app-shell/src/preview/UnpublishedAppBar.tsx', + ]) { + const src = sourceOf(rel); + expect(src, `${rel} no longer imports useObjectTranslation`).toContain( + "import { useObjectTranslation } from '@object-ui/i18n'", + ); + expect(src, `${rel} gained a defaults-map translator`).not.toContain('createSafeTranslation'); + } + }); + + it.each(['en', 'zh'])('%s resolves every sampled key from the pack', (lang) => { + const { result } = renderHook(() => useObjectTranslation(), { wrapper: wrapperFor(lang) }); + for (const [key, owner] of SAMPLE) { + const value = result.current.t(key); + expect(value, `${lang} ${owner} rendered the raw key for ${key}`).not.toBe(key); + expect(value, `${lang}.${key}`).toBe(at(builtInLocales[lang], key)); + } + }); + + it('zh is Chinese — the half that was red before the backfill', () => { + // Pre-fix each of these returned the inline English `defaultValue`, in a zh + // session. That is the whole defect, and only a non-en assertion sees it. + const { result } = renderHook(() => useObjectTranslation(), { wrapper: wrapperFor('zh') }); + const { t } = result.current; + expect(t('marketplace.disclosure.containsCode')).toBe('此软件包包含代码'); + expect(t('marketplace.disclosure.acknowledge')).toBe('我了解此软件包会运行代码,并将获得上述权限。'); + expect(t('preview.history.title')).toBe('构建历史'); + expect(t('preview.unpublishedBar.published')).toBe('已发布!应用现在对您的用户可见。'); + }); + + it('en interpolates the version and name holes through the real binding', () => { + // The three interpolating keys, exercised the way their call sites call + // them — a pack value with the hole spelled wrongly renders the braces. + const { result } = renderHook(() => useObjectTranslation(), { wrapper: wrapperFor('en') }); + const { t } = result.current; + expect(t('marketplace.install.updateTo', { version: '2.1.0' })).toBe('Update → v2.1.0'); + expect(t('marketplace.install.installedVersion', { version: '1.4.2' })).toBe('Installed v1.4.2'); + expect(t('marketplace.org.installed', { name: 'Acme CRM' })).toBe('Installed Acme CRM'); + }); + + it('zh interpolates the same holes — the nine packs kept them', () => { + const { result } = renderHook(() => useObjectTranslation(), { wrapper: wrapperFor('zh') }); + const { t } = result.current; + expect(t('marketplace.install.updateTo', { version: '2.1.0' })).toBe('更新 → v2.1.0'); + expect(t('marketplace.org.installed', { name: 'Acme CRM' })).toBe('已安装 Acme CRM'); + }); + + it.each([ + ['fr', 'preview.history.title', 'Historique des générations'], + ['de', 'marketplace.disclosure.noPermissions', 'Fordert keine besonderen Berechtigungen an.'], + ['es', 'preview.history.empty', 'Aún no hay historial para esta aplicación.'], + ['pt', 'marketplace.disclosure.fs', 'Acesso ao sistema de arquivos'], + ['ru', 'marketplace.disclosure.unreviewed', 'Ещё не проверено'], + ['ja', 'preview.history.revertFailed', '取り消しに失敗しました'], + ['ko', 'marketplace.disclosure.grantsIntro', '설치하면 이 패키지에 다음 권한이 부여됩니다:'], + ['ar', 'preview.unpublishedBar.published', 'تم النشر! التطبيق مرئي الآن لمستخدميك.'], + ])('%s renders a user-visible string from the pack', (lang, key, expected) => { + // One pinned surface per remaining pack, across four writing systems, so a + // pack that silently reverts to English is caught by name and not only by + // the aggregate above. + const { result } = renderHook(() => useObjectTranslation(), { wrapper: wrapperFor(lang) }); + expect(result.current.t(key)).toBe(expected); + }); + + it('the ru pack keeps ё, matching its own neighbours', () => { + // The pack writes ё in 161 places (`grid.import.undoConfirm` has + // «обновлённые», `console.shortcuts.toggleDarkMode` «тёмный»), and + // "Ещё не проверено" is exactly where a backfill would collapse it to е. + const { result } = renderHook(() => useObjectTranslation(), { wrapper: wrapperFor('ru') }); + expect(result.current.t('marketplace.disclosure.unreviewed')).toContain('Ещё'); + }); + + it('the ar pack does not open an RTL sentence with a Latin token', () => { + // Same rule slices three and four applied. These strings are about code, + // sandboxes and processes — the places a Latin technical token would most + // easily have led the sentence and put the bidi boundary in the wrong place. + const { result } = renderHook(() => useObjectTranslation(), { wrapper: wrapperFor('ar') }); + for (const key of KEYS) { + const value = result.current.t(key); + expect(/^[A-Za-z]/.test(value), `${key} starts with a Latin token: ${value}`).toBe(false); + } + // The one place a Latin run is unavoidable is the interpolated version, and + // it stays inside the string rather than leading it. + expect(result.current.t('marketplace.install.updateTo', { version: '2.1.0' })).toBe('تحديث → v2.1.0'); + }); + + it('fr keeps its straight apostrophe and its space before punctuation', () => { + // The pack writes U+0027 (502 occurrences against 21 U+2019) and puts a + // narrow space before `:` and `!` (6/0 and 49/0), which is where a + // copy-paste from `en` shows up first. + const { result } = renderHook(() => useObjectTranslation(), { wrapper: wrapperFor('fr') }); + const { t } = result.current; + expect(t('preview.history.loadFailed')).toBe("Impossible de charger l'historique :"); + expect(t('preview.unpublishedBar.published')).toBe( + "Publié ! L'application est maintenant visible par vos utilisateurs.", + ); + for (const key of KEYS) { + expect((t(key) as string).includes('’'), `fr ${key} used a curly apostrophe`).toBe(false); + } + }); + }); +}); diff --git a/packages/i18n/src/__tests__/organization-namespace-3546.test.tsx b/packages/i18n/src/__tests__/organization-namespace-3546.test.tsx index cc3b1cfbd..c68023ef2 100644 --- a/packages/i18n/src/__tests__/organization-namespace-3546.test.tsx +++ b/packages/i18n/src/__tests__/organization-namespace-3546.test.tsx @@ -263,10 +263,10 @@ describe('objectui#3546 slice two — the organization namespace', () => { // prefix-family slice — this assertion is what stops it being forgotten. expect(Object.keys(baseline.missingPrefixes)).toContain('organization.invitations.status.'); // The other namespaces' debt is not this slice's to spend. Slice three - // (auth/oauth/acceptInvitation, 54 keys) took it from 163 to 109 and slice - // four (console, 41 keys) to 68; this number moves once per slice, and only - // downwards. - expect(Object.keys(baseline.missingKeys).length).toBe(68); + // (auth/oauth/acceptInvitation, 54 keys) took it from 163 to 109, slice four + // (console, 41 keys) to 68 and slice five (marketplace + preview, 37 keys) to + // 31; this number moves once per slice, and only downwards. + expect(Object.keys(baseline.missingKeys).length).toBe(31); }); describe('through the real binding — bare useObjectTranslation, provider mounted', () => { diff --git a/packages/i18n/src/locales/ar.ts b/packages/i18n/src/locales/ar.ts index 336f87cd7..653ed888c 100644 --- a/packages/i18n/src/locales/ar.ts +++ b/packages/i18n/src/locales/ar.ts @@ -2984,6 +2984,7 @@ const ar = { dismiss: "رفض", openOnCloud: "فتح في السحابة", backHome: "العودة إلى الرئيسية", + updateTo: "تحديث", }, install: { dialogTitle: "تثبيت {{name}}", @@ -3000,6 +3001,37 @@ const ar = { localManifestConflict: "{{message}}\nملاحظة: تطبيق محلي يمتلك بالفعل هذا المعرف. أزله أولاً من objectstack.config.ts.", localUnauthorized: "سجل الدخول إلى هذا الوقت التشغيل أولاً.", localMarketplaceUnavailable: "هذا الوقت التشغيل لا يحتوي على OS_CLOUD_URL مُهيأ.", + updateTo: "تحديث → v{{version}}", + installedVersion: "مثبت 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: "عند التثبيت، ستُمنح هذه الحزمة:", + services: "خدمات المنصة", + hooks: "خطافات دورة الحياة", + network: "الوصول إلى الشبكة", + fs: "الوصول إلى نظام الملفات", + noPermissions: "لا تطلب أي أذونات خاصة.", + acknowledge: "أفهم أن هذه الحزمة تنفّذ تعليمات برمجية وتحصل على الأذونات المذكورة أعلاه.", + runtime: { + node: "داخل العملية · ثقة كاملة", + sandbox: "في بيئة معزولة", + worker: "خارج العملية", + }, }, uninstall: { confirm: "إلغاء تثبيت {{manifestId}} v{{version}} من هذا الوقت التشغيل؟\n\nسيتم إزالة المانيفست المخزن مؤقتاً.", @@ -3249,6 +3281,32 @@ const ar = { confirmNote: 'النشر يُصدر كل المسودات المعلقة ({{count}}) لهذه الحزمة دفعة واحدة.', publishConfirm: 'نشر الكل', }, + // ADR-0045 — the materialized-but-unlisted app banner + // (UnpublishedAppBar.tsx), sibling of draftBar above. + unpublishedBar: { + message: 'تطبيق غير منشور — يعمل بالكامل، لكن لا يراه سوى المنشئين. انشره ليصبح مرئياً لمستخدميك.', + publish: 'نشر', + publishing: 'جارٍ النشر…', + published: 'تم النشر! التطبيق مرئي الآن لمستخدميك.', + publishFailed: 'فشل النشر', + }, + // ADR-0067 — the append-only build/revert timeline (CommitTimeline.tsx); + // `button` is the banner's entry point into it. + history: { + button: 'السجل', + title: 'سجل البناء', + description: 'كل تغيير في هذا التطبيق، الأحدث أولاً. تراجع عن أي خطوة لإلغائها — لا حاجة لتأكيد النشر.', + loadFailed: 'تعذر تحميل السجل:', + loading: 'جارٍ تحميل السجل…', + empty: 'لا يوجد سجل لهذا التطبيق بعد.', + revertLabel: 'تم التراجع عن تغيير', + applyLabel: 'تغيير من البناء', + revert: 'تراجع', + items: 'عناصر', + revertAction: 'تراجع', + reverted: 'تم التراجع — أُلغي التغيير.', + revertFailed: 'فشل التراجع', + }, }, filterBuilder: { where: "حيث", diff --git a/packages/i18n/src/locales/de.ts b/packages/i18n/src/locales/de.ts index 403532a50..adfb98b9e 100644 --- a/packages/i18n/src/locales/de.ts +++ b/packages/i18n/src/locales/de.ts @@ -2984,6 +2984,7 @@ const de = { dismiss: "Schließen", openOnCloud: "In der Cloud öffnen", backHome: "Zurück zur Startseite", + updateTo: "Aktualisieren", }, install: { dialogTitle: "{{name}} installieren", @@ -3000,6 +3001,37 @@ const de = { 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}}", + }, + // 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: "Bei der Installation erhält dieses Paket:", + 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\nDas zwischengespeicherte Manifest wird entfernt.", @@ -3249,6 +3281,32 @@ const de = { confirmNote: 'Beim Veröffentlichen werden alle {{count}} ausstehenden Entwürfe dieses Pakets atomar freigegeben.', publishConfirm: 'Alle veröffentlichen', }, + // ADR-0045 — the materialized-but-unlisted app banner + // (UnpublishedAppBar.tsx), sibling of draftBar above. + unpublishedBar: { + message: 'Unveröffentlichte App — voll funktionsfähig, aber nur für Builder sichtbar. Veröffentlichen Sie sie, damit Ihre Benutzer sie sehen.', + publish: 'Veröffentlichen', + publishing: 'Wird veröffentlicht…', + published: 'Veröffentlicht! Die App ist jetzt für Ihre Benutzer sichtbar.', + publishFailed: 'Veröffentlichen fehlgeschlagen', + }, + // ADR-0067 — the append-only build/revert timeline (CommitTimeline.tsx); + // `button` is the banner's entry point into it. + history: { + button: 'Verlauf', + title: 'Build-Verlauf', + description: 'Jede Änderung an dieser App, neueste zuerst. Machen Sie jeden Schritt rückgängig — eine Veröffentlichungsbestätigung ist nicht nötig.', + loadFailed: 'Verlauf konnte nicht geladen werden:', + loading: 'Verlauf wird geladen…', + empty: 'Für diese App gibt es noch keinen Verlauf.', + revertLabel: 'Eine Änderung rückgängig gemacht', + applyLabel: 'Build-Änderung', + revert: 'rückgängig', + items: 'Elemente', + revertAction: 'Rückgängig', + reverted: 'Rückgängig gemacht — die Änderung wurde zurückgenommen.', + revertFailed: 'Rückgängig machen fehlgeschlagen', + }, }, filterBuilder: { where: "Wo", diff --git a/packages/i18n/src/locales/en.ts b/packages/i18n/src/locales/en.ts index 5d9373b4d..0c01b0fce 100644 --- a/packages/i18n/src/locales/en.ts +++ b/packages/i18n/src/locales/en.ts @@ -2643,6 +2643,32 @@ const en = { confirmNote: 'Publishing releases all {{count}} pending drafts of this package atomically.', publishConfirm: 'Publish all', }, + // ADR-0045 — the materialized-but-unlisted app banner + // (UnpublishedAppBar.tsx), sibling of draftBar above. + unpublishedBar: { + message: 'Unpublished app — fully functional, but only builders can see it. Publish to make it visible to your users.', + publish: 'Publish', + publishing: 'Publishing…', + published: 'Published! The app is now visible to your users.', + publishFailed: 'Publish failed', + }, + // ADR-0067 — the append-only build/revert timeline (CommitTimeline.tsx); + // `button` is the banner's entry point into it. + history: { + button: 'History', + title: 'Build history', + description: 'Every change to this app, newest first. Revert any step to undo it — no publish confirmation needed.', + loadFailed: 'Could not load history:', + loading: 'Loading history…', + empty: 'No history yet for this app.', + revertLabel: 'Reverted a change', + applyLabel: 'Build change', + revert: 'revert', + items: 'item(s)', + revertAction: 'Revert', + reverted: 'Reverted — the change has been undone.', + revertFailed: 'Revert failed', + }, }, renderer: { noPageSchema: 'No page schema provided', @@ -3158,6 +3184,7 @@ const en = { dismiss: 'Dismiss', openOnCloud: 'Open on cloud', backHome: 'Back to home', + updateTo: 'Update', }, install: { dialogTitle: 'Install {{name}}', @@ -3174,6 +3201,37 @@ const en = { 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#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: 'On install, this package will be granted:', + 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; diff --git a/packages/i18n/src/locales/es.ts b/packages/i18n/src/locales/es.ts index 943513a8a..66fd45cb1 100644 --- a/packages/i18n/src/locales/es.ts +++ b/packages/i18n/src/locales/es.ts @@ -2989,6 +2989,7 @@ const es = { dismiss: "Descartar", openOnCloud: "Abrir en la nube", backHome: "Volver al inicio", + updateTo: "Actualizar", }, install: { dialogTitle: "Instalar {{name}}", @@ -3005,6 +3006,37 @@ const es = { 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}}", + }, + // 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: "Al instalarlo, este paquete recibirá:", + 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\nEl manifiesto en caché será eliminado.", @@ -3254,6 +3286,32 @@ const es = { confirmNote: 'Publicar libera atómicamente los {{count}} borradores pendientes de este paquete.', publishConfirm: 'Publicar todo', }, + // ADR-0045 — the materialized-but-unlisted app banner + // (UnpublishedAppBar.tsx), sibling of draftBar above. + unpublishedBar: { + message: 'Aplicación sin publicar — totalmente funcional, pero solo la ven los creadores. Publíquela para que sus usuarios la vean.', + publish: 'Publicar', + publishing: 'Publicando…', + published: '¡Publicado! La aplicación ya es visible para sus usuarios.', + publishFailed: 'Error al publicar', + }, + // ADR-0067 — the append-only build/revert timeline (CommitTimeline.tsx); + // `button` is the banner's entry point into it. + history: { + button: 'Historial', + title: 'Historial de compilaciones', + description: 'Todos los cambios de esta aplicación, los más recientes primero. Deshaga cualquier paso para revertirlo — no hace falta confirmar la publicación.', + loadFailed: 'No se pudo cargar el historial:', + loading: 'Cargando historial…', + empty: 'Aún no hay historial para esta aplicación.', + revertLabel: 'Se deshizo un cambio', + applyLabel: 'Cambio de compilación', + revert: 'deshacer', + items: 'elementos', + revertAction: 'Deshacer', + reverted: 'Deshecho — el cambio se ha revertido.', + revertFailed: 'Error al deshacer', + }, }, filterBuilder: { where: "Donde", diff --git a/packages/i18n/src/locales/fr.ts b/packages/i18n/src/locales/fr.ts index c0e57972d..b22df97b0 100644 --- a/packages/i18n/src/locales/fr.ts +++ b/packages/i18n/src/locales/fr.ts @@ -2984,6 +2984,7 @@ const fr = { dismiss: "Fermer", openOnCloud: "Ouvrir dans le cloud", backHome: "Retour à l'accueil", + updateTo: "Mettre à jour", }, install: { dialogTitle: "Installer {{name}}", @@ -3000,6 +3001,37 @@ const fr = { 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}}", + }, + // 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: "À l'installation, ce paquet obtiendra :", + 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\nLe manifeste en cache sera supprimé.", @@ -3249,6 +3281,32 @@ const fr = { confirmNote: 'La publication libère atomiquement les {{count}} brouillons en attente de ce paquet.', publishConfirm: 'Tout publier', }, + // ADR-0045 — the materialized-but-unlisted app banner + // (UnpublishedAppBar.tsx), sibling of draftBar above. + unpublishedBar: { + message: 'Application non publiée — entièrement fonctionnelle, mais visible seulement par les créateurs. Publiez-la pour la rendre visible à vos utilisateurs.', + publish: 'Publier', + publishing: 'Publication…', + published: 'Publié ! L\'application est maintenant visible par vos utilisateurs.', + publishFailed: 'Échec de la publication', + }, + // ADR-0067 — the append-only build/revert timeline (CommitTimeline.tsx); + // `button` is the banner's entry point into it. + history: { + button: 'Historique', + title: 'Historique des générations', + description: 'Toutes les modifications de cette application, les plus récentes d\'abord. Annulez n\'importe quelle étape — aucune confirmation de publication n\'est nécessaire.', + loadFailed: 'Impossible de charger l\'historique :', + loading: 'Chargement de l\'historique…', + empty: 'Aucun historique pour cette application.', + revertLabel: 'Modification annulée', + applyLabel: 'Modification de génération', + revert: 'annulation', + items: 'éléments', + revertAction: 'Annuler', + reverted: 'Annulé — la modification a été retirée.', + revertFailed: 'Échec de l\'annulation', + }, }, filterBuilder: { where: "Où", diff --git a/packages/i18n/src/locales/ja.ts b/packages/i18n/src/locales/ja.ts index 53d226a35..be034f5ed 100644 --- a/packages/i18n/src/locales/ja.ts +++ b/packages/i18n/src/locales/ja.ts @@ -2984,6 +2984,7 @@ const ja = { dismiss: "閉じる", openOnCloud: "クラウドで開く", backHome: "ホームに戻る", + updateTo: "更新", }, install: { dialogTitle: "{{name}} をインストール", @@ -3000,6 +3001,37 @@ const ja = { localManifestConflict: "{{message}}\nヒント:ローカルアプリがすでにこのmanifest_idを使用しています。最初にobjectstack.config.tsから削除してください。", localUnauthorized: "最初にこのランタイムにサインインしてから再試行してください。", localMarketplaceUnavailable: "このランタイムにはOS_CLOUD_URLが設定されていないため、マーケットプレイスカタログにアクセスできません。", + updateTo: "更新 → v{{version}}", + installedVersion: "インストール済み 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: "インストール時に、このパッケージには次の権限が付与されます:", + services: "プラットフォームサービス", + hooks: "ライフサイクルフック", + network: "ネットワークアクセス", + fs: "ファイルシステムアクセス", + noPermissions: "特別な権限は要求しません。", + acknowledge: "このパッケージがコードを実行し、上記の権限を取得することを理解しました。", + runtime: { + node: "プロセス内 · 完全信頼", + sandbox: "サンドボックス", + worker: "プロセス外", + }, }, uninstall: { confirm: "このランタイムから {{manifestId}} v{{version}} をアンインストールしますか?\n\nキャッシュされたマニフェストが削除されます。アプリは次の再起動まで実行中のカーネルに読み込まれたままになります。", @@ -3249,6 +3281,32 @@ const ja = { confirmNote: '公開すると、このパッケージの保留中ドラフト {{count}} 件がまとめて(アトミックに)公開されます。', publishConfirm: 'すべて公開', }, + // ADR-0045 — the materialized-but-unlisted app banner + // (UnpublishedAppBar.tsx), sibling of draftBar above. + unpublishedBar: { + message: '未公開のアプリ — 機能はすべて使えますが、ビルダーにしか表示されません。公開するとユーザーに表示されます。', + publish: '公開', + publishing: '公開中…', + published: '公開しました。アプリがユーザーに表示されます。', + publishFailed: '公開に失敗しました', + }, + // ADR-0067 — the append-only build/revert timeline (CommitTimeline.tsx); + // `button` is the banner's entry point into it. + history: { + button: '履歴', + title: 'ビルド履歴', + description: 'このアプリのすべての変更を新しい順に表示します。任意のステップを取り消して元に戻せます — 公開の確認は不要です。', + loadFailed: '履歴を読み込めませんでした:', + loading: '履歴を読み込み中…', + empty: 'このアプリの履歴はまだありません。', + revertLabel: '変更を取り消しました', + applyLabel: 'ビルドによる変更', + revert: '取り消し', + items: '件', + revertAction: '取り消す', + reverted: '取り消しました — 変更を元に戻しました。', + revertFailed: '取り消しに失敗しました', + }, }, filterBuilder: { where: "条件", diff --git a/packages/i18n/src/locales/ko.ts b/packages/i18n/src/locales/ko.ts index 92b16e765..b2c3b166e 100644 --- a/packages/i18n/src/locales/ko.ts +++ b/packages/i18n/src/locales/ko.ts @@ -2984,6 +2984,7 @@ const ko = { dismiss: "닫기", openOnCloud: "클라우드에서 열기", backHome: "홈으로 돌아가기", + updateTo: "업데이트", }, install: { dialogTitle: "{{name}} 설치", @@ -3000,6 +3001,37 @@ const ko = { localManifestConflict: "{{message}}\n참고: 로컬 앱이 이미 이 manifest_id를 가지고 있습니다. 먼저 objectstack.config.ts에서 제거하세요.", localUnauthorized: "먼저 이 런타임에 로그인하세요.", localMarketplaceUnavailable: "이 런타임에 OS_CLOUD_URL이 구성되지 않았습니다.", + updateTo: "업데이트 → v{{version}}", + installedVersion: "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: "설치하면 이 패키지에 다음 권한이 부여됩니다:", + services: "플랫폼 서비스", + hooks: "라이프사이클 후크", + network: "네트워크 액세스", + fs: "파일 시스템 액세스", + noPermissions: "특별한 권한을 요청하지 않습니다.", + acknowledge: "이 패키지가 코드를 실행하고 위 권한을 부여받는다는 점을 이해합니다.", + runtime: { + node: "프로세스 내 · 완전 신뢰", + sandbox: "샌드박스", + worker: "프로세스 외부", + }, }, uninstall: { confirm: "이 런타임에서 {{manifestId}} v{{version}}을(를) 제거하시겠습니까?\n\n캐시된 매니페스트가 삭제됩니다.", @@ -3249,6 +3281,32 @@ const ko = { confirmNote: '게시하면 이 패키지의 대기 중인 초안 {{count}}개가 한 번에(원자적으로) 게시됩니다.', publishConfirm: '모두 게시', }, + // ADR-0045 — the materialized-but-unlisted app banner + // (UnpublishedAppBar.tsx), sibling of draftBar above. + unpublishedBar: { + message: '게시되지 않은 앱 — 기능은 모두 작동하지만 빌더만 볼 수 있습니다. 게시하면 사용자에게 표시됩니다.', + publish: '게시', + publishing: '게시 중…', + published: '게시했습니다! 이제 사용자에게 앱이 표시됩니다.', + publishFailed: '게시에 실패했습니다', + }, + // ADR-0067 — the append-only build/revert timeline (CommitTimeline.tsx); + // `button` is the banner's entry point into it. + history: { + button: '기록', + title: '빌드 기록', + description: '이 앱의 모든 변경을 최신순으로 표시합니다. 어떤 단계든 되돌려 취소할 수 있습니다 — 게시 확인은 필요하지 않습니다.', + loadFailed: '기록을 불러올 수 없습니다:', + loading: '기록을 불러오는 중…', + empty: '이 앱의 기록이 아직 없습니다.', + revertLabel: '변경을 되돌렸습니다', + applyLabel: '빌드 변경', + revert: '되돌림', + items: '항목', + revertAction: '되돌리기', + reverted: '되돌렸습니다 — 변경이 취소되었습니다.', + revertFailed: '되돌리기 실패', + }, }, filterBuilder: { where: "조건", diff --git a/packages/i18n/src/locales/pt.ts b/packages/i18n/src/locales/pt.ts index ee1764802..bd4e718d3 100644 --- a/packages/i18n/src/locales/pt.ts +++ b/packages/i18n/src/locales/pt.ts @@ -2984,6 +2984,7 @@ const pt = { dismiss: "Fechar", openOnCloud: "Abrir na nuvem", backHome: "Voltar ao início", + updateTo: "Atualizar", }, install: { dialogTitle: "Instalar {{name}}", @@ -3000,6 +3001,37 @@ const pt = { 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}}", + }, + // 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: "Na instalação, este pacote receberá:", + 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 manifesto em cache será removido.", @@ -3249,6 +3281,32 @@ const pt = { confirmNote: 'Publicar libera atomicamente todos os {{count}} rascunhos pendentes deste pacote.', publishConfirm: 'Publicar tudo', }, + // ADR-0045 — the materialized-but-unlisted app banner + // (UnpublishedAppBar.tsx), sibling of draftBar above. + unpublishedBar: { + message: 'App não publicado — totalmente funcional, mas somente os criadores o veem. Publique-o para que seus usuários o vejam.', + publish: 'Publicar', + publishing: 'Publicando…', + published: 'Publicado! O app já está visível para seus usuários.', + publishFailed: 'Falha ao publicar', + }, + // ADR-0067 — the append-only build/revert timeline (CommitTimeline.tsx); + // `button` is the banner's entry point into it. + history: { + button: 'Histórico', + title: 'Histórico de compilações', + description: 'Todas as alterações deste app, as mais recentes primeiro. Desfaça qualquer etapa para revertê-la — não é necessário confirmar a publicação.', + loadFailed: 'Não foi possível carregar o histórico:', + loading: 'Carregando histórico…', + empty: 'Ainda não há histórico para este app.', + revertLabel: 'Uma alteração foi desfeita', + applyLabel: 'Alteração de compilação', + revert: 'desfazer', + items: 'itens', + revertAction: 'Desfazer', + reverted: 'Desfeito — a alteração foi revertida.', + revertFailed: 'Falha ao desfazer', + }, }, filterBuilder: { where: "Onde", diff --git a/packages/i18n/src/locales/ru.ts b/packages/i18n/src/locales/ru.ts index a5ff33b93..1ce1d0283 100644 --- a/packages/i18n/src/locales/ru.ts +++ b/packages/i18n/src/locales/ru.ts @@ -2984,6 +2984,7 @@ const ru = { dismiss: "Закрыть", openOnCloud: "Открыть в облаке", backHome: "На главную", + updateTo: "Обновить", }, install: { dialogTitle: "Установить {{name}}", @@ -3000,6 +3001,37 @@ const ru = { localManifestConflict: "{{message}}\nПримечание: Локальное приложение уже имеет этот manifest_id. Сначала удалите его из objectstack.config.ts.", localUnauthorized: "Сначала войдите в этот рантайм.", localMarketplaceUnavailable: "OS_CLOUD_URL не настроен.", + updateTo: "Обновить → v{{version}}", + installedVersion: "Установлено 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: "При установке пакет получит:", + services: "Сервисы платформы", + hooks: "Хуки жизненного цикла", + network: "Доступ к сети", + fs: "Доступ к файловой системе", + noPermissions: "Не запрашивает особых разрешений.", + acknowledge: "Я понимаю, что этот пакет выполняет код и получает указанные выше разрешения.", + runtime: { + node: "В процессе · полное доверие", + sandbox: "В песочнице", + worker: "Вне процесса", + }, }, uninstall: { confirm: "Удалить {{manifestId}} v{{version}}?\n\nКэшированный манифест будет удалён.", @@ -3249,6 +3281,32 @@ const ru = { confirmNote: 'Публикация атомарно выпускает все {{count}} ожидающих черновиков этого пакета.', publishConfirm: 'Опубликовать всё', }, + // ADR-0045 — the materialized-but-unlisted app banner + // (UnpublishedAppBar.tsx), sibling of draftBar above. + unpublishedBar: { + message: 'Неопубликованное приложение — полностью работает, но видно только создателям. Опубликуйте, чтобы его увидели ваши пользователи.', + publish: 'Опубликовать', + publishing: 'Публикация…', + published: 'Опубликовано! Приложение теперь видно вашим пользователям.', + publishFailed: 'Не удалось опубликовать', + }, + // ADR-0067 — the append-only build/revert timeline (CommitTimeline.tsx); + // `button` is the banner's entry point into it. + history: { + button: 'История', + title: 'История сборок', + description: 'Все изменения этого приложения, новые сверху. Любой шаг можно отменить — подтверждать публикацию не нужно.', + loadFailed: 'Не удалось загрузить историю:', + loading: 'Загрузка истории…', + empty: 'Истории для этого приложения пока нет.', + revertLabel: 'Изменение отменено', + applyLabel: 'Изменение сборки', + revert: 'отмена', + items: 'элементов', + revertAction: 'Отменить', + reverted: 'Отменено — изменение возвращено.', + revertFailed: 'Не удалось отменить', + }, }, filterBuilder: { where: "Где", diff --git a/packages/i18n/src/locales/zh.ts b/packages/i18n/src/locales/zh.ts index 2cc6cb065..e88c929eb 100644 --- a/packages/i18n/src/locales/zh.ts +++ b/packages/i18n/src/locales/zh.ts @@ -2565,6 +2565,32 @@ const zh = { confirmNote: '发布将一次性(原子地)发布此包全部 {{count}} 个待发布草稿。', publishConfirm: '全部发布', }, + // ADR-0045 — the materialized-but-unlisted app banner + // (UnpublishedAppBar.tsx), sibling of draftBar above. + unpublishedBar: { + message: '未发布的应用 —— 功能完整,但只有构建者能看到。发布后即可让您的用户看到。', + publish: '发布', + publishing: '发布中…', + published: '已发布!应用现在对您的用户可见。', + publishFailed: '发布失败', + }, + // ADR-0067 — the append-only build/revert timeline (CommitTimeline.tsx); + // `button` is the banner's entry point into it. + history: { + button: '历史', + title: '构建历史', + description: '此应用的每一次变更,最新的在最前。撤销任一步骤即可回退 —— 无需确认发布。', + loadFailed: '无法加载历史记录:', + loading: '正在加载历史记录…', + empty: '此应用暂无历史记录。', + revertLabel: '已撤销一处变更', + applyLabel: '构建变更', + revert: '撤销', + items: '项', + revertAction: '撤销', + reverted: '已撤销 —— 该变更已还原。', + revertFailed: '撤销失败', + }, }, renderer: { noPageSchema: '未提供页面 schema', @@ -3063,6 +3089,7 @@ const zh = { dismiss: '关闭', openOnCloud: '在云端打开', backHome: '返回首页', + updateTo: '更新', }, install: { dialogTitle: '安装 {{name}}', @@ -3079,6 +3106,37 @@ const zh = { localManifestConflict: '{{message}}\n提示:本地已有应用占用此 manifest_id,请先从 objectstack.config.ts 中移除。', localUnauthorized: '请先登录本运行时再试。', localMarketplaceUnavailable: '本运行时未配置 OS_CLOUD_URL,无法访问应用市场目录。', + updateTo: '更新 → v{{version}}', + installedVersion: '已安装 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: '安装后,此软件包将获得以下权限:', + services: '平台服务', + hooks: '生命周期钩子', + network: '网络访问', + fs: '文件系统访问', + noPermissions: '不请求任何特殊权限。', + acknowledge: '我了解此软件包会运行代码,并将获得上述权限。', + runtime: { + node: '进程内 · 完全信任', + sandbox: '沙箱隔离', + worker: '进程外', + }, }, // ADR-0090 D5/D9 — 安装时建议:包声明 isDefault 的权限集建议绑定到 // everyone/guest 岗位,由管理员在此确认,服务端绝不自动绑定。 diff --git a/scripts/i18n-call-site-key-baseline.json b/scripts/i18n-call-site-key-baseline.json index e983c6bff..79c51c06c 100644 --- a/scripts/i18n-call-site-key-baseline.json +++ b/scripts/i18n-call-site-key-baseline.json @@ -31,25 +31,6 @@ "layout.systemNav.administration": { "issue": "objectui#3546" }, "layout.systemNav.datasources": { "issue": "objectui#3546" }, "layout.systemNav.documentation": { "issue": "objectui#3546" }, - "marketplace.action.updateTo": { "issue": "objectui#3546" }, - "marketplace.disclosure.acknowledge": { "issue": "objectui#3546" }, - "marketplace.disclosure.containsCode": { "issue": "objectui#3546" }, - "marketplace.disclosure.fs": { "issue": "objectui#3546" }, - "marketplace.disclosure.grantsIntro": { "issue": "objectui#3546" }, - "marketplace.disclosure.hooks": { "issue": "objectui#3546" }, - "marketplace.disclosure.network": { "issue": "objectui#3546" }, - "marketplace.disclosure.noPermissions": { "issue": "objectui#3546" }, - "marketplace.disclosure.reviewed": { "issue": "objectui#3546" }, - "marketplace.disclosure.services": { "issue": "objectui#3546" }, - "marketplace.disclosure.signed": { "issue": "objectui#3546" }, - "marketplace.disclosure.unreviewed": { "issue": "objectui#3546" }, - "marketplace.install.installedVersion": { "issue": "objectui#3546" }, - "marketplace.install.updateTo": { "issue": "objectui#3546" }, - "marketplace.org.heading": { "issue": "objectui#3546" }, - "marketplace.org.install": { "issue": "objectui#3546" }, - "marketplace.org.installed": { "issue": "objectui#3546" }, - "marketplace.org.installedBadge": { "issue": "objectui#3546" }, - "marketplace.org.installing": { "issue": "objectui#3546" }, "perm.facet.adminScope": { "issue": "objectui#3546" }, "perm.facet.designInStudio": { "issue": "objectui#3546" }, "perm.facet.designInStudioHint": { "issue": "objectui#3546" }, @@ -59,31 +40,12 @@ "perm.facet.objects": { "issue": "objectui#3546" }, "perm.facet.rls": { "issue": "objectui#3546" }, "perm.facet.tabs": { "issue": "objectui#3546" }, - "preview.history.applyLabel": { "issue": "objectui#3546" }, - "preview.history.button": { "issue": "objectui#3546" }, - "preview.history.description": { "issue": "objectui#3546" }, - "preview.history.empty": { "issue": "objectui#3546" }, - "preview.history.items": { "issue": "objectui#3546" }, - "preview.history.loadFailed": { "issue": "objectui#3546" }, - "preview.history.loading": { "issue": "objectui#3546" }, - "preview.history.revert": { "issue": "objectui#3546" }, - "preview.history.revertAction": { "issue": "objectui#3546" }, - "preview.history.revertFailed": { "issue": "objectui#3546" }, - "preview.history.revertLabel": { "issue": "objectui#3546" }, - "preview.history.reverted": { "issue": "objectui#3546" }, - "preview.history.title": { "issue": "objectui#3546" }, - "preview.unpublishedBar.message": { "issue": "objectui#3546" }, - "preview.unpublishedBar.publish": { "issue": "objectui#3546" }, - "preview.unpublishedBar.publishFailed": { "issue": "objectui#3546" }, - "preview.unpublishedBar.published": { "issue": "objectui#3546" }, - "preview.unpublishedBar.publishing": { "issue": "objectui#3546" }, "workspace.multiOrgDisabled": { "issue": "objectui#3546" } }, "//": "Template keys whose static head matches no en key at all, so every expansion misses.", "missingPrefixes": { "gantt.linkEnd.": { "issue": "objectui#3546" }, - "marketplace.disclosure.runtime.": { "issue": "objectui#3546" }, "organization.invitations.status.": { "issue": "objectui#3546" } } }