diff --git a/.changeset/11659-console-record-ux.md b/.changeset/11659-console-record-ux.md
new file mode 100644
index 0000000000..fea85d89fe
--- /dev/null
+++ b/.changeset/11659-console-record-ux.md
@@ -0,0 +1,19 @@
+---
+'@object-ui/app-shell': patch
+'@object-ui/i18n': patch
+'@object-ui/plugin-detail': patch
+---
+
+Console, record page and Studio copy from the 2026-10-05 cloud acceptance run (objectui#11659).
+
+**The cloud home's primary button reads "Open workspace" (zh 「进入工作区」), not "Open Production".** A new customer has exactly one environment, and "production" is control-plane vocabulary. The `cloud:onboarding-next` widget's ready-state button now resolves `cloudOnboarding.openWorkspace`, which replaces `cloudOnboarding.openProduction` in all ten language packs, and the hint under it (`cloudOnboarding.hintReady`) says "workspace" instead of "production environment". The button still navigates to the page's `openProductionUrl`; the page-metadata contract is unchanged.
+
+**The create-workspace dialog asks for the name only.** `CreateWorkspaceDialog` no longer shows the "URL slug" field: the customer never sees the slug take effect at this step. The slug is still generated from the name, by the same rule as before, and sent with the create call; the owner can change it later in organization settings. Because the slug is no longer the user's to fix in the dialog, a slug collision (`ORGANIZATION_ALREADY_EXISTS` or `ORGANIZATION_SLUG_ALREADY_TAKEN`) is retried with a short random suffix, up to three attempts in all; any other refusal is shown as before. The `workspace.slugLabel` and `workspace.slugHint` pack keys are left in place, now unread.
+
+**The backup-password reminder no longer appears in the user's first session.** 「建议设置一个备用密码」 showed on the environment home right after a new user built their first app: the reminder was gated on a home-visit count (quiet on the first home mount, shown on the second), and coming back to home after building is the second mount inside the first sitting. It now stays quiet for 12 hours after the first home visit on the device, so it first shows on a later day's visit. The first-visit record (`os:recovery-pw-first-seen`) now holds a timestamp; a device that holds the old `'1'` flag starts the 12 hours over rather than reading it as long ago. When storage is unavailable it stays quiet. `RecoveryPasswordReminder` moves out of `HomePage.tsx` into its own module; it is not exported from the package entry, and its other conditions (dismissed, SSO-enforced, has a local password) are unchanged.
+
+**The record header's highlight row no longer cuts a phone number.** A drawer's highlight row read 「0574-876」 for the stored `0574-8765-4321`, with no ellipsis. The phone cell draws a dial icon, the number and a copy button in one `inline-flex` box, and the chip's single-line clip cannot put an ellipsis on such a box, so a 9rem chip cut the number mid-way. `HeaderHighlight` now gives a `phone` chip the wide basis that `email` and `url` already take, so a whole number shows; the full value stays in the chip's hover title. Other field types keep their chip width.
+
+**Studio's dashboard widgets list names each widget's kind in the designer's language.** The list read 「按客户状态统计数量 · bar」: beside each title, `DashboardDefaultInspector` printed the stored `type` id. It now prints the name the add-widget picker shows for that kind (`engine.widgetPicker.type.*`, zh 「柱状图」, en "Bar chart"), with the id on hover. A type the catalogue does not know still prints its id. The stored `type` is unchanged.
+
+**Studio's automation pillar copy is plain language in zh.** The list heading read 「自动化 · flow」 and the top bar 「默认 OFF · 审阅后再启用」: an internal metadata type and an English switch state inside Chinese copy. They now read 「自动化」 and 「默认停用 · 审阅后再启用」, matching the pillar's own 已启用 / 已停用, and the canvas hint 「可视化编排 · 点选节点配置」 reads 「点选画布上的节点即可配置」. The en heading drops the type too ("Automations"). Whether the pillar is offered on a given plan is not decided here.
diff --git a/packages/app-shell/src/console/home/CloudOnboardingNext.tsx b/packages/app-shell/src/console/home/CloudOnboardingNext.tsx
index 1bb0c2d179..71733d2b7f 100644
--- a/packages/app-shell/src/console/home/CloudOnboardingNext.tsx
+++ b/packages/app-shell/src/console/home/CloudOnboardingNext.tsx
@@ -13,13 +13,18 @@
* the same `/cloud/environment-entitlements` endpoint the environment list uses
* and renders the right primary action:
*
- * • has production env → "Open Production" (the doorway into the env, where
+ * • has production env → "Open workspace" (the doorway into the env, where
* building happens) + a "Manage environments" link.
+ * The label names the place in the customer's words,
+ * not the operator's: a new customer has exactly one
+ * environment, and "production" is control-plane
+ * vocabulary (objectui#11659). The destination is
+ * still the org's production environment.
* • no production env → "Create your environment" (the real first step).
* • loading → a neutral skeleton (no CTA flashes / layout jump).
* • unknown / error → degrade to BOTH actions, so the button always works.
*
- * Routes and the "Open Production" endpoint come from the page metadata
+ * Routes and the "Open workspace" endpoint come from the page metadata
* (`properties`) so the Cloud app owns its URLs; this component owns only the
* state logic. It mirrors `useEnvironmentEntitlements`' authoritative summary
* fetch (org-scoped GET) without its list-only gating.
@@ -53,7 +58,7 @@ interface CloudOnboardingNextProps {
* Optional backend pre-warm endpoint. When set and the caller already has
* a production env, the widget fires a best-effort GET the moment it knows
* the env exists — nudging a possibly-asleep container awake WHILE the user
- * reads the hint, so the later "Open Production" click lands in an
+ * reads the hint, so the later "Open workspace" click lands in an
* already-waking env instead of paying the full cold-start. No-op when
* unset (older page metadata), so this is safe to ship ahead of the page.
*/
@@ -118,7 +123,7 @@ function useProductionEnvState(warmUrl?: string): Resolved {
setState({ phase: 'ready', hasProductionEnv });
// Pre-warm the prod env the instant we know it exists — fire-and-forget,
// best-effort, once per resolve. The user is now reading the hint and
- // will click "Open Production" seconds later; by then the container is
+ // will click "Open workspace" seconds later; by then the container is
// already waking. The sso-open click warms again, so a failed warm here
// costs nothing. Only when the page passes a warmUrl (else no-op).
if (hasProductionEnv && warmUrl) {
@@ -194,7 +199,7 @@ export function CloudOnboardingNext({ properties }: CloudOnboardingNextProps) {
) : (
)}
- {w?.type}
+ {/* The widget's KIND in the designer's language — the name
+ the add-widget picker shows for it (objectui#11659), not
+ the stored `type` id (`bar`). The id stays on hover for
+ authors. A type the catalogue does not know has no name
+ to show, so it prints its id as before. */}
+
+ {meta ? tr(meta.labelKey) : w?.type}
+
{!readOnly && (
{
+ it('every pillar row is a real zh row', () => {
+ for (const key of PILLAR_ROWS) {
+ const zh = t(key, 'zh-CN');
+ expect(zh, `${key}: a missing zh row echoes the key back`).not.toBe(key);
+ expect(zh, `${key}: the zh row must not be the English one`).not.toBe(t(key, 'en-US'));
+ }
+ });
+
+ it('no zh pillar row carries the metadata type `flow` or an English ON/OFF', () => {
+ const offenders = PILLAR_ROWS.flatMap((key) => {
+ const zh = t(key, 'zh-CN');
+ return INTERNAL_TOKENS.filter((re) => re.test(zh)).map((re) => `${key}: ${re} in 「${zh}」`);
+ });
+ expect(offenders).toEqual([]);
+ });
+
+ it('the regexes see the tokens they are meant to catch (positive control)', () => {
+ expect(INTERNAL_TOKENS[0].test('自动化 · flow')).toBe(true);
+ expect(INTERNAL_TOKENS[1].test('默认 OFF · 审阅后再启用')).toBe(true);
+ expect(INTERNAL_TOKENS[1].test('默认OFF')).toBe(true);
+ expect(INTERNAL_TOKENS[2].test('已 ON')).toBe(true);
+ });
+
+ it('the en heading names the pillar without the metadata type', () => {
+ expect(t('engine.studio.auto.heading', 'en-US')).not.toMatch(/\bflow\b/i);
+ });
+});
diff --git a/packages/i18n/src/locales/ar.ts b/packages/i18n/src/locales/ar.ts
index f489838e2e..5a5ddf7d49 100644
--- a/packages/i18n/src/locales/ar.ts
+++ b/packages/i18n/src/locales/ar.ts
@@ -3334,9 +3334,9 @@ const ar = {
},
cloudOnboarding: {
hintCreate: "أنشئ بيئتك الأولى — مساحة عمل خاصة لها رابطها وقاعدة بياناتها وخطتها. يجري بناء التطبيقات داخلها.",
- hintReady: "بيئة الإنتاج جاهزة. افتحها لبناء تطبيقاتك وتشغيلها — كل ذلك يحدث داخل البيئة.",
+ hintReady: "مساحة العمل جاهزة. افتحها لبناء تطبيقاتك وتشغيلها — كل ذلك يحدث داخلها.",
createEnvironment: "أنشئ بيئتك",
- openProduction: "فتح بيئة الإنتاج",
+ openWorkspace: "فتح مساحة العمل",
manageEnvironments: "إدارة البيئات",
},
cloudPlanStatus: {
diff --git a/packages/i18n/src/locales/de.ts b/packages/i18n/src/locales/de.ts
index a05211323a..2df6ce5617 100644
--- a/packages/i18n/src/locales/de.ts
+++ b/packages/i18n/src/locales/de.ts
@@ -3091,9 +3091,9 @@ const de = {
},
cloudOnboarding: {
hintCreate: "Erstellen Sie Ihre erste Umgebung — ein privater Arbeitsbereich mit eigener URL, Datenbank und eigenem Tarif. Die Entwicklung findet darin statt.",
- hintReady: "Ihre Produktionsumgebung ist bereit. Öffnen Sie sie, um Ihre Apps zu erstellen und auszuführen — alles geschieht innerhalb der Umgebung.",
+ hintReady: "Ihr Arbeitsbereich ist bereit. Öffnen Sie ihn, um Ihre Apps zu erstellen und auszuführen — alles geschieht innerhalb des Arbeitsbereichs.",
createEnvironment: "Umgebung erstellen",
- openProduction: "Produktion öffnen",
+ openWorkspace: "Arbeitsbereich öffnen",
manageEnvironments: "Umgebungen verwalten",
},
cloudPlanStatus: {
diff --git a/packages/i18n/src/locales/en.ts b/packages/i18n/src/locales/en.ts
index 0d24ab5d74..b8541e0438 100644
--- a/packages/i18n/src/locales/en.ts
+++ b/packages/i18n/src/locales/en.ts
@@ -3664,9 +3664,9 @@ const en = {
},
cloudOnboarding: {
hintCreate: 'Spin up your first environment — a private workspace with its own URL, database, and plan. Building happens inside it.',
- hintReady: 'Your production environment is ready. Open it to build and run your apps — that all happens inside the environment.',
+ hintReady: 'Your workspace is ready. Open it to build and run your apps — that all happens inside the workspace.',
createEnvironment: 'Create your environment',
- openProduction: 'Open Production',
+ openWorkspace: 'Open workspace',
manageEnvironments: 'Manage environments',
},
// `cloud:plan-status` — the Cloud pricing page's current-plan marker (objectui#10919).
diff --git a/packages/i18n/src/locales/es.ts b/packages/i18n/src/locales/es.ts
index a0c4719be6..77c561f6a9 100644
--- a/packages/i18n/src/locales/es.ts
+++ b/packages/i18n/src/locales/es.ts
@@ -3150,9 +3150,9 @@ const es = {
},
cloudOnboarding: {
hintCreate: "Cree su primer entorno: un espacio de trabajo privado con su propia URL, base de datos y plan. La creación de aplicaciones ocurre dentro de él.",
- hintReady: "Su entorno de producción está listo. Ábralo para crear y ejecutar sus aplicaciones: todo ocurre dentro del entorno.",
+ hintReady: "Su espacio de trabajo está listo. Ábralo para crear y ejecutar sus aplicaciones: todo ocurre dentro de él.",
createEnvironment: "Cree su entorno",
- openProduction: "Abrir producción",
+ openWorkspace: "Abrir espacio de trabajo",
manageEnvironments: "Gestionar entornos",
},
cloudPlanStatus: {
diff --git a/packages/i18n/src/locales/fr.ts b/packages/i18n/src/locales/fr.ts
index 36dd8c2c74..9fb505878a 100644
--- a/packages/i18n/src/locales/fr.ts
+++ b/packages/i18n/src/locales/fr.ts
@@ -3147,9 +3147,9 @@ const fr = {
},
cloudOnboarding: {
hintCreate: "Créez votre premier environnement — un espace de travail privé avec sa propre URL, sa base de données et son offre. C'est là que se fait la création.",
- hintReady: "Votre environnement de production est prêt. Ouvrez-le pour créer et exécuter vos applications — tout se passe à l'intérieur.",
+ hintReady: "Votre espace de travail est prêt. Ouvrez-le pour créer et exécuter vos applications — tout se passe à l'intérieur.",
createEnvironment: "Créer votre environnement",
- openProduction: "Ouvrir la production",
+ openWorkspace: "Ouvrir l'espace de travail",
manageEnvironments: "Gérer les environnements",
},
cloudPlanStatus: {
diff --git a/packages/i18n/src/locales/ja.ts b/packages/i18n/src/locales/ja.ts
index fbc1594935..cc2fc3efa7 100644
--- a/packages/i18n/src/locales/ja.ts
+++ b/packages/i18n/src/locales/ja.ts
@@ -3093,9 +3093,9 @@ const ja = {
},
cloudOnboarding: {
hintCreate: "最初の環境を作成しましょう。独自の URL・データベース・プランを持つ専用ワークスペースで、アプリの構築はその中で行います。",
- hintReady: "本番環境の準備が整いました。開いてアプリの構築と実行を行ってください。すべて環境の中で行われます。",
+ hintReady: "ワークスペースの準備が整いました。開いてアプリの構築と実行を行ってください。すべてワークスペースの中で行われます。",
createEnvironment: "環境を作成",
- openProduction: "本番環境を開く",
+ openWorkspace: "ワークスペースを開く",
manageEnvironments: "環境を管理",
},
cloudPlanStatus: {
diff --git a/packages/i18n/src/locales/ko.ts b/packages/i18n/src/locales/ko.ts
index 760f0166eb..de704a54a3 100644
--- a/packages/i18n/src/locales/ko.ts
+++ b/packages/i18n/src/locales/ko.ts
@@ -3090,9 +3090,9 @@ const ko = {
},
cloudOnboarding: {
hintCreate: "첫 환경을 만들어 보세요. 고유한 URL과 데이터베이스, 요금제를 갖춘 전용 작업 공간이며 앱 구축은 그 안에서 이루어집니다.",
- hintReady: "프로덕션 환경이 준비되었습니다. 열어서 앱을 만들고 실행하세요. 모두 환경 안에서 이루어집니다.",
+ hintReady: "워크스페이스가 준비되었습니다. 열어서 앱을 만들고 실행하세요. 모두 워크스페이스 안에서 이루어집니다.",
createEnvironment: "환경 만들기",
- openProduction: "프로덕션 열기",
+ openWorkspace: "워크스페이스 열기",
manageEnvironments: "환경 관리",
},
cloudPlanStatus: {
diff --git a/packages/i18n/src/locales/pt.ts b/packages/i18n/src/locales/pt.ts
index 69f6fcfcda..e6fe4c215d 100644
--- a/packages/i18n/src/locales/pt.ts
+++ b/packages/i18n/src/locales/pt.ts
@@ -3145,9 +3145,9 @@ const pt = {
},
cloudOnboarding: {
hintCreate: "Crie seu primeiro ambiente — um espaço de trabalho privado com URL, banco de dados e plano próprios. A construção acontece dentro dele.",
- hintReady: "Seu ambiente de produção está pronto. Abra-o para criar e executar seus aplicativos — tudo acontece dentro do ambiente.",
+ hintReady: "Seu espaço de trabalho está pronto. Abra-o para criar e executar seus aplicativos — tudo acontece dentro dele.",
createEnvironment: "Crie seu ambiente",
- openProduction: "Abrir produção",
+ openWorkspace: "Abrir espaço de trabalho",
manageEnvironments: "Gerenciar ambientes",
},
cloudPlanStatus: {
diff --git a/packages/i18n/src/locales/ru.ts b/packages/i18n/src/locales/ru.ts
index 0498036bcc..9361f56021 100644
--- a/packages/i18n/src/locales/ru.ts
+++ b/packages/i18n/src/locales/ru.ts
@@ -3225,9 +3225,9 @@ const ru = {
},
cloudOnboarding: {
hintCreate: "Создайте первое окружение — отдельное рабочее пространство со своим URL, базой данных и тарифом. Разработка ведётся внутри него.",
- hintReady: "Продакшн-окружение готово. Откройте его, чтобы создавать и запускать приложения — всё происходит внутри окружения.",
+ hintReady: "Рабочее пространство готово. Откройте его, чтобы создавать и запускать приложения — всё происходит внутри него.",
createEnvironment: "Создать окружение",
- openProduction: "Открыть продакшн",
+ openWorkspace: "Открыть рабочее пространство",
manageEnvironments: "Управление окружениями",
},
cloudPlanStatus: {
diff --git a/packages/i18n/src/locales/zh.ts b/packages/i18n/src/locales/zh.ts
index 6ab7151c21..d40a97a439 100644
--- a/packages/i18n/src/locales/zh.ts
+++ b/packages/i18n/src/locales/zh.ts
@@ -3238,9 +3238,9 @@ const zh = {
},
cloudOnboarding: {
hintCreate: '创建你的第一个环境——一个独立的工作区,有自己的网址、数据库和套餐。应用的搭建在里面进行。',
- hintReady: '你的生产环境已就绪。打开它来搭建和运行应用——这些都在环境内部进行。',
+ hintReady: '你的工作区已就绪。进入工作区来搭建和运行应用——这些都在工作区里进行。',
createEnvironment: '创建你的环境',
- openProduction: '打开生产环境',
+ openWorkspace: '进入工作区',
manageEnvironments: '管理环境',
},
cloudPlanStatus: {
diff --git a/packages/plugin-detail/src/HeaderHighlight.tsx b/packages/plugin-detail/src/HeaderHighlight.tsx
index e37547242a..86089cadcf 100644
--- a/packages/plugin-detail/src/HeaderHighlight.tsx
+++ b/packages/plugin-detail/src/HeaderHighlight.tsx
@@ -170,7 +170,13 @@ export const HeaderHighlight: React.FC = ({
// Two independent reasons a chip needs the wide basis, kept
// separate because only one of them is a shared table:
// 1. long-text DISPLAY types, whose value simply does not fit a
- // 9rem column — this surface's own list, unchanged here;
+ // 9rem column — this surface's own list. `phone` joined it in
+ // objectui#11659: its cell draws a dial icon and a copy button
+ // around the number inside an `inline-flex` box, and the
+ // `truncate` span below cannot put an ellipsis on an
+ // inline-flex child — it clips it. A 9rem chip cut
+ // `0574-8765-4321` to `0574-876` with no ellipsis; the wide
+ // basis shows a whole number;
// 2. REFERENCE-BEARING types, whose inline editor is a record
// picker (see the `dataSource` prop doc above, which already
// names `lookup` / `user` together as the reference editors).
@@ -191,6 +197,7 @@ export const HeaderHighlight: React.FC = ({
const isWide =
resolvedType === 'email' ||
resolvedType === 'url' ||
+ resolvedType === 'phone' ||
resolvedType === 'textarea' ||
(!!resolvedType && EXPANDABLE_FIELD_TYPES.has(resolvedType));
const isBoolean = resolvedType === 'boolean';
diff --git a/packages/plugin-detail/src/__tests__/RecordHighlightsRenderer.phoneWidth-11659.test.tsx b/packages/plugin-detail/src/__tests__/RecordHighlightsRenderer.phoneWidth-11659.test.tsx
new file mode 100644
index 0000000000..3cb51bd550
--- /dev/null
+++ b/packages/plugin-detail/src/__tests__/RecordHighlightsRenderer.phoneWidth-11659.test.tsx
@@ -0,0 +1,80 @@
+/**
+ * ObjectUI
+ * Copyright (c) 2024-present ObjectStack Inc.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+/**
+ * objectui#11659 ruling 4 — the record header does not cut a phone number.
+ *
+ * The cloud acceptance run read 「0574-876」 in the highlight row of a record
+ * drawer for the stored `0574-8765-4321`, with no ellipsis. The phone cell
+ * draws a dial icon, the number and a copy button inside an `inline-flex` box;
+ * the chip's `truncate` span cannot put an ellipsis on an inline-flex child, so
+ * a 9rem chip clipped the number mid-way. A phone chip now takes the wide basis,
+ * as `email` and `url` already did, so a whole number fits.
+ *
+ * happy-dom has no layout engine, so this is a class-semantics pin, not a pixel
+ * measurement (same approach as `RecordHighlightsRenderer.percentClip.test.tsx`).
+ */
+
+import { describe, it, expect } from 'vitest';
+import { render, screen } from '@testing-library/react';
+import * as React from 'react';
+import { RecordContextProvider } from '@object-ui/react';
+import { RecordHighlightsRenderer } from '../renderers/record-highlights';
+
+const PHONE = '0574-8765-4321';
+
+const objectSchema = {
+ fields: {
+ phone: { type: 'phone', label: 'Phone' },
+ status: { type: 'text', label: 'Status' },
+ },
+};
+
+const renderStrip = () =>
+ render(
+
+
+ ,
+ );
+
+/** The chip column (`group flex flex-col …`) that holds a given value. */
+const chipOf = (text: string) => screen.getByText(text).closest('div.group') as HTMLElement;
+
+describe('record:highlights — a phone chip shows the whole number (objectui#11659)', () => {
+ it('gives the phone chip the wide basis, not the 9rem one', () => {
+ renderStrip();
+
+ const chip = chipOf(PHONE);
+ expect(chip).not.toBeNull();
+ expect(chip).toHaveClass('basis-[16rem]');
+ expect(chip).not.toHaveClass('basis-[9rem]');
+ });
+
+ it('keeps a short text chip on the 9rem basis (the change is phone-only)', () => {
+ renderStrip();
+
+ const chip = chipOf('Active');
+ expect(chip).toHaveClass('basis-[9rem]');
+ expect(chip).not.toHaveClass('basis-[16rem]');
+ });
+
+ it('carries the whole number in the DOM and as the hover title', () => {
+ const { container } = renderStrip();
+
+ expect(screen.getByText(PHONE)).toBeInTheDocument();
+ const clipBox = chipOf(PHONE).querySelector('span.truncate');
+ expect(clipBox).not.toBeNull();
+ expect(clipBox).toHaveAttribute('title', PHONE);
+ expect(container.querySelector(`a[href="tel:${PHONE}"]`)).not.toBeNull();
+ });
+});