Skip to content
This repository was archived by the owner on Oct 26, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/12956-console-copy-system.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@object-ui/i18n': patch
'@object-ui/app-shell': patch
'@object-ui/console': patch
---

Console copy names the system the user is already in, not the cloud noun "environment" (objectui#12956). Inside a running deployment, the Settings subtitle and its first category, the timezone prompt, the read-rate banner, the sign-in, sign-up and email-verification notices, the profile password descriptions, the home page loading, empty and build-refusal states, the system-settings quick action, the Connect-an-agent endpoint card and the AI model status error now say "this system" (zh 「本系统」), or drop the noun where the sentence reads without it. The Settings category that holds Company, Branding and Localization reads "General" (zh 「常规」). All ten language packs follow; no key is renamed, added or removed. Inline English fallbacks at the call sites follow the `en` values.
2 changes: 1 addition & 1 deletion apps/console/src/pages/auth/AuthLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ export function AuthLayout({ children, formWidth = 'sm' }: AuthLayoutProps) {
<span
className="rounded-full border border-border/60 bg-background/60 px-2 py-0.5 font-mono text-[10px] uppercase tracking-wide text-muted-foreground"
title={t('auth.shell.tenantHostHint', {
defaultValue: 'You are signing in to this environment',
defaultValue: 'You are signing in to this system',
})}
>
{host}
Expand Down
2 changes: 1 addition & 1 deletion apps/console/src/pages/auth/RegisterPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ export function RegisterPage() {
title={t('auth.register.title', { defaultValue: 'Create an account' })}
description={t('auth.register.errors.selfRegistrationClosed', {
defaultValue:
'Self-registration is not open on this environment. Ask an administrator for an invitation.',
'Self-registration is not open on this system. Ask an administrator for an invitation.',
})}
/>
<p className="px-8 text-center text-sm text-muted-foreground">
Expand Down
2 changes: 1 addition & 1 deletion apps/console/src/pages/auth/VerifyEmailPromptPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export function VerifyEmailPromptPage() {
(err as Error)?.message?.trim() ||
t('auth.verifyEmail.resendUnavailable', {
defaultValue:
'Email delivery may not be configured for this environment. Contact support if this persists.',
'Email delivery may not be configured for this system. Contact support if this persists.',
});
setResent(false);
setResendError(description);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ describe('asks once while the zone is the default and the session may write it (
it('en: renders once, pre-filled with the browser zone, naming the current default', async () => {
renderPrompt(ADMIN, 'en');
const dialog = await screen.findByRole('dialog');
expect(within(dialog).getByText('Set the environment timezone')).toBeTruthy();
expect(within(dialog).getByText('Set the system timezone')).toBeTruthy();
expect(within(dialog).getByText(/platform default timezone, UTC\./)).toBeTruthy();
// The Settings page's own field for the key, pre-filled with the browser's zone.
const field = within(dialog).getByLabelText('Default timezone') as HTMLInputElement;
Expand All @@ -208,10 +208,10 @@ describe('asks once while the zone is the default and the session may write it (
it('zh-CN: the same prompt reads in the pack, pre-filled the same way', async () => {
renderPrompt(ADMIN, 'zh');
const dialog = await screen.findByRole('dialog');
expect(within(dialog).getByText('设置环境时区')).toBeTruthy();
expect(within(dialog).getByText('设置系统时区')).toBeTruthy();
expect(within(dialog).getByRole('button', { name: '保持默认' })).toBeTruthy();
expect(within(dialog).getByRole('button', { name: '设置时区' })).toBeTruthy();
expect(screen.queryByText('Set the environment timezone')).toBeNull();
expect(screen.queryByText('Set the system timezone')).toBeNull();
// An input carrying a suggestion `list` is a combobox to assistive tech.
expect((within(dialog).getByRole('combobox') as HTMLInputElement).value).toBe(BROWSER_ZONE);
});
Expand Down Expand Up @@ -268,7 +268,7 @@ describe('confirm writes once through the Settings write path; decline writes no
const puts = settingsCalls('PUT');
expect(puts).toHaveLength(1);
expect(JSON.parse(String((puts[0][1] as RequestInit).body))).toEqual({ timezone: 'Europe/Berlin' });
expect(toastSuccess).toHaveBeenCalledWith('Environment timezone set to Europe/Berlin');
expect(toastSuccess).toHaveBeenCalledWith('System timezone set to Europe/Berlin');
});

it('confirm without editing writes the pre-filled browser zone', async () => {
Expand Down
6 changes: 3 additions & 3 deletions apps/console/src/pages/system/ProfilePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -709,7 +709,7 @@ function PasswordCard({ changePassword, setInitialPassword, hasLocalPassword, hi
} else {
await setInitialPassword(newPassword);
setSuccess(t('profile.password.localSet', {
defaultValue: 'Local password set. You can now sign in with email and password on this environment.',
defaultValue: 'Local password set. You can now sign in with email and password.',
}));
setHasPassword(true);
onPasswordSet?.();
Expand Down Expand Up @@ -739,11 +739,11 @@ function PasswordCard({ changePassword, setInitialPassword, hasLocalPassword, hi
<CardDescription>
{hasPassword
? t('profile.password.changeDescription', {
defaultValue: 'Update the password you use to sign in to this environment.',
defaultValue: 'Update the password you use to sign in.',
})
: t('profile.password.setDescription', {
defaultValue:
'You signed in via single sign-on. Set a local password to also sign in with email and password on this environment.',
'You signed in via single sign-on. Set a local password to also sign in with email and password.',
})}
</CardDescription>
</CardHeader>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export function signUpRefusalMessages(t: TranslateFn): Record<string, string> {
// invitation only, or only from allowlisted email domains.
SELF_REGISTRATION_CLOSED: t('auth.register.errors.selfRegistrationClosed', {
defaultValue:
'Self-registration is not open on this environment. Ask an administrator for an invitation.',
'Self-registration is not open on this system. Ask an administrator for an invitation.',
}),
EMAIL_DOMAIN_NOT_ALLOWED: t('auth.register.errors.emailDomainNotAllowed', {
defaultValue:
Expand Down
6 changes: 3 additions & 3 deletions packages/app-shell/src/console/home/HomePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ export function HomePage() {
// the zero-app empty state as its description. Localized in all ten packs.
const authoringGateReason = t('home.build.noCapability', {
defaultValue:
"Building apps isn't available in this environment — it requires the “Manage Metadata” permission, which your account doesn't have.",
"Building apps isn't available in this system — it requires the “Manage Metadata” permission, which your account doesn't have.",
});
const marketplaceGateReason = t('home.template.marketplaceDisabled', {
defaultValue: 'This runtime has no app marketplace configured, so there are no templates to install here.',
Expand Down Expand Up @@ -472,7 +472,7 @@ export function HomePage() {
if (loading || !isAdminResolved) {
return (
<div className="flex flex-1 items-center justify-center py-20">
<div className="text-muted-foreground">{t('home.loading', { defaultValue: 'Loading environment…' })}</div>
<div className="text-muted-foreground">{t('home.loading', { defaultValue: 'Loading…' })}</div>
</div>
);
}
Expand Down Expand Up @@ -560,7 +560,7 @@ export function HomePage() {
<EmptyDescription>
{t('home.noAppsDescription', {
defaultValue:
'Your environment is being set up — apps your admin shares with you will show up here.',
'The system is being set up — apps your admin shares with you will show up here.',
})}
</EmptyDescription>
</Empty>
Expand Down
2 changes: 1 addition & 1 deletion packages/app-shell/src/console/home/QuickActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export function QuickActions() {
{
id: 'system-settings',
label: t('home.quickActions.systemSettings', { defaultValue: 'System Settings' }),
description: t('home.quickActions.systemSettingsDesc', { defaultValue: 'Configure your environment' }),
description: t('home.quickActions.systemSettingsDesc', { defaultValue: 'Configure your system' }),
icon: Settings,
// #3611 — the system hub, not the bare `/apps/setup` (which is the
// "No Apps Configured" empty state's own URL on a zero-app deployment).
Expand Down
4 changes: 2 additions & 2 deletions packages/app-shell/src/layout/ReadRateBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,9 @@ export function ReadRateBanner({ apiBase, className }: ReadRateBannerProps) {

const title = noWrites
? t('console.readRate.noWritesTitle', {
defaultValue: 'Reads with no writes at all in this environment',
defaultValue: 'Reads with no writes at all in this system',
})
: t('console.readRate.ratioTitle', { defaultValue: 'Unusual read volume in this environment' });
: t('console.readRate.ratioTitle', { defaultValue: 'Unusual read volume in this system' });

const body = noWrites
? t('console.readRate.noWrites', {
Expand Down
68 changes: 68 additions & 0 deletions packages/i18n/src/__tests__/console-copy-system-noun-12956.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* objectui#12956 — inside the running system, the console does not call
* itself 「此环境 / 本环境」.
*
* Ruling A on objectstack-ai/cloud#3002 keeps 「环境」 as the cloud's noun for
* a deployment and keeps it out of the way of a user who is already inside
* one. The strings in the families below are shown by every ObjectStack
* deployment to that user, so they say 「本系统」, or drop the noun where the
* sentence reads without it.
*
* The pin enumerates every string under each named family in the `zh` pack,
* so a value added to one of these families later is held too. It asserts the
* enumeration is non-empty per family first: a family path that stopped
* resolving would otherwise pass with nothing checked.
*
* Out of scope on purpose (the card's exclusions): `console.settingsView`,
* where 「环境变量」 means environment variables, and `cloudOnboarding.*`,
* which explains the cloud noun once.
*/
import { describe, it, expect } from 'vitest';
import { builtInLocales } from '../locales';

const FAMILIES = [
'console.settingsHub',
'console.workspaceTimezonePrompt',
'console.readRate',
'auth',
'profile.password',
'home',
'connectAgent',
'aiModelStatus',
] as const;

/** "This / the said environment", the phrasing the ruling retires here. */
const SELF_AS_ENVIRONMENT = /[此本该]环境/u;

function resolve(root: unknown, path: string): unknown {
return path.split('.').reduce<unknown>(
(node, key) => (node && typeof node === 'object' ? (node as Record<string, unknown>)[key] : undefined),
root,
);
}

function strings(node: unknown, path: string, out: Array<[string, string]> = []): Array<[string, string]> {
if (typeof node === 'string') out.push([path, node]);
else if (node && typeof node === 'object') {
for (const [key, child] of Object.entries(node)) strings(child, `${path}.${key}`, out);
}
return out;
}

describe('console copy names the system the user is in, not the cloud noun (objectui#12956)', () => {
const zh = builtInLocales.zh as unknown;

it.each(FAMILIES)('zh %s carries no 此环境 / 本环境 / 该环境', (family) => {
const entries = strings(resolve(zh, family), family);
expect(entries.length).toBeGreaterThan(0);
const offenders = entries.filter(([, value]) => SELF_AS_ENVIRONMENT.test(value));
expect(offenders).toEqual([]);
});

it('the matcher catches each retired spelling and leaves environment variables alone', () => {
expect(SELF_AS_ENVIRONMENT.test('本环境读取量异常')).toBe(true);
expect(SELF_AS_ENVIRONMENT.test('您正在登录此环境')).toBe(true);
expect(SELF_AS_ENVIRONMENT.test('该环境可能未运行 AI 服务')).toBe(true);
expect(SELF_AS_ENVIRONMENT.test('已被环境变量锁定')).toBe(false);
});
});
14 changes: 9 additions & 5 deletions packages/i18n/src/__tests__/ellipsis-glyph-3878.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,10 @@ describe('objectui#3878 — the ten packs spell the ellipsis U+2026 and only U+2
const LOADING_GROUP_EN = 'Loading…';

/**
* The group as measured on `main` @ `22ba9271f`. Pinned by name so the
* The group as measured on `main` @ `22ba9271f`, plus `home.loading`, which
* joined when objectui#12956 dropped the cloud noun from its `en` value
* (`Loading environment…` became `Loading…`); every pack now renders it the
* way it renders the rest of the group. Pinned by name so the
* uniformity rule below cannot go green by the group quietly emptying out —
* a uniformity assertion over nothing passes, which is this pin's failure mode.
*/
Expand All @@ -346,6 +349,7 @@ const LOADING_GROUP = [
'fields.recipient.loading',
'grid.bulk.loading',
'grid.import.historyLoading',
'home.loading',
'lookup.loading',
'report.loading',
] as const;
Expand All @@ -363,16 +367,16 @@ const LOADING_GROUP = [
const LOADING_GROUP_FORKS: ReadonlyArray<{ lang: Lang; key: string; value: string }> = [];

describe('objectui#5972 — each pack renders the merged `Loading…` group exactly one way', () => {
it('derives the group from en and finds exactly the ten keys pinned above', () => {
it('derives the group from en and finds exactly the keys pinned above', () => {
// Membership first, uniformity second. If `en` drifts — a key renamed, a
// value edited to `Loading more…`, a new `Loading…` key landing — the group
// this file reasons about is no longer the group it names, and the rule
// below would be silently measuring something else.
const derived = PACKS.en.filter(([, value]) => value === LOADING_GROUP_EN).map(([key]) => key);
expect(derived.length, 'the group emptied or changed size — re-measure before editing the list').toBe(10);
expect(derived.length, 'the group emptied or changed size — re-measure before editing the list').toBe(11);
expect([...derived].sort()).toEqual([...LOADING_GROUP].sort());

// And every pack must actually define all ten. An absent key is a key-set
// And every pack must actually define every member. An absent key is a key-set
// fact owned by all-locales-key-parity, but if one went missing here the
// uniformity rule would happily pass over the survivors.
for (const lang of LANGS) {
Expand All @@ -390,7 +394,7 @@ describe('objectui#5972 — each pack renders the merged `Loading…` group exac
for (const lang of LANGS) {
const byKey = new Map(PACKS[lang]);
const ruled = LOADING_GROUP.filter((key) => !forked.has(`${lang} ${key}`));
// Non-vacuity per language: 10 keys, minus this pack's exemptions. A
// Non-vacuity per language: every member, minus this pack's exemptions. A
// count assertion here is what stops a collapsed pack or a typo'd key
// name from turning the set check below into a check of nothing.
const expectedCount = LOADING_GROUP.length - LOADING_GROUP_FORKS.filter((f) => f.lang === lang).length;
Expand Down
Loading
Loading