diff --git a/.changeset/cloud-connection-panel-i18n.md b/.changeset/cloud-connection-panel-i18n.md new file mode 100644 index 000000000..ddcffa7c2 --- /dev/null +++ b/.changeset/cloud-connection-panel-i18n.md @@ -0,0 +1,33 @@ +--- +"@object-ui/i18n": patch +"@object-ui/app-shell": patch +--- + +fix(cloud-connection): localize the Cloud Connection panel (objectstack#3589 follow-up) + +`CloudConnectionPanel` — the `cloud-connection:panel` SDUI widget that is the +entire body of the Cloud Connection Setup page — had no i18n at all: no +`@object-ui/i18n` import, and no `cloudConnection` namespace in any of the ten +built-in locale packs. Its siblings on neighbouring pages +(`marketplace:installed-list`, `mcp:connect-agent`) were already fully +localized, so this one page rendered a translated header above an English body +once the framework-side `page:header` resolution landed. + +- New `cloudConnection` namespace in all ten packs (en, zh, ja, ko, de, fr, es, + pt, ru, ar), matching the coverage its sibling namespaces already had. Covers + every phase of the device-code flow: checking, error + retry, waiting + (approval prompt, user code, copy), bound (connection detail labels), and + unbound (call to action). +- The three hard-coded failure messages (expired request, bind failure, device + code request failure) are translated where they are raised, not where they + are rendered, since they are stored in component state. +- The "code is pre-filled…" line was one sentence stitched together across JSX + with a conditional tail and a bare `'.'`. It is now two self-contained + strings, so a translator never receives a dangling clause whose word order + they cannot change. +- The `bound_at` timestamp now formats with the active UI language rather than + the browser default, matching the surrounding copy. + +Also adds a locale-parity test asserting the `cloudConnection` key set is +identical across all ten packs — partial coverage degrades quietly, because +i18next falls back to `en` and the result merely looks half-translated. diff --git a/packages/app-shell/src/console/cloud-connection/CloudConnectionPanel.tsx b/packages/app-shell/src/console/cloud-connection/CloudConnectionPanel.tsx index f113d466a..bda3d9dee 100644 --- a/packages/app-shell/src/console/cloud-connection/CloudConnectionPanel.tsx +++ b/packages/app-shell/src/console/cloud-connection/CloudConnectionPanel.tsx @@ -31,6 +31,7 @@ import { CheckCircle2, Unplug, } from 'lucide-react'; +import { useObjectTranslation } from '@object-ui/i18n'; import { ComponentRegistry } from '@object-ui/core'; const BASE = '/api/v1/cloud-connection'; @@ -79,6 +80,7 @@ async function getJson(url: string, init?: RequestInit): Promise { } export function CloudConnectionPanel() { + const { t, language } = useObjectTranslation(); const [phase, setPhase] = useState({ kind: 'loading' }); const [busy, setBusy] = useState(false); const [copied, setCopied] = useState(false); @@ -107,7 +109,7 @@ export function CloudConnectionPanel() { const intervalMs = Math.max(code.interval, 2) * 1000; const tick = async () => { if (Date.now() - startedAt > code.expires_in * 1000) { - setPhase({ kind: 'error', message: 'The request expired before it was approved. Start again.' }); + setPhase({ kind: 'error', message: t('cloudConnection.errors.expired') }); return; } try { @@ -123,20 +125,20 @@ export function CloudConnectionPanel() { await refreshStatus(); return; } - setPhase({ kind: 'error', message: body?.error?.code ?? 'Binding failed.' }); + setPhase({ kind: 'error', message: body?.error?.code ?? t('cloudConnection.errors.bindFailed') }); } catch (err: any) { setPhase({ kind: 'error', message: err?.message ?? String(err) }); } }; pollTimer.current = setTimeout(tick, intervalMs); - }, [refreshStatus]); + }, [refreshStatus, t]); const connect = useCallback(async () => { setBusy(true); try { const body = await getJson(`${BASE}/bind/start`, { method: 'POST', body: '{}' }); const code: DeviceCode = body?.data; - if (!code?.device_code || !code?.user_code) throw new Error('Device code request failed.'); + if (!code?.device_code || !code?.user_code) throw new Error(t('cloudConnection.errors.deviceCodeFailed')); // Auto-open the approval page — the GitHub-login moment. Still within // the click's transient activation, so popup blockers generally allow // it; the code display below is the blocked-popup fallback. @@ -154,7 +156,7 @@ export function CloudConnectionPanel() { } finally { setBusy(false); } - }, [poll]); + }, [poll, t]); const disconnect = useCallback(async () => { setBusy(true); @@ -179,7 +181,7 @@ export function CloudConnectionPanel() { if (phase.kind === 'loading') { return (
-
); } @@ -195,7 +197,7 @@ export function CloudConnectionPanel() { className="self-start rounded-md border px-3 py-1.5 text-sm hover:bg-accent" onClick={() => { stopPolling(); void refreshStatus(); }} > - Try again + {t('cloudConnection.retry')} ); @@ -208,8 +210,8 @@ export function CloudConnectionPanel() {
{!phase.popupOpened && link ? ( - Open the approval page ) : null}
@@ -230,27 +232,29 @@ export function CloudConnectionPanel() { className="inline-flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-sm hover:bg-accent" onClick={() => void copyCode(phase.code.user_code)} > -
+ {/* Two self-contained strings rather than one sentence stitched across + JSX — a translator never receives a dangling clause or a bare '.'. */}

- The code is pre-filled on the approval page + {t('cloudConnection.waiting.codePrefilled')} {phase.popupOpened && link ? ( <> - {' '}— if the window did not appear,{' '} + {' '} - open it here - . - ) : '.'} + ) : null}

); @@ -263,18 +267,18 @@ export function CloudConnectionPanel() {
- {conn.name ? (<>
Runtime
{conn.name}
) : null} - {conn.organization_id ? (<>
Organization
{conn.organization_id}
) : null} - {conn.account_email ? (<>
Approved by
{conn.account_email}
) : null} - {runtimeId ? (<>
Runtime ID
{runtimeId}
) : null} - {phase.status.environmentId ? (<>
Environment
{phase.status.environmentId}
) : null} - {conn.bound_at ? (<>
Since
{new Date(conn.bound_at).toLocaleString()}
) : null} + {conn.name ? (<>
{t('cloudConnection.bound.runtime')}
{conn.name}
) : null} + {conn.organization_id ? (<>
{t('cloudConnection.bound.organization')}
{conn.organization_id}
) : null} + {conn.account_email ? (<>
{t('cloudConnection.bound.approvedBy')}
{conn.account_email}
) : null} + {runtimeId ? (<>
{t('cloudConnection.bound.runtimeId')}
{runtimeId}
) : null} + {phase.status.environmentId ? (<>
{t('cloudConnection.bound.environment')}
{phase.status.environmentId}
) : null} + {conn.bound_at ? (<>
{t('cloudConnection.bound.since')}
{new Date(conn.bound_at).toLocaleString(language)}
) : null}

- Your organization's private packages now appear in the Marketplace under “Your organization”. + {t('cloudConnection.bound.privatePackages')}

); @@ -293,13 +297,10 @@ export function CloudConnectionPanel() {

- Connect this runtime to an ObjectStack control plane to browse your - organization's private packages and install them here. Approval is a - single click in your cloud account — no ids or credentials are typed - into this page. + {t('cloudConnection.unbound.body')}

); diff --git a/packages/i18n/src/__tests__/cloudConnection-locale-parity.test.ts b/packages/i18n/src/__tests__/cloudConnection-locale-parity.test.ts new file mode 100644 index 000000000..acb6aa57d --- /dev/null +++ b/packages/i18n/src/__tests__/cloudConnection-locale-parity.test.ts @@ -0,0 +1,82 @@ +/** + * cloudConnection locale parity (objectstack#3589 follow-up). + * + * The Cloud Connection panel shipped with every string hard-coded in English + * while its sibling widgets (marketplace, connectAgent) were fully localized — + * so the page rendered a translated header above an English body. The related + * framework-side gap had the same shape: `nav_cloud_connection` existed only + * in zh-CN, leaving ja-JP/es-ES silently on the English fallback. + * + * Partial coverage is the failure mode worth pinning, because it degrades + * quietly: i18next falls back to `en` and the UI just looks half-translated. + * This asserts the `cloudConnection` key set is identical across all ten + * built-in packs, so adding a key to `en` alone fails here rather than in a + * user's browser. + */ +import { describe, it, expect } from 'vitest'; +import { builtInLocales } from '../locales'; + +/** Flatten a nested translation node into sorted dot-paths. */ +function keyPaths(node: unknown, prefix = ''): string[] { + if (node === null || typeof node !== 'object') return [prefix]; + return Object.entries(node as Record) + .flatMap(([k, v]) => keyPaths(v, prefix ? `${prefix}.${k}` : k)) + .sort(); +} + +/** Read a dot-path out of a nested translation node. */ +function readPath(node: unknown, path: string): unknown { + return path.split('.').reduce( + (acc, seg) => (acc && typeof acc === 'object' ? (acc as Record)[seg] : undefined), + node, + ); +} + +type LocaleCode = keyof typeof builtInLocales; + +const LOCALES = Object.keys(builtInLocales) as LocaleCode[]; +const cloudConnectionOf = (code: LocaleCode): unknown => + (builtInLocales[code] as Record).cloudConnection; + +describe('cloudConnection translations', () => { + it('is present in every built-in locale pack', () => { + const missing = LOCALES.filter((code) => !cloudConnectionOf(code)); + expect(missing).toEqual([]); + }); + + it('exposes an identical key set in every locale', () => { + const expected = keyPaths(cloudConnectionOf('en')); + // Guard against the flattener silently returning nothing. + expect(expected.length).toBeGreaterThan(15); + + for (const code of LOCALES) { + expect({ code, keys: keyPaths(cloudConnectionOf(code)) }).toEqual({ code, keys: expected }); + } + }); + + it('has a non-empty string at every leaf', () => { + for (const code of LOCALES) { + const node = cloudConnectionOf(code); + for (const path of keyPaths(node)) { + const value = readPath(node, path); + expect({ code, path, ok: typeof value === 'string' && value.trim().length > 0 }) + .toEqual({ code, path, ok: true }); + } + } + }); + + it('actually translates the user-facing prose, not just the key structure', () => { + // Short labels legitimately collide across languages ("Runtime" is + // "Runtime" in de/fr/es), so only the prose keys are checked — those + // matching English verbatim means the pack was never translated. + const prose = ['unbound.body', 'bound.privatePackages', 'waiting.popupOpened'] as const; + const read = (code: LocaleCode, path: string) => readPath(cloudConnectionOf(code), path); + + for (const code of LOCALES.filter((c) => c !== 'en')) { + for (const path of prose) { + expect({ code, path, sameAsEnglish: read(code, path) === read('en', path) }) + .toEqual({ code, path, sameAsEnglish: false }); + } + } + }); +}); diff --git a/packages/i18n/src/locales/ar.ts b/packages/i18n/src/locales/ar.ts index 3fa3a5bc9..90fd90ec7 100644 --- a/packages/i18n/src/locales/ar.ts +++ b/packages/i18n/src/locales/ar.ts @@ -1842,6 +1842,41 @@ const ar = { done: 'تم — إخفاء', }, }, + cloudConnection: { + checking: "جارٍ التحقق من الاتصال…", + retry: "إعادة المحاولة", + errors: { + expired: "انتهت صلاحية الطلب قبل الموافقة عليه. ابدأ من جديد.", + bindFailed: "فشل الربط.", + deviceCodeFailed: "فشل طلب رمز الجهاز.", + }, + waiting: { + popupOpened: "وافق على الاتصال في النافذة التي فُتحت للتو — سيتم تحديث هذه الصفحة تلقائيًا.", + polling: "في انتظار الموافقة في وحدة التحكم السحابية…", + openApproval: "فتح صفحة الموافقة", + copy: "نسخ", + copied: "تم النسخ", + codePrefilled: "الرمز مُعبَّأ مسبقًا في صفحة الموافقة.", + openItHere: "لم تظهر النافذة؟ افتحها من هنا", + cancel: "إلغاء", + }, + bound: { + title: "متصل بـ ObjectStack Cloud", + runtime: "بيئة التشغيل", + organization: "المؤسسة", + approvedBy: "تمت الموافقة بواسطة", + runtimeId: "معرّف بيئة التشغيل", + environment: "البيئة", + since: "متصل منذ", + privatePackages: "تظهر الآن الحزم الخاصة بمؤسستك في السوق ضمن «مؤسستك».", + disconnect: "قطع الاتصال", + }, + unbound: { + title: "غير متصل", + body: "اربط بيئة التشغيل هذه بمستوى تحكم ObjectStack لتصفّح الحزم الخاصة بمؤسستك وتثبيتها هنا. الموافقة بنقرة واحدة في حسابك السحابي — دون إدخال أي معرّفات أو بيانات اعتماد في هذه الصفحة.", + connect: "اتصال", + }, + }, marketplace: { title: "سوق التطبيقات", subtitle: "استعرض التطبيقات المعتمدة المنشورة في كتالوج ObjectStack. انقر على تطبيق لرؤية التفاصيل وتثبيته في أحد بيئاتك.", diff --git a/packages/i18n/src/locales/de.ts b/packages/i18n/src/locales/de.ts index 14e3617fb..a36010321 100644 --- a/packages/i18n/src/locales/de.ts +++ b/packages/i18n/src/locales/de.ts @@ -1844,6 +1844,41 @@ const de = { done: 'Fertig — ausblenden', }, }, + cloudConnection: { + checking: "Verbindung wird geprüft…", + retry: "Erneut versuchen", + errors: { + expired: "Die Anfrage ist abgelaufen, bevor sie genehmigt wurde. Starten Sie erneut.", + bindFailed: "Verbindung fehlgeschlagen.", + deviceCodeFailed: "Anforderung des Gerätecodes fehlgeschlagen.", + }, + waiting: { + popupOpened: "Genehmigen Sie die Verbindung im soeben geöffneten Fenster — diese Seite aktualisiert sich von selbst.", + polling: "Warten auf die Genehmigung in der Cloud-Konsole…", + openApproval: "Genehmigungsseite öffnen", + copy: "Kopieren", + copied: "Kopiert", + codePrefilled: "Der Code ist auf der Genehmigungsseite bereits eingetragen.", + openItHere: "Fenster nicht erschienen? Hier öffnen", + cancel: "Abbrechen", + }, + bound: { + title: "Mit ObjectStack Cloud verbunden", + runtime: "Runtime", + organization: "Organisation", + approvedBy: "Genehmigt von", + runtimeId: "Runtime-ID", + environment: "Umgebung", + since: "Verbunden seit", + privatePackages: "Die privaten Pakete Ihrer Organisation erscheinen jetzt im Marktplatz unter „Ihre Organisation“.", + disconnect: "Verbindung trennen", + }, + unbound: { + title: "Nicht verbunden", + body: "Verbinden Sie diese Runtime mit einer ObjectStack Control Plane, um die privaten Pakete Ihrer Organisation zu durchsuchen und hier zu installieren. Die Genehmigung erfolgt mit einem einzigen Klick in Ihrem Cloud-Konto — es werden keine IDs oder Zugangsdaten auf dieser Seite eingegeben.", + connect: "Verbinden", + }, + }, marketplace: { 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.", diff --git a/packages/i18n/src/locales/en.ts b/packages/i18n/src/locales/en.ts index 5bcd7a687..e316ac89a 100644 --- a/packages/i18n/src/locales/en.ts +++ b/packages/i18n/src/locales/en.ts @@ -2402,6 +2402,41 @@ const en = { done: 'Done — hide it', }, }, + cloudConnection: { + checking: 'Checking connection…', + retry: 'Try again', + errors: { + expired: 'The request expired before it was approved. Start again.', + bindFailed: 'Binding failed.', + deviceCodeFailed: 'Device code request failed.', + }, + waiting: { + popupOpened: 'Approve the connection in the window that just opened — this page updates by itself.', + polling: 'Waiting for approval in the cloud console…', + openApproval: 'Open the approval page', + copy: 'Copy', + copied: 'Copied', + codePrefilled: 'The code is pre-filled on the approval page.', + openItHere: 'Window did not appear? Open it here', + cancel: 'Cancel', + }, + bound: { + title: 'Connected to ObjectStack Cloud', + runtime: 'Runtime', + organization: 'Organization', + approvedBy: 'Approved by', + runtimeId: 'Runtime ID', + environment: 'Environment', + since: 'Since', + privatePackages: 'Your organization\'s private packages now appear in the Marketplace under “Your organization”.', + disconnect: 'Disconnect', + }, + unbound: { + title: 'Not connected', + body: 'Connect this runtime to an ObjectStack control plane to browse your organization\'s private packages and install them here. Approval is a single click in your cloud account — no ids or credentials are typed into this page.', + connect: 'Connect', + }, + }, 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.', diff --git a/packages/i18n/src/locales/es.ts b/packages/i18n/src/locales/es.ts index ca84f0d9f..5ddd2cb2f 100644 --- a/packages/i18n/src/locales/es.ts +++ b/packages/i18n/src/locales/es.ts @@ -1844,6 +1844,41 @@ const es = { done: 'Listo — ocultar', }, }, + cloudConnection: { + checking: "Comprobando la conexión…", + retry: "Reintentar", + errors: { + expired: "La solicitud caducó antes de ser aprobada. Vuelva a empezar.", + bindFailed: "Error al vincular.", + deviceCodeFailed: "Error en la solicitud del código de dispositivo.", + }, + waiting: { + popupOpened: "Apruebe la conexión en la ventana que acaba de abrirse: esta página se actualiza sola.", + polling: "Esperando la aprobación en la consola de la nube…", + openApproval: "Abrir la página de aprobación", + copy: "Copiar", + copied: "Copiado", + codePrefilled: "El código ya está rellenado en la página de aprobación.", + openItHere: "¿No apareció la ventana? Ábrala aquí", + cancel: "Cancelar", + }, + bound: { + title: "Conectado a ObjectStack Cloud", + runtime: "Runtime", + organization: "Organización", + approvedBy: "Aprobado por", + runtimeId: "ID del runtime", + environment: "Entorno", + since: "Desde", + privatePackages: "Los paquetes privados de su organización ya aparecen en el Marketplace en «Su organización».", + disconnect: "Desconectar", + }, + unbound: { + title: "No conectado", + body: "Conecte este runtime a un plano de control de ObjectStack para explorar los paquetes privados de su organización e instalarlos aquí. La aprobación es un solo clic en su cuenta de la nube: no se escriben identificadores ni credenciales en esta página.", + connect: "Conectar", + }, + }, marketplace: { 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.", diff --git a/packages/i18n/src/locales/fr.ts b/packages/i18n/src/locales/fr.ts index 576922201..a56dc9a32 100644 --- a/packages/i18n/src/locales/fr.ts +++ b/packages/i18n/src/locales/fr.ts @@ -1842,6 +1842,41 @@ const fr = { done: 'Terminé — masquer', }, }, + cloudConnection: { + checking: "Vérification de la connexion…", + retry: "Réessayer", + errors: { + expired: "La demande a expiré avant d'être approuvée. Recommencez.", + bindFailed: "Échec de la liaison.", + deviceCodeFailed: "Échec de la demande de code d'appareil.", + }, + waiting: { + popupOpened: "Approuvez la connexion dans la fenêtre qui vient de s'ouvrir — cette page se met à jour toute seule.", + polling: "En attente d'approbation dans la console cloud…", + openApproval: "Ouvrir la page d'approbation", + copy: "Copier", + copied: "Copié", + codePrefilled: "Le code est pré-rempli sur la page d'approbation.", + openItHere: "La fenêtre ne s'est pas affichée ? Ouvrez-la ici", + cancel: "Annuler", + }, + bound: { + title: "Connecté à ObjectStack Cloud", + runtime: "Runtime", + organization: "Organisation", + approvedBy: "Approuvé par", + runtimeId: "ID du runtime", + environment: "Environnement", + since: "Depuis", + privatePackages: "Les paquets privés de votre organisation apparaissent désormais dans la Marketplace sous « Votre organisation ».", + disconnect: "Déconnecter", + }, + unbound: { + title: "Non connecté", + body: "Connectez ce runtime à un plan de contrôle ObjectStack pour parcourir les paquets privés de votre organisation et les installer ici. L'approbation se fait en un seul clic dans votre compte cloud — aucun identifiant ni justificatif n'est saisi sur cette page.", + connect: "Connecter", + }, + }, marketplace: { 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.", diff --git a/packages/i18n/src/locales/ja.ts b/packages/i18n/src/locales/ja.ts index e2c795a85..7378693e6 100644 --- a/packages/i18n/src/locales/ja.ts +++ b/packages/i18n/src/locales/ja.ts @@ -1844,6 +1844,41 @@ const ja = { done: '完了 — 非表示にする', }, }, + cloudConnection: { + checking: "接続を確認しています…", + retry: "再試行", + errors: { + expired: "承認される前にリクエストの有効期限が切れました。もう一度やり直してください。", + bindFailed: "バインドに失敗しました。", + deviceCodeFailed: "デバイスコードのリクエストに失敗しました。", + }, + waiting: { + popupOpened: "今開いたウィンドウで接続を承認してください。このページは自動的に更新されます。", + polling: "クラウドコンソールでの承認を待っています…", + openApproval: "承認ページを開く", + copy: "コピー", + copied: "コピーしました", + codePrefilled: "承認ページにはコードが自動入力されています。", + openItHere: "ウィンドウが表示されませんか? こちらから開く", + cancel: "キャンセル", + }, + bound: { + title: "ObjectStack Cloud に接続済み", + runtime: "ランタイム", + organization: "組織", + approvedBy: "承認者", + runtimeId: "ランタイム ID", + environment: "環境", + since: "接続日時", + privatePackages: "組織のプライベートパッケージが、マーケットプレイスの「自分の組織」に表示されるようになりました。", + disconnect: "接続を解除", + }, + unbound: { + title: "未接続", + body: "このランタイムを ObjectStack コントロールプレーンに接続すると、組織のプライベートパッケージを閲覧してここにインストールできます。承認はクラウドアカウントでワンクリック。ID や認証情報をこのページに入力する必要はありません。", + connect: "接続", + }, + }, marketplace: { title: "アプリマーケットプレイス", subtitle: "ObjectStackカタログに公開された承認済みアプリを参照します。アプリをクリックして詳細を表示し、環境にインストールしてください。", diff --git a/packages/i18n/src/locales/ko.ts b/packages/i18n/src/locales/ko.ts index a56990928..ce580e7f1 100644 --- a/packages/i18n/src/locales/ko.ts +++ b/packages/i18n/src/locales/ko.ts @@ -1844,6 +1844,41 @@ const ko = { done: '완료 — 숨기기', }, }, + cloudConnection: { + checking: "연결을 확인하는 중…", + retry: "다시 시도", + errors: { + expired: "승인되기 전에 요청이 만료되었습니다. 다시 시작하세요.", + bindFailed: "바인딩에 실패했습니다.", + deviceCodeFailed: "장치 코드 요청에 실패했습니다.", + }, + waiting: { + popupOpened: "방금 열린 창에서 연결을 승인하세요. 이 페이지는 자동으로 업데이트됩니다.", + polling: "클라우드 콘솔에서의 승인을 기다리는 중…", + openApproval: "승인 페이지 열기", + copy: "복사", + copied: "복사됨", + codePrefilled: "승인 페이지에 코드가 미리 입력되어 있습니다.", + openItHere: "창이 나타나지 않았나요? 여기에서 열기", + cancel: "취소", + }, + bound: { + title: "ObjectStack Cloud에 연결됨", + runtime: "런타임", + organization: "조직", + approvedBy: "승인자", + runtimeId: "런타임 ID", + environment: "환경", + since: "연결 시각", + privatePackages: "조직의 비공개 패키지가 이제 마켓플레이스의 '내 조직'에 표시됩니다.", + disconnect: "연결 해제", + }, + unbound: { + title: "연결되지 않음", + body: "이 런타임을 ObjectStack 컨트롤 플레인에 연결하면 조직의 비공개 패키지를 탐색하고 여기에 설치할 수 있습니다. 승인은 클라우드 계정에서 한 번만 클릭하면 되며, 이 페이지에 ID나 자격 증명을 입력할 필요가 없습니다.", + connect: "연결", + }, + }, marketplace: { title: "앱 마켓플레이스", subtitle: "ObjectStack 카탈로그에 게시된 승인된 앱을 탐색하세요. 앱을 클릭하여 세부 정보를 보고 환경 중 하나에 설치하세요.", diff --git a/packages/i18n/src/locales/pt.ts b/packages/i18n/src/locales/pt.ts index 8a645d0c3..da973b617 100644 --- a/packages/i18n/src/locales/pt.ts +++ b/packages/i18n/src/locales/pt.ts @@ -1842,6 +1842,41 @@ const pt = { done: 'Concluído — ocultar', }, }, + cloudConnection: { + checking: "Verificando a conexão…", + retry: "Tentar novamente", + errors: { + expired: "A solicitação expirou antes de ser aprovada. Comece novamente.", + bindFailed: "Falha ao vincular.", + deviceCodeFailed: "Falha na solicitação do código do dispositivo.", + }, + waiting: { + popupOpened: "Aprove a conexão na janela que acabou de abrir — esta página se atualiza sozinha.", + polling: "Aguardando a aprovação no console da nuvem…", + openApproval: "Abrir a página de aprovação", + copy: "Copiar", + copied: "Copiado", + codePrefilled: "O código já vem preenchido na página de aprovação.", + openItHere: "A janela não apareceu? Abra aqui", + cancel: "Cancelar", + }, + bound: { + title: "Conectado ao ObjectStack Cloud", + runtime: "Runtime", + organization: "Organização", + approvedBy: "Aprovado por", + runtimeId: "ID do runtime", + environment: "Ambiente", + since: "Desde", + privatePackages: "Os pacotes privados da sua organização agora aparecem no Marketplace em “Sua organização”.", + disconnect: "Desconectar", + }, + unbound: { + title: "Não conectado", + body: "Conecte este runtime a um plano de controle do ObjectStack para explorar os pacotes privados da sua organização e instalá-los aqui. A aprovação é um único clique na sua conta da nuvem — nenhum id ou credencial é digitado nesta página.", + connect: "Conectar", + }, + }, marketplace: { 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.", diff --git a/packages/i18n/src/locales/ru.ts b/packages/i18n/src/locales/ru.ts index e0146d950..2fc08a0b4 100644 --- a/packages/i18n/src/locales/ru.ts +++ b/packages/i18n/src/locales/ru.ts @@ -1842,6 +1842,41 @@ const ru = { done: 'Готово — скрыть', }, }, + cloudConnection: { + checking: "Проверка подключения…", + retry: "Повторить", + errors: { + expired: "Срок действия запроса истёк до подтверждения. Начните заново.", + bindFailed: "Не удалось выполнить привязку.", + deviceCodeFailed: "Не удалось запросить код устройства.", + }, + waiting: { + popupOpened: "Подтвердите подключение в только что открывшемся окне — эта страница обновится сама.", + polling: "Ожидание подтверждения в облачной консоли…", + openApproval: "Открыть страницу подтверждения", + copy: "Копировать", + copied: "Скопировано", + codePrefilled: "Код уже подставлен на странице подтверждения.", + openItHere: "Окно не появилось? Откройте здесь", + cancel: "Отмена", + }, + bound: { + title: "Подключено к ObjectStack Cloud", + runtime: "Среда выполнения", + organization: "Организация", + approvedBy: "Подтвердил", + runtimeId: "ID среды выполнения", + environment: "Окружение", + since: "Подключено", + privatePackages: "Приватные пакеты вашей организации теперь отображаются в Маркетплейсе в разделе «Ваша организация».", + disconnect: "Отключить", + }, + unbound: { + title: "Не подключено", + body: "Подключите эту среду выполнения к управляющему слою ObjectStack, чтобы просматривать приватные пакеты вашей организации и устанавливать их здесь. Подтверждение — один клик в вашей облачной учётной записи; никакие идентификаторы и учётные данные на этой странице не вводятся.", + connect: "Подключить", + }, + }, marketplace: { title: "Маркетплейс приложений", subtitle: "Просматривайте одобренные приложения из каталога ObjectStack. Нажмите на приложение, чтобы увидеть подробности и установить его.", diff --git a/packages/i18n/src/locales/zh.ts b/packages/i18n/src/locales/zh.ts index 7527d22e4..215b5fd53 100644 --- a/packages/i18n/src/locales/zh.ts +++ b/packages/i18n/src/locales/zh.ts @@ -2482,6 +2482,41 @@ const zh = { done: '完成——隐藏', }, }, + cloudConnection: { + checking: '正在检查连接…', + retry: '重试', + errors: { + expired: '请求在获批前已过期,请重新开始。', + bindFailed: '绑定失败。', + deviceCodeFailed: '设备码请求失败。', + }, + waiting: { + popupOpened: '请在刚打开的窗口中批准此连接——本页面会自动更新。', + polling: '正在等待云端控制台批准…', + openApproval: '打开批准页面', + copy: '复制', + copied: '已复制', + codePrefilled: '批准页面已自动填入该代码。', + openItHere: '窗口没有出现?点此打开', + cancel: '取消', + }, + bound: { + title: '已连接到 ObjectStack 云', + runtime: '运行时', + organization: '组织', + approvedBy: '批准人', + runtimeId: '运行时 ID', + environment: '环境', + since: '连接于', + privatePackages: '组织的私有包现已显示在应用市场的「你的组织」分类下。', + disconnect: '断开连接', + }, + unbound: { + title: '未连接', + body: '将此运行时连接到 ObjectStack 控制平面,即可浏览并安装组织的私有包。只需在云账户中点击一次批准——无需在本页面输入任何 ID 或凭据。', + connect: '连接', + }, + }, marketplace: { title: '应用市场', subtitle: '浏览已通过审核、发布到 ObjectStack 目录中的应用。点击应用查看详情并安装到你的某个环境中。',