Skip to content
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
8 changes: 8 additions & 0 deletions examples/app-showcase/src/system/translations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1243,6 +1243,14 @@ export const ShowcaseTranslationBundle = {
showcase_active_projects: { label: '进行中的项目' },
// `ui/pages/invoice-document.page.ts` — the invoice print page.
showcase_invoice_document: { label: '发票' },
// `ui/pages/task-detail.page.ts` — the banner a task in review shows.
// Only its `title` has a key on this face; its `body` carries its
// zh-CN text inline on the page (see the comment on the node there).
showcase_task_detail: {
components: {
review_alert: { title: '待评审' },
},
},
showcase_contact_form: {
components: {
field_name: { label: '姓名', placeholder: '艾达·洛夫莱斯' },
Expand Down
22 changes: 21 additions & 1 deletion examples/app-showcase/src/ui/pages/task-detail.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,15 +91,35 @@ export const TaskDetailPage = definePage({
// hence no `visible` beside it: a node `visibleWhen` and
// `properties.visible` compose as AND, so leaving both would keep the
// legacy predicate load-bearing.
//
// The banner's copy reaches a zh-CN reader by two routes, one per key.
// `title` is translated by the bundle at
// `pages.showcase_task_detail.components.review_alert.title`, which is
// why the node carries an `id`: the page-component route addresses a
// component by its id, so an id-less node has no bundle key at all.
// `body` has no bundle key on that face (`PAGE_COMPONENT_COPY_KEYS` in
// `@objectstack/spec/system` is title / description / label /
// placeholder / emptyText, and the bundle schema refuses any other),
// so it carries its translation at the authoring site, as the inline
// locale map `RecordAlertProps.body` accepts. `dismissKey` keeps the
// dismissal under one name in every locale: its default is the English
// resolution of `title`, and a bundle-translated title would resolve
// to the translation, so a banner dismissed in one locale would come
// back in the other.
{
id: 'review_alert',
type: 'record:alert',
visibleWhen: "has(record.status) && record.status == 'in_review'",
properties: {
severity: 'warning',
icon: 'eye',
title: 'Awaiting review',
body: 'This task is in review — confirm the work before marking it done.',
body: {
en: 'This task is in review — confirm the work before marking it done.',
'zh-CN': '此任务正在评审中——请先确认工作成果,再标记为完成。',
},
dismissible: true,
dismissKey: 'showcase_task_review',
},
},
{
Expand Down
59 changes: 59 additions & 0 deletions examples/app-showcase/test/task-detail-review-alert-zh-cn.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* Under zh-CN, the Task Detail page's "Awaiting review" banner
* (`record:alert`, shown while a task is in review) is served in Chinese
* like the rest of the page.
*
* The banner's two strings travel by two routes. `title` is translated by the
* bundle through the page-component route
* (`pages.showcase_task_detail.components.review_alert.title`), which needs
* the node's `id` — before it had one the route could not address it and the
* title stayed English. `body` has no key on that face, so it is an inline
* locale map on the page; the renderer picks the reader's locale from it
* (`pickLocalized`), so the served document must still carry that map
* intact. The pin is the translator applied to the page with the app's own
* bundle, which is what the REST boundary does per request locale.
*/
import { describe, expect, it } from 'vitest';
import {
translateMetadataDocument,
TranslationBundleSchema,
type TranslationBundle,
} from '@objectstack/spec/system';
import { TaskDetailPage } from '../src/ui/pages/task-detail.page.js';
import { ShowcaseTranslationBundle } from '../src/system/translations/index.js';

const bundle = ShowcaseTranslationBundle as unknown as TranslationBundle;

type Node = { id?: string; type: string; properties?: Record<string, unknown> };
const reviewAlert = (page: { regions?: Array<{ components: Node[] }> }): Node | undefined =>
page.regions?.flatMap((r) => r.components).find((c) => c.type === 'record:alert');

describe('showcase Task Detail — the review banner under zh-CN', () => {
it('serves the banner title from the bundle and keeps the body\'s locale map', () => {
const out = translateMetadataDocument('page', TaskDetailPage, bundle, { locale: 'zh-CN' });
const alert = reviewAlert(out);
expect(alert?.id).toBe('review_alert');
expect(alert?.properties?.title).toBe('待评审');
expect(alert?.properties?.body).toEqual({
en: 'This task is in review — confirm the work before marking it done.',
'zh-CN': '此任务正在评审中——请先确认工作成果,再标记为完成。',
});
// The dismissal key does not move with the translated title.
expect(alert?.properties?.dismissKey).toBe('showcase_task_review');
});

it('leaves the authored English title in place for the default locale', () => {
const out = translateMetadataDocument('page', TaskDetailPage, bundle, { locale: 'en' });
expect(reviewAlert(out)?.properties?.title).toBe('Awaiting review');
});

it('carries the title entry in a bundle the spec accepts', () => {
// The bundle schema is strict on `components.<id>`: a key outside the
// page-component copy face (such as `body`) is refused, so the entry
// this page relies on must parse as written.
const zh = (ShowcaseTranslationBundle as Record<string, unknown>)['zh-CN'];
expect(TranslationBundleSchema.safeParse({ 'zh-CN': zh }).success).toBe(true);
});
});
Loading