From 561e499ab22649bce5e196bf8a638e625e23923f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 08:47:27 +0000 Subject: [PATCH 01/19] fix(plugin-map): degrade a failed map style to the record list with translated notices (WIP) Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01HUu4jGnAzZqaB5qTG2fsX4 --- .changeset/25889-map-style-failure-notice.md | 14 ++ ui/packages/i18n/src/locales/ar.ts | 8 + ui/packages/i18n/src/locales/de.ts | 8 + ui/packages/i18n/src/locales/en.ts | 12 + ui/packages/i18n/src/locales/es.ts | 8 + ui/packages/i18n/src/locales/fr.ts | 8 + ui/packages/i18n/src/locales/ja.ts | 8 + ui/packages/i18n/src/locales/ko.ts | 8 + ui/packages/i18n/src/locales/pt.ts | 8 + ui/packages/i18n/src/locales/ru.ts | 8 + ui/packages/i18n/src/locales/zh.ts | 8 + .../src/ObjectMap.noticesI18n-25889.test.tsx | 73 ++++++ .../ObjectMap.styleFailureList-25889.test.tsx | 219 ++++++++++++++++++ .../src/ObjectMap.styleFallback.test.tsx | 5 +- ui/packages/plugin-map/src/ObjectMap.tsx | 106 ++++++--- .../ObjectMap.webgl2Controls-11819.test.tsx | 14 +- .../ObjectMap.webgl2Fallback-11819.test.tsx | 2 + 17 files changed, 480 insertions(+), 37 deletions(-) create mode 100644 .changeset/25889-map-style-failure-notice.md create mode 100644 ui/packages/plugin-map/src/ObjectMap.noticesI18n-25889.test.tsx create mode 100644 ui/packages/plugin-map/src/ObjectMap.styleFailureList-25889.test.tsx diff --git a/.changeset/25889-map-style-failure-notice.md b/.changeset/25889-map-style-failure-notice.md new file mode 100644 index 00000000000..97c7c30c239 --- /dev/null +++ b/.changeset/25889-map-style-failure-notice.md @@ -0,0 +1,14 @@ +--- +'@object-ui/plugin-map': patch +'@object-ui/i18n': patch +--- + +fix(plugin-map): a map whose style or tiles fail to load shows the record list with a translated notice, instead of a grey canvas under an English alert + +Clause-②: no +Metadata-protocol: none + +- **`@object-ui/plugin-map`:** when MapLibre reports a style or tile load failure (an intranet or proxied deployment that cannot reach the tile host, an unreachable style URL), the map view now removes the map and lists its records in its place, the same list a browser without WebGL2 already got: searchable, and each row opens the record the way its marker did. A "Try the map again" button brings the map back. The notice above the list keeps MapLibre's reason and names a remedy that exists: a view with its own map style is told to check that URL, and a view on the public default style is told to ask an administrator to set a map style for the view. It no longer says the markers are "listed in the search box" or asks for a "custom map style for production use" setting that does not exist. That notice, the WebGL2 notice, the list's label and empty text, and the "records excluded for missing or invalid coordinates" count now read the translation bundle, so a zh-CN console shows them in Chinese. +- **`@object-ui/i18n`:** eight new `element.map.*` keys in all ten packs. + +What to do: nothing. A deployment-wide map style setting is not part of this change. diff --git a/ui/packages/i18n/src/locales/ar.ts b/ui/packages/i18n/src/locales/ar.ts index ac057c1c960..a58b74c149b 100644 --- a/ui/packages/i18n/src/locales/ar.ts +++ b/ui/packages/i18n/src/locales/ar.ts @@ -5142,6 +5142,14 @@ const ar = { }, map: { sourceUnresolved: "تعذّر تحديد مصدر بيانات هذه الخريطة", + invalidCoordinatesExcluded: "السجلات المستبعدة من الخريطة بسبب إحداثيات مفقودة أو غير صالحة: {{excluded}}", + styleFailedConfigured: "تعذّر تحميل الخريطة ({{reason}}). السجلات مدرجة أدناه؛ تحقّق من عنوان URL لنمط الخريطة المحدد في طريقة العرض هذه.", + styleFailedDefault: "تعذّر تحميل الخريطة ({{reason}}). السجلات مدرجة أدناه؛ تعذّر تحميل مربعات الخريطة العامة الافتراضية هنا، لذا اطلب من المسؤول تعيين نمط خريطة لطريقة العرض هذه.", + styleFailedUnknownReason: "تعذّر تحميل نمط الخريطة أو مربعاتها", + webgl2Unavailable: "تعذّر تحميل الخريطة (هذا المتصفح لا يوفّر WebGL2 الذي تحتاجه الخريطة). السجلات مدرجة أدناه؛ فعّل تسريع الأجهزة أو استخدم متصفحًا يدعم WebGL2 لعرض الخريطة.", + retry: "إعادة محاولة تحميل الخريطة", + listLabel: "المواقع", + listEmpty: "لا توجد مواقع لعرضها.", }, timeline: { sourceUnresolved: "تعذّر تحديد مصدر بيانات هذا المخطط الزمني", diff --git a/ui/packages/i18n/src/locales/de.ts b/ui/packages/i18n/src/locales/de.ts index 2dca8512ae5..c8f048ff4b9 100644 --- a/ui/packages/i18n/src/locales/de.ts +++ b/ui/packages/i18n/src/locales/de.ts @@ -4688,6 +4688,14 @@ const de = { }, map: { sourceUnresolved: "Die Datenquelle dieser Karte konnte nicht aufgelöst werden", + invalidCoordinatesExcluded: "Wegen fehlender oder ungültiger Koordinaten nicht auf der Karte angezeigte Datensätze: {{excluded}}", + styleFailedConfigured: "Die Karte konnte nicht geladen werden ({{reason}}). Die Datensätze sind unten aufgelistet; prüfen Sie die für diese Ansicht festgelegte Kartenstil-URL.", + styleFailedDefault: "Die Karte konnte nicht geladen werden ({{reason}}). Die Datensätze sind unten aufgelistet; die öffentlichen Standard-Kartenkacheln konnten hier nicht geladen werden. Bitten Sie Ihren Administrator, für diese Ansicht einen Kartenstil festzulegen.", + styleFailedUnknownReason: "Kartenstil oder Kacheln konnten nicht geladen werden", + webgl2Unavailable: "Die Karte konnte nicht geladen werden (dieser Browser unterstützt kein WebGL2, das die Karte benötigt). Die Datensätze sind unten aufgelistet; aktivieren Sie die Hardwarebeschleunigung oder verwenden Sie einen Browser mit WebGL2, um die Karte zu sehen.", + retry: "Karte erneut laden", + listLabel: "Orte", + listEmpty: "Keine Orte vorhanden.", }, timeline: { sourceUnresolved: "Die Datenquelle dieser Zeitleiste konnte nicht aufgelöst werden", diff --git a/ui/packages/i18n/src/locales/en.ts b/ui/packages/i18n/src/locales/en.ts index ef2ecfa2f7c..a7595352a6e 100644 --- a/ui/packages/i18n/src/locales/en.ts +++ b/ui/packages/i18n/src/locales/en.ts @@ -5479,6 +5479,18 @@ const en = { }, map: { sourceUnresolved: 'This map’s data source could not be resolved', + // objectstack#25889 — the map view's own notices. Both style failures + // fill `{{reason}}` with MapLibre's message, or `styleFailedUnknownReason` + // when it gave none; the excluded count reads "Label: N" so no key needs + // a plural family. + invalidCoordinatesExcluded: 'Records excluded from the map for missing or invalid coordinates: {{excluded}}', + styleFailedConfigured: 'Map failed to load ({{reason}}). The records are listed below; check the map style URL set on this view.', + styleFailedDefault: 'Map failed to load ({{reason}}). The records are listed below; the public default map tiles could not be loaded here, so ask your administrator to set a map style for this view.', + styleFailedUnknownReason: 'the map style or tiles could not be loaded', + webgl2Unavailable: 'Map failed to load (this browser does not provide WebGL2, which the map needs). The records are listed below; turn on hardware acceleration or use a browser with WebGL2 to see the map.', + retry: 'Try the map again', + listLabel: 'Locations', + listEmpty: 'No locations to list.', }, timeline: { sourceUnresolved: 'This timeline’s data source could not be resolved', diff --git a/ui/packages/i18n/src/locales/es.ts b/ui/packages/i18n/src/locales/es.ts index 464ece759f5..efd1f24cea2 100644 --- a/ui/packages/i18n/src/locales/es.ts +++ b/ui/packages/i18n/src/locales/es.ts @@ -4801,6 +4801,14 @@ const es = { }, map: { sourceUnresolved: "No se pudo resolver el origen de datos de este mapa", + invalidCoordinatesExcluded: "Registros excluidos del mapa por coordenadas ausentes o no válidas: {{excluded}}", + styleFailedConfigured: "No se pudo cargar el mapa ({{reason}}). Los registros aparecen a continuación; revise la URL del estilo de mapa configurada en esta vista.", + styleFailedDefault: "No se pudo cargar el mapa ({{reason}}). Los registros aparecen a continuación; aquí no se pudieron cargar los mosaicos de mapa públicos predeterminados, así que pida a su administrador que configure un estilo de mapa para esta vista.", + styleFailedUnknownReason: "no se pudieron cargar el estilo o los mosaicos del mapa", + webgl2Unavailable: "No se pudo cargar el mapa (este navegador no admite WebGL2, que el mapa necesita). Los registros aparecen a continuación; active la aceleración por hardware o use un navegador con WebGL2 para ver el mapa.", + retry: "Reintentar el mapa", + listLabel: "Ubicaciones", + listEmpty: "No hay ubicaciones para mostrar.", }, timeline: { sourceUnresolved: "No se pudo resolver el origen de datos de esta línea de tiempo", diff --git a/ui/packages/i18n/src/locales/fr.ts b/ui/packages/i18n/src/locales/fr.ts index 16c2d6bef70..922604a33e9 100644 --- a/ui/packages/i18n/src/locales/fr.ts +++ b/ui/packages/i18n/src/locales/fr.ts @@ -4797,6 +4797,14 @@ const fr = { }, map: { sourceUnresolved: "La source de données de cette carte n’a pas pu être résolue", + invalidCoordinatesExcluded: "Enregistrements exclus de la carte (coordonnées manquantes ou invalides) : {{excluded}}", + styleFailedConfigured: "Échec du chargement de la carte ({{reason}}). Les enregistrements sont listés ci-dessous ; vérifiez l’URL du style de carte défini sur cette vue.", + styleFailedDefault: "Échec du chargement de la carte ({{reason}}). Les enregistrements sont listés ci-dessous ; les tuiles de carte publiques par défaut n’ont pas pu être chargées ici, demandez à votre administrateur de définir un style de carte pour cette vue.", + styleFailedUnknownReason: "le style ou les tuiles de la carte n’ont pas pu être chargés", + webgl2Unavailable: "Échec du chargement de la carte (ce navigateur ne prend pas en charge WebGL2, nécessaire à la carte). Les enregistrements sont listés ci-dessous ; activez l’accélération matérielle ou utilisez un navigateur compatible WebGL2 pour afficher la carte.", + retry: "Réessayer la carte", + listLabel: "Lieux", + listEmpty: "Aucun lieu à afficher.", }, timeline: { sourceUnresolved: "La source de données de cette chronologie n’a pas pu être résolue", diff --git a/ui/packages/i18n/src/locales/ja.ts b/ui/packages/i18n/src/locales/ja.ts index ae922186d1a..5acb1ec03ef 100644 --- a/ui/packages/i18n/src/locales/ja.ts +++ b/ui/packages/i18n/src/locales/ja.ts @@ -4689,6 +4689,14 @@ const ja = { }, map: { sourceUnresolved: "この地図のデータソースを解決できませんでした", + invalidCoordinatesExcluded: "座標が未設定または無効なため地図から除外されたレコード:{{excluded}}", + styleFailedConfigured: "地図を読み込めませんでした({{reason}})。レコードは下に一覧表示しています。このビューに設定された地図スタイルの URL を確認してください。", + styleFailedDefault: "地図を読み込めませんでした({{reason}})。レコードは下に一覧表示しています。公開のデフォルト地図タイルをここでは読み込めないため、このビューの地図スタイルを設定するよう管理者に依頼してください。", + styleFailedUnknownReason: "地図スタイルまたはタイルを読み込めませんでした", + webgl2Unavailable: "地図を読み込めませんでした(このブラウザーは地図に必要な WebGL2 に対応していません)。レコードは下に一覧表示しています。ハードウェアアクセラレーションを有効にするか、WebGL2 対応のブラウザーで地図を表示してください。", + retry: "地図を再読み込み", + listLabel: "場所", + listEmpty: "表示する場所はありません。", }, timeline: { sourceUnresolved: "このタイムラインのデータソースを解決できませんでした", diff --git a/ui/packages/i18n/src/locales/ko.ts b/ui/packages/i18n/src/locales/ko.ts index 4d2e691ec58..d4acada2e0b 100644 --- a/ui/packages/i18n/src/locales/ko.ts +++ b/ui/packages/i18n/src/locales/ko.ts @@ -4686,6 +4686,14 @@ const ko = { }, map: { sourceUnresolved: "이 지도의 데이터 소스를 확인할 수 없습니다", + invalidCoordinatesExcluded: "좌표가 없거나 잘못되어 지도에서 제외된 레코드: {{excluded}}", + styleFailedConfigured: "지도를 불러오지 못했습니다({{reason}}). 레코드는 아래에 나열되어 있습니다. 이 보기에 설정된 지도 스타일 URL을 확인하세요.", + styleFailedDefault: "지도를 불러오지 못했습니다({{reason}}). 레코드는 아래에 나열되어 있습니다. 여기에서는 공개 기본 지도 타일을 불러올 수 없으므로 관리자에게 이 보기의 지도 스타일을 설정해 달라고 요청하세요.", + styleFailedUnknownReason: "지도 스타일 또는 타일을 불러올 수 없습니다", + webgl2Unavailable: "지도를 불러오지 못했습니다(이 브라우저는 지도에 필요한 WebGL2를 지원하지 않습니다). 레코드는 아래에 나열되어 있습니다. 하드웨어 가속을 켜거나 WebGL2를 지원하는 브라우저에서 지도를 확인하세요.", + retry: "지도 다시 시도", + listLabel: "위치", + listEmpty: "표시할 위치가 없습니다.", }, timeline: { sourceUnresolved: "이 타임라인의 데이터 소스를 확인할 수 없습니다", diff --git a/ui/packages/i18n/src/locales/pt.ts b/ui/packages/i18n/src/locales/pt.ts index 6947902e67b..32d741de3b9 100644 --- a/ui/packages/i18n/src/locales/pt.ts +++ b/ui/packages/i18n/src/locales/pt.ts @@ -4796,6 +4796,14 @@ const pt = { }, map: { sourceUnresolved: "Não foi possível resolver a fonte de dados deste mapa", + invalidCoordinatesExcluded: "Registros excluídos do mapa por coordenadas ausentes ou inválidas: {{excluded}}", + styleFailedConfigured: "Não foi possível carregar o mapa ({{reason}}). Os registros estão listados abaixo; verifique a URL do estilo de mapa definida nesta visualização.", + styleFailedDefault: "Não foi possível carregar o mapa ({{reason}}). Os registros estão listados abaixo; não foi possível carregar aqui os blocos de mapa públicos padrão, então peça ao seu administrador que defina um estilo de mapa para esta visualização.", + styleFailedUnknownReason: "não foi possível carregar o estilo ou os blocos do mapa", + webgl2Unavailable: "Não foi possível carregar o mapa (este navegador não oferece WebGL2, necessário para o mapa). Os registros estão listados abaixo; ative a aceleração de hardware ou use um navegador com WebGL2 para ver o mapa.", + retry: "Tentar carregar o mapa novamente", + listLabel: "Locais", + listEmpty: "Nenhum local para listar.", }, timeline: { sourceUnresolved: "Não foi possível resolver a fonte de dados desta linha do tempo", diff --git a/ui/packages/i18n/src/locales/ru.ts b/ui/packages/i18n/src/locales/ru.ts index bd5316c6919..0eb0c9e04f3 100644 --- a/ui/packages/i18n/src/locales/ru.ts +++ b/ui/packages/i18n/src/locales/ru.ts @@ -4923,6 +4923,14 @@ const ru = { }, map: { sourceUnresolved: "Не удалось определить источник данных этой карты", + invalidCoordinatesExcluded: "Записи, исключённые с карты из-за отсутствующих или неверных координат: {{excluded}}", + styleFailedConfigured: "Не удалось загрузить карту ({{reason}}). Записи перечислены ниже; проверьте URL стиля карты, заданный для этого представления.", + styleFailedDefault: "Не удалось загрузить карту ({{reason}}). Записи перечислены ниже; общедоступные тайлы карты по умолчанию здесь загрузить не удалось, попросите администратора задать стиль карты для этого представления.", + styleFailedUnknownReason: "не удалось загрузить стиль или тайлы карты", + webgl2Unavailable: "Не удалось загрузить карту (этот браузер не поддерживает WebGL2, необходимый для карты). Записи перечислены ниже; включите аппаратное ускорение или откройте карту в браузере с поддержкой WebGL2.", + retry: "Повторить загрузку карты", + listLabel: "Местоположения", + listEmpty: "Нет местоположений для отображения.", }, timeline: { sourceUnresolved: "Не удалось определить источник данных этой временной шкалы", diff --git a/ui/packages/i18n/src/locales/zh.ts b/ui/packages/i18n/src/locales/zh.ts index e7253201c8d..83c7690e8c5 100644 --- a/ui/packages/i18n/src/locales/zh.ts +++ b/ui/packages/i18n/src/locales/zh.ts @@ -4744,6 +4744,14 @@ const zh = { }, map: { sourceUnresolved: '无法解析此地图的数据源', + invalidCoordinatesExcluded: '因坐标缺失或无效而未显示在地图上的记录:{{excluded}}', + styleFailedConfigured: '地图加载失败({{reason}})。记录已列在下方;请检查此视图设置的地图样式 URL。', + styleFailedDefault: '地图加载失败({{reason}})。记录已列在下方;此处无法加载公共默认地图瓦片,请联系管理员为此视图设置地图样式。', + styleFailedUnknownReason: '无法加载地图样式或瓦片', + webgl2Unavailable: '地图加载失败(此浏览器不支持地图所需的 WebGL2)。记录已列在下方;请开启硬件加速,或使用支持 WebGL2 的浏览器查看地图。', + retry: '重试加载地图', + listLabel: '位置', + listEmpty: '没有可列出的位置。', }, timeline: { sourceUnresolved: '无法解析此时间线的数据源', diff --git a/ui/packages/plugin-map/src/ObjectMap.noticesI18n-25889.test.tsx b/ui/packages/plugin-map/src/ObjectMap.noticesI18n-25889.test.tsx new file mode 100644 index 00000000000..27635bb745f --- /dev/null +++ b/ui/packages/plugin-map/src/ObjectMap.noticesI18n-25889.test.tsx @@ -0,0 +1,73 @@ +/** + * 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. + */ + +/** + * objectstack#25889 — the map's notices read the bundle: a zh-CN console shows + * them in Chinese, not as the English literals they used to be. + * + * Its own FILE because mounting `I18nProvider` registers its instance as + * react-i18next's module-global default for the rest of the file; the + * provider-less English path is pinned in + * `ObjectMap.styleFailureList-25889.test.tsx`, which never mounts one. + */ + +import React from 'react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { describe, it, expect, vi } from 'vitest'; +import type { ObjectMapSchema } from '@object-ui/types'; +import { I18nProvider } from '@object-ui/react'; +import { ObjectMap } from './ObjectMap'; +import './__tests__/webgl2Available'; + +type Slot = { children?: React.ReactNode }; + +vi.mock('react-map-gl/maplibre', () => ({ + default: (props: Slot & { onError?: (e: { error?: Error }) => void }) => ( +
+
+ ), + NavigationControl: () =>
, + Marker: ({ children }: Slot) =>
{children}
, + Popup: ({ children }: Slot) =>
{children}
, +})); + +const records = [ + { id: 't1', title: 'Install rooftop unit', location: { lat: 47.6062, lng: -122.3321 } }, + { id: 't2', title: 'No coordinates' }, +]; + +const schema: ObjectMapSchema = { type: 'object-map', map: { locationField: 'location', titleField: 'title' } }; + +describe('ObjectMap notices under zh-CN (objectstack#25889)', () => { + it('the excluded-records notice and the style-failure alert are in Chinese', async () => { + render( + + + , + ); + await screen.findByLabelText('Map'); + await waitFor(() => + expect(screen.getByText('因坐标缺失或无效而未显示在地图上的记录:1')).toBeInTheDocument(), + ); + + fireEvent.click(screen.getByTestId('simulate-style-error')); + + const alert = await screen.findByRole('alert'); + expect(alert).toHaveTextContent( + '地图加载失败(Failed to fetch)。记录已列在下方;此处无法加载公共默认地图瓦片,请联系管理员为此视图设置地图样式。', + ); + expect(screen.getByTestId('map-style-retry')).toHaveTextContent('重试加载地图'); + expect(screen.getByRole('list', { name: '位置' })).toBeInTheDocument(); + }); +}); diff --git a/ui/packages/plugin-map/src/ObjectMap.styleFailureList-25889.test.tsx b/ui/packages/plugin-map/src/ObjectMap.styleFailureList-25889.test.tsx new file mode 100644 index 00000000000..8fd25ae074f --- /dev/null +++ b/ui/packages/plugin-map/src/ObjectMap.styleFailureList-25889.test.tsx @@ -0,0 +1,219 @@ +/** + * 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. + */ + +/** + * objectstack#25889 — a style or tile load failure swaps the map for the record + * list, with a notice that says what happened and who can fix it. + * + * A deployment whose network cannot reach the tile host (an intranet, a + * proxy, a policy that forbids third-party hosts) used to keep a grey canvas + * with the markers floating over nothing, under an English alert that sent the + * reader to a "search box" listing that did not exist and to a "custom map + * style for production use" setting that does not exist either. + * + * The map here is a stub that stands in for react-map-gl: it mounts like the + * real one (an effect whose cleanup is the real one's `map.remove()`), and it + * fires `onError` the way MapLibre's `error` event reaches react-map-gl. The + * cleanup counter is what makes the unmount path readable: swapping the map out + * must remove it exactly once, and unmounting the view afterwards must not + * reach into a map that is already gone. + */ + +import React, { useEffect } from 'react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import type { ObjectMapSchema } from '@object-ui/types'; +import { ObjectMap } from './ObjectMap'; +import './__tests__/webgl2Available'; + +type Slot = { children?: React.ReactNode }; + +const lifecycle = { mounts: 0, removes: 0 }; + +vi.mock('react-map-gl/maplibre', () => ({ + default: function StubMap( + props: Slot & { mapStyle?: string; onError?: (e: { error?: Error }) => void }, + ) { + useEffect(() => { + lifecycle.mounts += 1; + return () => { + lifecycle.removes += 1; + }; + }, []); + return ( +
+
+ ); + }, + NavigationControl: () =>
, + Marker: ({ children }: Slot) =>
{children}
, + Popup: ({ children }: Slot) =>
{children}
, +})); + +/** Stands in for the page's error boundary: what it catches is the crash card. */ +class CrashCard extends React.Component< + { onCatch: (error: Error) => void; children: React.ReactNode }, + { crashed: boolean } +> { + state = { crashed: false }; + static getDerivedStateFromError() { + return { crashed: true }; + } + componentDidCatch(error: Error) { + this.props.onCatch(error); + } + render() { + return this.state.crashed ?
: this.props.children; + } +} + +const records = [ + { id: 't1', title: 'Install rooftop unit', notes: 'North wing', location: { lat: 47.6062, lng: -122.3321 } }, + { id: 't2', title: 'Replace boiler', notes: 'Basement', location: { lat: 45.5152, lng: -122.6784 } }, +]; + +const MAP = { locationField: 'location', titleField: 'title', descriptionField: 'notes' }; +const schema: ObjectMapSchema = { type: 'object-map', map: MAP }; + +let caught: Error[]; + +beforeEach(() => { + lifecycle.mounts = 0; + lifecycle.removes = 0; + caught = []; +}); + +function mount(props: Partial> = {}) { + return render( + caught.push(error)}> + + , + ); +} + +async function failStyle(testId = 'simulate-style-error') { + await screen.findByLabelText('Map'); + fireEvent.click(screen.getByTestId(testId)); + return screen.findByTestId('map-style-fallback'); +} + +describe('ObjectMap — a style or tile failure degrades to the record list (objectstack#25889)', () => { + it('replaces the map with the record list, removing the map exactly once', async () => { + mount(); + const list = await failStyle(); + + expect(screen.queryByLabelText('Map')).toBeNull(); + expect(screen.queryAllByTestId('map-marker')).toHaveLength(0); + expect(lifecycle).toEqual({ mounts: 1, removes: 1 }); + expect(list).toHaveTextContent('Install rooftop unit'); + expect(screen.getByRole('button', { name: /Replace boiler/ })).toHaveTextContent('Basement'); + // Not the WebGL2 case: that one has its own notice and no retry. + expect(screen.queryByTestId('map-webgl2-fallback')).toBeNull(); + expect(caught).toEqual([]); + }); + + it('the notice keeps MapLibre’s reason and sends the default-style reader to an administrator', async () => { + mount(); + await failStyle(); + + const alert = screen.getByRole('alert'); + expect(alert).toHaveTextContent( + 'Map failed to load (AJAXError: Failed to fetch (0): https://demotiles.maplibre.org/style.json).', + ); + expect(alert).toHaveTextContent('The records are listed below'); + expect(alert).toHaveTextContent('ask your administrator to set a map style for this view'); + // The two claims the old alert made and nothing delivered. + expect(alert.textContent).not.toMatch(/search box/); + expect(alert.textContent).not.toMatch(/production/); + }); + + it('a view with its own style is told to check that style', async () => { + mount({ schema: { ...schema, map: { ...MAP, style: 'https://tiles.example.com/style.json' } } }); + await failStyle(); + + const alert = screen.getByRole('alert'); + expect(alert).toHaveTextContent('(AJAXError: Failed to fetch (0): https://tiles.example.com/style.json)'); + expect(alert).toHaveTextContent('check the map style URL set on this view'); + expect(alert.textContent).not.toMatch(/administrator/); + }); + + it('an error event with no message still names a reason', async () => { + mount(); + await failStyle('simulate-bare-error'); + + expect(screen.getByRole('alert')).toHaveTextContent( + 'Map failed to load (the map style or tiles could not be loaded).', + ); + }); + + it('keeps the listed records searchable', async () => { + mount(); + await failStyle(); + + fireEvent.change(screen.getByPlaceholderText('Search locations…'), { target: { value: 'boiler' } }); + + expect(screen.getByRole('button', { name: /Replace boiler/ })).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: /Install rooftop unit/ })).toBeNull(); + }); + + it('a listed record takes the marker click path', async () => { + const onMarkerClick = vi.fn(); + const onRowClick = vi.fn(); + mount({ onMarkerClick, onRowClick }); + await failStyle(); + + fireEvent.click(screen.getByRole('button', { name: /Install rooftop unit/ })); + + expect(onMarkerClick).toHaveBeenCalledWith(records[0]); + expect(onRowClick).toHaveBeenCalledWith(records[0], undefined); + }); + + it('“Try the map again” mounts a fresh map with its markers', async () => { + mount(); + await failStyle(); + + fireEvent.click(screen.getByTestId('map-style-retry')); + + expect(await screen.findByLabelText('Map')).toBeInTheDocument(); + expect(screen.getAllByTestId('map-marker')).toHaveLength(2); + expect(screen.queryByTestId('map-style-fallback')).toBeNull(); + expect(screen.queryByRole('alert')).toBeNull(); + expect(lifecycle).toEqual({ mounts: 2, removes: 1 }); + }); + + it('unmounts cleanly after degrading: the removed map is not removed again', async () => { + const { unmount } = mount(); + await failStyle(); + + expect(() => unmount()).not.toThrow(); + expect(lifecycle).toEqual({ mounts: 1, removes: 1 }); + expect(caught).toEqual([]); + }); + + it('the excluded-records notice reads the bundle’s English default', async () => { + mount({ data: [...records, { id: 't3', title: 'No coordinates' }] }); + await screen.findByLabelText('Map'); + + await waitFor(() => + expect( + screen.getByText('Records excluded from the map for missing or invalid coordinates: 1'), + ).toBeInTheDocument(), + ); + }); +}); diff --git a/ui/packages/plugin-map/src/ObjectMap.styleFallback.test.tsx b/ui/packages/plugin-map/src/ObjectMap.styleFallback.test.tsx index 479c9b59cc3..d1251194119 100644 --- a/ui/packages/plugin-map/src/ObjectMap.styleFallback.test.tsx +++ b/ui/packages/plugin-map/src/ObjectMap.styleFallback.test.tsx @@ -3,8 +3,9 @@ * Copyright (c) 2024-present ObjectStack Inc. * * Regression: a failed MapLibre style/tile load used to render a blank map - * with no indication anything went wrong. `onError` now surfaces a banner, - * and a schema-configured `style` overrides the public demo default. + * with no indication anything went wrong. `onError` now surfaces an alert over + * the record list that replaces the map (objectstack#25889), and a + * schema-configured `style` overrides the public demo default. */ import React from 'react'; import { render, screen, waitFor, fireEvent } from '@testing-library/react'; diff --git a/ui/packages/plugin-map/src/ObjectMap.tsx b/ui/packages/plugin-map/src/ObjectMap.tsx index 1711052d510..c1b8fedeb40 100644 --- a/ui/packages/plugin-map/src/ObjectMap.tsx +++ b/ui/packages/plugin-map/src/ObjectMap.tsx @@ -30,6 +30,7 @@ import { useSettledSchema, useFilterScope, useResolvedFilter, + useObjectTranslation, } from '@object-ui/react'; import { NavigationOverlay, cn, useIsMobile } from '@object-ui/components'; import { usePermissions } from '@object-ui/permissions'; @@ -61,8 +62,8 @@ import { /** * Public MapLibre demo style — free, unauthenticated, but not intended for * production traffic (rate-limited, no uptime guarantee). Used only when no - * `style` is configured; a load failure surfaces the banner below rather - * than failing silently. + * `style` is configured; a load failure swaps the map for the record list + * below rather than leaving a grey canvas. */ const DEFAULT_MAP_STYLE = 'https://demotiles.maplibre.org/style.json'; @@ -679,17 +680,25 @@ export const ObjectMap: React.FC = ({ const [userLocation, setUserLocation] = useState<{ lng: number; lat: number } | null>(null); const [geoError, setGeoError] = useState(null); const [geoBusy, setGeoBusy] = useState(false); - // Style/tile load failures (bad network, unreachable style server) don't - // throw from react-map-gl — they emit an `error` event. Without this, a - // failed load renders a blank map with no indication anything went wrong. + const { t } = useObjectTranslation(); + // Style/tile load failures (bad network, unreachable style server, a tile + // host the deployment's network blocks) don't throw from react-map-gl — they + // emit an `error` event. The map left behind is a grey canvas: the markers + // float over nothing, so the view swaps it for the same record list a + // browser without WebGL2 gets (objectstack#25889). Holds MapLibre's own + // reason, `''` when it gave none; `null` while the map is healthy. Kept + // across refetches — the same style would fail again — until the user asks + // for the map back. const [mapStyleError, setMapStyleError] = useState(null); const handleMapError = useCallback((e: { error?: Error & { status?: number } }) => { - setMapStyleError(e?.error?.message || 'Failed to load the map style/tiles.'); + setMapStyleError(e?.error?.message ?? ''); }, []); + const retryMap = useCallback(() => setMapStyleError(null), []); // Asked once per mount, before `MapGL` is ever mounted: a browser without - // WebGL2 gets the record list instead of a map (see `canStartMap`). Not a - // style/tile failure — that one keeps the map and its markers, above. + // WebGL2 gets the record list instead of a map (see `canStartMap`). There is + // no retry for that one — the browser will not grow WebGL2 by asking again. const [mapCanStart] = useState(canStartMap); + const showRecordList = !mapCanStart || mapStyleError !== null; const requestUserLocation = useCallback(() => { if (typeof navigator === 'undefined' || !navigator.geolocation) { setGeoError('Geolocation is not available in this browser.'); @@ -1329,10 +1338,10 @@ export const ObjectMap: React.FC = ({ * `map.` because the declared block is the authoring surface — the flat * top-level spelling this component also reads is the internal transport form * ObjectView / ListView produce (see `FlatMapConfigKeys`), which no author - * writes. ⛔ Not translated: `@object-ui/plugin-map` carries no i18n - * dependency and every user-facing string in this file is a literal, the same - * shape as the "Calendar/Gantt configuration required" refusals in the - * sibling plugins. + * writes. ⛔ Not translated: it is an authoring diagnostic whose words are + * metadata keys, the same shape as the "Calendar/Gantt configuration + * required" refusals in the sibling plugins. The notices a VIEWER reads — + * the excluded-records count, the map-failed alerts — go through the bundle. */ if (!hasCoordinateBinding(mapConfig)) { return ( @@ -1372,11 +1381,38 @@ export const ObjectMap: React.FC = ({ ); } + // Why the record list stands where the map should be. No deployment-wide map + // style setting exists, so neither style remedy promises one: a configured + // style is this view's own to check, and the public default is for an + // administrator to replace with a style set on this view. + const styleFailureReason = + mapStyleError || + t('element.map.styleFailedUnknownReason', { defaultValue: 'the map style or tiles could not be loaded' }); + const recordListNotice = !mapCanStart + ? t('element.map.webgl2Unavailable', { + defaultValue: + 'Map failed to load (this browser does not provide WebGL2, which the map needs). The records are listed below; turn on hardware acceleration or use a browser with WebGL2 to see the map.', + }) + : mapConfig.style + ? t('element.map.styleFailedConfigured', { + reason: styleFailureReason, + defaultValue: + 'Map failed to load ({{reason}}). The records are listed below; check the map style URL set on this view.', + }) + : t('element.map.styleFailedDefault', { + reason: styleFailureReason, + defaultValue: + 'Map failed to load ({{reason}}). The records are listed below; the public default map tiles could not be loaded here, so ask your administrator to set a map style for this view.', + }); + return (
{invalidCount > 0 && (
- {`${invalidCount} record${invalidCount !== 1 ? 's' : ''} with missing or invalid coordinates excluded from the map.`} + {t('element.map.invalidCoordinatesExcluded', { + excluded: invalidCount, + defaultValue: 'Records excluded from the map for missing or invalid coordinates: {{excluded}}', + })}
)} {markers.length > 0 && ( @@ -1391,22 +1427,41 @@ export const ObjectMap: React.FC = ({
)}
- {!mapCanStart ? ( - // No WebGL2: `MapGL` is never mounted (see `canStartMap`), so the - // records the markers would draw are listed here instead — the same + {showRecordList ? ( + // No map to draw on — no WebGL2 (`MapGL` is never mounted, see + // `canStartMap`), or a style/tile load failed (`MapGL` is unmounted, + // which runs its `map.remove()` on a map that did start). The + // records the markers would draw are listed here instead: the same // `filteredMarkers` the search box above narrows, each row taking // the marker's own click path. -
+
- Map failed to load (this browser does not provide WebGL2, which the map needs). The records are - listed below; turn on hardware acceleration or use a browser with WebGL2 to see the map. + {recordListNotice} + {mapCanStart && ( + + )}
-
    +
      {filteredMarkers.length === 0 && ( -
    • No locations to list.
    • +
    • + {t('element.map.listEmpty', { defaultValue: 'No locations to list.' })} +
    • )} {filteredMarkers.map((marker) => (
    • @@ -1440,15 +1495,6 @@ export const ObjectMap: React.FC = ({ onZoom={(e) => setCurrentZoom(Math.round(e.viewState.zoom))} onError={handleMapError} > - {mapStyleError && ( -
      - Map failed to load ({mapStyleError}). Markers below are still listed in the search box;{' '} - {mapConfig.style ? 'check the configured map style URL.' : 'configure a custom map style for production use.'} -
      - )} {/* Geolocation button — explicit user-initiated location request */} diff --git a/ui/packages/plugin-map/src/ObjectMap.webgl2Controls-11819.test.tsx b/ui/packages/plugin-map/src/ObjectMap.webgl2Controls-11819.test.tsx index dfdd46180ba..7cba1b61472 100644 --- a/ui/packages/plugin-map/src/ObjectMap.webgl2Controls-11819.test.tsx +++ b/ui/packages/plugin-map/src/ObjectMap.webgl2Controls-11819.test.tsx @@ -12,8 +12,10 @@ * * - A browser that answers the probe mounts the map, markers and all, and * shows no fallback list. - * - A style or tile failure is a different, non-fatal case: the map keeps - * running, so it keeps its markers and gains the alert over them. + * - A style or tile failure is a different case: the map did start, so it + * is removed and the record list takes its place with a retry, under its + * own test id (objectstack#25889; the full battery is + * `ObjectMap.styleFailureList-25889.test.tsx`). * - The probe is asked once per mount and hands its context straight back. * * The map is the stub the sibling tests use; `./__tests__/webgl2Available` @@ -62,16 +64,18 @@ describe('ObjectMap with WebGL2 — what the probe leaves alone (objectui#11819) expect(screen.queryByRole('alert')).toBeNull(); }); - it('a style or tile failure keeps the map and its markers, with the alert over them', async () => { + it('a style or tile failure swaps the map for the record list, not the WebGL2 one', async () => { render(); await screen.findByLabelText('Map'); fireEvent.click(screen.getByTestId('simulate-style-error')); expect(await screen.findByRole('alert')).toHaveTextContent('Map failed to load (Failed to fetch)'); - expect(screen.getByLabelText('Map')).toBeInTheDocument(); - expect(screen.getAllByTestId('map-marker')).toHaveLength(2); + expect(screen.queryByLabelText('Map')).toBeNull(); + expect(screen.queryAllByTestId('map-marker')).toHaveLength(0); + expect(screen.getByTestId('map-style-fallback')).toHaveTextContent('Replace boiler'); expect(screen.queryByTestId('map-webgl2-fallback')).toBeNull(); + expect(screen.getByTestId('map-style-retry')).toBeInTheDocument(); }); it('asks for WebGL2 once per mount and releases the probe context', async () => { diff --git a/ui/packages/plugin-map/src/ObjectMap.webgl2Fallback-11819.test.tsx b/ui/packages/plugin-map/src/ObjectMap.webgl2Fallback-11819.test.tsx index ba0585f763a..1039dc5163b 100644 --- a/ui/packages/plugin-map/src/ObjectMap.webgl2Fallback-11819.test.tsx +++ b/ui/packages/plugin-map/src/ObjectMap.webgl2Fallback-11819.test.tsx @@ -110,6 +110,8 @@ describe('ObjectMap without WebGL2 (objectui#11819)', () => { expect(alert).toHaveTextContent('WebGL2'); expect(screen.getByRole('button', { name: /Install rooftop unit/ })).toHaveTextContent('North wing'); expect(screen.getByRole('button', { name: /Replace boiler/ })).toHaveTextContent('Basement'); + // No retry here: the browser will not grow WebGL2 by asking again. + expect(screen.queryByTestId('map-style-retry')).toBeNull(); }); it('keeps the records searchable: the search box narrows the list', async () => { From 5f0bd606a768a72852616e6a00fd55129eeb6bae Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 08:58:04 +0000 Subject: [PATCH 02/19] test(i18n): the map's record-list label leaves the aria-label literal backlog Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01HUu4jGnAzZqaB5qTG2fsX4 --- .../i18n/src/__tests__/ariaLabelLiteralCensus-12145.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/packages/i18n/src/__tests__/ariaLabelLiteralCensus-12145.test.ts b/ui/packages/i18n/src/__tests__/ariaLabelLiteralCensus-12145.test.ts index dc1a925e719..e9942242f24 100644 --- a/ui/packages/i18n/src/__tests__/ariaLabelLiteralCensus-12145.test.ts +++ b/ui/packages/i18n/src/__tests__/ariaLabelLiteralCensus-12145.test.ts @@ -282,7 +282,7 @@ const BACKLOG: Declared[] = ( ['packages/plugin-list/src/ListView.tsx', ['Bulk actions']], [ 'packages/plugin-map/src/ObjectMap.tsx', - ['Locations', 'Show my location', 'Your location', 'Location details', 'Close details'], + ['Show my location', 'Your location', 'Location details', 'Close details'], ], ] as Array<[string, string[]]> ).map(([file, names]) => ({ file, names, verdict: 'backlog' as const, reason: BACKLOG_REASON })); From cf9a965e96e6d7e64112dd1429488c663203b1a5 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 09:10:19 +0000 Subject: [PATCH 03/19] fix(components): a refused submit focuses the first invalid field instead of toasting the field list The invalid-submit toast repeated what each field already says under its input, truncated its own list after three names, and was drawn over the record dialog's title row. Drop it; keep reveal + focus on the first invalid field (activating its tab first). A server rejection naming a field with no drawn input (a predicate-hidden tab or section) now falls through to the form-level banner, which the toast used to cover. Claude-Session: https://claude.ai/code/session_01CzUs1oNtFWhWUXT7rmoGy2 Co-authored-by: Claude --- .../fieldtab-visiblewhen-6237.test.tsx | 37 ++++++++ .../__tests__/form-invalid-scroll.test.tsx | 27 +++--- .../form-invalid-toast-joiner.test.tsx | 24 +++-- .../form-server-field-errors.test.tsx | 13 ++- .../components/src/renderers/form/form.tsx | 91 +++++++++++-------- 5 files changed, 133 insertions(+), 59 deletions(-) diff --git a/ui/packages/components/src/renderers/form/__tests__/fieldtab-visiblewhen-6237.test.tsx b/ui/packages/components/src/renderers/form/__tests__/fieldtab-visiblewhen-6237.test.tsx index 12ec3fb18e0..ee725eac242 100644 --- a/ui/packages/components/src/renderers/form/__tests__/fieldtab-visiblewhen-6237.test.tsx +++ b/ui/packages/components/src/renderers/form/__tests__/fieldtab-visiblewhen-6237.test.tsx @@ -57,6 +57,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { render, screen, cleanup, fireEvent, waitFor } from '@testing-library/react'; import { ComponentRegistry } from '@object-ui/core'; import { PredicateScopeProvider } from '@object-ui/react'; +import { toast } from '../../../ui/sonner'; // Module-scope import (not `beforeAll`) — objectui#3010/#3021. import '../../../renderers'; @@ -208,6 +209,42 @@ describe('#6237 — a tab predicate gates the whole tab', () => { expect(document.body.textContent).not.toMatch(/required/i); }); + it('a SERVER rejection of a hidden tab\'s field is said at form level — no input can carry it (objectstack#25874)', async () => { + // The invalid-submit toast that used to name every rejected field is gone: + // a drawn field carries its own message. A field on a hidden tab has no + // drawn input, so its rejection must not be taken over as "marked inline" + // — the form-level banner says the server's sentence instead. + const toastError = vi.spyOn(toast, 'error').mockImplementation(() => 'id' as never); + const message = 'Salary is required'; + const fields = [{ field: 'salary', code: 'required', message }]; + const onSubmit = vi.fn().mockRejectedValue( + Object.assign(new Error(message), { + code: 'VALIDATION_FAILED', + httpStatus: 400, + details: { error: message, code: 'VALIDATION_FAILED', fields, object: 'crm_employee' }, + }), + ); + renderForm( + { + fields: FIELDS(), + fieldTabs: TABS(GATE), + showSubmit: true, + submitLabel: 'Save', + onSubmit, + }, + DENIED, + ); + expect(member(/salary/i)).toBeNull(); + fireEvent.click(submitBtn()); + + await waitFor(() => expect(screen.getByText(message)).toBeTruthy()); + expect(toastError).toHaveBeenCalledTimes(1); + expect(String(toastError.mock.calls[0][0])).toBe(message); + // The hidden tab is never activated to "reveal" a field it cannot draw. + expect(trigger()).toBeNull(); + expect(screen.getByRole('tab', { name: /basics/i })).toHaveAttribute('data-state', 'active'); + }); + it('control: the SAME required field on a VISIBLE (even inactive) tab still blocks the submit', async () => { const onSubmit = vi.fn(); renderForm( diff --git a/ui/packages/components/src/renderers/form/__tests__/form-invalid-scroll.test.tsx b/ui/packages/components/src/renderers/form/__tests__/form-invalid-scroll.test.tsx index a2a389187f9..29543054d3a 100644 --- a/ui/packages/components/src/renderers/form/__tests__/form-invalid-scroll.test.tsx +++ b/ui/packages/components/src/renderers/form/__tests__/form-invalid-scroll.test.tsx @@ -8,10 +8,15 @@ /** * #2793: submitting a form with a missing required field must give visible - * feedback — a toast naming the fields (already shipped in #2329) AND scroll - * the first errored field into view + focus it. Before this, react-hook-form's - * native focus-on-error silently no-opped for custom widgets (lookup / select / - * master-detail), so on a long form the user clicked 创建 and saw nothing. + * feedback — scroll the first errored field into view + focus it. Before this, + * react-hook-form's native focus-on-error silently no-opped for custom widgets + * (lookup / select / master-detail), so on a long form the user clicked 创建 + * and saw nothing. + * + * And NO toast (objectstack#25874). The toast that used to name the fields + * (#2329) repeated what each field already says under its input, truncated its + * own list after three names, and was drawn over the record dialog's title. The + * inline messages carry the detail; focus carries the "something is wrong". */ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; @@ -58,7 +63,7 @@ function renderForm(fields: any[], defaultValues?: Record) { const submit = () => fireEvent.click(screen.getByRole('button', { name: /create/i })); describe('form renderer — invalid-submit feedback (#2793)', () => { - it('toasts and scrolls+focuses the first errored field on a missing required field', async () => { + it('scrolls+focuses the first errored field on a missing required field, with no toast', async () => { const { container } = renderForm([ { name: 'title', label: 'Title', type: 'input', required: true }, { name: 'project', label: 'Project', type: 'input', required: true }, @@ -70,14 +75,15 @@ describe('form renderer — invalid-submit feedback (#2793)', () => { submit(); - await waitFor(() => expect(toastErrorSpy).toHaveBeenCalled()); - // Names the missing field(s). - expect(String(toastErrorSpy.mock.calls[0][0])).toMatch(/Title/); // First errored field (visual order) scrolled into view + focused. - expect(scrollSpy).toHaveBeenCalled(); + await waitFor(() => expect(scrollSpy).toHaveBeenCalled()); await waitFor(() => expect(titleWrap.contains(document.activeElement)).toBe(true), ); + // The per-field message is the only error text: it is under the input… + expect(titleWrap.textContent).toMatch(/required/i); + // …and no toast repeats it over the dialog. + expect(toastErrorSpy).not.toHaveBeenCalled(); }); it('scrolls the first errored field even when an earlier field is valid', async () => { @@ -98,9 +104,8 @@ describe('form renderer — invalid-submit feedback (#2793)', () => { submit(); - await waitFor(() => expect(toastErrorSpy).toHaveBeenCalled()); // Only `project` is invalid → it, not the valid `title`, is scrolled to. - expect(projectScroll).toHaveBeenCalled(); + await waitFor(() => expect(projectScroll).toHaveBeenCalled()); expect(titleScroll).not.toHaveBeenCalled(); }); diff --git a/ui/packages/components/src/renderers/form/__tests__/form-invalid-toast-joiner.test.tsx b/ui/packages/components/src/renderers/form/__tests__/form-invalid-toast-joiner.test.tsx index 14dcd8f5b97..7be6e840689 100644 --- a/ui/packages/components/src/renderers/form/__tests__/form-invalid-toast-joiner.test.tsx +++ b/ui/packages/components/src/renderers/form/__tests__/form-invalid-toast-joiner.test.tsx @@ -7,9 +7,14 @@ */ /** - * The invalid-submit toast joins the field names with a PER-LOCALE separator — + * A refused submit joins the field names with a PER-LOCALE separator — * objectstack#5407. * + * The list was first built for the invalid-submit toast, which is gone + * (objectstack#25874: each field's own message carries the detail). The form's + * remaining field-name list is the faulted-`visibleWhen` refusal, which reads + * the same `validation.formInvalidJoiner` entry, so the pin moved there. + * * `announceFieldErrors` used to build the list with a hardcoded `、` (U+3001). * That is the CJK enumeration comma: correct for zh/ja by accident, wrong in * every Latin locale, and most visibly wrong in English, where the toast read @@ -42,6 +47,8 @@ let toastErrorSpy: ReturnType; beforeEach(() => { toastErrorSpy = vi.spyOn(toast, 'error').mockImplementation(() => 'id' as any); + // The faulted predicates are reported once each; not what this file asserts. + vi.spyOn(console, 'warn').mockImplementation(() => {}); if (!(Element.prototype as any).scrollIntoView) { (Element.prototype as any).scrollIntoView = () => {}; } @@ -52,11 +59,14 @@ afterEach(() => { vi.restoreAllMocks(); }); -/** Three required fields, so the joiner appears twice and truncation does not. */ +/** A predicate the engine cannot parse: a fault whatever the record holds. */ +const BROKEN = 'record.stage =='; + +/** Three faulted fields, so the joiner appears twice. */ const fields = [ - { name: 'subject', label: 'Subject', type: 'input', required: true }, - { name: 'account', label: 'Account', type: 'input', required: true }, - { name: 'status', label: 'Status', type: 'input', required: true }, + { name: 'subject', label: 'Subject', type: 'input', visibleWhen: BROKEN }, + { name: 'account', label: 'Account', type: 'input', visibleWhen: BROKEN }, + { name: 'status', label: 'Status', type: 'input', visibleWhen: BROKEN }, ]; function renderFormIn(language: string) { @@ -84,7 +94,7 @@ async function toastTextAfterSubmit(language: string): Promise { return String(toastErrorSpy.mock.calls[0][0]); } -describe('form renderer — invalid-submit toast list joiner (objectstack#5407)', () => { +describe('form renderer — refused-submit field list joiner (objectstack#5407)', () => { it('joins with a comma+space under an en session, never the CJK comma', async () => { const text = await toastTextAfterSubmit('en'); @@ -100,6 +110,6 @@ describe('form renderer — invalid-submit toast list joiner (objectstack#5407)' expect(text).toContain('Subject、Account、Status'); // zh's sentence, to prove the joiner is being read from the zh pack and not // from an `en` fallback that happens to carry the same punctuation. - expect(text).toContain('请检查表单中标记的字段'); + expect(text).toContain('无法提交'); }); }); diff --git a/ui/packages/components/src/renderers/form/__tests__/form-server-field-errors.test.tsx b/ui/packages/components/src/renderers/form/__tests__/form-server-field-errors.test.tsx index 9b8a93cf043..d8adaca3733 100644 --- a/ui/packages/components/src/renderers/form/__tests__/form-server-field-errors.test.tsx +++ b/ui/packages/components/src/renderers/form/__tests__/form-server-field-errors.test.tsx @@ -104,17 +104,20 @@ describe('form renderer — server-rejected fields', () => { await waitFor(() => expect(stageWrap.contains(document.activeElement)).toBe(true)); }); - it('names the rejected fields in the toast', async () => { + it('raises no toast when every rejected field carries its own message (objectstack#25874)', async () => { const onSubmit = vi.fn().mockRejectedValue( validationFailure([{ field: 'title', message: 'Title is required' }]), ); - renderForm(FIELDS, onSubmit); + const { container } = renderForm(FIELDS, onSubmit); + const titleWrap = container.querySelector('[data-field="title"]') as HTMLElement; submit(); - await waitFor(() => expect(toastErrorSpy).toHaveBeenCalled()); - // The field LABEL, not its machine name. - expect(String(toastErrorSpy.mock.calls[0][0])).toMatch(/Title/); + // The reason is written under the input it belongs to… + await waitFor(() => expect(titleWrap.textContent).toMatch(/Title is required/)); + // …so a toast naming the same field, drawn over the dialog's title, would + // only repeat it. + expect(toastErrorSpy).not.toHaveBeenCalled(); }); it('marks every rejected field, not just the first', async () => { diff --git a/ui/packages/components/src/renderers/form/form.tsx b/ui/packages/components/src/renderers/form/form.tsx index 6eb65585ed5..c9e451d839f 100644 --- a/ui/packages/components/src/renderers/form/form.tsx +++ b/ui/packages/components/src/renderers/form/form.tsx @@ -2843,13 +2843,16 @@ ComponentRegistry.register('form', * carries `data-field` (FormItem), so this reaches custom widgets that RHF's * own focus cannot (#2793). */ - const revealField = (name: string) => { + const fieldWrapper = (name: string): HTMLElement | null => { const root: ParentNode = formRef.current ?? document; const escaped = typeof CSS !== 'undefined' && typeof CSS.escape === 'function' ? CSS.escape(name) : name.replace(/["\\]/g, '\\$&'); - const target = root.querySelector(`[data-field="${escaped}"]`); + return root.querySelector(`[data-field="${escaped}"]`); + }; + const revealField = (name: string) => { + const target = fieldWrapper(name); if (target) { target.scrollIntoView?.({ behavior: 'smooth', block: 'center' }); const focusable = target.querySelector( @@ -2860,54 +2863,64 @@ ComponentRegistry.register('form', }; /** - * Tell the user WHICH fields are wrong, wherever the verdict came from. + * Point the user at the fields that are wrong, wherever the verdict came + * from: reveal and focus the FIRST of them (in declared/visual order, not + * the caller's key order), activating its tab first when it sits on a + * background one. + * + * No toast. Every field this reaches carries its own message under the + * input, so a toast naming the same fields only repeated them — truncated + * after three names, and drawn over the record dialog's title row, which it + * hid. Moving focus to the first offender is what makes a refused submit + * visible on a long form; the inline messages say the rest. * - * Toast naming the offending fields, then reveal the FIRST of them (in - * declared/visual order, not the caller's key order) — activating its tab - * first when it sits on a background one. + * Returns the rejected names that have NO drawn input — a field on a + * predicate-hidden tab (#6237) or section, or hidden by its own rule. Only + * the SERVER can reject those (their client rules are skipped with them), + * and nothing inline can carry the message, so the caller must say it at + * form level instead. * * Shared by the client-side invalid handler and the SERVER-rejection path: * the two failures are the same event as far as the person filling the form * is concerned; only the referee differs. */ - const announceFieldErrors = (names: string[]) => { - if (names.length === 0) return; + const announceFieldErrors = (names: string[]): string[] => { + if (names.length === 0) return []; setRejectedFieldNames(names); - const labels = names.map((n) => fieldLabelByName[n] || n); - const MAX = 3; - const fieldsText = - labels.slice(0, MAX).join(t('validation.formInvalidJoiner')) + - (labels.length > MAX ? '…' : ''); - toast.error(t('validation.formInvalid', { fields: fieldsText }), { id: outcomeToastId }); - const errored = new Set(names); - const firstName = - (fields as FormFieldConfig[]) + const ordered = [ + ...(fields as FormFieldConfig[]) .map((f) => f?.name) - .find((n): n is string => Boolean(n) && errored.has(n as string)) ?? names[0]; + .filter((n): n is string => Boolean(n) && errored.has(n as string)), + ...names.filter((n) => !(fields as FormFieldConfig[]).some((f) => f?.name === n)), + ]; + // A field is drawn when its wrapper is in the DOM. A background tab's + // panel stays mounted (CSS-hidden), so its fields count as drawn and are + // reached below by activating the tab; a predicate-hidden tab's panel is + // not rendered at all, so its fields do not. + const undrawn = ordered.filter((n) => !fieldWrapper(n)); + const firstName = ordered.find((n) => !undrawn.includes(n)); + if (!firstName) return undrawn; // In a tabbed layout the offending field may sit on a tab the user is not - // looking at — naming it is useless if they can't see it. Activate its tab - // first, then reveal it once that panel has actually been painted (it is - // display:none until then, so scroll/focus would no-op). - // - // Unless the tab is predicate-HIDDEN (#6237): only the SERVER can reject - // a field there (its client rules are skipped with it), and there is no - // panel to activate or control to reveal — the toast above already names - // the field. Recording the activation as a pick anyway would yank the - // view to that tab whenever its predicate later re-admits it. + // looking at — the message under it is useless if they can't see it. + // Activate its tab first, then reveal it once that panel has actually + // been painted (it is display:none until then, so scroll/focus would + // no-op). A predicate-HIDDEN tab is never activated: recording that as a + // pick would yank the view to it whenever its predicate later re-admits + // it, and a drawn field it also claims is rendered in a visible panel. const tabKey = tabKeyByFieldName.get(firstName); - if (tabKey && hiddenFieldTabKeys.has(tabKey)) return; - if (tabKey && tabKey !== activeFieldTab) { + if (tabKey && !hiddenFieldTabKeys.has(tabKey) && tabKey !== activeFieldTab) { setPickedFieldTab(tabKey); if (typeof requestAnimationFrame === 'function') { requestAnimationFrame(() => revealField(firstName)); } else { setTimeout(() => revealField(firstName), 0); } - return; + return undrawn; } revealField(firstName); + return undrawn; }; // Handle form submission — react-hook-form's validation, then the write. @@ -3008,11 +3021,14 @@ ComponentRegistry.register('form', // this form does not render, fall through: the top-level message // concatenates every field's reason, so the part the user cannot see // inline is still said out loud instead of silently dropped. + // The same holds for a rendered-schema field with no DRAWN input + // (a predicate-hidden tab or section): `announceFieldErrors` names + // those, and any one of them sends the failure down the same path. if (known.length > 0 && known.length === fieldErrors.length) { - announceFieldErrors(known.map((fe) => fe.field)); + const undrawn = announceFieldErrors(known.map((fe) => fe.field)); // A fully-attributed rejection is not a form-level failure — the // banner would just repeat what is now written under each input. - return; + if (undrawn.length === 0) return; } } // A rejected write used to be rendered verbatim, which put raw server @@ -3055,8 +3071,10 @@ ComponentRegistry.register('form', // Client-side (react-hook-form) validation blocked the submit. The // per-field errors render inline, but in long forms the offending field // is often scrolled out of view — the user clicks 创建 and sees nothing - // happen. Surface a toast naming the fields so the feedback is visible - // regardless of scroll position (mirrors the server-error toast above). + // happen. Move focus to the first offender so the feedback is visible + // regardless of scroll position. Every name here has a drawn input: + // react-hook-form validates only mounted controls, and an undrawn field + // is unmounted, so the returned list is always empty on this path. announceFieldErrors(Object.keys(validationErrors || {})); }); @@ -3458,8 +3476,9 @@ ComponentRegistry.register('form', // parents still read two ways (objectui#4026). The separator is a // property of the locale, so all of them now read // `validation.formInvalidJoiner`: the key objectstack#5407 added for the - // invalid-submit toast a few hundred lines up, which is the same class - // of truncated-name list, rather than a second gate-only twin that would + // invalid-submit field list (that toast is gone; the `visibleWhen` + // refusal a few hundred lines up still joins with it), which is the same + // class of name list, rather than a second gate-only twin that would // reintroduce the divergence. const gatedHint = optionGroupGated ? t('fields.options.selectFirst', { From f4d90cad07453c1dc96c57247356fc577b3795ee Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 09:16:16 +0000 Subject: [PATCH 04/19] chore(sdui): re-record the manifest record for the plugin-map source change Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01HUu4jGnAzZqaB5qTG2fsX4 --- scripts/sdui-manifest.record.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/sdui-manifest.record.json b/scripts/sdui-manifest.record.json index 1555759c7e3..86207e4e9a6 100644 --- a/scripts/sdui-manifest.record.json +++ b/scripts/sdui-manifest.record.json @@ -49,6 +49,6 @@ "ui/packages/types" ], "registrySources": 922, - "registrySourcesSha256": "ad9e0875b83f339e03c528fc3be774592615dfdc5ee5fe9becc4376ddb5f0d52" + "registrySourcesSha256": "9cf3c26e17780d508ecd40e2923c32263b2a3c1111c2974c6ae1abfcb62c47ad" } } From ab645af6f7c55afdb68520683f55755213684113 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 09:18:48 +0000 Subject: [PATCH 05/19] fix(components): a client-side refusal retires the previous outcome banner and toast Claude-Session: https://claude.ai/code/session_01CzUs1oNtFWhWUXT7rmoGy2 Co-authored-by: Claude --- .../form-server-field-errors.test.tsx | 28 +++++++++++++++++++ .../visiblewhen-clear-notice-8070.test.tsx | 18 +++++++----- .../components/src/renderers/form/form.tsx | 8 ++++++ 3 files changed, 47 insertions(+), 7 deletions(-) diff --git a/ui/packages/components/src/renderers/form/__tests__/form-server-field-errors.test.tsx b/ui/packages/components/src/renderers/form/__tests__/form-server-field-errors.test.tsx index d8adaca3733..db8fdca41f1 100644 --- a/ui/packages/components/src/renderers/form/__tests__/form-server-field-errors.test.tsx +++ b/ui/packages/components/src/renderers/form/__tests__/form-server-field-errors.test.tsx @@ -153,6 +153,34 @@ describe('form renderer — server-rejected fields', () => { expect(String(toastErrorSpy.mock.calls[0][0])).toMatch(/Owner is required/); }); + it('a later client-side refusal retires the previous refusal`s banner and toast (objectstack#25874)', async () => { + // The client refusal raises no toast of its own any more, so nothing would + // supersede the earlier server refusal under the shared outcome id: the + // attempt has to take the stale banner and toast down itself. + const dismissSpy = vi.spyOn(toast, 'dismiss').mockImplementation(() => 'id' as any); + const forbidden = Object.assign(new Error('insufficient privileges'), { + code: 'FORBIDDEN', + httpStatus: 403, + details: { error: 'insufficient privileges', code: 'FORBIDDEN' }, + }); + const onSubmit = vi.fn().mockRejectedValue(forbidden); + vi.spyOn(console, 'warn').mockImplementation(() => {}); + renderForm([{ name: 'title', label: 'Title', type: 'input', required: true }], onSubmit); + const input = screen.getByLabelText(/title/i); + + fireEvent.change(input, { target: { value: 'Filled' } }); + submit(); + await waitFor(() => expect(screen.getByText(/permission to save/i)).toBeTruthy()); + + dismissSpy.mockClear(); + fireEvent.change(input, { target: { value: '' } }); + submit(); + + await waitFor(() => expect(screen.queryByText(/permission to save/i)).toBeNull()); + expect(dismissSpy).toHaveBeenCalled(); + expect(onSubmit).toHaveBeenCalledTimes(1); + }); + it('leaves a non-field failure on the generic path', async () => { const forbidden = Object.assign(new Error('insufficient privileges'), { code: 'FORBIDDEN', diff --git a/ui/packages/components/src/renderers/form/__tests__/visiblewhen-clear-notice-8070.test.tsx b/ui/packages/components/src/renderers/form/__tests__/visiblewhen-clear-notice-8070.test.tsx index ea2f2d71613..e8518dcb672 100644 --- a/ui/packages/components/src/renderers/form/__tests__/visiblewhen-clear-notice-8070.test.tsx +++ b/ui/packages/components/src/renderers/form/__tests__/visiblewhen-clear-notice-8070.test.tsx @@ -22,7 +22,9 @@ * the ruling's default — one form-level notice under the form's existing * outcome-toast id, naming every cleared column at once, so it retires like the * form's other outcome messages: a later submit outcome supersedes it, and the - * dismissal at the next attempt takes it down. + * dismissal at the next attempt takes it down. A client-side refusal raises no + * toast of its own (objectstack#25874 — each field's message carries it), so + * that attempt dismisses the id instead of superseding it. * * Only a field that HELD something is named: an already-empty value, an empty * list and an unchecked two-state control (whose empty state is `false`) are @@ -199,7 +201,7 @@ describe('#8070 — a visibleWhen clear is named to the user', () => { expect(notices()[1]).not.toContain('Contact'); }); - it('a later submit refusal supersedes the notice, and the next accepted attempt retires it — one id throughout', async () => { + it('a later submit attempt retires the notice, refused or accepted — one id throughout', async () => { const onSubmit = vi.fn(); renderForm({ fields: [{ name: 'subject', label: 'Subject', type: 'input', required: true }, ...FIELDS.slice(0, 2)], @@ -211,13 +213,15 @@ describe('#8070 — a visibleWhen clear is named to the user', () => { await waitFor(() => expect(warningSpy).toHaveBeenCalledTimes(1)); const noticeId = (warningSpy.mock.calls[0][1] as { id: string }).id; - // Refused on the client: the refusal is published under the SAME id, which - // sonner treats as an update of the notice rather than a second toast. + // Refused on the client: the refusal is the inline message under Subject, + // and the attempt dismisses the notice's id rather than leaving it up. save(); - await waitFor(() => expect(errorSpy).toHaveBeenCalledTimes(1)); - expect(errorSpy.mock.calls[0][1]).toMatchObject({ id: noticeId }); + await waitFor(() => expect(dismissSpy).toHaveBeenCalledWith(noticeId)); + expect(errorSpy).not.toHaveBeenCalled(); + expect(onSubmit).not.toHaveBeenCalled(); - // Accepted on the client: the attempt starts by dismissing that id. + // Accepted on the client: the attempt starts by dismissing that id too. + dismissSpy.mockClear(); fireEvent.change(screen.getByLabelText(/subject/i), { target: { value: 'Kickoff' } }); save(); await waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1)); diff --git a/ui/packages/components/src/renderers/form/form.tsx b/ui/packages/components/src/renderers/form/form.tsx index c9e451d839f..4c8551d8930 100644 --- a/ui/packages/components/src/renderers/form/form.tsx +++ b/ui/packages/components/src/renderers/form/form.tsx @@ -3075,6 +3075,14 @@ ComponentRegistry.register('form', // regardless of scroll position. Every name here has a drawn input: // react-hook-form validates only mounted controls, and an undrawn field // is unmounted, so the returned list is always empty on this path. + // + // This attempt's outcome is those inline messages, so whatever outcome + // the form showed before it — the previous attempt's banner and its toast + // twin, or a clear-on-hide notice (objectui#8070) — retires here, as it + // does at the start of an attempt that clears validation. The refusal + // toast used to do that by superseding the same id. + setSubmitError(null); + toast.dismiss(outcomeToastId); announceFieldErrors(Object.keys(validationErrors || {})); }); From 214f11de8895fdb9c5a2ae429683174b84c4019f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 09:23:29 +0000 Subject: [PATCH 06/19] fix(app-shell,console): greet a signed-out invitee for the invitation and name the inviter on the accept page A visitor who opens an invitation link signed out lands on /login with ?redirect=/accept-invitation/ID. Both login pages now title the card for the invitation instead of "Welcome back", and where sign-up is offered show a full-width "Create an account and join" link above the sign-in form in place of the small "Sign up" link. Both register pages ask for the invited address. The accept page shows "Invited by" with the inviterEmail that get-invitation already returns. No organization, inviter or invited address is shown signed out: the only invitation read is session-gated (401 signed out), so that part needs a server read and is left out. Claude-Session: https://claude.ai/code/session_016msqupguN18F4BUKKWYbWY Co-authored-by: Claude --- .changeset/25886-invitation-arrival-copy.md | 16 +++ ui/apps/console/src/pages/auth/LoginPage.tsx | 32 +++-- .../console/src/pages/auth/RegisterPage.tsx | 21 +++- .../signUpFollowsPosture-11691.test.tsx | 26 +++- .../app-shell/src/console/auth/LoginPage.tsx | 36 +++++- .../src/console/auth/RegisterPage.tsx | 10 +- .../defaultPagesFollowPosture-11705.test.tsx | 19 ++- .../src/console/auth/invitationArrival.tsx | 61 ++++++++++ .../acceptInvitationInviter-25886.test.tsx | 111 ++++++++++++++++++ .../manage/AcceptInvitationPage.tsx | 16 +++ ui/packages/app-shell/src/index.ts | 3 + ui/packages/i18n/src/locales/ar.ts | 7 ++ ui/packages/i18n/src/locales/de.ts | 7 ++ ui/packages/i18n/src/locales/en.ts | 11 ++ ui/packages/i18n/src/locales/es.ts | 7 ++ ui/packages/i18n/src/locales/fr.ts | 7 ++ ui/packages/i18n/src/locales/ja.ts | 7 ++ ui/packages/i18n/src/locales/ko.ts | 7 ++ ui/packages/i18n/src/locales/pt.ts | 7 ++ ui/packages/i18n/src/locales/ru.ts | 7 ++ ui/packages/i18n/src/locales/zh.ts | 7 ++ 21 files changed, 403 insertions(+), 22 deletions(-) create mode 100644 .changeset/25886-invitation-arrival-copy.md create mode 100644 ui/packages/app-shell/src/console/auth/invitationArrival.tsx create mode 100644 ui/packages/app-shell/src/console/organizations/__tests__/acceptInvitationInviter-25886.test.tsx diff --git a/.changeset/25886-invitation-arrival-copy.md b/.changeset/25886-invitation-arrival-copy.md new file mode 100644 index 00000000000..e7762cb479b --- /dev/null +++ b/.changeset/25886-invitation-arrival-copy.md @@ -0,0 +1,16 @@ +--- +'@object-ui/app-shell': patch +'@object-ui/console': patch +'@object-ui/i18n': patch +--- + +fix(app-shell,console): an invitee who opens the invitation link signed out is greeted for the invitation, offered "Create an account and join" first, and the accept page names the inviter + +Clause-②: no +Metadata-protocol: none + +- **Login page.** A signed-out visitor arriving at `/login?redirect=/accept-invitation/ID` used to see the returning-user card ("Welcome back · Sign in to continue your work") with a small "Sign up" link under the form. Both login pages (the console's own and `DefaultLoginPage`) now title the card "Accept your invitation · Use the email address the invitation was sent to." Where the server offers sign-up to this visitor, a full-width "Create an account and join" link (the new `InvitationSignUpPrompt` export) sits above the sign-in form in place of the small link. With sign-up switched off, the visitor gets the invitation header and the sign-in form only. +- **Register page.** From an invitation redirect, both register pages title the form "Create an account to join" and ask for the address the invitation was sent to, because only that account can accept it. +- **Accept page.** `DefaultAcceptInvitationPage` shows an "Invited by" row with the inviting member's email, which `GET /organization/get-invitation` already returns as `inviterEmail`. That route returns no inviter name. +- **Not in this change.** The signed-out pages name no organization, inviter or invited address, and do not prefill the email. The only invitation read the server serves answers a signed-out caller `401`, so a preview for signed-out visitors needs a new server read first. +- New locale keys in all ten packs: `auth.login.invitationTitle`, `auth.login.invitationDescription`, `auth.login.invitationSignUpButton`, `auth.login.invitationSignInDivider`, `auth.register.invitationTitle`, `auth.register.invitationDescription`, `organization.accept.invitedBy`. diff --git a/ui/apps/console/src/pages/auth/LoginPage.tsx b/ui/apps/console/src/pages/auth/LoginPage.tsx index 98238560ed8..373bb325b30 100644 --- a/ui/apps/console/src/pages/auth/LoginPage.tsx +++ b/ui/apps/console/src/pages/auth/LoginPage.tsx @@ -26,6 +26,12 @@ * (objectui#11806). The sign-in request goes to the same server, so a * live-looking form would only have deferred that news to the submit. * + * A visitor who arrived signed out from an invitation link + * (`?redirect=/accept-invitation/ID`) is greeted for that invitation instead + * of as a returning user, and, where sign-up is offered, gets app-shell's + * `InvitationSignUpPrompt` above the form in place of the small "Sign up" + * link — the same prompt `DefaultLoginPage` shows (#25886). + * * Layout (objectui#12600): one 400 px column — brand mark, the card, a footer. * The card carries this page's own two-line header, so `` renders * with `hideHeader`. On a dev instance the card's last row offers a one-click @@ -54,6 +60,7 @@ import { needsBootstrapProbe, useBootstrapStatus, LocaleSwitcher, + InvitationSignUpPrompt, } from '@object-ui/app-shell'; import { AuthLayout } from './AuthLayout'; import { followOauthAuthorize } from './followAuthorize'; @@ -304,6 +311,7 @@ export function LoginPage() { ? `/register?redirect=${encodeURIComponent(redirect)}` : '/register'; const legalLinks = readLegalLinks(authConfig); + const offerSignUp = configRead === 'known' && signUpOffer === 'form'; return ( @@ -345,18 +353,28 @@ export function LoginPage() { <>

      - {t('auth.login.welcomeTitle', { defaultValue: 'Welcome back' })} + {invitationRedirect + ? t('auth.login.invitationTitle', { defaultValue: 'Accept your invitation' }) + : t('auth.login.welcomeTitle', { defaultValue: 'Welcome back' })}

      - {t('auth.login.welcomeDescription', { - defaultValue: 'Sign in to continue your work', - })} + {invitationRedirect + ? t('auth.login.invitationDescription', { + defaultValue: 'Use the email address the invitation was sent to.', + }) + : t('auth.login.welcomeDescription', { + defaultValue: 'Sign in to continue your work', + })}

      + {invitationRedirect && offerSignUp ? ( +
      + +
      + ) : null} {devSeedAdmin ? : null} diff --git a/ui/apps/console/src/pages/auth/RegisterPage.tsx b/ui/apps/console/src/pages/auth/RegisterPage.tsx index 1d55ad0e912..c0894dfc8bc 100644 --- a/ui/apps/console/src/pages/auth/RegisterPage.tsx +++ b/ui/apps/console/src/pages/auth/RegisterPage.tsx @@ -257,10 +257,23 @@ export function RegisterPage() { { + it('under invite_only, still offers sign-up to an invitation redirect, carrying the redirect', async () => { renderAt(`/login${INVITE_QUERY}`, configFor('invite_only')); await loginConfigApplied(); - expect(screen.getByRole('link', SIGN_UP_LINK).getAttribute('href')).toBe(`/register${INVITE_QUERY}`); + // #25886 — greeted for the invitation, not as a returning user, with the + // register-first prompt in place of the small link, never both. + expect( + screen.getByRole('heading', { name: builtInLocales.en.auth.login.invitationTitle }), + ).toBeInTheDocument(); + expect( + screen.queryByRole('heading', { name: builtInLocales.en.auth.login.welcomeTitle }), + ).toBeNull(); + expect(screen.getByRole('link', INVITE_SIGN_UP).getAttribute('href')).toBe(`/register${INVITE_QUERY}`); + expect(screen.queryByRole('link', SIGN_UP_LINK)).toBeNull(); expect(wire.bootstrapProbes).toBe(0); }); @@ -202,6 +213,11 @@ describe('LoginPage — the "Sign up" link follows the audience posture (objectu await loginConfigApplied(); expect(screen.queryByRole('link', SIGN_UP_LINK)).toBeNull(); + // #25886 — still greeted for the invitation, but offered no registration. + expect( + screen.getByRole('heading', { name: builtInLocales.en.auth.login.invitationTitle }), + ).toBeInTheDocument(); + expect(screen.queryByRole('link', INVITE_SIGN_UP)).toBeNull(); }); }); @@ -220,9 +236,13 @@ describe('RegisterPage — explains invitation-only registration before the form it('an invitation redirect reaches a working registration under invite_only, from /login on', async () => { renderAt(`/login${INVITE_QUERY}`, configFor('invite_only')); await loginConfigApplied(); - await userEvent.click(screen.getByRole('link', SIGN_UP_LINK)); + await userEvent.click(screen.getByRole('link', INVITE_SIGN_UP)); await userEvent.type(await screen.findByLabelText('Name'), 'Ada'); + // #25886 — the form asks for the invited address. + expect( + screen.getByRole('heading', { name: builtInLocales.en.auth.register.invitationTitle }), + ).toBeInTheDocument(); await userEvent.type(screen.getByLabelText('Email'), 'ada@example.com'); await userEvent.type(screen.getByLabelText('Password'), 'hunter2hunter2'); await userEvent.type(screen.getByLabelText('Confirm Password'), 'hunter2hunter2'); diff --git a/ui/packages/app-shell/src/console/auth/LoginPage.tsx b/ui/packages/app-shell/src/console/auth/LoginPage.tsx index ebb7e268d18..f7e43f633c2 100644 --- a/ui/packages/app-shell/src/console/auth/LoginPage.tsx +++ b/ui/packages/app-shell/src/console/auth/LoginPage.tsx @@ -14,6 +14,12 @@ * Retry that reads it again, in place of the form (objectui#11806). The * sign-in request goes to the same server, so a live-looking form would only * have deferred that news to the submit. + * + * A visitor who arrived from an invitation link (`?redirect=/accept-invitation/ID`) + * is greeted for that invitation instead of as a returning user, and, where + * sign-up is offered, gets `InvitationSignUpPrompt` (`./invitationArrival`) + * above the form in place of the small "Sign up" link (#25886). The console's + * own login page does the same. */ import { useEffect, useState } from 'react'; @@ -33,6 +39,7 @@ import { AuthPageLayout } from './AuthPageLayout.js'; import { signInRefusalMessages } from './signInRefusalMessages.js'; import { decideSignUpOffer, isInvitationRedirect, needsBootstrapProbe } from './signUpOffer.js'; import { useBootstrapStatus } from './bootstrapStatus.js'; +import { InvitationSignUpPrompt } from './invitationArrival.js'; const RouterLink = ({ href, className, children }: AuthLinkComponentProps) => ( {children} @@ -121,6 +128,11 @@ export function LoginPage() { ? `/register?redirect=${encodeURIComponent(redirect)}` : '/register'; + const offerSignUp = configRead === 'known' && signUpOffer === 'form'; + // #25886 — an invitee is greeted for the invitation, and offered the + // register-first prompt in place of `LoginForm`'s own small link. + const invitationSignUpUrl = invitationRedirect && offerSignUp ? registerUrl : undefined; + if (serverUnreachable) { return ( @@ -129,14 +141,15 @@ export function LoginPage() { ); } - return ( - + const form = ( navigate('/')} - registerUrl={configRead === 'known' && signUpOffer === 'form' ? registerUrl : undefined} + registerUrl={offerSignUp && !invitationRedirect ? registerUrl : undefined} forgotPasswordUrl="/forgot-password" title={t('auth.login.title')} description={t('auth.login.description')} + // An invitee's header is drawn above the prompt, below. + hideHeader={invitationRedirect} linkComponent={RouterLink} errorMessages={signInRefusalMessages(t)} labels={{ @@ -178,6 +191,23 @@ export function LoginPage() { backToSsoText: t('auth.login.backToSsoText'), }} /> + ); + + if (!invitationRedirect) return {form}; + + return ( + +
      + + {invitationSignUpUrl ? : null} + {form} +
      ); } diff --git a/ui/packages/app-shell/src/console/auth/RegisterPage.tsx b/ui/packages/app-shell/src/console/auth/RegisterPage.tsx index 96ca820c27c..3f39a89b1bb 100644 --- a/ui/packages/app-shell/src/console/auth/RegisterPage.tsx +++ b/ui/packages/app-shell/src/console/auth/RegisterPage.tsx @@ -225,8 +225,14 @@ export function RegisterPage() { onSuccess={() => navigate('/')} onVerificationRequired={(email) => setPendingEmail(email)} loginUrl={loginUrl} - title={t('auth.register.title')} - description={t('auth.register.description')} + // #25886 — an invitee is told to register the invited address: the + // invitation is accepted only by the account that carries it. + title={invitationRedirect ? t('auth.register.invitationTitle') : t('auth.register.title')} + description={ + invitationRedirect + ? t('auth.register.invitationDescription') + : t('auth.register.description') + } linkComponent={RouterLink} errorMessages={signUpRefusalMessages(t)} labels={{ diff --git a/ui/packages/app-shell/src/console/auth/__tests__/defaultPagesFollowPosture-11705.test.tsx b/ui/packages/app-shell/src/console/auth/__tests__/defaultPagesFollowPosture-11705.test.tsx index 9a758689f91..ae3419bb1e8 100644 --- a/ui/packages/app-shell/src/console/auth/__tests__/defaultPagesFollowPosture-11705.test.tsx +++ b/ui/packages/app-shell/src/console/auth/__tests__/defaultPagesFollowPosture-11705.test.tsx @@ -51,6 +51,8 @@ import { RegisterPage } from '../RegisterPage'; const AUTH_URL = 'http://localhost/api/v1/auth'; const en = builtInLocales.en.auth; const SIGN_UP_LINK = { name: en.login.signUpText } as const; +/** #25886 — an invitee's sign-up is the register-first prompt, not the small link. */ +const INVITE_SIGN_UP = { name: en.login.invitationSignUpButton } as const; const CREATE_ACCOUNT = { name: en.register.submitButton } as const; const INVITATION = '/accept-invitation/inv_1'; const INVITE_QUERY = `?redirect=${encodeURIComponent(INVITATION)}`; @@ -167,11 +169,16 @@ describe('DefaultLoginPage — the "Sign up" link follows the audience posture ( expect(screen.queryByRole('link', SIGN_UP_LINK)).toBeNull(); }); - it('under invite_only, still offers "Sign up" to an invitation redirect, carrying the redirect', async () => { + it('under invite_only, still offers sign-up to an invitation redirect, carrying the redirect', async () => { renderAt(`/login${INVITE_QUERY}`, configFor('invite_only')); await loginConfigApplied(); - expect(screen.getByRole('link', SIGN_UP_LINK).getAttribute('href')).toBe(`/register${INVITE_QUERY}`); + // #25886 — greeted for the invitation, with the register-first prompt in + // place of the small link, never both. + expect(screen.getByRole('heading', { name: en.login.invitationTitle })).toBeInTheDocument(); + expect(screen.queryByRole('heading', { name: en.login.title })).toBeNull(); + expect(screen.getByRole('link', INVITE_SIGN_UP).getAttribute('href')).toBe(`/register${INVITE_QUERY}`); + expect(screen.queryByRole('link', SIGN_UP_LINK)).toBeNull(); expect(wire.bootstrapProbes).toBe(0); }); @@ -207,6 +214,9 @@ describe('DefaultLoginPage — the "Sign up" link follows the audience posture ( await loginConfigApplied(); expect(screen.queryByRole('link', SIGN_UP_LINK)).toBeNull(); + // #25886 — still greeted for the invitation, but offered no registration. + expect(screen.getByRole('heading', { name: en.login.invitationTitle })).toBeInTheDocument(); + expect(screen.queryByRole('link', INVITE_SIGN_UP)).toBeNull(); expect(wire.bootstrapProbes).toBe(0); }); }); @@ -227,9 +237,12 @@ describe('DefaultRegisterPage — explains invitation-only registration before t it('an invitation redirect reaches a working registration under invite_only, from /login on', async () => { renderAt(`/login${INVITE_QUERY}`, configFor('invite_only')); await loginConfigApplied(); - await userEvent.click(screen.getByRole('link', SIGN_UP_LINK)); + await userEvent.click(screen.getByRole('link', INVITE_SIGN_UP)); await userEvent.type(await screen.findByLabelText(en.register.nameLabel), 'Ada'); + // #25886 — the form asks for the invited address. + expect(screen.getByRole('heading', { name: en.register.invitationTitle })).toBeInTheDocument(); + expect(screen.getByText(en.register.invitationDescription)).toBeInTheDocument(); await userEvent.type(screen.getByLabelText(en.register.emailLabel), 'ada@example.com'); await userEvent.type(screen.getByLabelText(en.register.passwordLabel), 'hunter2hunter2'); await userEvent.type(screen.getByLabelText(en.register.confirmPasswordLabel), 'hunter2hunter2'); diff --git a/ui/packages/app-shell/src/console/auth/invitationArrival.tsx b/ui/packages/app-shell/src/console/auth/invitationArrival.tsx new file mode 100644 index 00000000000..d659d671e34 --- /dev/null +++ b/ui/packages/app-shell/src/console/auth/invitationArrival.tsx @@ -0,0 +1,61 @@ +/** + * 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. + */ + +/** + * invitationArrival — the register-first action a login page shows a visitor + * who arrived signed out from an invitation link (#25886). + * + * `DefaultAcceptInvitationPage` bounces a signed-out visitor to + * `/login?redirect=/accept-invitation/ID` (`isInvitationRedirect` in + * `./signUpOffer`). Most invitees have never had an account here, so for them + * the generic "Welcome back" card with a small "Sign up" link under the form + * read as the wrong site. Both login pages — the console's own + * (`apps/console/src/pages/auth/LoginPage.tsx`) and this package's + * `DefaultLoginPage` — retitle the card for that visitor and render this + * prompt above the form in place of the small link, so there is ONE prompt and + * no second copy of it. + * + * The prompt names no organization and no inviter: the only invitation read + * the server serves (`GET /organization/get-invitation`) answers a signed-out + * caller `401`, and an anonymous preview is a server decision this package + * does not make. It is rendered only when the page's sign-up offer is `form`, + * so a server with sign-up switched off is never offered a registration it + * refuses. + */ + +import { Link } from 'react-router-dom'; +import { useObjectTranslation } from '@object-ui/i18n'; + +/** A full-width outlined link with the auth primary button's box. */ +const INVITATION_SIGN_UP_CLASS = + 'inline-flex h-10 w-full items-center justify-center gap-2 rounded-lg border border-primary/40 bg-primary/5 px-4 py-2 text-sm font-medium text-primary shadow-sm transition-colors hover:bg-primary/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 focus-visible:ring-offset-2'; + +/** + * "Create an account and join", then a divider that hands over to the sign-in + * form below it. `registerUrl` is the page's own `/register?redirect=…`, so the + * invitation survives the hop. + */ +export function InvitationSignUpPrompt({ registerUrl }: { registerUrl: string }) { + const { t } = useObjectTranslation(); + return ( +
      + + {t('auth.login.invitationSignUpButton', { defaultValue: 'Create an account and join' })} + +
      +
      +
      + ); +} diff --git a/ui/packages/app-shell/src/console/organizations/__tests__/acceptInvitationInviter-25886.test.tsx b/ui/packages/app-shell/src/console/organizations/__tests__/acceptInvitationInviter-25886.test.tsx new file mode 100644 index 00000000000..f634967b89e --- /dev/null +++ b/ui/packages/app-shell/src/console/organizations/__tests__/acceptInvitationInviter-25886.test.tsx @@ -0,0 +1,111 @@ +/** + * 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. + */ + +/** + * #25886 — the accept page names who sent the invitation. + * + * better-auth's `GET /organization/get-invitation` answers with the inviting + * member's email (`inviterEmail`) beside the organization snapshot; it returns + * no inviter name. The page shows that address under the translated + * `organization.accept.invitedBy` label, and draws no row for an answer that + * carries none. + */ + +import '@testing-library/jest-dom/vitest'; +import * as React from 'react'; +import { describe, it, expect, vi, afterEach } from 'vitest'; +import { render, screen, cleanup } from '@testing-library/react'; +import { I18nProvider } from '@object-ui/i18n'; +import { builtInLocales } from '@object-ui/i18n/locales'; + +const INVITATION = { + id: 'inv-1', + organizationId: 'org-42', + organizationName: 'Acme', + email: 'ada@example.com', + role: 'member', + status: 'pending', + expiresAt: '2099-03-04T12:00:00.000Z', + inviterId: 'u0', + inviterEmail: 'grace@example.com', +}; + +const answer: { current: Record } = { current: INVITATION }; + +// ⚠️ STABLE identities: the page keys its fetch effect on `getInvitation`, so +// a mock that minted a new function per render would loop the effect. +vi.mock('@object-ui/auth', async (importActual) => { + const auth = { + isAuthenticated: true, + isLoading: false, + getInvitation: async () => answer.current, + acceptInvitation: async () => undefined, + rejectInvitation: async () => undefined, + switchOrganization: async () => undefined, + }; + return { + ...(await importActual()), + useAuth: () => auth, + }; +}); + +vi.mock('react-router-dom', async (importActual) => { + const navigate = () => {}; + const location = { pathname: '/accept-invitation/inv-1', search: '' }; + const params = { invitationId: 'inv-1' }; + return { + ...(await importActual()), + useNavigate: () => navigate, + useLocation: () => location, + useParams: () => params, + }; +}); + +vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } })); + +import { AcceptInvitationPage } from '../manage/AcceptInvitationPage'; + +afterEach(() => { + cleanup(); + answer.current = INVITATION; +}); + +function renderIn(language: 'en' | 'zh'): void { + render( + + + , + ); +} + +describe('AcceptInvitationPage — names the inviter (#25886)', () => { + it('shows the inviting member under the "Invited by" label', async () => { + renderIn('en'); + + const inviter = await screen.findByTestId('accept-invitation-inviter'); + expect(inviter).toHaveTextContent('grace@example.com'); + expect(screen.getByText(builtInLocales.en.organization.accept.invitedBy)).toBeInTheDocument(); + }); + + it('reads the label from the session language', async () => { + renderIn('zh'); + + await screen.findByTestId('accept-invitation-inviter'); + expect(screen.getByText(builtInLocales.zh.organization.accept.invitedBy)).toBeInTheDocument(); + }); + + it('draws no inviter row when the answer carries no inviter address', async () => { + const { inviterEmail: _omitted, ...withoutInviter } = INVITATION; + answer.current = withoutInviter; + renderIn('en'); + + await screen.findByTestId('accept-invitation-role'); + expect(screen.queryByTestId('accept-invitation-inviter')).toBeNull(); + expect(screen.queryByText(builtInLocales.en.organization.accept.invitedBy)).toBeNull(); + }); +}); diff --git a/ui/packages/app-shell/src/console/organizations/manage/AcceptInvitationPage.tsx b/ui/packages/app-shell/src/console/organizations/manage/AcceptInvitationPage.tsx index d95cf2ffed0..28919dd0d12 100644 --- a/ui/packages/app-shell/src/console/organizations/manage/AcceptInvitationPage.tsx +++ b/ui/packages/app-shell/src/console/organizations/manage/AcceptInvitationPage.tsx @@ -24,6 +24,12 @@ import { resolveRootUrl } from '../resolveHomeUrl.js'; type InvitationWithOrg = AuthInvitation & { organizationName?: string; organizationSlug?: string; + /** + * The inviting member's email, which better-auth's `get-invitation` adds to + * its answer. The route returns no inviter name, so the page names the + * inviter by this address (#25886). + */ + inviterEmail?: string; }; export function AcceptInvitationPage() { @@ -225,6 +231,16 @@ export function AcceptInvitationPage() { {resolveOrgRoleLabel(invitation.role, t)}
+ {invitation.inviterEmail && ( +
+ + {t('organization.accept.invitedBy', { defaultValue: 'Invited by' })} + + + {invitation.inviterEmail} + +
+ )} {invitation.expiresAt && (
diff --git a/ui/packages/app-shell/src/index.ts b/ui/packages/app-shell/src/index.ts index c0e26384e2d..807d46d84c4 100644 --- a/ui/packages/app-shell/src/index.ts +++ b/ui/packages/app-shell/src/index.ts @@ -296,6 +296,9 @@ export { type SignUpOfferContext, } from './console/auth/signUpOffer.js'; export { useBootstrapStatus, type BootstrapStatus } from './console/auth/bootstrapStatus.js'; +// The register-first prompt both login pages show a signed-out visitor who +// arrived from an invitation link (#25886). +export { InvitationSignUpPrompt } from './console/auth/invitationArrival.js'; export { ForgotPasswordPage as DefaultForgotPasswordPage } from './console/auth/ForgotPasswordPage.js'; export { HomeLayout as DefaultHomeLayout, HomeLayout } from './console/home/HomeLayout.js'; export { HomePage as DefaultHomePage, HomePage } from './console/home/HomePage.js'; diff --git a/ui/packages/i18n/src/locales/ar.ts b/ui/packages/i18n/src/locales/ar.ts index ac057c1c960..c8b844101b9 100644 --- a/ui/packages/i18n/src/locales/ar.ts +++ b/ui/packages/i18n/src/locales/ar.ts @@ -2763,6 +2763,10 @@ const ar = { // field's reveal toggle, and the footer's legal links. welcomeTitle: "مرحبًا بعودتك", welcomeDescription: "سجّل الدخول لمتابعة عملك", + invitationTitle: "اقبل دعوتك", + invitationDescription: "استخدم البريد الإلكتروني الذي أُرسلت إليه الدعوة.", + invitationSignUpButton: "إنشاء حساب والانضمام", + invitationSignInDivider: "أو سجّل الدخول بحساب موجود", showPasswordText: "إظهار كلمة المرور", hidePasswordText: "إخفاء كلمة المرور", footer: { @@ -2773,6 +2777,8 @@ const ar = { register: { title: "إنشاء حساب", description: "أدخل معلوماتك للبدء", + invitationTitle: "أنشئ حسابًا للانضمام", + invitationDescription: "سجّل بالبريد الإلكتروني الذي أُرسلت إليه الدعوة، فلا يمكن قبولها إلا بهذا العنوان.", nameLabel: "الاسم", namePlaceholder: "محمد أحمد", emailLabel: "البريد الإلكتروني", @@ -3599,6 +3605,7 @@ const ar = { organization: "المؤسسة", role: "الدور", expiresAt: "تنتهي في", + invitedBy: "الدعوة من", accept: "قبول الدعوة", accepted: "تم قبول الدعوة", acceptFailed: "فشل قبول الدعوة", diff --git a/ui/packages/i18n/src/locales/de.ts b/ui/packages/i18n/src/locales/de.ts index 2dca8512ae5..a010e84161c 100644 --- a/ui/packages/i18n/src/locales/de.ts +++ b/ui/packages/i18n/src/locales/de.ts @@ -2561,6 +2561,10 @@ const de = { // field's reveal toggle, and the footer's legal links. welcomeTitle: "Willkommen zurück", welcomeDescription: "Melden Sie sich an, um weiterzuarbeiten", + invitationTitle: "Einladung annehmen", + invitationDescription: "Verwenden Sie die E-Mail-Adresse, an die die Einladung gesendet wurde.", + invitationSignUpButton: "Konto erstellen und beitreten", + invitationSignInDivider: "oder mit einem bestehenden Konto anmelden", showPasswordText: "Passwort anzeigen", hidePasswordText: "Passwort verbergen", footer: { @@ -2571,6 +2575,8 @@ const de = { register: { title: "Konto erstellen", description: "Geben Sie Ihre Daten ein, um loszulegen", + invitationTitle: "Konto erstellen, um beizutreten", + invitationDescription: "Registrieren Sie sich mit der E-Mail-Adresse, an die die Einladung gesendet wurde. Nur mit dieser Adresse kann sie angenommen werden.", nameLabel: "Name", namePlaceholder: "Max Mustermann", emailLabel: "E-Mail", @@ -3370,6 +3376,7 @@ const de = { organization: "Organisation", role: "Rolle", expiresAt: "Läuft ab am", + invitedBy: "Eingeladen von", accept: "Einladung annehmen", accepted: "Einladung angenommen", acceptFailed: "Einladung konnte nicht angenommen werden", diff --git a/ui/packages/i18n/src/locales/en.ts b/ui/packages/i18n/src/locales/en.ts index ef2ecfa2f7c..131966cb494 100644 --- a/ui/packages/i18n/src/locales/en.ts +++ b/ui/packages/i18n/src/locales/en.ts @@ -3058,6 +3058,14 @@ const en = { // field's reveal toggle, and the footer's legal links. welcomeTitle: 'Welcome back', welcomeDescription: 'Sign in to continue your work', + // The same card's header when the visitor arrived signed out from an + // invitation link (`?redirect=/accept-invitation/ID`), and the + // register-first action above the form. The copy names no organization + // or inviter: a signed-out page has no read of the invitation (#25886). + invitationTitle: 'Accept your invitation', + invitationDescription: 'Use the email address the invitation was sent to.', + invitationSignUpButton: 'Create an account and join', + invitationSignInDivider: 'or sign in to an existing account', showPasswordText: 'Show password', hidePasswordText: 'Hide password', footer: { @@ -3077,6 +3085,8 @@ const en = { register: { title: 'Create an account', description: 'Create your account to start building.', + invitationTitle: 'Create an account to join', + invitationDescription: 'Register with the email address the invitation was sent to. The invitation can only be accepted with that address.', nameLabel: 'Name', namePlaceholder: 'John Doe', emailLabel: 'Email', @@ -4073,6 +4083,7 @@ const en = { organization: 'Organization', role: 'Role', expiresAt: 'Expires', + invitedBy: 'Invited by', accept: 'Accept invitation', accepted: 'Invitation accepted', acceptFailed: 'Failed to accept invitation', diff --git a/ui/packages/i18n/src/locales/es.ts b/ui/packages/i18n/src/locales/es.ts index 464ece759f5..4daee6f8749 100644 --- a/ui/packages/i18n/src/locales/es.ts +++ b/ui/packages/i18n/src/locales/es.ts @@ -2613,6 +2613,10 @@ const es = { // field's reveal toggle, and the footer's legal links. welcomeTitle: "Bienvenido de nuevo", welcomeDescription: "Inicia sesión para continuar con tu trabajo", + invitationTitle: "Acepta tu invitación", + invitationDescription: "Usa el correo al que se envió la invitación.", + invitationSignUpButton: "Crear una cuenta y unirme", + invitationSignInDivider: "o inicia sesión con una cuenta existente", showPasswordText: "Mostrar contraseña", hidePasswordText: "Ocultar contraseña", footer: { @@ -2623,6 +2627,8 @@ const es = { register: { title: "Crear una cuenta", description: "Ingresa tu información para comenzar", + invitationTitle: "Crea una cuenta para unirte", + invitationDescription: "Regístrate con el correo al que se envió la invitación. Solo se puede aceptar con esa dirección.", nameLabel: "Nombre", namePlaceholder: "Juan Pérez", emailLabel: "Correo electrónico", @@ -3428,6 +3434,7 @@ const es = { organization: "Organización", role: "Rol", expiresAt: "Caduca", + invitedBy: "Invitado por", accept: "Aceptar invitación", accepted: "Invitación aceptada", acceptFailed: "No se pudo aceptar la invitación", diff --git a/ui/packages/i18n/src/locales/fr.ts b/ui/packages/i18n/src/locales/fr.ts index 16c2d6bef70..d48bbfe3197 100644 --- a/ui/packages/i18n/src/locales/fr.ts +++ b/ui/packages/i18n/src/locales/fr.ts @@ -2609,6 +2609,10 @@ const fr = { // field's reveal toggle, and the footer's legal links. welcomeTitle: "Bon retour", welcomeDescription: "Connectez-vous pour reprendre votre travail", + invitationTitle: "Acceptez votre invitation", + invitationDescription: "Utilisez l'adresse e-mail à laquelle l'invitation a été envoyée.", + invitationSignUpButton: "Créer un compte et rejoindre", + invitationSignInDivider: "ou connectez-vous à un compte existant", showPasswordText: "Afficher le mot de passe", hidePasswordText: "Masquer le mot de passe", footer: { @@ -2619,6 +2623,8 @@ const fr = { register: { title: "Créer un compte", description: "Entrez vos informations pour commencer", + invitationTitle: "Créez un compte pour rejoindre", + invitationDescription: "Inscrivez-vous avec l'adresse e-mail à laquelle l'invitation a été envoyée. Elle ne peut être acceptée qu'avec cette adresse.", nameLabel: "Nom", namePlaceholder: "Jean Dupont", emailLabel: "E-mail", @@ -3424,6 +3430,7 @@ const fr = { organization: "Organisation", role: "Rôle", expiresAt: "Expire le", + invitedBy: "Invité par", accept: "Accepter l'invitation", accepted: "Invitation acceptée", acceptFailed: "Impossible d'accepter l'invitation", diff --git a/ui/packages/i18n/src/locales/ja.ts b/ui/packages/i18n/src/locales/ja.ts index ae922186d1a..f6b28fc2389 100644 --- a/ui/packages/i18n/src/locales/ja.ts +++ b/ui/packages/i18n/src/locales/ja.ts @@ -2563,6 +2563,10 @@ const ja = { // field's reveal toggle, and the footer's legal links. welcomeTitle: "おかえりなさい", welcomeDescription: "サインインして作業を続けましょう", + invitationTitle: "招待を承諾", + invitationDescription: "招待を受け取ったメールアドレスをご利用ください。", + invitationSignUpButton: "アカウントを作成して参加", + invitationSignInDivider: "または既存のアカウントでサインイン", showPasswordText: "パスワードを表示", hidePasswordText: "パスワードを非表示", footer: { @@ -2573,6 +2577,8 @@ const ja = { register: { title: "アカウントを作成", description: "情報を入力して始めましょう", + invitationTitle: "アカウントを作成して参加する", + invitationDescription: "招待を受け取ったメールアドレスで登録してください。招待はそのアドレスでのみ承諾できます。", nameLabel: "名前", namePlaceholder: "田中太郎", emailLabel: "メールアドレス", @@ -3372,6 +3378,7 @@ const ja = { organization: "組織", role: "ロール", expiresAt: "有効期限", + invitedBy: "招待者", accept: "招待を承諾", accepted: "招待を承諾しました", acceptFailed: "招待の承諾に失敗しました", diff --git a/ui/packages/i18n/src/locales/ko.ts b/ui/packages/i18n/src/locales/ko.ts index 4d2e691ec58..afb9139c654 100644 --- a/ui/packages/i18n/src/locales/ko.ts +++ b/ui/packages/i18n/src/locales/ko.ts @@ -2561,6 +2561,10 @@ const ko = { // field's reveal toggle, and the footer's legal links. welcomeTitle: "다시 오신 것을 환영합니다", welcomeDescription: "로그인하고 작업을 계속하세요", + invitationTitle: "초대 수락", + invitationDescription: "초대를 받은 이메일 주소를 사용하세요.", + invitationSignUpButton: "계정 만들고 참여하기", + invitationSignInDivider: "또는 기존 계정으로 로그인", showPasswordText: "비밀번호 표시", hidePasswordText: "비밀번호 숨기기", footer: { @@ -2571,6 +2575,8 @@ const ko = { register: { title: "계정 만들기", description: "정보를 입력하여 시작하세요", + invitationTitle: "계정을 만들고 참여하기", + invitationDescription: "초대를 받은 이메일 주소로 가입하세요. 초대는 해당 주소로만 수락할 수 있습니다.", nameLabel: "이름", namePlaceholder: "홍길동", emailLabel: "이메일", @@ -3369,6 +3375,7 @@ const ko = { organization: "조직", role: "역할", expiresAt: "만료", + invitedBy: "초대한 사람", accept: "초대 수락", accepted: "초대를 수락함", acceptFailed: "초대 수락 실패", diff --git a/ui/packages/i18n/src/locales/pt.ts b/ui/packages/i18n/src/locales/pt.ts index 6947902e67b..eceb4acd3b5 100644 --- a/ui/packages/i18n/src/locales/pt.ts +++ b/ui/packages/i18n/src/locales/pt.ts @@ -2608,6 +2608,10 @@ const pt = { // field's reveal toggle, and the footer's legal links. welcomeTitle: "Bem-vindo de volta", welcomeDescription: "Entre para continuar seu trabalho", + invitationTitle: "Aceite seu convite", + invitationDescription: "Use o e-mail para o qual o convite foi enviado.", + invitationSignUpButton: "Criar conta e participar", + invitationSignInDivider: "ou entre com uma conta existente", showPasswordText: "Mostrar senha", hidePasswordText: "Ocultar senha", footer: { @@ -2618,6 +2622,8 @@ const pt = { register: { title: "Criar uma conta", description: "Digite suas informações para começar", + invitationTitle: "Crie uma conta para participar", + invitationDescription: "Cadastre-se com o e-mail para o qual o convite foi enviado. Ele só pode ser aceito com esse endereço.", nameLabel: "Nome", namePlaceholder: "João Silva", emailLabel: "E-mail", @@ -3423,6 +3429,7 @@ const pt = { organization: "Organização", role: "Função", expiresAt: "Expira em", + invitedBy: "Convidado por", accept: "Aceitar convite", accepted: "Convite aceito", acceptFailed: "Falha ao aceitar o convite", diff --git a/ui/packages/i18n/src/locales/ru.ts b/ui/packages/i18n/src/locales/ru.ts index bd5316c6919..d5b2cfd72ab 100644 --- a/ui/packages/i18n/src/locales/ru.ts +++ b/ui/packages/i18n/src/locales/ru.ts @@ -2670,6 +2670,10 @@ const ru = { // field's reveal toggle, and the footer's legal links. welcomeTitle: "С возвращением", welcomeDescription: "Войдите, чтобы продолжить работу", + invitationTitle: "Примите приглашение", + invitationDescription: "Используйте адрес электронной почты, на который пришло приглашение.", + invitationSignUpButton: "Создать учётную запись и присоединиться", + invitationSignInDivider: "или войдите в существующую учётную запись", showPasswordText: "Показать пароль", hidePasswordText: "Скрыть пароль", footer: { @@ -2680,6 +2684,8 @@ const ru = { register: { title: "Создать аккаунт", description: "Введите свои данные, чтобы начать", + invitationTitle: "Создайте учётную запись, чтобы присоединиться", + invitationDescription: "Зарегистрируйтесь с адресом, на который пришло приглашение. Принять его можно только с этим адресом.", nameLabel: "Имя", namePlaceholder: "Иван Иванов", emailLabel: "Электронная почта", @@ -3495,6 +3501,7 @@ const ru = { organization: "Организация", role: "Роль", expiresAt: "Истекает", + invitedBy: "Приглашение от", accept: "Принять приглашение", accepted: "Приглашение принято", acceptFailed: "Не удалось принять приглашение", diff --git a/ui/packages/i18n/src/locales/zh.ts b/ui/packages/i18n/src/locales/zh.ts index e7253201c8d..28e4ad26b34 100644 --- a/ui/packages/i18n/src/locales/zh.ts +++ b/ui/packages/i18n/src/locales/zh.ts @@ -2625,6 +2625,10 @@ const zh = { // field's reveal toggle, and the footer's legal links. welcomeTitle: '欢迎回来', welcomeDescription: '登录后继续你的工作', + invitationTitle: '接受邀请', + invitationDescription: '请使用收到邀请的邮箱。', + invitationSignUpButton: '注册并加入', + invitationSignInDivider: '或登录已有账号', showPasswordText: '显示密码', hidePasswordText: '隐藏密码', footer: { @@ -2635,6 +2639,8 @@ const zh = { register: { title: '创建账户', description: '创建账号,开始搭建。', + invitationTitle: '注册账号并加入', + invitationDescription: '请使用收到邀请的邮箱注册,邀请只能由该邮箱接受。', nameLabel: '姓名', namePlaceholder: '张三', emailLabel: '邮箱', @@ -3550,6 +3556,7 @@ const zh = { organization: '组织', role: '角色', expiresAt: '过期时间', + invitedBy: '邀请人', accept: '接受邀请', accepted: '邀请已接受', acceptFailed: '接受邀请失败', From 60aa2b49a9c072610cfa25957ee74cef1c48d095 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 09:27:13 +0000 Subject: [PATCH 07/19] chore: changeset and sdui manifest record for the form refusal change Claude-Session: https://claude.ai/code/session_01CzUs1oNtFWhWUXT7rmoGy2 Co-authored-by: Claude --- .changeset/25874-form-refusal-no-field-toast.md | 13 +++++++++++++ scripts/sdui-manifest.record.json | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 .changeset/25874-form-refusal-no-field-toast.md diff --git a/.changeset/25874-form-refusal-no-field-toast.md b/.changeset/25874-form-refusal-no-field-toast.md new file mode 100644 index 00000000000..ccc684d92f8 --- /dev/null +++ b/.changeset/25874-form-refusal-no-field-toast.md @@ -0,0 +1,13 @@ +--- +'@object-ui/components': patch +--- + +fix(components): a refused submit moves focus to the first invalid field and no longer toasts the field list over the dialog's title + +Clause-②: no +Metadata-protocol: none + +- **Before.** A submit refused by field validation, on the client or by the server's `fields[]`, raised an error toast naming the fields ("Please check the highlighted fields: First name, Last name, Company…") on top of the message already written under each input. The toast cut its list after three names, and in a record create / edit dialog it was drawn over the title row. +- **Now.** The form raises no toast for a refusal that every field carries inline. It scrolls to the first invalid field in form order and focuses it, and first activates that field's tab when it is on a background tab. The message under each input is the whole report. +- **A field with nothing drawn to carry it.** When the server rejects a field that has no drawn input, for example one on a tab or section a `visibleWhen` predicate hides, the form shows the form-level banner and its toast with the server's sentence, the same path a rejection naming an unknown field already takes. The hidden tab is not activated. +- **Earlier outcomes retire.** A refusal on the client now takes down the previous attempt's banner and toast, and a clear-on-hide notice, as an attempt that passes validation already did. Before, the refusal toast replaced them under the form's outcome id. diff --git a/scripts/sdui-manifest.record.json b/scripts/sdui-manifest.record.json index 5d728022f56..ae9176b4274 100644 --- a/scripts/sdui-manifest.record.json +++ b/scripts/sdui-manifest.record.json @@ -49,6 +49,6 @@ "ui/packages/types" ], "registrySources": 922, - "registrySourcesSha256": "7734fe6bc3763fe8318db624b9e0dd4c48b2dc7b15bf2a3fb1f78e46bb8d959e" + "registrySourcesSha256": "82ef0997c76d847286a27141753bcbde5b9904cc3f9306b65b2d8145918d2e5d" } } From baa3ba945889def09faf0212d8a5276451c4b753 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 09:39:28 +0000 Subject: [PATCH 08/19] chore(sdui): re-record the manifest's registry hash for the invitation-arrival change Claude-Session: https://claude.ai/code/session_016msqupguN18F4BUKKWYbWY Co-authored-by: Claude --- scripts/sdui-manifest.record.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/sdui-manifest.record.json b/scripts/sdui-manifest.record.json index f65f470a705..d9a07499712 100644 --- a/scripts/sdui-manifest.record.json +++ b/scripts/sdui-manifest.record.json @@ -49,6 +49,6 @@ "ui/packages/types" ], "registrySources": 922, - "registrySourcesSha256": "3fe916234f1b5346bff70f6f99322077630b851ebef81c9ae6ba8c4ac08e2063" + "registrySourcesSha256": "40f7b3974c5d267fd653d8150160cd49287e218bbc394064e673ca4748b148c2" } } From 79dcecfdd5733b4954e829df82b33bc0f05cdecf Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 09:47:39 +0000 Subject: [PATCH 09/19] chore(sdui): re-record the manifest record after merging main Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01HUu4jGnAzZqaB5qTG2fsX4 --- scripts/sdui-manifest.record.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/sdui-manifest.record.json b/scripts/sdui-manifest.record.json index f65f470a705..1ffac470455 100644 --- a/scripts/sdui-manifest.record.json +++ b/scripts/sdui-manifest.record.json @@ -49,6 +49,6 @@ "ui/packages/types" ], "registrySources": 922, - "registrySourcesSha256": "3fe916234f1b5346bff70f6f99322077630b851ebef81c9ae6ba8c4ac08e2063" + "registrySourcesSha256": "28bb61d046e46c8286a75a7b887d31bfc0d23220c8fd51d2fdff1d99134c932f" } } From cb3fe1a02f5944429ec9eb95710b849fc32cd907 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 09:58:35 +0000 Subject: [PATCH 10/19] chore: re-record the sdui manifest hash after merging main Claude-Session: https://claude.ai/code/session_01CzUs1oNtFWhWUXT7rmoGy2 Co-authored-by: Claude --- scripts/sdui-manifest.record.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/sdui-manifest.record.json b/scripts/sdui-manifest.record.json index f65f470a705..0ec69dc13de 100644 --- a/scripts/sdui-manifest.record.json +++ b/scripts/sdui-manifest.record.json @@ -49,6 +49,6 @@ "ui/packages/types" ], "registrySources": 922, - "registrySourcesSha256": "3fe916234f1b5346bff70f6f99322077630b851ebef81c9ae6ba8c4ac08e2063" + "registrySourcesSha256": "a71feb98497ec8c6958f52d5e1d6d85b5f64cfd30efed87e1f4a80678dc82909" } } From 5a7ff8392dfb9c5a7fa83aab58f569bf0134391f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 10:10:42 +0000 Subject: [PATCH 11/19] =?UTF-8?q?chore(changeset):=20declare=20Clause-?= =?UTF-8?q?=E2=91=A1=20yes=20for=20the=20new=20InvitationSignUpPrompt=20ex?= =?UTF-8?q?port?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The prompt is a new root export of the published @object-ui/app-shell, so the change widens a public surface. Claude-Session: https://claude.ai/code/session_016msqupguN18F4BUKKWYbWY Co-authored-by: Claude --- .changeset/25886-invitation-arrival-copy.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/25886-invitation-arrival-copy.md b/.changeset/25886-invitation-arrival-copy.md index e7762cb479b..7d5e3b382c0 100644 --- a/.changeset/25886-invitation-arrival-copy.md +++ b/.changeset/25886-invitation-arrival-copy.md @@ -6,7 +6,7 @@ fix(app-shell,console): an invitee who opens the invitation link signed out is greeted for the invitation, offered "Create an account and join" first, and the accept page names the inviter -Clause-②: no +Clause-②: yes Metadata-protocol: none - **Login page.** A signed-out visitor arriving at `/login?redirect=/accept-invitation/ID` used to see the returning-user card ("Welcome back · Sign in to continue your work") with a small "Sign up" link under the form. Both login pages (the console's own and `DefaultLoginPage`) now title the card "Accept your invitation · Use the email address the invitation was sent to." Where the server offers sign-up to this visitor, a full-width "Create an account and join" link (the new `InvitationSignUpPrompt` export) sits above the sign-in form in place of the small link. With sign-up switched off, the visitor gets the invitation header and the sign-in form only. From 32a5792b3db788ffcfbcb847fdec4210908e0fbf Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 10:21:16 +0000 Subject: [PATCH 12/19] fix(components,i18n): drop the now-unread validation.formInvalid key and bring two comments in line Claude-Session: https://claude.ai/code/session_01CzUs1oNtFWhWUXT7rmoGy2 Co-authored-by: Claude --- .../25874-form-refusal-no-field-toast.md | 2 ++ .../components/src/renderers/form/form.tsx | 18 ++++++++++-------- ui/packages/i18n/src/locales/ar.ts | 1 - ui/packages/i18n/src/locales/de.ts | 1 - ui/packages/i18n/src/locales/en.ts | 8 +++++--- ui/packages/i18n/src/locales/es.ts | 1 - ui/packages/i18n/src/locales/fr.ts | 1 - ui/packages/i18n/src/locales/ja.ts | 1 - ui/packages/i18n/src/locales/ko.ts | 1 - ui/packages/i18n/src/locales/pt.ts | 1 - ui/packages/i18n/src/locales/ru.ts | 1 - ui/packages/i18n/src/locales/zh.ts | 1 - 12 files changed, 17 insertions(+), 20 deletions(-) diff --git a/.changeset/25874-form-refusal-no-field-toast.md b/.changeset/25874-form-refusal-no-field-toast.md index ccc684d92f8..d1ad37599cc 100644 --- a/.changeset/25874-form-refusal-no-field-toast.md +++ b/.changeset/25874-form-refusal-no-field-toast.md @@ -1,5 +1,6 @@ --- '@object-ui/components': patch +'@object-ui/i18n': patch --- fix(components): a refused submit moves focus to the first invalid field and no longer toasts the field list over the dialog's title @@ -11,3 +12,4 @@ Metadata-protocol: none - **Now.** The form raises no toast for a refusal that every field carries inline. It scrolls to the first invalid field in form order and focuses it, and first activates that field's tab when it is on a background tab. The message under each input is the whole report. - **A field with nothing drawn to carry it.** When the server rejects a field that has no drawn input, for example one on a tab or section a `visibleWhen` predicate hides, the form shows the form-level banner and its toast with the server's sentence, the same path a rejection naming an unknown field already takes. The hidden tab is not activated. - **Earlier outcomes retire.** A refusal on the client now takes down the previous attempt's banner and toast, and a clear-on-hide notice, as an attempt that passes validation already did. Before, the refusal toast replaced them under the form's outcome id. +- **`validation.formInvalid` removed.** The toast's sentence ("Please check the highlighted fields: {{fields}}") had no other reader, so it leaves the form's default map and all ten `@object-ui/i18n` locale packs. `validation.formInvalidJoiner` stays: the clear-on-hide notice, the faulted-`visibleWhen` refusal and the option-group hint still join field labels with it. A host bundle that overrides `validation.formInvalid` can drop the entry; nothing reads it. diff --git a/ui/packages/components/src/renderers/form/form.tsx b/ui/packages/components/src/renderers/form/form.tsx index 4c8551d8930..fa032bb62d8 100644 --- a/ui/packages/components/src/renderers/form/form.tsx +++ b/ui/packages/components/src/renderers/form/form.tsx @@ -472,12 +472,12 @@ const useSafeFormTranslation = createSafeTranslation( 'validation.pattern': '{{field}} format is invalid', 'validation.email': 'Please enter a valid email address', 'validation.url': 'Please enter a valid URL', - 'validation.formInvalid': 'Please check the highlighted fields: {{fields}}', // objectstack#5407 — the joiner between the field names is a LOCALE // property, not a code constant. It used to be a hardcoded `、` (U+3001), // which is right for zh/ja and wrong everywhere else — including `en`, - // where the toast read "Subject、Account、Status". A locale that ships no - // entry falls back through i18next to `en`'s ", ". + // where the invalid-submit toast (since removed, objectstack#25874) read + // "Subject、Account、Status". A locale that ships no entry falls back + // through i18next to `en`'s ", ". 'validation.formInvalidJoiner': ', ', 'errors.forbidden': 'Access denied.', 'form.noPermissionToSave': "You don't have permission to save this record.", @@ -2484,11 +2484,13 @@ ComponentRegistry.register('form', // ruling on objectui#8070 (letter A) ports the naming half of the // objectui#6499 ruling to this surface: the user is told WHICH fields were // cleared and WHY. So a pass that empties anything raises one notice naming - // the cleared fields by the label the form draws (the source - // `validation.formInvalid` names them from), joined by the locale's own - // `validation.formInvalidJoiner`, under `outcomeToastId`. Sharing that id - // is what makes it retire like the form's other outcome messages: a later - // submit refusal replaces it, and the next accepted attempt dismisses it. + // the cleared fields by the label the form draws (`fieldLabelByName`), + // joined by the locale's own `validation.formInvalidJoiner`, under + // `outcomeToastId`. Sharing that id is what makes it retire like the form's + // other outcome messages: every later submit attempt takes it down. One + // that clears validation and a client-side refusal (whose report is the + // inline field messages, objectstack#25874) both dismiss the id, and the + // faulted-`visibleWhen` refusal dismisses it before raising its own. // // Only a field that held something is named. An empty list is rewritten // as itself, and `false` on a two-state control is that control's EMPTY diff --git a/ui/packages/i18n/src/locales/ar.ts b/ui/packages/i18n/src/locales/ar.ts index 891cfe2b3cf..9c63bb30b8e 100644 --- a/ui/packages/i18n/src/locales/ar.ts +++ b/ui/packages/i18n/src/locales/ar.ts @@ -172,7 +172,6 @@ const ar = { email: "يرجى إدخال بريد إلكتروني صالح", url: "يرجى إدخال رابط صالح", pattern: "صيغة {{field}} غير صالحة", - formInvalid: "يرجى التحقق من الحقول المميزة: {{fields}}", formInvalidJoiner: "، ", unique: "{{field}} يجب أن يكون فريداً", type: "{{field}} يجب أن يكون {{type}} صالحاً", diff --git a/ui/packages/i18n/src/locales/de.ts b/ui/packages/i18n/src/locales/de.ts index 45df300eb6b..c7fc9c90d9c 100644 --- a/ui/packages/i18n/src/locales/de.ts +++ b/ui/packages/i18n/src/locales/de.ts @@ -158,7 +158,6 @@ const de = { email: "Bitte geben Sie eine gültige E-Mail-Adresse ein", url: "Bitte geben Sie eine gültige URL ein", pattern: "{{field}} hat ein ungültiges Format", - formInvalid: "Bitte überprüfen Sie die markierten Felder: {{fields}}", formInvalidJoiner: ", ", unique: "{{field}} muss eindeutig sein", type: "{{field}} muss ein gültiger {{type}} sein", diff --git a/ui/packages/i18n/src/locales/en.ts b/ui/packages/i18n/src/locales/en.ts index 912c9ea46ae..c5726213ca6 100644 --- a/ui/packages/i18n/src/locales/en.ts +++ b/ui/packages/i18n/src/locales/en.ts @@ -202,12 +202,14 @@ const en = { email: 'Please enter a valid email address', url: 'Please enter a valid URL', pattern: '{{field}} format is invalid', - formInvalid: 'Please check the highlighted fields: {{fields}}', - // Separator between the field names interpolated into `formInvalid`. + // Separator between the field labels a form names in one sentence: the + // record form's clear-on-hide notice (`form.clearedOnHide`) and + // faulted-`visibleWhen` refusal (`form.visibleWhenFaulted`), and the + // option-group "select the parent first" hint (`fields.options.selectFirst`). // Per-locale because list punctuation is a locale property, not a code // constant: CJK enumerates with U+3001, Latin scripts with a comma+space, // Arabic with U+060C. Hardcoding one of them in the renderer put the CJK - // comma into the English toast (objectstack#5407). + // comma into the English invalid-submit toast (objectstack#5407). formInvalidJoiner: ', ', unique: '{{field}} must be unique', type: '{{field}} must be a valid {{type}}', diff --git a/ui/packages/i18n/src/locales/es.ts b/ui/packages/i18n/src/locales/es.ts index 49755a9440b..8eb29b4ede8 100644 --- a/ui/packages/i18n/src/locales/es.ts +++ b/ui/packages/i18n/src/locales/es.ts @@ -165,7 +165,6 @@ const es = { email: "Por favor, introduzca un correo electrónico válido", url: "Por favor, introduzca una URL válida", pattern: "El formato de {{field}} no es válido", - formInvalid: "Por favor, revise los campos resaltados: {{fields}}", formInvalidJoiner: ", ", unique: "El campo {{field}} debe ser único", type: "{{field}} debe ser un {{type}} válido", diff --git a/ui/packages/i18n/src/locales/fr.ts b/ui/packages/i18n/src/locales/fr.ts index 977e180ee73..63fd179c81b 100644 --- a/ui/packages/i18n/src/locales/fr.ts +++ b/ui/packages/i18n/src/locales/fr.ts @@ -160,7 +160,6 @@ const fr = { email: "Veuillez saisir une adresse e-mail valide", url: "Veuillez saisir une URL valide", pattern: "Le format de {{field}} est invalide", - formInvalid: "Veuillez vérifier les champs en surbrillance : {{fields}}", formInvalidJoiner: ", ", unique: "{{field}} doit être unique", type: "{{field}} doit être un {{type}} valide", diff --git a/ui/packages/i18n/src/locales/ja.ts b/ui/packages/i18n/src/locales/ja.ts index 567ea51bf82..f6883afaab8 100644 --- a/ui/packages/i18n/src/locales/ja.ts +++ b/ui/packages/i18n/src/locales/ja.ts @@ -158,7 +158,6 @@ const ja = { email: "有効なメールアドレスを入力してください", url: "有効なURLを入力してください", pattern: "{{field}}の形式が正しくありません", - formInvalid: "ハイライトされた項目を確認してください: {{fields}}", formInvalidJoiner: "、", unique: "{{field}}は一意である必要があります", type: "{{field}}は有効な{{type}}である必要があります", diff --git a/ui/packages/i18n/src/locales/ko.ts b/ui/packages/i18n/src/locales/ko.ts index 72a5459b2e7..dc7d252544a 100644 --- a/ui/packages/i18n/src/locales/ko.ts +++ b/ui/packages/i18n/src/locales/ko.ts @@ -158,7 +158,6 @@ const ko = { email: "유효한 이메일 주소를 입력해주세요", url: "유효한 URL을 입력해주세요", pattern: "{{field}} 형식이 올바르지 않습니다", - formInvalid: "표시된 필드를 확인해 주세요: {{fields}}", formInvalidJoiner: ", ", unique: "{{field}}은(는) 고유해야 합니다", type: "{{field}}은(는) 유효한 {{type}}이어야 합니다", diff --git a/ui/packages/i18n/src/locales/pt.ts b/ui/packages/i18n/src/locales/pt.ts index 02b58db8da4..c549cb60d26 100644 --- a/ui/packages/i18n/src/locales/pt.ts +++ b/ui/packages/i18n/src/locales/pt.ts @@ -160,7 +160,6 @@ const pt = { email: "Por favor, insira um endereço de e-mail válido", url: "Por favor, insira uma URL válida", pattern: "O formato de {{field}} é inválido", - formInvalid: "Verifique os campos destacados: {{fields}}", formInvalidJoiner: ", ", unique: "{{field}} deve ser único", type: "{{field}} deve ser um {{type}} válido", diff --git a/ui/packages/i18n/src/locales/ru.ts b/ui/packages/i18n/src/locales/ru.ts index 747023fad25..7c810acfd27 100644 --- a/ui/packages/i18n/src/locales/ru.ts +++ b/ui/packages/i18n/src/locales/ru.ts @@ -165,7 +165,6 @@ const ru = { email: "Пожалуйста, введите корректный адрес электронной почты", url: "Пожалуйста, введите корректный URL", pattern: "Неверный формат поля {{field}}", - formInvalid: "Проверьте выделенные поля: {{fields}}", formInvalidJoiner: ", ", unique: "{{field}} должно быть уникальным", type: "{{field}} должно быть допустимым {{type}}", diff --git a/ui/packages/i18n/src/locales/zh.ts b/ui/packages/i18n/src/locales/zh.ts index aee0eb1ff9d..068cccba26f 100644 --- a/ui/packages/i18n/src/locales/zh.ts +++ b/ui/packages/i18n/src/locales/zh.ts @@ -163,7 +163,6 @@ const zh = { email: '请输入有效的邮箱地址', url: '请输入有效的URL', pattern: '{{field}}格式不正确', - formInvalid: '请检查表单中标记的字段:{{fields}}', formInvalidJoiner: '、', unique: '{{field}}必须唯一', type: '{{field}}必须是有效的{{type}}', From 024bef86193273ea191485f17e8f4f86840675af Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 10:28:47 +0000 Subject: [PATCH 13/19] chore: re-record the sdui manifest hash Claude-Session: https://claude.ai/code/session_01CzUs1oNtFWhWUXT7rmoGy2 Co-authored-by: Claude --- scripts/sdui-manifest.record.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/sdui-manifest.record.json b/scripts/sdui-manifest.record.json index 0ec69dc13de..c4de1db3cd0 100644 --- a/scripts/sdui-manifest.record.json +++ b/scripts/sdui-manifest.record.json @@ -49,6 +49,6 @@ "ui/packages/types" ], "registrySources": 922, - "registrySourcesSha256": "a71feb98497ec8c6958f52d5e1d6d85b5f64cfd30efed87e1f4a80678dc82909" + "registrySourcesSha256": "6ae9bbeaed45f5221fd9f6163c3a6b66744a4aae8b74878d242fb334d42b7f0f" } } From b5d9b5532aae9d84b6785fee9384076d2bf1d7f4 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 13:12:49 +0000 Subject: [PATCH 14/19] chore(i18n): delete 92 dead keys from all ten locale packs Re-measured with ui/scripts/check-i18n-dead-keys.mjs on 6d15acab: 122 CONFIRMED. 92 are deleted here from ar/de/en/es/fr/ja/ko/pt/ru/zh. The other 30 stay: 28 are the `_one`/`_other` plural forms of keys that call sites pass with `count` (i18next resolves them), and 2 are `console.ai.agentLabels.*` entries the pack marks as kept on purpose. Claude-Session: https://claude.ai/code/session_01FpUtgwNrSiRgWJ8GAepPeP Co-authored-by: Claude --- ui/packages/i18n/src/locales/ar.ts | 92 ----------------------------- ui/packages/i18n/src/locales/de.ts | 92 ----------------------------- ui/packages/i18n/src/locales/en.ts | 93 ------------------------------ ui/packages/i18n/src/locales/es.ts | 92 ----------------------------- ui/packages/i18n/src/locales/fr.ts | 92 ----------------------------- ui/packages/i18n/src/locales/ja.ts | 92 ----------------------------- ui/packages/i18n/src/locales/ko.ts | 92 ----------------------------- ui/packages/i18n/src/locales/pt.ts | 92 ----------------------------- ui/packages/i18n/src/locales/ru.ts | 92 ----------------------------- ui/packages/i18n/src/locales/zh.ts | 92 ----------------------------- 10 files changed, 921 deletions(-) diff --git a/ui/packages/i18n/src/locales/ar.ts b/ui/packages/i18n/src/locales/ar.ts index eac8180d458..aba4f7625aa 100644 --- a/ui/packages/i18n/src/locales/ar.ts +++ b/ui/packages/i18n/src/locales/ar.ts @@ -175,7 +175,6 @@ const ar = { formInvalid: "يرجى التحقق من الحقول المميزة: {{fields}}", formInvalidJoiner: "، ", unique: "{{field}} يجب أن يكون فريداً", - type: "{{field}} يجب أن يكون {{type}} صالحاً", }, form: { noPermissionToSave: "ليس لديك إذن لحفظ هذا السجل.", @@ -220,10 +219,7 @@ const ar = { conflictMessage: "قام شخص آخر بتغيير هذا السجل أثناء تحريرك. الكتابة فوقه ستستبدل تغييراته بتغييراتك.", conflictLatestVersion: "وقت حفظه: {{time}}", conflictOverwrite: "الكتابة فوقه", - removeItem: "إزالة عنصر", fieldRequired: "هذا الحقل مطلوب", - invalidFormat: "صيغة غير صالحة", - saveSuccess: "تم الحفظ بنجاح", saveError: "فشل في الحفظ", unsavedChanges: "لديك تغييرات غير محفوظة. هل أنت متأكد أنك تريد المغادرة؟", stepOf: "الخطوة {{current}} من {{total}}", @@ -236,7 +232,6 @@ const ar = { update: "تحديث", createSuccess: "تم إنشاء {{object}}", updateSuccess: "تم تحديث {{object}}", - deleteSuccess: "تم حذف {{object}}", created: "تم الإنشاء", saved: "تم الحفظ", savedNamed: "تم حفظ {{title}}", @@ -421,22 +416,15 @@ const ar = { }, table: { rowsPerPage: "صفوف في الصفحة", - showing: "عرض {{from}} إلى {{to}} من {{total}}", noRows: "لا توجد صفوف للعرض", sortAsc: "ترتيب تصاعدي", sortDesc: "ترتيب تنازلي", - filterColumn: "تصفية {{column}}", columns: "الأعمدة", exportCSV: "تصدير CSV", - exportExcel: "تصدير Excel", selectRow: "تحديد صف", selectAllRows: "تحديد جميع الصفوف", - expandRow: "توسيع الصف", - collapseRow: "طي الصف", rowActions: "إجراءات الصف", hideColumn: "إخفاء العمود", - freezeColumn: "تجميد العمود", - unfreezeColumn: "إلغاء تجميد العمود", pageInfo: "صفحة {{current}} من {{total}}", firstPage: "الصفحة الأولى", previousPage: "الصفحة السابقة", @@ -546,7 +534,6 @@ const ar = { refreshing: "جارٍ التحديث…", openRecord: "فتح السجل", recordUnavailable: "{{object}} · السجل غير متاح", - rowHeight: "ارتفاع الصف: {{mode}}", openMenu: "فتح القائمة", empty: "فارغ", yes: "نعم", @@ -980,8 +967,6 @@ const ar = { addCard: "إضافة بطاقة", addColumn: "إضافة عمود", moveCard: "نقل بطاقة", - deleteCard: "حذف بطاقة", - deleteColumn: "حذف عمود", noCards: "لا بطاقات", columnCards: "بطاقات العمود {{column}}", laneCards: "بطاقات {{column}} - {{lane}}", @@ -1181,11 +1166,7 @@ const ar = { changeViewType: "تغيير نوع العرض", deleteView: "حذف العرض", addView: "إضافة عرض", - unsavedChanges: "تغييرات غير محفوظة", - saveAsView: "حفظ كعرض", moreViews: "{{count}} المزيد", - activeFilters: "الفلاتر نشطة", - activeSort: "الترتيب نشط", manageViews: "إدارة العروض", manageAllViews: "إدارة جميع العروض…", manageViewsDescription: "إعادة ترتيب جميع طرق عرض هذا الكائن أو إعادة تسميتها أو تثبيتها أو حذفها.", @@ -1551,10 +1532,6 @@ const ar = { last_90_days: "آخر 90 يومًا", }, }, - addWidget: "إضافة أداة", - editLayout: "تعديل التخطيط", - saveLayout: "حفظ التخطيط", - resetLayout: "إعادة تعيين التخطيط", total: "الإجمالي", noDataSourceFor: "لا مصدر بيانات لـ", config: { @@ -1666,8 +1643,6 @@ const ar = { }, }, appDesigner: { - createApp: "إنشاء تطبيق", - editApp: "تعديل التطبيق", basicInfo: "المعلومات الأساسية", objects: "الكائنات", navigation: "التنقل", @@ -1677,11 +1652,9 @@ const ar = { appDescription: "الوصف", appIcon: "الأيقونة", template: "القالب", - selectObjects: "تحديد الكائنات", searchObjects: "البحث في الكائنات…", selectAll: "تحديد الكل", deselectAll: "إلغاء تحديد الكل", - navBuilder: "منشئ التنقل", addGroup: "إضافة مجموعة", addUrl: "إضافة URL", addSeparator: "إضافة فاصل", @@ -1692,25 +1665,9 @@ const ar = { complete: "إكمال", snakeCaseHint: "يجب أن يكون بصيغة snake_case (مثل my_app)", modeEdit: "تعديل", - modePreview: "معاينة", - modeCode: "كود", addWidget: "إضافة عنصر واجهة", widgetProperties: "خصائص العنصر", dataSource: "مصدر البيانات", - valueField: "حقل القيمة", - aggregate: "تجميع", - colorVariant: "متغير اللون", - addComponent: "إضافة مكون", - componentProperties: "خصائص المكون", - viewType: "نوع العرض", - fields: "الحقول", - toolbar: "شريط الأدوات", - showSearch: "إظهار البحث", - showFilters: "إظهار الفلاتر", - showSort: "إظهار الترتيب", - appearance: "المظهر", - rowHeight: "ارتفاع الصف", - livePreview: "معاينة مباشرة", stepBasicDesc: "الاسم والعنوان والأيقونة", stepObjectsDesc: "اختيار كائنات الأعمال", stepNavigationDesc: "بناء شجرة التنقل", @@ -1949,8 +1906,6 @@ const ar = { ctaTopUp: "أضف أرصدة للمتابعة", ariaLabel: "استخدام الذكاء الاصطناعي: {{status}}", }, - workspaceTitle: "مساحة عمل الذكاء الاصطناعي", - workspaceSubtitle: "اسأل وافحص واستأنف المحادثات", openChats: "فتح المحادثات", chats: "المحادثات", chatsDescription: "تصفّح محادثات الذكاء الاصطناعي وأدرها.", @@ -1993,8 +1948,6 @@ const ar = { saveRename: "حفظ الاسم", cancelRename: "إلغاء إعادة التسمية", loadingHistory: "جارٍ تحميل سجل المحادثة…", - conversationReady: "المحادثة جاهزة", - preparingConversation: "جارٍ تجهيز محادثة جديدة", offlineDemoMode: "وضع العرض دون اتصال — قائمة الوكلاء غير متاحة", sendFailedRateLimited: "أنت ترسل الرسائل بسرعة كبيرة. رسالتك محفوظة بالأسفل — انتظر لحظة ثم أعد المحاولة.", sendFailedGeneric: "تعذّر إرسال رسالتك. وهي محفوظة بالأسفل — يرجى إعادة المحاولة.", @@ -2189,7 +2142,6 @@ const ar = { older: "أقدم", }, }, - title: "وحدة تحكم ObjectStack", initializing: "جارٍ فتح وحدة التحكم…", skipToContent: "الانتقال إلى المحتوى", breadcrumb: { @@ -2258,9 +2210,7 @@ const ar = { }, error: { connectionFailed: "تعذّر الاتصال بالخادم", - serverUnreachable: "الخادم {{url}} غير قابل للوصول.", checkServer: "يرجى التحقق من اتصال الشبكة أو من تشغيل الواجهة الخلفية.", - timeout: "انتهت مهلة الاتصال بعد 10 ثوانٍ.", }, actions: { retry: "إعادة المحاولة", @@ -2313,7 +2263,6 @@ const ar = { systemTheme: "مظهر النظام", actions: "الإجراءات", openFullSearch: "فتح صفحة البحث الكاملة", - createApp: "إنشاء تطبيق جديد", searching: "جارٍ البحث…", records: "السجلات", recentRecords: "شوهد مؤخراً", @@ -2459,7 +2408,6 @@ const ar = { selectOption: "تحديد…", noEligibleFieldForType: "لا يوجد حقل مؤهل من هذا النوع. أضف حقلاً إلى الكائن أولاً.", viewTypeUnavailable: "لا يحتوي هذا الكائن على حقل يمكن أن يعمل كـ \"{{field}}\".", - viewTypeUnavailableShort: "غير مدعوم من هذا الكائن.", chartType: "نوع المخطط", chartTypeHelp: "يحدد كيفية عرض البيانات.", chartTypeBar: "مخطط شريطي", @@ -2475,7 +2423,6 @@ const ar = { chartDimensionHelp: "بُعد مجموعة البيانات الذي تُجمَّع القيم حسبه (اختياري).", viewTypeUnavailableDataset: "لا يوفّر هذا الكائن أي مجموعة بيانات للمخطط.", noDatasetMeasure: "لا تعلن مجموعة البيانات هذه عن أي مقياس للمخطط.", - ufTabs: "علامات تبويب", ufAddField: "+ إضافة حقل فلتر…", ufShowAllRecords: "إظهار علامة \"جميع السجلات\"", }, @@ -3041,12 +2988,8 @@ const ar = { }, }, errors: { - networkError: "خطأ في الشبكة. يرجى التحقق من اتصالك.", - serverError: "خطأ في الخادم. يرجى المحاولة مرة أخرى لاحقاً.", - notFound: "المورد غير موجود.", unauthorized: "ليس لديك صلاحية لتنفيذ هذا الإجراء.", forbidden: "تم رفض الوصول.", - timeout: "انتهت مهلة الطلب. يرجى المحاولة مرة أخرى.", unknown: "حدث خطأ غير متوقع.", }, refusalDetails: { @@ -3066,8 +3009,6 @@ const ar = { namePlaceholder: "مثلاً، Acme Inc", slugLabel: "معرف URL", slugHint: "يستخدم في عناوين URL. أحرف صغيرة وأرقام وشرطات فقط.", - invite: "دعوة عضو", - members: "الأعضاء", settings: "إعدادات المؤسسة", multiOrgDisabled: "إنشاء مؤسسات جديدة معطّل في هذا النشر.", createFailed: 'تعذّر إنشاء المؤسسة', @@ -3088,7 +3029,6 @@ const ar = { sidebar: { packageManagement: "إدارة الحزم", searchNavigation: "البحث في التنقل…", - settings: "الإعدادات", helpTooltip: "المساعدة والوثائق", activityFeed: "موجز النشاط", notifications: "الإشعارات", @@ -3096,7 +3036,6 @@ const ar = { inbox: "صندوق الوارد", inboxAriaLabel: "فتح صندوق الوارد", area: "المنطقة", - scope: "النطاق", recent: "الأخيرة", starred: "المميزة", removeFromFavorites: "إزالة {{name}} من المفضلة", @@ -3170,9 +3109,7 @@ const ar = { publishedVerified: "تم النشر والتحقق — {{count}} صف(صفوف) من البيانات التجريبية فعّالة الآن.", capabilityWarn: "فحص التأليف: {{count}} مرجع(مراجع) قدرات لا تشير إلى شيء.", }, - title: "الرئيسية", nav: "الرئيسية", - allApps: "جميع التطبيقات", yourApps: "تطبيقاتك", showMoreApps: "المزيد", showLess: "عرض أقل", @@ -3190,8 +3127,6 @@ const ar = { }, loading: "جارٍ التحميل…", welcome: "مرحباً بك في ObjectUI", - welcomeDescription: "ابدأ بإنشاء تطبيقك الأول أو تكوين إعدادات النظام.", - createFirstApp: "أنشئ أول تطبيق لك", systemSettings: "إعدادات النظام", browseMarketplace: "تصفح متجر التطبيقات", quickActions: { @@ -3242,9 +3177,7 @@ const ar = { title: "المميزة", }, gettingStarted: { - title: "تخصيص الصفحة الرئيسية", description: "ضع علامة مميزة على تطبيق لتثبيته هنا والوصول إليه بنقرة واحدة. سيظهر كل ما تفتحه تلقائياً في \"فُتح مؤخراً\".", - cta: "استعراض جميع التطبيقات", }, }, layout: { @@ -3264,13 +3197,6 @@ const ar = { appSwitcher: { switchApplication: "تبديل التطبيق", appsAvailable: "{{count}} من التطبيقات المتاحة", - editNavigation: "تحرير التنقل", - home: "الرئيسية", - addApp: "إضافة تطبيق", - editApp: "تعديل التطبيق", - manageAllApps: "إدارة جميع التطبيقات", - systemConsole: "وحدة تحكم النظام", - noAppsConfigured: "لا تطبيقات مُهيأة", allApps: "جميع التطبيقات", editAppInStudio: "تعديل {{app}} في Studio", backToHome: "العودة إلى الرئيسية", @@ -3439,7 +3365,6 @@ const ar = { // objectui#13461 — the top bar's avatar button, which holds only the avatar. accountMenu: 'قائمة الحساب', profile: "الملف الشخصي", - settings: "الإعدادات", logout: "تسجيل الخروج", preferences: "التفضيلات", theme: "السمة", @@ -3447,17 +3372,8 @@ const ar = { }, report: { total: "الإجمالي", - rowTotal: "إجمالي الصف", - columnTotal: "إجمالي العمود", - grandTotal: "الإجمالي الكلي", totals: "الإجماليات", - rowsLabel: "صف", - columnsLabel: "عمود", - allLabel: "(الكل)", - emptyLabel: "(فارغ)", loading: "جارٍ التحميل…", - failedToLoad: "فشل تحميل المصفوفة: {{message}}", - needsAcross: "يتطلب تقرير المصفوفة حقل `groupingsAcross` واحداً على الأقل.", forbiddenMessage: "ليست لديك صلاحية لعرض البيانات الخاصة بهذا التقرير.", aggregate: { count: "عدد", @@ -3748,22 +3664,14 @@ const ar = { publicForm: { submit: "إرسال", submitting: "جارٍ الإرسال…", - submitAnother: "إرسال رد آخر", poweredBy: "مدعوم بـ ObjectStack", - secureNotice: "يتم نقل معلوماتك بأمان وتستخدم فقط لمعالجة طلبك.", thankYouTitle: "شكراً!", thankYouMessage: "تم استلام إرسالك بنجاح.", redirecting: "إعادة توجيه خلال {{seconds}} ثانية(ثوانٍ)…", redirectPending: "جارٍ إعادة التوجيه…", - unavailableTitle: "النموذج غير متاح", - unavailableDescription: "لا يوجد نموذج عام متاح على هذا الرابط. تأكد من تمكين المشاركة المجهولة للعرض الأساسي.", - tryDemo: "تجربة العرض التوضيحي", - retry: "إعادة المحاولة", loading: "جارٍ تحميل النموذج…", requiredHint: "* حقل مطلوب", requiredFields: "الحقول المطلوبة: {{fields}}", - consentLabelDefault: "أوافق على سياسة الخصوصية وأعطي موافقتي على معالجة بياناتي لهذا الطلب.", - consentLink: "سياسة الخصوصية", consentRequired: "يرجى قبول سياسة الخصوصية للمتابعة.", rateLimited: "خذ لحظة لمراجعة إجاباتك قبل الإرسال.", redirectBlocked: "تم قبول الإرسال، لكن رابط إعادة التوجيه تم حظره لأسباب أمنية.", diff --git a/ui/packages/i18n/src/locales/de.ts b/ui/packages/i18n/src/locales/de.ts index a8695048e2f..4bcef188377 100644 --- a/ui/packages/i18n/src/locales/de.ts +++ b/ui/packages/i18n/src/locales/de.ts @@ -161,7 +161,6 @@ const de = { formInvalid: "Bitte überprüfen Sie die markierten Felder: {{fields}}", formInvalidJoiner: ", ", unique: "{{field}} muss eindeutig sein", - type: "{{field}} muss ein gültiger {{type}} sein", }, form: { noPermissionToSave: "Sie haben keine Berechtigung, diesen Datensatz zu speichern.", @@ -206,10 +205,7 @@ const de = { conflictMessage: "Dieser Datensatz wurde während Ihrer Bearbeitung von jemand anderem geändert. Beim Überschreiben werden dessen Änderungen durch Ihre ersetzt.", conflictLatestVersion: "Deren Speicherung: {{time}}", conflictOverwrite: "Überschreiben", - removeItem: "Element entfernen", fieldRequired: "Dieses Feld ist erforderlich", - invalidFormat: "Ungültiges Format", - saveSuccess: "Erfolgreich gespeichert", saveError: "Speichern fehlgeschlagen", unsavedChanges: "Sie haben nicht gespeicherte Änderungen. Möchten Sie die Seite wirklich verlassen?", stepOf: "Schritt {{current}} von {{total}}", @@ -222,7 +218,6 @@ const de = { update: "Aktualisieren", createSuccess: "{{object}} erfolgreich erstellt", updateSuccess: "{{object}} erfolgreich aktualisiert", - deleteSuccess: "{{object}} erfolgreich gelöscht", created: "Erstellt", saved: "Gespeichert", savedNamed: "{{title}} gespeichert", @@ -403,22 +398,15 @@ const de = { }, table: { rowsPerPage: "Zeilen pro Seite", - showing: "{{from}} bis {{to}} von {{total}} angezeigt", noRows: "Keine Zeilen vorhanden", sortAsc: "Aufsteigend sortieren", sortDesc: "Absteigend sortieren", - filterColumn: "{{column}} filtern", columns: "Spalten", exportCSV: "CSV exportieren", - exportExcel: "Excel exportieren", selectRow: "Zeile auswählen", selectAllRows: "Alle Zeilen auswählen", - expandRow: "Zeile erweitern", - collapseRow: "Zeile reduzieren", rowActions: "Zeilenaktionen", hideColumn: "Spalte ausblenden", - freezeColumn: "Spalte fixieren", - unfreezeColumn: "Spalte lösen", pageInfo: "Seite {{current}} von {{total}}", firstPage: "Erste Seite", previousPage: "Vorherige Seite", @@ -500,7 +488,6 @@ const de = { refreshing: "Wird aktualisiert…", openRecord: "Datensatz öffnen", recordUnavailable: "{{object}} · Datensatz nicht verfügbar", - rowHeight: "Zeilenhöhe: {{mode}}", openMenu: "Menü öffnen", empty: "Leer", yes: "Ja", @@ -879,8 +866,6 @@ const de = { addCard: "Karte hinzufügen", addColumn: "Spalte hinzufügen", moveCard: "Karte verschieben", - deleteCard: "Karte löschen", - deleteColumn: "Spalte löschen", noCards: "Keine Karten", columnCards: "Karten in Spalte {{column}}", laneCards: "Karten in {{column}} - {{lane}}", @@ -1068,11 +1053,7 @@ const de = { changeViewType: "Ansichtstyp ändern", deleteView: "Ansicht löschen", addView: "Ansicht hinzufügen", - unsavedChanges: "Ungespeicherte Änderungen", - saveAsView: "Als Ansicht speichern", moreViews: "{{count}} weitere", - activeFilters: "Filter aktiv", - activeSort: "Sortierung aktiv", manageViews: "Ansichten verwalten", manageAllViews: "Alle Ansichten verwalten…", manageViewsDescription: "Alle Ansichten dieses Objekts neu anordnen, umbenennen, anheften oder löschen.", @@ -1388,10 +1369,6 @@ const de = { last_90_days: "Letzte 90 Tage", }, }, - addWidget: "Widget hinzufügen", - editLayout: "Layout bearbeiten", - saveLayout: "Layout speichern", - resetLayout: "Layout zurücksetzen", total: "Gesamt", noDataSourceFor: "Keine Datenquelle verfügbar für", config: { @@ -1503,8 +1480,6 @@ const de = { }, }, appDesigner: { - createApp: "Anwendung erstellen", - editApp: "Anwendung bearbeiten", basicInfo: "Grundlegende Infos", objects: "Objekte", navigation: "Navigation", @@ -1514,11 +1489,9 @@ const de = { appDescription: "Beschreibung", appIcon: "Symbol", template: "Vorlage", - selectObjects: "Objekte auswählen", searchObjects: "Objekte suchen…", selectAll: "Alle auswählen", deselectAll: "Alle abwählen", - navBuilder: "Navigations-Builder", addGroup: "Gruppe hinzufügen", addUrl: "URL hinzufügen", addSeparator: "Trenner hinzufügen", @@ -1529,25 +1502,9 @@ const de = { complete: "Fertigstellen", snakeCaseHint: "Muss snake_case sein (z.B. my_app)", modeEdit: "Bearbeiten", - modePreview: "Vorschau", - modeCode: "Code", addWidget: "Widget hinzufügen", widgetProperties: "Widget-Eigenschaften", dataSource: "Datenquelle", - valueField: "Wertfeld", - aggregate: "Aggregat", - colorVariant: "Farbvariante", - addComponent: "Komponente hinzufügen", - componentProperties: "Komponenteneigenschaften", - viewType: "Ansichtstyp", - fields: "Felder", - toolbar: "Symbolleiste", - showSearch: "Suche anzeigen", - showFilters: "Filter anzeigen", - showSort: "Sortierung anzeigen", - appearance: "Erscheinungsbild", - rowHeight: "Zeilenhöhe", - livePreview: "Echtzeit-Vorschau", stepBasicDesc: "Name, Titel und Symbol", stepObjectsDesc: "Geschäftsobjekte auswählen", stepNavigationDesc: "Navigationsbaum erstellen", @@ -1770,8 +1727,6 @@ const de = { ctaTopUp: "Credits hinzufügen, um fortzufahren", ariaLabel: "KI-Nutzung: {{status}}", }, - workspaceTitle: "KI-Workspace", - workspaceSubtitle: "Fragen stellen, prüfen und Unterhaltungen fortsetzen", openChats: "Chats öffnen", chats: "Chats", chatsDescription: "KI-Unterhaltungen durchsuchen und verwalten.", @@ -1814,8 +1769,6 @@ const de = { saveRename: "Umbenennung speichern", cancelRename: "Umbenennung abbrechen", loadingHistory: "Unterhaltungsverlauf wird geladen…", - conversationReady: "Unterhaltung bereit", - preparingConversation: "Neue Unterhaltung wird vorbereitet", offlineDemoMode: "Offline-Demomodus — Agentenliste nicht verfügbar", sendFailedRateLimited: "Sie senden zu schnell Nachrichten. Ihre Nachricht wird unten aufbewahrt — warten Sie einen Moment und versuchen Sie es erneut.", sendFailedGeneric: "Ihre Nachricht konnte nicht gesendet werden. Sie wird unten aufbewahrt — bitte versuchen Sie es erneut.", @@ -2010,7 +1963,6 @@ const de = { older: "Älter", }, }, - title: "ObjectStack Konsole", initializing: "Ihre Konsole wird geöffnet…", skipToContent: "Zum Inhalt springen", breadcrumb: { @@ -2071,9 +2023,7 @@ const de = { }, error: { connectionFailed: "Verbindung zum Server nicht möglich", - serverUnreachable: "Der Server {{url}} ist nicht erreichbar.", checkServer: "Bitte prüfen Sie Ihre Netzwerkverbindung oder ob das Backend läuft.", - timeout: "Zeitüberschreitung der Verbindung nach 10 Sekunden.", }, actions: { retry: "Erneut versuchen", @@ -2126,7 +2076,6 @@ const de = { systemTheme: "Systemdesign", actions: "Aktionen", openFullSearch: "Vollständige Suchseite öffnen", - createApp: "Neue App erstellen", searching: "Suche…", records: "Datensätze", recentRecords: "Zuletzt angesehen", @@ -2257,7 +2206,6 @@ const de = { selectOption: "Auswählen…", noEligibleFieldForType: "Kein geeignetes Feld dieses Typs. Fügen Sie zuerst eines zum Objekt hinzu.", viewTypeUnavailable: "Dieses Objekt hat kein Feld, das als „{{field}}“ dienen kann.", - viewTypeUnavailableShort: "Nicht von diesem Objekt unterstützt.", chartType: "Diagrammtyp", chartTypeHelp: "Bestimmt, wie die Daten gerendert werden.", chartTypeBar: "Balkendiagramm", @@ -2273,7 +2221,6 @@ const de = { chartDimensionHelp: "Die Dimension des Datensatzes, nach der die Werte gruppiert werden (optional).", viewTypeUnavailableDataset: "Dieses Objekt stellt keinen Datensatz für Diagramme bereit.", noDatasetMeasure: "Dieser Datensatz deklariert keine Kennzahl für ein Diagramm.", - ufTabs: "Registerkarten", ufAddField: "+ Filterfeld hinzufügen…", ufShowAllRecords: "Registerkarte „Alle Datensätze“ anzeigen", }, @@ -2839,12 +2786,8 @@ const de = { }, }, errors: { - networkError: "Netzwerkfehler. Bitte überprüfen Sie Ihre Verbindung.", - serverError: "Serverfehler. Bitte versuchen Sie es später erneut.", - notFound: "Ressource nicht gefunden.", unauthorized: "Sie sind nicht berechtigt, diese Aktion auszuführen.", forbidden: "Zugriff verweigert.", - timeout: "Zeitüberschreitung. Bitte versuchen Sie es erneut.", unknown: "Ein unerwarteter Fehler ist aufgetreten.", }, refusalDetails: { @@ -2864,8 +2807,6 @@ const de = { namePlaceholder: "z.B. Acme Inc", slugLabel: "URL-Slug", slugHint: "In URLs verwendet. Nur Kleinbuchstaben, Zahlen und Bindestriche.", - invite: "Mitglied einladen", - members: "Mitglieder", settings: "Organisationseinstellungen", multiOrgDisabled: "Das Erstellen neuer Organisationen ist auf dieser Instanz deaktiviert.", createFailed: 'Organisation konnte nicht erstellt werden', @@ -2886,7 +2827,6 @@ const de = { sidebar: { packageManagement: "Paketverwaltung", searchNavigation: "Navigation durchsuchen…", - settings: "Einstellungen", helpTooltip: "Hilfe und Dokumentation", activityFeed: "Aktivitätsfeed", notifications: "Benachrichtigungen", @@ -2894,7 +2834,6 @@ const de = { inbox: "Posteingang", inboxAriaLabel: "Posteingang öffnen", area: "Bereich", - scope: "Bereich", recent: "Zuletzt", starred: "Markiert", removeFromFavorites: "{{name}} aus Favoriten entfernen", @@ -2964,9 +2903,7 @@ const de = { publishedVerified: "Veröffentlicht und geprüft — {{count}} Beispielzeile(n) sind live.", capabilityWarn: "Autorenprüfung: {{count}} Capability-Verweis(e) verweisen ins Leere.", }, - title: "Startseite", nav: "Startseite", - allApps: "Alle Anwendungen", yourApps: "Ihre Apps", showMoreApps: "Mehr", showLess: "Weniger anzeigen", @@ -2984,8 +2921,6 @@ const de = { }, loading: "Wird geladen…", welcome: "Willkommen bei ObjectUI", - welcomeDescription: "Erstellen Sie zunächst Ihre erste Anwendung oder konfigurieren Sie Ihre Systemeinstellungen.", - createFirstApp: "Erstellen Sie Ihre erste App", systemSettings: "Systemeinstellungen", browseMarketplace: "App-Marktplatz durchsuchen", quickActions: { @@ -3032,9 +2967,7 @@ const de = { title: "Markiert", }, gettingStarted: { - title: "Startseite anpassen", description: "Markieren Sie eine App, um sie hier anzuheften und mit einem Klick darauf zuzugreifen. Alles, was Sie öffnen, wird automatisch unter „Zuletzt geöffnet“ angezeigt.", - cta: "Alle Anwendungen durchsuchen", }, }, layout: { @@ -3054,13 +2987,6 @@ const de = { appSwitcher: { switchApplication: "Anwendung wechseln", appsAvailable: "{{count}} Apps verfügbar", - editNavigation: "Navigation bearbeiten", - home: "Startseite", - addApp: "App hinzufügen", - editApp: "App bearbeiten", - manageAllApps: "Alle Apps verwalten", - systemConsole: "Systemkonsole", - noAppsConfigured: "Keine Apps konfiguriert", allApps: "Alle Apps", editAppInStudio: "{{app}} im Studio bearbeiten", backToHome: "Zurück zur Startseite", @@ -3210,7 +3136,6 @@ const de = { // objectui#13461 — the top bar's avatar button, which holds only the avatar. accountMenu: 'Kontomenü', profile: "Profil", - settings: "Einstellungen", logout: "Abmelden", preferences: "Einstellungen", theme: "Design", @@ -3218,17 +3143,8 @@ const de = { }, report: { total: "Gesamt", - rowTotal: "Zeilensumme", - columnTotal: "Spaltensumme", - grandTotal: "Gesamtsumme", totals: "Summen", - rowsLabel: "Zeile", - columnsLabel: "Spalte", - allLabel: "(Alle)", - emptyLabel: "(Leer)", loading: "Wird geladen…", - failedToLoad: "Matrix konnte nicht geladen werden: {{message}}", - needsAcross: "Matrixbericht erfordert mindestens ein `groupingsAcross`-Feld.", forbiddenMessage: "Sie haben keine Berechtigung, die Daten dieses Berichts anzuzeigen.", aggregate: { count: "Anzahl", @@ -3495,22 +3411,14 @@ const de = { publicForm: { submit: "Absenden", submitting: "Sende…", - submitAnother: "Weitere Antwort senden", poweredBy: "Bereitgestellt von ObjectStack", - secureNotice: "Ihre Informationen werden sicher übertragen und nur zur Bearbeitung Ihrer Anfrage verwendet.", thankYouTitle: "Vielen Dank!", thankYouMessage: "Ihre Einreichung wurde erfolgreich empfangen.", redirecting: "Weiterleitung in {{seconds}} Sekunden…", redirectPending: "Weiterleitung…", - unavailableTitle: "Formular nicht verfügbar", - unavailableDescription: "Unter dieser URL ist kein öffentliches Formular verfügbar. Stellen Sie sicher, dass die zugrunde liegende Ansicht die anonyme Freigabe aktiviert hat.", - tryDemo: "Demo ausprobieren", - retry: "Erneut versuchen", loading: "Formular wird geladen…", requiredHint: "* Pflichtfeld", requiredFields: "Pflichtfelder: {{fields}}", - consentLabelDefault: "Ich stimme der Datenschutzrichtlinie zu und willige in die Verarbeitung meiner Daten für diese Anfrage ein.", - consentLink: "Datenschutzrichtlinie", consentRequired: "Bitte akzeptieren Sie die Datenschutzrichtlinie, um fortzufahren.", rateLimited: "Bitte nehmen Sie sich einen Moment, um Ihre Antworten zu überprüfen, bevor Sie absenden.", redirectBlocked: "Einreichung akzeptiert, aber die Weiterleitungs-URL wurde aus Sicherheitsgründen blockiert.", diff --git a/ui/packages/i18n/src/locales/en.ts b/ui/packages/i18n/src/locales/en.ts index a33346faa3b..3ece495d971 100644 --- a/ui/packages/i18n/src/locales/en.ts +++ b/ui/packages/i18n/src/locales/en.ts @@ -210,7 +210,6 @@ const en = { // comma into the English toast (objectstack#5407). formInvalidJoiner: ', ', unique: '{{field}} must be unique', - type: '{{field}} must be a valid {{type}}', }, form: { noPermissionToSave: "You don't have permission to save this record.", @@ -234,10 +233,7 @@ const en = { // `validation.formInvalidJoiner` — because the fix is to the metadata, not // to anything the person filling the form can type. visibleWhenFaulted: "Can't submit: the visibleWhen rule of {{fields}} could not be evaluated. The rule must be fixed before this form can be submitted.", - removeItem: 'Remove item', fieldRequired: 'This field is required', - invalidFormat: 'Invalid format', - saveSuccess: 'Saved successfully', saveError: 'Failed to save', unsavedChanges: 'You have unsaved changes. Are you sure you want to leave?', discardTitle: 'Discard changes?', @@ -324,7 +320,6 @@ const en = { update: 'Update', createSuccess: '{{object}} created successfully', updateSuccess: '{{object}} updated successfully', - deleteSuccess: '{{object}} deleted successfully', // plugin-form's own feedback chrome (objectui#11039): the success toast // a form raises when the author declared no `successMessage` (`created` // after a create, `saved` after an edit), the master-detail form's edit @@ -632,22 +627,15 @@ const en = { }, table: { rowsPerPage: 'Rows per page', - showing: 'Showing {{from}} to {{to}} of {{total}}', noRows: 'No rows to display', sortAsc: 'Sort ascending', sortDesc: 'Sort descending', - filterColumn: 'Filter {{column}}', columns: 'Columns', exportCSV: 'Export CSV', - exportExcel: 'Export Excel', selectRow: 'Select row', selectAllRows: 'Select all rows', - expandRow: 'Expand row', - collapseRow: 'Collapse row', rowActions: 'Row actions', hideColumn: 'Hide column', - freezeColumn: 'Freeze column', - unfreezeColumn: 'Unfreeze column', pageInfo: 'Page {{current}} of {{total}}', firstPage: 'First page', previousPage: 'Previous page', @@ -702,7 +690,6 @@ const en = { bulkSelectAllMatching: 'Select all {{count}} matching', bulkAllMatchingSelected: 'All {{count}} matching records are selected.', bulkActions: 'Bulk actions', - rowHeight: 'Row height: {{mode}}', empty: 'Empty', yes: 'Yes', no: 'No', @@ -1127,8 +1114,6 @@ const en = { addCard: 'Add card', addColumn: 'Add column', moveCard: 'Move card', - deleteCard: 'Delete card', - deleteColumn: 'Delete column', noCards: 'No cards', columnCards: '{{column}} cards', laneCards: '{{column}} - {{lane}} cards', @@ -1325,11 +1310,7 @@ const en = { changeViewType: 'Change View Type', deleteView: 'Delete View', addView: 'Add View', - unsavedChanges: 'Unsaved changes', - saveAsView: 'Save as View', moreViews: '{{count}} more', - activeFilters: 'Active filters', - activeSort: 'Active sort', manageViews: 'Manage views', manageAllViews: 'Manage all views…', manageViewsDescription: 'Reorder, rename, pin, or delete every view in this object.', @@ -1667,17 +1648,8 @@ const en = { }, report: { total: 'Total', - rowTotal: 'Row Total', - columnTotal: 'Column Total', - grandTotal: 'Grand Total', totals: 'Totals', - rowsLabel: 'Row', - columnsLabel: 'Column', - allLabel: '(All)', - emptyLabel: '(Empty)', loading: 'Loading…', - failedToLoad: 'Failed to load matrix: {{message}}', - needsAcross: 'Matrix report requires at least one `groupingsAcross` field.', // objectui#12330 — the report's no-access state; its title is `dashboard.widgetForbiddenTitle`. forbiddenMessage: 'You don’t have permission to view the data behind this report.', aggregate: { @@ -1714,10 +1686,6 @@ const en = { versionHistory: 'Version history', }, dashboard: { - addWidget: 'Add widget', - editLayout: 'Edit layout', - saveLayout: 'Save layout', - resetLayout: 'Reset layout', total: 'Total', noDataSourceFor: 'No data source available for', // objectui#7063 — the DEFAULT empty state every dashboard widget renders @@ -1876,8 +1844,6 @@ const en = { }, }, appDesigner: { - createApp: 'Create Application', - editApp: 'Edit Application', basicInfo: 'Basic Info', objects: 'Objects', navigation: 'Navigation', @@ -1887,11 +1853,9 @@ const en = { appDescription: 'Description', appIcon: 'Icon', template: 'Template', - selectObjects: 'Select Objects', searchObjects: 'Search objects…', selectAll: 'Select All', deselectAll: 'Deselect All', - navBuilder: 'Navigation Builder', addGroup: 'Add Group', addUrl: 'Add URL', addSeparator: 'Add Separator', @@ -1902,25 +1866,9 @@ const en = { complete: 'Complete', snakeCaseHint: 'Must be snake_case (e.g. my_app)', modeEdit: 'Edit', - modePreview: 'Preview', - modeCode: 'Code', addWidget: 'Add Widget', widgetProperties: 'Widget Properties', dataSource: 'Data Source', - valueField: 'Value Field', - aggregate: 'Aggregate', - colorVariant: 'Color Variant', - addComponent: 'Add Component', - componentProperties: 'Component Properties', - viewType: 'View Type', - fields: 'Fields', - toolbar: 'Toolbar', - showSearch: 'Show Search', - showFilters: 'Show Filters', - showSort: 'Show Sort', - appearance: 'Appearance', - rowHeight: 'Row Height', - livePreview: 'Live Preview', stepBasicDesc: 'Name, title, and icon', stepObjectsDesc: 'Select business objects', stepNavigationDesc: 'Build navigation tree', @@ -2079,7 +2027,6 @@ const en = { savedViewsUnavailable: 'Saved views for {{object}} could not be loaded', savedViewsRefused: 'The server refused this request, so this list is empty because it could not be read — not because this object has no saved views. Sign in again, or ask an administrator for access.', savedViewsUnreadable: 'This list is empty because it could not be read, not because this object has no saved views. Try again, and report this if it keeps happening.', - title: 'ObjectOS', initializing: 'Opening your console…', skipToContent: 'Skip to content', search: 'Search…', @@ -2222,9 +2169,7 @@ const en = { }, error: { connectionFailed: 'Cannot connect to server', - serverUnreachable: 'The server at {{url}} is unreachable.', checkServer: 'Please check your network connection or that the backend is running.', - timeout: 'Connection timed out after 10 seconds.', }, actions: { retry: 'Retry', @@ -2284,7 +2229,6 @@ const en = { systemTheme: 'System Theme', actions: 'Actions', openFullSearch: 'Open Full Search Page', - createApp: 'Create New App', }, ai: { pendingDrafts: { @@ -2321,8 +2265,6 @@ const en = { ctaTopUp: 'Add credits to continue', ariaLabel: 'AI usage: {{status}}', }, - workspaceTitle: 'AI Workspace', - workspaceSubtitle: 'Ask, inspect, and resume conversations', openChats: 'Open chats', chats: 'Chats', chatsDescription: 'Browse and manage AI conversations.', @@ -2372,8 +2314,6 @@ const en = { saveRename: 'Save rename', cancelRename: 'Cancel rename', loadingHistory: 'Loading conversation history…', - conversationReady: 'Conversation ready', - preparingConversation: 'Preparing a new conversation', offlineDemoMode: 'Offline demo mode — agent list unavailable', sendFailedRateLimited: "You're sending messages too quickly. Your message is kept below — wait a moment and try again.", @@ -2777,7 +2717,6 @@ const en = { selectOption: 'Select…', noEligibleFieldForType: 'No eligible field of this type. Add one to the object first.', viewTypeUnavailable: 'This object has no field that can serve as "{{field}}".', - viewTypeUnavailableShort: 'Not supported by this object.', chartType: 'Chart type', chartTypeHelp: 'Determines how the data is rendered.', chartTypeBar: 'Bar chart', @@ -2794,7 +2733,6 @@ const en = { viewTypeUnavailableDataset: 'This object exposes no dataset to chart.', noDatasetMeasure: 'This dataset declares no measure to chart.', endDateField: 'End date field', - ufTabs: 'Tabs', ufAddField: '+ Add filter field…', ufShowAllRecords: 'Show "All records" tab', }, @@ -3385,12 +3323,8 @@ const en = { }, }, errors: { - networkError: 'Network error. Please check your connection.', - serverError: 'Server error. Please try again later.', - notFound: 'Resource not found.', unauthorized: 'You are not authorized to perform this action.', forbidden: 'Access denied.', - timeout: 'Request timed out. Please try again.', unknown: 'An unexpected error occurred.', }, // A refused write's message (objectui#13332): its first sentence is shown, @@ -3413,8 +3347,6 @@ const en = { namePlaceholder: 'e.g., Acme Inc', slugLabel: 'URL slug', slugHint: 'Used in URLs. Only lowercase letters, numbers, and hyphens.', - invite: 'Invite member', - members: 'Members', settings: 'Organization settings', multiOrgDisabled: 'Creating new organizations is disabled on this instance.', createFailed: 'Failed to create organization', @@ -3433,7 +3365,6 @@ const en = { aboutBuildUnknown: 'Unknown build (no version was set when this console was built)', }, sidebar: { - settings: 'Settings', helpTooltip: 'Help & Documentation', activityFeed: 'Activity feed', notifications: 'Notifications', @@ -3441,7 +3372,6 @@ const en = { inbox: 'Inbox', inboxAriaLabel: 'Open inbox', area: 'Area', - scope: 'Scope', packageManagement: 'Package management', searchNavigation: 'Search navigation…', recent: 'Recent', @@ -3471,9 +3401,7 @@ const en = { }, }, home: { - title: 'Home', nav: 'Home', - allApps: 'All Applications', yourApps: 'Your apps', showMoreApps: 'More', showLess: 'Show less', @@ -3529,7 +3457,6 @@ const en = { open: 'Open', loading: 'Loading…', welcome: 'Build your business system with AI', - welcomeDescription: 'Describe your business in one sentence — AI generates the objects, screens, APIs and agent tools. Or start from scratch.', welcomeAdminDescription: 'Describe your business in one sentence — AI generates the objects, screens, APIs and agent tools. Or set things up yourself from the menu on the left.', welcomeAdminDescriptionNoBuild: 'Set up your first application from the Administration menu on the left. Once you have data, the AI assistant can help you explore it.', welcomeAdminDescriptionNoAi: 'Set up your first application from the Administration menu on the left.', @@ -3566,7 +3493,6 @@ const en = { publishedVerified: 'Published & verified — {{count}} sample row(s) live.', capabilityWarn: 'Authoring check: {{count}} capability reference(s) resolve nowhere.', }, - createFirstApp: 'Create app manually', systemSettings: 'System Settings', browseMarketplace: 'Browse App Marketplace', quickActions: { @@ -3593,10 +3519,8 @@ const en = { title: 'Starred', }, gettingStarted: { - title: 'Make this home yours', description: 'Star an app to pin it here for one-click access. Anything you open will show up under Recently Accessed automatically.', - cta: 'Browse all applications', }, appCard: { noDescription: 'No description', @@ -3605,15 +3529,8 @@ const en = { }, layout: { appSwitcher: { - home: 'Home', switchApplication: 'Switch Application', appsAvailable: '{{count}} apps available', - addApp: 'Add App', - editApp: 'Edit App', - editNavigation: 'Edit Navigation', - manageAllApps: 'Manage All Apps', - systemConsole: 'System Console', - noAppsConfigured: 'No apps configured', allApps: 'All apps', editAppInStudio: 'Edit {{app}} in Studio', backToHome: 'Back to home', @@ -3933,7 +3850,6 @@ const en = { // objectui#13461 — the top bar's avatar button, which holds only the avatar. accountMenu: 'Account menu', profile: 'Profile', - settings: 'Settings', logout: 'Log out', preferences: 'Preferences', theme: 'Theme', @@ -4205,9 +4121,7 @@ const en = { publicForm: { submit: 'Submit', submitting: 'Submitting…', - submitAnother: 'Submit another response', poweredBy: 'Powered by ObjectStack', - secureNotice: 'Your information is transmitted securely and only used to respond to your request.', thankYouTitle: 'Thank you!', thankYouMessage: 'Your submission has been received successfully.', redirecting: 'Redirecting in {{seconds}} seconds…', @@ -4215,19 +4129,12 @@ const en = { // pending (objectui#11071). `redirecting` above is the embeddable form's // countdown and carries a `{{seconds}}` hole this page has no value for. redirectPending: 'Redirecting…', - unavailableTitle: 'Form unavailable', - unavailableDescription: - 'No public form is available at this URL. Make sure the underlying view has anonymous sharing enabled and matches this slug.', - tryDemo: 'Try the demo', - retry: 'Retry', loading: 'Loading form…', requiredHint: '* Required field', // The console form page's client-side refusal when required rows are empty // (objectui#11071). `{{fields}}` is the empty rows' labels joined with a // comma; the colon, its spacing and the order are the pack's. requiredFields: 'Required: {{fields}}', - consentLabelDefault: 'I agree to the privacy policy and consent to my data being processed for this request.', - consentLink: 'Privacy policy', consentRequired: 'Please accept the privacy policy to continue.', rateLimited: 'Please take a moment to review your answers before submitting.', redirectBlocked: 'Submission accepted, but the redirect URL was blocked for security.', diff --git a/ui/packages/i18n/src/locales/es.ts b/ui/packages/i18n/src/locales/es.ts index e4447cd7f52..2aedf1d09c1 100644 --- a/ui/packages/i18n/src/locales/es.ts +++ b/ui/packages/i18n/src/locales/es.ts @@ -168,7 +168,6 @@ const es = { formInvalid: "Por favor, revise los campos resaltados: {{fields}}", formInvalidJoiner: ", ", unique: "El campo {{field}} debe ser único", - type: "{{field}} debe ser un {{type}} válido", }, form: { noPermissionToSave: "No tienes permiso para guardar este registro.", @@ -213,10 +212,7 @@ const es = { conflictMessage: "Otra persona modificó este registro mientras lo editaba. Sobrescribir reemplazará sus cambios por los suyos.", conflictLatestVersion: "Su guardado: {{time}}", conflictOverwrite: "Sobrescribir", - removeItem: "Eliminar elemento", fieldRequired: "Este campo es obligatorio", - invalidFormat: "Formato no válido", - saveSuccess: "Guardado correctamente", saveError: "Error al guardar", unsavedChanges: "Tiene cambios sin guardar. ¿Está seguro de que desea salir?", stepOf: "Paso {{current}} de {{total}}", @@ -229,7 +225,6 @@ const es = { update: "Actualizar", createSuccess: "{{object}} creado", updateSuccess: "{{object}} actualizado", - deleteSuccess: "{{object}} eliminado", created: "Creado", saved: "Guardado", savedNamed: "{{title}} guardado", @@ -411,22 +406,15 @@ const es = { }, table: { rowsPerPage: "Filas por página", - showing: "Mostrando {{from}} a {{to}} de {{total}}", noRows: "No hay filas para mostrar", sortAsc: "Ordenar ascendente", sortDesc: "Ordenar descendente", - filterColumn: "Filtrar {{column}}", columns: "Columnas", exportCSV: "Exportar CSV", - exportExcel: "Exportar Excel", selectRow: "Seleccionar fila", selectAllRows: "Seleccionar todas las filas", - expandRow: "Expandir fila", - collapseRow: "Contraer fila", rowActions: "Acciones de fila", hideColumn: "Ocultar columna", - freezeColumn: "Fijar columna", - unfreezeColumn: "Desfijar columna", pageInfo: "Página {{current}} de {{total}}", firstPage: "Primera página", previousPage: "Página anterior", @@ -515,7 +503,6 @@ const es = { refreshing: "Actualizando…", openRecord: "Abrir registro", recordUnavailable: "{{object}} · registro no disponible", - rowHeight: "Altura de fila: {{mode}}", openMenu: "Abrir menú", empty: "Vacío", yes: "Sí", @@ -907,8 +894,6 @@ const es = { addCard: "Añadir tarjeta", addColumn: "Añadir columna", moveCard: "Mover tarjeta", - deleteCard: "Eliminar tarjeta", - deleteColumn: "Eliminar columna", noCards: "Sin tarjetas", columnCards: "Tarjetas de la columna {{column}}", laneCards: "Tarjetas de {{column}} - {{lane}}", @@ -1099,11 +1084,7 @@ const es = { changeViewType: "Cambiar tipo de vista", deleteView: "Eliminar vista", addView: "Añadir vista", - unsavedChanges: "Cambios sin guardar", - saveAsView: "Guardar como vista", moreViews: "{{count}} más", - activeFilters: "Filtros activos", - activeSort: "Orden activo", manageViews: "Administrar vistas", manageAllViews: "Administrar todas las vistas…", manageViewsDescription: "Reordenar, renombrar, fijar o eliminar todas las vistas de este objeto.", @@ -1431,10 +1412,6 @@ const es = { last_90_days: "Últimos 90 días", }, }, - addWidget: "Añadir widget", - editLayout: "Editar diseño", - saveLayout: "Guardar diseño", - resetLayout: "Restablecer diseño", total: "Total", noDataSourceFor: "Sin fuente de datos para", config: { @@ -1546,8 +1523,6 @@ const es = { }, }, appDesigner: { - createApp: "Crear aplicación", - editApp: "Editar aplicación", basicInfo: "Información básica", objects: "Objetos", navigation: "Navegación", @@ -1557,11 +1532,9 @@ const es = { appDescription: "Descripción", appIcon: "Icono", template: "Plantilla", - selectObjects: "Seleccionar objetos", searchObjects: "Buscar objetos…", selectAll: "Seleccionar todo", deselectAll: "Deseleccionar todo", - navBuilder: "Constructor de navegación", addGroup: "Agregar grupo", addUrl: "Agregar URL", addSeparator: "Agregar separador", @@ -1572,25 +1545,9 @@ const es = { complete: "Completar", snakeCaseHint: "Debe ser snake_case (ej. my_app)", modeEdit: "Editar", - modePreview: "Vista previa", - modeCode: "Código", addWidget: "Agregar widget", widgetProperties: "Propiedades del widget", dataSource: "Fuente de datos", - valueField: "Campo de valor", - aggregate: "Agregar", - colorVariant: "Variante de color", - addComponent: "Agregar componente", - componentProperties: "Propiedades del componente", - viewType: "Tipo de vista", - fields: "Campos", - toolbar: "Barra de herramientas", - showSearch: "Mostrar búsqueda", - showFilters: "Mostrar filtros", - showSort: "Mostrar orden", - appearance: "Apariencia", - rowHeight: "Altura de fila", - livePreview: "Vista previa en vivo", stepBasicDesc: "Nombre, título e icono", stepObjectsDesc: "Seleccionar objetos de negocio", stepNavigationDesc: "Construir árbol de navegación", @@ -1817,8 +1774,6 @@ const es = { ctaTopUp: "Añada créditos para continuar", ariaLabel: "Uso de IA: {{status}}", }, - workspaceTitle: "Espacio de trabajo de IA", - workspaceSubtitle: "Pregunte, inspeccione y retome conversaciones", openChats: "Abrir chats", chats: "Chats", chatsDescription: "Explore y gestione las conversaciones con la IA.", @@ -1861,8 +1816,6 @@ const es = { saveRename: "Guardar el nombre", cancelRename: "Cancelar el cambio de nombre", loadingHistory: "Cargando el historial de la conversación…", - conversationReady: "Conversación lista", - preparingConversation: "Preparando una nueva conversación", offlineDemoMode: "Modo de demostración sin conexión — lista de agentes no disponible", sendFailedRateLimited: "Está enviando mensajes demasiado rápido. Su mensaje se conserva abajo — espere un momento y vuelva a intentarlo.", sendFailedGeneric: "No se pudo enviar su mensaje. Se conserva abajo — inténtelo de nuevo.", @@ -2057,7 +2010,6 @@ const es = { older: "Anteriores", }, }, - title: "Consola ObjectStack", initializing: "Abriendo su consola…", skipToContent: "Saltar al contenido", breadcrumb: { @@ -2120,9 +2072,7 @@ const es = { }, error: { connectionFailed: "No se puede conectar al servidor", - serverUnreachable: "El servidor {{url}} no está accesible.", checkServer: "Compruebe su conexión de red o que el backend esté en ejecución.", - timeout: "Tiempo de conexión agotado tras 10 segundos.", }, actions: { retry: "Reintentar", @@ -2175,7 +2125,6 @@ const es = { systemTheme: "Tema del sistema", actions: "Acciones", openFullSearch: "Abrir página de búsqueda completa", - createApp: "Crear nueva aplicación", searching: "Buscando…", records: "Registros", recentRecords: "Visto recientemente", @@ -2309,7 +2258,6 @@ const es = { selectOption: "Seleccionar…", noEligibleFieldForType: "Ningún campo elegible de ese tipo. Agregue uno al objeto primero.", viewTypeUnavailable: "Este objeto no tiene un campo que sirva como \"{{field}}\".", - viewTypeUnavailableShort: "No compatible con este objeto.", chartType: "Tipo de gráfico", chartTypeHelp: "Determina cómo se renderizan los datos.", chartTypeBar: "Gráfico de barras", @@ -2325,7 +2273,6 @@ const es = { chartDimensionHelp: "La dimensión del conjunto de datos por la que se agrupan los valores (opcional).", viewTypeUnavailableDataset: "Este objeto no expone ningún conjunto de datos para graficar.", noDatasetMeasure: "Este conjunto de datos no declara ninguna medida para graficar.", - ufTabs: "Pestañas", ufAddField: "+ Agregar campo de filtro…", ufShowAllRecords: "Mostrar pestaña \"Todos los registros\"", }, @@ -2891,12 +2838,8 @@ const es = { }, }, errors: { - networkError: "Error de red. Por favor, compruebe su conexión.", - serverError: "Error del servidor. Por favor, inténtelo de nuevo más tarde.", - notFound: "Recurso no encontrado.", unauthorized: "No está autorizado para realizar esta acción.", forbidden: "Acceso denegado.", - timeout: "Tiempo de espera agotado. Por favor, inténtelo de nuevo.", unknown: "Ha ocurrido un error inesperado.", }, refusalDetails: { @@ -2916,8 +2859,6 @@ const es = { namePlaceholder: "p.ej., Acme Inc", slugLabel: "Slug de URL", slugHint: "Se utiliza en URLs. Solo minúsculas, números y guiones.", - invite: "Invitar miembro", - members: "Miembros", settings: "Configuración de la organización", multiOrgDisabled: "La creación de nuevas organizaciones está deshabilitada en esta instancia.", createFailed: 'No se pudo crear la organización', @@ -2938,7 +2879,6 @@ const es = { sidebar: { packageManagement: "Gestión de paquetes", searchNavigation: "Buscar en la navegación…", - settings: "Configuración", helpTooltip: "Ayuda y documentación", activityFeed: "Feed de actividad", notifications: "Notificaciones", @@ -2946,7 +2886,6 @@ const es = { inbox: "Bandeja de entrada", inboxAriaLabel: "Abrir bandeja de entrada", area: "Área", - scope: "Ámbito", recent: "Reciente", starred: "Destacados", removeFromFavorites: "Quitar {{name}} de favoritos", @@ -3017,9 +2956,7 @@ const es = { publishedVerified: "Publicado y verificado — {{count}} fila(s) de ejemplo activa(s).", capabilityWarn: "Comprobación de autoría: {{count}} referencia(s) de capacidad no apuntan a nada.", }, - title: "Inicio", nav: "Inicio", - allApps: "Todas las aplicaciones", yourApps: "Tus aplicaciones", showMoreApps: "Más", showLess: "Mostrar menos", @@ -3037,8 +2974,6 @@ const es = { }, loading: "Cargando…", welcome: "Bienvenido a ObjectUI", - welcomeDescription: "Comienza creando tu primera aplicación o configura los ajustes del sistema.", - createFirstApp: "Crea tu primera aplicación", systemSettings: "Configuración del sistema", browseMarketplace: "Explorar marketplace de apps", quickActions: { @@ -3086,9 +3021,7 @@ const es = { title: "Destacados", }, gettingStarted: { - title: "Personalizar inicio", description: "Marque una aplicación como favorita para anclarla aquí y acceder con un clic. Todo lo que abra aparecerá automáticamente en \"Abiertos recientemente\".", - cta: "Explorar todas las aplicaciones", }, }, layout: { @@ -3108,13 +3041,6 @@ const es = { appSwitcher: { switchApplication: "Cambiar de aplicación", appsAvailable: "{{count}} aplicaciones disponibles", - editNavigation: "Editar la navegación", - home: "Inicio", - addApp: "Agregar aplicación", - editApp: "Editar aplicación", - manageAllApps: "Administrar todas las aplicaciones", - systemConsole: "Consola del sistema", - noAppsConfigured: "Sin aplicaciones configuradas", allApps: "Todas las aplicaciones", editAppInStudio: "Editar {{app}} en Studio", backToHome: "Volver al inicio", @@ -3268,7 +3194,6 @@ const es = { // objectui#13461 — the top bar's avatar button, which holds only the avatar. accountMenu: 'Menú de la cuenta', profile: "Perfil", - settings: "Configuración", logout: "Cerrar sesión", preferences: "Preferencias", theme: "Tema", @@ -3276,17 +3201,8 @@ const es = { }, report: { total: "Total", - rowTotal: "Total fila", - columnTotal: "Total columna", - grandTotal: "Gran total", totals: "Totales", - rowsLabel: "Fila", - columnsLabel: "Columna", - allLabel: "(Todo)", - emptyLabel: "(Vacío)", loading: "Cargando…", - failedToLoad: "Error al cargar la matriz: {{message}}", - needsAcross: "El informe de matriz requiere al menos un campo `groupingsAcross`.", forbiddenMessage: "No tienes permiso para ver los datos de este informe.", aggregate: { count: "Contar", @@ -3559,22 +3475,14 @@ const es = { publicForm: { submit: "Enviar", submitting: "Enviando…", - submitAnother: "Enviar otra respuesta", poweredBy: "Desarrollado por ObjectStack", - secureNotice: "Su información se transmite de forma segura y solo se usa para atender su solicitud.", thankYouTitle: "¡Gracias!", thankYouMessage: "Su envío fue recibido correctamente.", redirecting: "Redirigiendo en {{seconds}} segundos…", redirectPending: "Redirigiendo…", - unavailableTitle: "Formulario no disponible", - unavailableDescription: "No hay formulario público disponible en esta URL. Asegúrese de que la vista subyacente tenga habilitado el uso compartido anónimo.", - tryDemo: "Probar demo", - retry: "Reintentar", loading: "Cargando formulario…", requiredHint: "* Campo obligatorio", requiredFields: "Campos obligatorios: {{fields}}", - consentLabelDefault: "Acepto la política de privacidad y doy mi consentimiento para procesar mis datos para esta solicitud.", - consentLink: "Política de privacidad", consentRequired: "Acepte la política de privacidad para continuar.", rateLimited: "Tómese un momento para revisar sus respuestas antes de enviar.", redirectBlocked: "Envío aceptado, pero la URL de redirección fue bloqueada por seguridad.", diff --git a/ui/packages/i18n/src/locales/fr.ts b/ui/packages/i18n/src/locales/fr.ts index e676ea2c1b9..82e0503adb3 100644 --- a/ui/packages/i18n/src/locales/fr.ts +++ b/ui/packages/i18n/src/locales/fr.ts @@ -163,7 +163,6 @@ const fr = { formInvalid: "Veuillez vérifier les champs en surbrillance : {{fields}}", formInvalidJoiner: ", ", unique: "{{field}} doit être unique", - type: "{{field}} doit être un {{type}} valide", }, form: { noPermissionToSave: "Vous n'avez pas l'autorisation d'enregistrer cet enregistrement.", @@ -208,10 +207,7 @@ const fr = { conflictMessage: "Cet enregistrement a été modifié par quelqu'un d'autre pendant votre édition. L'écrasement remplacera ses modifications par les vôtres.", conflictLatestVersion: "Son enregistrement : {{time}}", conflictOverwrite: "Écraser", - removeItem: "Supprimer un élément", fieldRequired: "Ce champ est obligatoire", - invalidFormat: "Format invalide", - saveSuccess: "Enregistré avec succès", saveError: "Échec de l'enregistrement", unsavedChanges: "Vous avez des modifications non enregistrées. Voulez-vous vraiment quitter ?", stepOf: "Étape {{current}} sur {{total}}", @@ -224,7 +220,6 @@ const fr = { update: "Mettre à jour", createSuccess: "{{object}} créé", updateSuccess: "{{object}} mis à jour", - deleteSuccess: "{{object}} supprimé", created: "Créé", saved: "Enregistré", savedNamed: "{{title}} enregistré", @@ -406,22 +401,15 @@ const fr = { }, table: { rowsPerPage: "Lignes par page", - showing: "Affichage de {{from}} à {{to}} sur {{total}}", noRows: "Aucune ligne à afficher", sortAsc: "Tri croissant", sortDesc: "Tri décroissant", - filterColumn: "Filtrer {{column}}", columns: "Colonnes", exportCSV: "Exporter en CSV", - exportExcel: "Exporter en Excel", selectRow: "Sélectionner la ligne", selectAllRows: "Sélectionner toutes les lignes", - expandRow: "Développer la ligne", - collapseRow: "Réduire la ligne", rowActions: "Actions de la ligne", hideColumn: "Masquer la colonne", - freezeColumn: "Figer la colonne", - unfreezeColumn: "Libérer la colonne", pageInfo: "Page {{current}} sur {{total}}", firstPage: "Première page", previousPage: "Page précédente", @@ -510,7 +498,6 @@ const fr = { refreshing: "Actualisation…", openRecord: "Ouvrir l'enregistrement", recordUnavailable: "{{object}} · enregistrement indisponible", - rowHeight: "Hauteur de ligne : {{mode}}", openMenu: "Ouvrir le menu", empty: "Vide", yes: "Oui", @@ -902,8 +889,6 @@ const fr = { addCard: "Ajouter une carte", addColumn: "Ajouter une colonne", moveCard: "Déplacer la carte", - deleteCard: "Supprimer la carte", - deleteColumn: "Supprimer la colonne", noCards: "Aucune carte", columnCards: "Cartes de la colonne {{column}}", laneCards: "Cartes de {{column}} - {{lane}}", @@ -1094,11 +1079,7 @@ const fr = { changeViewType: "Changer le type de vue", deleteView: "Supprimer la vue", addView: "Ajouter une vue", - unsavedChanges: "Modifications non enregistrées", - saveAsView: "Enregistrer comme vue", moreViews: "{{count}} de plus", - activeFilters: "Filtres actifs", - activeSort: "Tri actif", manageViews: "Gérer les vues", manageAllViews: "Gérer toutes les vues…", manageViewsDescription: "Réorganiser, renommer, épingler ou supprimer toutes les vues de cet objet.", @@ -1427,10 +1408,6 @@ const fr = { last_90_days: "90 derniers jours", }, }, - addWidget: "Ajouter un widget", - editLayout: "Modifier la mise en page", - saveLayout: "Enregistrer la mise en page", - resetLayout: "Réinitialiser la mise en page", total: "Total", noDataSourceFor: "Aucune source de données pour", config: { @@ -1542,8 +1519,6 @@ const fr = { }, }, appDesigner: { - createApp: "Créer une application", - editApp: "Modifier l'application", basicInfo: "Informations de base", objects: "Objets", navigation: "Navigation", @@ -1553,11 +1528,9 @@ const fr = { appDescription: "Description", appIcon: "Icône", template: "Modèle", - selectObjects: "Sélectionner des objets", searchObjects: "Rechercher des objets…", selectAll: "Tout sélectionner", deselectAll: "Tout désélectionner", - navBuilder: "Constructeur de navigation", addGroup: "Ajouter un groupe", addUrl: "Ajouter une URL", addSeparator: "Ajouter un séparateur", @@ -1568,25 +1541,9 @@ const fr = { complete: "Terminer", snakeCaseHint: "Doit être en snake_case (ex. my_app)", modeEdit: "Modifier", - modePreview: "Aperçu", - modeCode: "Code", addWidget: "Ajouter un widget", widgetProperties: "Propriétés du widget", dataSource: "Source de données", - valueField: "Champ de valeur", - aggregate: "Agrégat", - colorVariant: "Variante de couleur", - addComponent: "Ajouter un composant", - componentProperties: "Propriétés du composant", - viewType: "Type de vue", - fields: "Champs", - toolbar: "Barre d'outils", - showSearch: "Afficher la recherche", - showFilters: "Afficher les filtres", - showSort: "Afficher le tri", - appearance: "Apparence", - rowHeight: "Hauteur de ligne", - livePreview: "Aperçu en direct", stepBasicDesc: "Nom, titre et icône", stepObjectsDesc: "Sélectionner les objets métier", stepNavigationDesc: "Construire l'arbre de navigation", @@ -1813,8 +1770,6 @@ const fr = { ctaTopUp: "Ajouter des crédits pour continuer", ariaLabel: "Utilisation de l'IA : {{status}}", }, - workspaceTitle: "Espace de travail IA", - workspaceSubtitle: "Posez des questions, inspectez et reprenez vos conversations", openChats: "Ouvrir les discussions", chats: "Discussions", chatsDescription: "Parcourez et gérez les conversations avec l'IA.", @@ -1857,8 +1812,6 @@ const fr = { saveRename: "Enregistrer le nom", cancelRename: "Annuler le changement de nom", loadingHistory: "Chargement de l'historique de la conversation…", - conversationReady: "Conversation prête", - preparingConversation: "Préparation d'une nouvelle conversation", offlineDemoMode: "Mode démo hors ligne — liste des agents indisponible", sendFailedRateLimited: "Vous envoyez des messages trop rapidement. Votre message est conservé ci-dessous — patientez un instant puis réessayez.", sendFailedGeneric: "Impossible d'envoyer votre message. Il est conservé ci-dessous — veuillez réessayer.", @@ -2053,7 +2006,6 @@ const fr = { older: "Plus anciennes", }, }, - title: "Console ObjectStack", initializing: "Ouverture de votre console…", skipToContent: "Aller au contenu", breadcrumb: { @@ -2116,9 +2068,7 @@ const fr = { }, error: { connectionFailed: "Impossible de se connecter au serveur", - serverUnreachable: "Le serveur {{url}} est inaccessible.", checkServer: "Veuillez vérifier votre connexion réseau ou que le backend est en cours d’exécution.", - timeout: "Délai de connexion dépassé après 10 secondes.", }, actions: { retry: "Réessayer", @@ -2171,7 +2121,6 @@ const fr = { systemTheme: "Thème système", actions: "Actions", openFullSearch: "Ouvrir la page de recherche complète", - createApp: "Créer une nouvelle application", searching: "Recherche…", records: "Enregistrements", recentRecords: "Récemment consultés", @@ -2305,7 +2254,6 @@ const fr = { selectOption: "Sélectionner…", noEligibleFieldForType: "Aucun champ éligible de ce type. Ajoutez-en un à l'objet d'abord.", viewTypeUnavailable: "Cet objet n'a pas de champ pouvant servir de \"{{field}}\".", - viewTypeUnavailableShort: "Non pris en charge par cet objet.", chartType: "Type de graphique", chartTypeHelp: "Détermine comment les données sont rendues.", chartTypeBar: "Graphique à barres", @@ -2321,7 +2269,6 @@ const fr = { chartDimensionHelp: "La dimension du jeu de données selon laquelle les valeurs sont regroupées (facultatif).", viewTypeUnavailableDataset: "Cet objet n’expose aucun jeu de données à représenter.", noDatasetMeasure: "Ce jeu de données ne déclare aucune mesure à représenter.", - ufTabs: "Onglets", ufAddField: "+ Ajouter un champ de filtre…", ufShowAllRecords: "Afficher l'onglet \"Tous les enregistrements\"", }, @@ -2887,12 +2834,8 @@ const fr = { }, }, errors: { - networkError: "Erreur réseau. Veuillez vérifier votre connexion.", - serverError: "Erreur serveur. Veuillez réessayer plus tard.", - notFound: "Ressource introuvable.", unauthorized: "Vous n'êtes pas autorisé à effectuer cette action.", forbidden: "Accès refusé.", - timeout: "Délai d'attente dépassé. Veuillez réessayer.", unknown: "Une erreur inattendue s'est produite.", }, refusalDetails: { @@ -2912,8 +2855,6 @@ const fr = { namePlaceholder: "p.ex., Acme Inc", slugLabel: "Slug URL", slugHint: "Utilisé dans les URL. Uniquement lettres minuscules, chiffres et tirets.", - invite: "Inviter un membre", - members: "Membres", settings: "Paramètres de l'organisation", multiOrgDisabled: "La création de nouvelles organisations est désactivée sur cette instance.", createFailed: "Échec de la création de l'organisation", @@ -2934,7 +2875,6 @@ const fr = { sidebar: { packageManagement: "Gestion des packages", searchNavigation: "Rechercher dans la navigation…", - settings: "Paramètres", helpTooltip: "Aide et documentation", activityFeed: "Flux d'activité", notifications: "Notifications", @@ -2942,7 +2882,6 @@ const fr = { inbox: "Boîte de réception", inboxAriaLabel: "Ouvrir la boîte de réception", area: "Zone", - scope: "Périmètre", recent: "Récent", starred: "Étoilés", removeFromFavorites: "Retirer {{name}} des favoris", @@ -3013,9 +2952,7 @@ const fr = { publishedVerified: "Publié et vérifié — {{count}} ligne(s) d'exemple active(s).", capabilityWarn: "Contrôle de conception : {{count}} référence(s) de capacité ne mènent à rien.", }, - title: "Accueil", nav: "Accueil", - allApps: "Toutes les applications", yourApps: "Vos applications", showMoreApps: "Plus", showLess: "Afficher moins", @@ -3033,8 +2970,6 @@ const fr = { }, loading: "Chargement…", welcome: "Bienvenue dans ObjectUI", - welcomeDescription: "Commencez par créer votre première application ou configurer les paramètres système.", - createFirstApp: "Créez votre première app", systemSettings: "Paramètres système", browseMarketplace: "Parcourir la marketplace d'apps", quickActions: { @@ -3082,9 +3017,7 @@ const fr = { title: "Étoilés", }, gettingStarted: { - title: "Personnaliser l'accueil", description: "Marquez une application en favori pour l'épingler ici et y accéder en un clic. Tout ce que vous ouvrez apparaîtra automatiquement sous \"Récemment ouverts\".", - cta: "Parcourir toutes les applications", }, }, layout: { @@ -3104,13 +3037,6 @@ const fr = { appSwitcher: { switchApplication: "Changer d'application", appsAvailable: "{{count}} applications disponibles", - editNavigation: "Modifier la navigation", - home: "Accueil", - addApp: "Ajouter une application", - editApp: "Modifier l'application", - manageAllApps: "Gérer toutes les applications", - systemConsole: "Console système", - noAppsConfigured: "Aucune application configurée", allApps: "Toutes les applications", editAppInStudio: "Modifier {{app}} dans Studio", backToHome: "Retour à l'accueil", @@ -3264,7 +3190,6 @@ const fr = { // objectui#13461 — the top bar's avatar button, which holds only the avatar. accountMenu: 'Menu du compte', profile: "Profil", - settings: "Paramètres", logout: "Se déconnecter", preferences: "Préférences", theme: "Thème", @@ -3272,17 +3197,8 @@ const fr = { }, report: { total: "Total", - rowTotal: "Total ligne", - columnTotal: "Total colonne", - grandTotal: "Total général", totals: "Totaux", - rowsLabel: "Ligne", - columnsLabel: "Colonne", - allLabel: "(Tout)", - emptyLabel: "(Vide)", loading: "Chargement…", - failedToLoad: "Échec du chargement de la matrice : {{message}}", - needsAcross: "Le rapport matriciel nécessite au moins un champ `groupingsAcross`.", forbiddenMessage: "Vous n’avez pas l’autorisation de voir les données de ce rapport.", aggregate: { count: "Compter", @@ -3555,22 +3471,14 @@ const fr = { publicForm: { submit: "Envoyer", submitting: "Envoi…", - submitAnother: "Envoyer une autre réponse", poweredBy: "Propulsé par ObjectStack", - secureNotice: "Vos informations sont transmises en toute sécurité et utilisées uniquement pour traiter votre demande.", thankYouTitle: "Merci !", thankYouMessage: "Votre soumission a été reçue avec succès.", redirecting: "Redirection dans {{seconds}} seconde(s)…", redirectPending: "Redirection…", - unavailableTitle: "Formulaire non disponible", - unavailableDescription: "Aucun formulaire public n'est disponible à cette URL. Assurez-vous que la vue sous-jacente a le partage anonyme activé.", - tryDemo: "Essayer la démo", - retry: "Réessayer", loading: "Chargement du formulaire…", requiredHint: "* Champ obligatoire", requiredFields: "Champs obligatoires : {{fields}}", - consentLabelDefault: "J'accepte la politique de confidentialité et consens au traitement de mes données pour cette demande.", - consentLink: "Politique de confidentialité", consentRequired: "Veuillez accepter la politique de confidentialité pour continuer.", rateLimited: "Prenez un moment pour vérifier vos réponses avant d'envoyer.", redirectBlocked: "Soumission acceptée, mais l'URL de redirection a été bloquée pour des raisons de sécurité.", diff --git a/ui/packages/i18n/src/locales/ja.ts b/ui/packages/i18n/src/locales/ja.ts index fdfd3525642..7fa853a533f 100644 --- a/ui/packages/i18n/src/locales/ja.ts +++ b/ui/packages/i18n/src/locales/ja.ts @@ -161,7 +161,6 @@ const ja = { formInvalid: "ハイライトされた項目を確認してください: {{fields}}", formInvalidJoiner: "、", unique: "{{field}}は一意である必要があります", - type: "{{field}}は有効な{{type}}である必要があります", }, form: { noPermissionToSave: "このレコードを保存する権限がありません。", @@ -206,10 +205,7 @@ const ja = { conflictMessage: "編集中にこのレコードが他のユーザーによって変更されました。上書きすると、相手の変更はあなたの内容で置き換えられます。", conflictLatestVersion: "相手の保存時刻: {{time}}", conflictOverwrite: "上書き", - removeItem: "項目を削除", fieldRequired: "この項目は必須です", - invalidFormat: "形式が正しくありません", - saveSuccess: "保存しました", saveError: "保存に失敗しました", unsavedChanges: "保存されていない変更があります。このページを離れますか?", stepOf: "ステップ {{current}} / {{total}}", @@ -222,7 +218,6 @@ const ja = { update: "更新", createSuccess: "{{object}}が作成されました", updateSuccess: "{{object}}が更新されました", - deleteSuccess: "{{object}}が削除されました", created: "作成しました", saved: "保存しました", savedNamed: "{{title}}を保存しました", @@ -403,22 +398,15 @@ const ja = { }, table: { rowsPerPage: "1ページの行数", - showing: "{{total}}件中{{from}}〜{{to}}件を表示", noRows: "表示するデータがありません", sortAsc: "昇順", sortDesc: "降順", - filterColumn: "{{column}}でフィルター", columns: "列", exportCSV: "CSVエクスポート", - exportExcel: "Excelエクスポート", selectRow: "行を選択", selectAllRows: "すべての行を選択", - expandRow: "行を展開", - collapseRow: "行を折りたたむ", rowActions: "行のアクション", hideColumn: "列を非表示", - freezeColumn: "列を固定", - unfreezeColumn: "列の固定を解除", pageInfo: "{{total}}ページ中{{current}}ページ", firstPage: "最初のページ", previousPage: "前のページ", @@ -500,7 +488,6 @@ const ja = { refreshing: "更新中…", openRecord: "レコードを開く", recordUnavailable: "{{object}} · レコードを表示できません", - rowHeight: "行の高さ: {{mode}}", openMenu: "メニューを開く", empty: "空", yes: "はい", @@ -879,8 +866,6 @@ const ja = { addCard: "カードを追加", addColumn: "カラムを追加", moveCard: "カードを移動", - deleteCard: "カードを削除", - deleteColumn: "カラムを削除", noCards: "カードがありません", columnCards: "{{column}} のカード", laneCards: "{{column}} - {{lane}} のカード", @@ -1068,11 +1053,7 @@ const ja = { changeViewType: "ビュータイプを変更", deleteView: "ビューを削除", addView: "ビューを追加", - unsavedChanges: "未保存の変更", - saveAsView: "ビューとして保存", moreViews: "あと {{count}} 件", - activeFilters: "フィルター適用中", - activeSort: "ソート適用中", manageViews: "ビューを管理", manageAllViews: "すべてのビューを管理…", manageViewsDescription: "このオブジェクトのすべてのビューを並べ替え、名前変更、固定、削除できます。", @@ -1388,10 +1369,6 @@ const ja = { last_90_days: "過去 90 日間", }, }, - addWidget: "ウィジェットを追加", - editLayout: "レイアウトを編集", - saveLayout: "レイアウトを保存", - resetLayout: "レイアウトをリセット", total: "合計", noDataSourceFor: "データソースが利用できません:", config: { @@ -1503,8 +1480,6 @@ const ja = { }, }, appDesigner: { - createApp: "アプリケーション作成", - editApp: "アプリケーション編集", basicInfo: "基本情報", objects: "オブジェクト", navigation: "ナビゲーション", @@ -1514,11 +1489,9 @@ const ja = { appDescription: "説明", appIcon: "アイコン", template: "テンプレート", - selectObjects: "オブジェクトを選択", searchObjects: "オブジェクトを検索…", selectAll: "すべて選択", deselectAll: "すべて解除", - navBuilder: "ナビゲーションビルダー", addGroup: "グループを追加", addUrl: "URLを追加", addSeparator: "区切りを追加", @@ -1529,25 +1502,9 @@ const ja = { complete: "完了", snakeCaseHint: "snake_case形式が必要です(例: my_app)", modeEdit: "編集", - modePreview: "プレビュー", - modeCode: "コード", addWidget: "ウィジェットを追加", widgetProperties: "ウィジェットプロパティ", dataSource: "データソース", - valueField: "値フィールド", - aggregate: "集計", - colorVariant: "カラーバリアント", - addComponent: "コンポーネントを追加", - componentProperties: "コンポーネントプロパティ", - viewType: "ビュータイプ", - fields: "フィールド", - toolbar: "ツールバー", - showSearch: "検索を表示", - showFilters: "フィルターを表示", - showSort: "ソートを表示", - appearance: "外観", - rowHeight: "行高さ", - livePreview: "リアルタイムプレビュー", stepBasicDesc: "名前、タイトル、アイコン", stepObjectsDesc: "ビジネスオブジェクトを選択", stepNavigationDesc: "ナビゲーションツリーを構築", @@ -1772,8 +1729,6 @@ const ja = { ctaTopUp: "クレジットを追加して続行", ariaLabel: "AI 使用状況: {{status}}", }, - workspaceTitle: "AI ワークスペース", - workspaceSubtitle: "質問し、確認し、会話を再開します", openChats: "チャットを開く", chats: "チャット", chatsDescription: "AI の会話を閲覧・管理します。", @@ -1816,8 +1771,6 @@ const ja = { saveRename: "名前を保存", cancelRename: "名前の変更をキャンセル", loadingHistory: "会話履歴を読み込んでいます…", - conversationReady: "会話の準備ができました", - preparingConversation: "新しい会話を準備しています", offlineDemoMode: "オフラインデモモード — エージェント一覧は利用できません", sendFailedRateLimited: "送信の間隔が短すぎます。メッセージは下に保持されています — 少し待ってから再試行してください。", sendFailedGeneric: "メッセージを送信できませんでした。下に保持されています — もう一度お試しください。", @@ -2012,7 +1965,6 @@ const ja = { older: "それ以前", }, }, - title: "ObjectStack コンソール", initializing: "コンソールを開いています…", skipToContent: "コンテンツへスキップ", breadcrumb: { @@ -2073,9 +2025,7 @@ const ja = { }, error: { connectionFailed: "サーバーに接続できません", - serverUnreachable: "サーバー {{url}} に接続できません。", checkServer: "ネットワーク接続、またはバックエンドが稼働していることを確認してください。", - timeout: "接続がタイムアウトしました(10秒)。", }, actions: { retry: "再試行", @@ -2128,7 +2078,6 @@ const ja = { systemTheme: "システムテーマ", actions: "アクション", openFullSearch: "完全な検索ページを開く", - createApp: "新しいアプリを作成", searching: "検索中…", records: "レコード", recentRecords: "最近閲覧", @@ -2259,7 +2208,6 @@ const ja = { selectOption: "選択…", noEligibleFieldForType: "このタイプに適したフィールドがありません。最初にオブジェクトに追加してください。", viewTypeUnavailable: "このオブジェクトには「{{field}}」として機能するフィールドがありません。", - viewTypeUnavailableShort: "このオブジェクトではサポートされていません。", chartType: "チャートタイプ", chartTypeHelp: "データのレンダリング方法を決定します。", chartTypeBar: "棒グラフ", @@ -2275,7 +2223,6 @@ const ja = { chartDimensionHelp: "値をグループ化するデータセットのディメンション(任意)。", viewTypeUnavailableDataset: "このオブジェクトにはグラフに使えるデータセットがありません。", noDatasetMeasure: "このデータセットにはグラフに使えるメジャーが宣言されていません。", - ufTabs: "タブ", ufAddField: "+ フィルターフィールドを追加…", ufShowAllRecords: "「すべてのレコード」タブを表示", }, @@ -2841,12 +2788,8 @@ const ja = { }, }, errors: { - networkError: "ネットワークエラーです。接続を確認してください。", - serverError: "サーバーエラーです。後でもう一度お試しください。", - notFound: "リソースが見つかりません。", unauthorized: "この操作を実行する権限がありません。", forbidden: "アクセスが拒否されました。", - timeout: "リクエストがタイムアウトしました。もう一度お試しください。", unknown: "予期しないエラーが発生しました。", }, refusalDetails: { @@ -2866,8 +2809,6 @@ const ja = { namePlaceholder: "例: Acme Inc", slugLabel: "URL スラッグ", slugHint: "URL に使用されます。小文字、数字、ハイフンのみ。", - invite: "メンバーを招待", - members: "メンバー", settings: "組織設定", multiOrgDisabled: "このインスタンスでは新しい組織の作成が無効です。", createFailed: '組織の作成に失敗しました', @@ -2888,7 +2829,6 @@ const ja = { sidebar: { packageManagement: "パッケージ管理", searchNavigation: "ナビゲーションを検索…", - settings: "設定", helpTooltip: "ヘルプとドキュメント", activityFeed: "アクティビティフィード", notifications: "通知", @@ -2896,7 +2836,6 @@ const ja = { inbox: "受信箱", inboxAriaLabel: "受信箱を開く", area: "エリア", - scope: "スコープ", recent: "最近", starred: "スター付き", removeFromFavorites: "お気に入りから {{name}} を削除", @@ -2966,9 +2905,7 @@ const ja = { publishedVerified: "公開して検証しました — サンプルデータ {{count}} 行が反映されています。", capabilityWarn: "作成時チェック:{{count}} 件のケイパビリティ参照が解決できません。", }, - title: "ホーム", nav: "ホーム", - allApps: "すべてのアプリケーション", yourApps: "あなたのアプリ", showMoreApps: "もっと見る", showLess: "折りたたむ", @@ -2986,8 +2923,6 @@ const ja = { }, loading: "読み込み中…", welcome: "ObjectUI へようこそ", - welcomeDescription: "最初のアプリケーションを作成するか、システム設定を行ってください。", - createFirstApp: "最初のアプリを作成", systemSettings: "システム設定", browseMarketplace: "アプリマーケットプレイスを参照", quickActions: { @@ -3034,9 +2969,7 @@ const ja = { title: "スター付き", }, gettingStarted: { - title: "ホームをカスタマイズ", description: "アプリにスターを付けてここにピン留めし、ワンクリックでアクセスできます。開いたものはすべて「最近アクセスしたもの」に自動的に表示されます。", - cta: "すべてのアプリを参照", }, }, layout: { @@ -3056,13 +2989,6 @@ const ja = { appSwitcher: { switchApplication: "アプリケーションを切り替え", appsAvailable: "{{count}} 個のアプリが利用可能", - editNavigation: "ナビゲーションを編集", - home: "ホーム", - addApp: "アプリを追加", - editApp: "アプリを編集", - manageAllApps: "すべてのアプリを管理", - systemConsole: "システムコンソール", - noAppsConfigured: "アプリが設定されていません", allApps: "すべてのアプリ", editAppInStudio: "Studio で {{app}} を編集", backToHome: "ホームに戻る", @@ -3212,7 +3138,6 @@ const ja = { // objectui#13461 — the top bar's avatar button, which holds only the avatar. accountMenu: 'アカウントメニュー', profile: "プロフィール", - settings: "設定", logout: "ログアウト", preferences: "設定", theme: "テーマ", @@ -3220,17 +3145,8 @@ const ja = { }, report: { total: "合計", - rowTotal: "行合計", - columnTotal: "列合計", - grandTotal: "総合計", totals: "合計", - rowsLabel: "行", - columnsLabel: "列", - allLabel: "(すべて)", - emptyLabel: "(空)", loading: "読み込み中…", - failedToLoad: "マトリックスの読み込みに失敗しました:{{message}}", - needsAcross: "マトリックスレポートには少なくとも1つの `groupingsAcross` フィールドが必要です。", forbiddenMessage: "このレポートのデータを表示する権限がありません。", aggregate: { count: "件数", @@ -3497,22 +3413,14 @@ const ja = { publicForm: { submit: "送信", submitting: "送信中…", - submitAnother: "別の回答を送信", poweredBy: "Powered by ObjectStack", - secureNotice: "お客様の情報は安全に転送され、リクエストへの対応にのみ使用されます。", thankYouTitle: "ありがとうございます!", thankYouMessage: "送信が正常に受信されました。", redirecting: "{{seconds}} 秒後にリダイレクトします…", redirectPending: "リダイレクトしています…", - unavailableTitle: "フォームが利用できません", - unavailableDescription: "このURLで利用できる公開フォームはありません。基になるビューが匿名共有を有効にし、このスラグに一致することを確認してください。", - tryDemo: "デモを試す", - retry: "再試行", loading: "フォームを読み込み中…", requiredHint: "* 必須項目", requiredFields: "必須項目: {{fields}}", - consentLabelDefault: "プライバシーポリシーに同意し、このリクエストのためにデータを処理することに同意します。", - consentLink: "プライバシーポリシー", consentRequired: "続行するにはプライバシーポリシーに同意してください。", rateLimited: "送信前に回答を確認してください。", redirectBlocked: "送信は受理されましたが、リダイレクトURLはセキュリティのためブロックされました。", diff --git a/ui/packages/i18n/src/locales/ko.ts b/ui/packages/i18n/src/locales/ko.ts index 0f23549e44e..46efeed14b8 100644 --- a/ui/packages/i18n/src/locales/ko.ts +++ b/ui/packages/i18n/src/locales/ko.ts @@ -161,7 +161,6 @@ const ko = { formInvalid: "표시된 필드를 확인해 주세요: {{fields}}", formInvalidJoiner: ", ", unique: "{{field}}은(는) 고유해야 합니다", - type: "{{field}}은(는) 유효한 {{type}}이어야 합니다", }, form: { noPermissionToSave: "이 레코드를 저장할 권한이 없습니다.", @@ -206,10 +205,7 @@ const ko = { conflictMessage: "편집하는 동안 다른 사용자가 이 레코드를 변경했습니다. 덮어쓰면 상대방의 변경 내용이 내 내용으로 대체됩니다.", conflictLatestVersion: "상대방 저장 시각: {{time}}", conflictOverwrite: "덮어쓰기", - removeItem: "항목 제거", fieldRequired: "이 필드는 필수입니다", - invalidFormat: "형식이 올바르지 않습니다", - saveSuccess: "저장되었습니다", saveError: "저장에 실패했습니다", unsavedChanges: "저장하지 않은 변경사항이 있습니다. 페이지를 떠나시겠습니까?", stepOf: "{{total}}단계 중 {{current}}단계", @@ -222,7 +218,6 @@ const ko = { update: "업데이트", createSuccess: "{{object}} 생성됨", updateSuccess: "{{object}} 업데이트됨", - deleteSuccess: "{{object}} 삭제됨", created: "생성됨", saved: "저장됨", savedNamed: "{{title}} 저장됨", @@ -403,22 +398,15 @@ const ko = { }, table: { rowsPerPage: "페이지당 행 수", - showing: "{{total}}개 중 {{from}}~{{to}} 표시", noRows: "표시할 데이터가 없습니다", sortAsc: "오름차순 정렬", sortDesc: "내림차순 정렬", - filterColumn: "{{column}} 필터", columns: "열", exportCSV: "CSV 내보내기", - exportExcel: "Excel 내보내기", selectRow: "행 선택", selectAllRows: "모든 행 선택", - expandRow: "행 펼치기", - collapseRow: "행 접기", rowActions: "행 작업", hideColumn: "열 숨기기", - freezeColumn: "열 고정", - unfreezeColumn: "열 고정 해제", pageInfo: "{{total}} 페이지 중 {{current}} 페이지", firstPage: "첫 페이지", previousPage: "이전 페이지", @@ -500,7 +488,6 @@ const ko = { refreshing: "새로고침 중…", openRecord: "레코드 열기", recordUnavailable: "{{object}} · 레코드를 사용할 수 없음", - rowHeight: "행 높이: {{mode}}", openMenu: "메뉴 열기", empty: "비어 있음", yes: "예", @@ -879,8 +866,6 @@ const ko = { addCard: "카드 추가", addColumn: "열 추가", moveCard: "카드 이동", - deleteCard: "카드 삭제", - deleteColumn: "열 삭제", noCards: "카드 없음", columnCards: "{{column}} 열의 카드", laneCards: "{{column}} - {{lane}}의 카드", @@ -1068,11 +1053,7 @@ const ko = { changeViewType: "뷰 유형 변경", deleteView: "뷰 삭제", addView: "뷰 추가", - unsavedChanges: "저장되지 않은 변경", - saveAsView: "뷰로 저장", moreViews: "{{count}}개 더", - activeFilters: "필터 적용 중", - activeSort: "정렬 적용 중", manageViews: "보기 관리", manageAllViews: "모든 보기 관리…", manageViewsDescription: "이 객체의 모든 보기를 재정렬, 이름 변경, 고정 또는 삭제합니다.", @@ -1388,10 +1369,6 @@ const ko = { last_90_days: "최근 90일", }, }, - addWidget: "위젯 추가", - editLayout: "레이아웃 편집", - saveLayout: "레이아웃 저장", - resetLayout: "레이아웃 초기화", total: "합계", noDataSourceFor: "데이터 소스 없음:", config: { @@ -1503,8 +1480,6 @@ const ko = { }, }, appDesigner: { - createApp: "앱 만들기", - editApp: "앱 편집", basicInfo: "기본 정보", objects: "오브젝트", navigation: "탐색", @@ -1514,11 +1489,9 @@ const ko = { appDescription: "설명", appIcon: "아이콘", template: "템플릿", - selectObjects: "오브젝트 선택", searchObjects: "오브젝트 검색…", selectAll: "모두 선택", deselectAll: "모두 해제", - navBuilder: "탐색 빌더", addGroup: "그룹 추가", addUrl: "URL 추가", addSeparator: "구분선 추가", @@ -1529,25 +1502,9 @@ const ko = { complete: "완료", snakeCaseHint: "snake_case 형식이어야 합니다 (예: my_app)", modeEdit: "편집", - modePreview: "미리보기", - modeCode: "코드", addWidget: "위젯 추가", widgetProperties: "위젯 속성", dataSource: "데이터 소스", - valueField: "값 필드", - aggregate: "집계", - colorVariant: "색상 변형", - addComponent: "컴포넌트 추가", - componentProperties: "컴포넌트 속성", - viewType: "보기 유형", - fields: "필드", - toolbar: "도구 모음", - showSearch: "검색 표시", - showFilters: "필터 표시", - showSort: "정렬 표시", - appearance: "외관", - rowHeight: "행 높이", - livePreview: "실시간 미리보기", stepBasicDesc: "이름, 제목 및 아이콘", stepObjectsDesc: "비즈니스 객체 선택", stepNavigationDesc: "네비게이션 트리 구성", @@ -1770,8 +1727,6 @@ const ko = { ctaTopUp: "크레딧을 추가하고 계속하기", ariaLabel: "AI 사용량: {{status}}", }, - workspaceTitle: "AI 워크스페이스", - workspaceSubtitle: "질문하고, 살펴보고, 대화를 이어가세요", openChats: "채팅 열기", chats: "채팅", chatsDescription: "AI 대화를 살펴보고 관리합니다.", @@ -1814,8 +1769,6 @@ const ko = { saveRename: "이름 저장", cancelRename: "이름 변경 취소", loadingHistory: "대화 기록을 불러오는 중…", - conversationReady: "대화 준비 완료", - preparingConversation: "새 대화를 준비하는 중", offlineDemoMode: "오프라인 데모 모드 — 에이전트 목록을 사용할 수 없습니다", sendFailedRateLimited: "메시지를 너무 빠르게 보내고 있습니다. 메시지는 아래에 보관되어 있습니다 — 잠시 후 다시 시도하세요.", sendFailedGeneric: "메시지를 보내지 못했습니다. 아래에 보관되어 있습니다 — 다시 시도해 주세요.", @@ -2010,7 +1963,6 @@ const ko = { older: "이전", }, }, - title: "ObjectStack 콘솔", initializing: "콘솔을 여는 중…", skipToContent: "본문으로 건너뛰기", breadcrumb: { @@ -2071,9 +2023,7 @@ const ko = { }, error: { connectionFailed: "서버에 연결할 수 없습니다", - serverUnreachable: "서버 {{url}}에 연결할 수 없습니다.", checkServer: "네트워크 연결을 확인하거나 백엔드가 실행 중인지 확인하세요.", - timeout: "연결 시간이 초과되었습니다 (10초).", }, actions: { retry: "재시도", @@ -2126,7 +2076,6 @@ const ko = { systemTheme: "시스템 테마", actions: "작업", openFullSearch: "전체 검색 페이지 열기", - createApp: "새 앱 만들기", searching: "검색 중…", records: "레코드", recentRecords: "최근 본 항목", @@ -2257,7 +2206,6 @@ const ko = { selectOption: "선택…", noEligibleFieldForType: "해당 유형의 적합한 필드가 없습니다. 먼저 개체에 추가하세요.", viewTypeUnavailable: "이 개체에는 \"{{field}}\"로 사용할 수 있는 필드가 없습니다.", - viewTypeUnavailableShort: "이 개체에서 지원되지 않습니다.", chartType: "차트 유형", chartTypeHelp: "데이터 렌더링 방식을 결정합니다.", chartTypeBar: "막대 차트", @@ -2273,7 +2221,6 @@ const ko = { chartDimensionHelp: "값을 그룹화할 데이터 세트의 차원입니다(선택 사항).", viewTypeUnavailableDataset: "이 객체에는 차트에 사용할 데이터 세트가 없습니다.", noDatasetMeasure: "이 데이터 세트에는 차트에 사용할 측정값이 선언되어 있지 않습니다.", - ufTabs: "탭", ufAddField: "+ 필터 필드 추가…", ufShowAllRecords: "\"모든 레코드\" 탭 표시", }, @@ -2839,12 +2786,8 @@ const ko = { }, }, errors: { - networkError: "네트워크 오류입니다. 연결을 확인해주세요.", - serverError: "서버 오류입니다. 나중에 다시 시도해주세요.", - notFound: "리소스를 찾을 수 없습니다.", unauthorized: "이 작업을 수행할 권한이 없습니다.", forbidden: "접근이 거부되었습니다.", - timeout: "요청 시간이 초과되었습니다. 다시 시도해주세요.", unknown: "예기치 않은 오류가 발생했습니다.", }, refusalDetails: { @@ -2864,8 +2807,6 @@ const ko = { namePlaceholder: "예: Acme Inc", slugLabel: "URL 슬러그", slugHint: "URL에 사용됩니다. 소문자, 숫자, 하이픈만 사용 가능합니다.", - invite: "구성원 초대", - members: "구성원", settings: "조직 설정", multiOrgDisabled: "이 인스턴스에서는 새 조직을 만들 수 없습니다.", createFailed: '조직을 만들지 못했습니다', @@ -2886,7 +2827,6 @@ const ko = { sidebar: { packageManagement: "패키지 관리", searchNavigation: "내비게이션 검색…", - settings: "설정", helpTooltip: "도움말 및 문서", activityFeed: "활동 피드", notifications: "알림", @@ -2894,7 +2834,6 @@ const ko = { inbox: "받은 편지함", inboxAriaLabel: "받은 편지함 열기", area: "영역", - scope: "범위", recent: "최근", starred: "즐겨찾기", removeFromFavorites: "즐겨찾기에서 {{name}} 제거", @@ -2963,9 +2902,7 @@ const ko = { publishedVerified: "게시 및 검증 완료 — 샘플 데이터 {{count}}개 행이 반영되었습니다.", capabilityWarn: "작성 검사: 기능 권한 참조 {{count}}개가 아무것도 가리키지 않습니다.", }, - title: "홈", nav: "홈", - allApps: "모든 애플리케이션", yourApps: "내 앱", showMoreApps: "더 보기", showLess: "간략히", @@ -2983,8 +2920,6 @@ const ko = { }, loading: "로딩 중…", welcome: "ObjectUI에 오신 것을 환영합니다", - welcomeDescription: "첫 애플리케이션을 만들거나 시스템 설정을 구성하여 시작하세요.", - createFirstApp: "첫 앱 만들기", systemSettings: "시스템 설정", browseMarketplace: "앱 마켓플레이스 탐색", quickActions: { @@ -3031,9 +2966,7 @@ const ko = { title: "즐겨찾기", }, gettingStarted: { - title: "홈 화면 맞춤설정", description: "앱을 즐겨찾기에 추가하면 여기에 고정되어 한 번의 클릭으로 접근할 수 있습니다. 열리는 모든 항목은 \"최근 열린 항목\"에 자동으로 표시됩니다.", - cta: "모든 앱 탐색", }, }, layout: { @@ -3053,13 +2986,6 @@ const ko = { appSwitcher: { switchApplication: "애플리케이션 전환", appsAvailable: "사용 가능한 앱 {{count}}개", - editNavigation: "내비게이션 편집", - home: "홈", - addApp: "앱 추가", - editApp: "앱 편집", - manageAllApps: "모든 앱 관리", - systemConsole: "시스템 콘솔", - noAppsConfigured: "구성된 앱 없음", allApps: "모든 앱", editAppInStudio: "Studio에서 {{app}} 편집", backToHome: "홈으로 돌아가기", @@ -3209,7 +3135,6 @@ const ko = { // objectui#13461 — the top bar's avatar button, which holds only the avatar. accountMenu: '계정 메뉴', profile: "프로필", - settings: "설정", logout: "로그아웃", preferences: "환경설정", theme: "테마", @@ -3217,17 +3142,8 @@ const ko = { }, report: { total: "합계", - rowTotal: "행 합계", - columnTotal: "열 합계", - grandTotal: "총합계", totals: "합계", - rowsLabel: "행", - columnsLabel: "열", - allLabel: "(전체)", - emptyLabel: "(비어 있음)", loading: "로딩 중…", - failedToLoad: "매트릭스 로드 실패: {{message}}", - needsAcross: "매트릭스 보고서에는 하나 이상의 `groupingsAcross` 필드가 필요합니다.", forbiddenMessage: "이 보고서의 데이터를 볼 수 있는 권한이 없습니다.", aggregate: { count: "개수", @@ -3494,22 +3410,14 @@ const ko = { publicForm: { submit: "제출", submitting: "제출 중…", - submitAnother: "다른 답변 제출", poweredBy: "ObjectStack 제공", - secureNotice: "귀하의 정보는 안전하게 전송되며 요청 처리에만 사용됩니다.", thankYouTitle: "감사합니다!", thankYouMessage: "제출이 성공적으로 수신되었습니다.", redirecting: "{{seconds}}초 후 리디렉션됩니다…", redirectPending: "리디렉션 중…", - unavailableTitle: "양식을 사용할 수 없습니다", - unavailableDescription: "이 URL에서 사용 가능한 공개 양식이 없습니다. 기본 보기에서 익명 공유가 활성화되어 있는지 확인하세요.", - tryDemo: "데모 시도", - retry: "다시 시도", loading: "양식 로딩 중…", requiredHint: "* 필수 필드", requiredFields: "필수 항목: {{fields}}", - consentLabelDefault: "개인정보 처리방침에 동의하며 이 요청을 위해 내 데이터 처리에 동의합니다.", - consentLink: "개인정보 처리방침", consentRequired: "계속하려면 개인정보 처리방침에 동의하세요.", rateLimited: "제출하기 전에 잠시 답변을 검토하세요.", redirectBlocked: "제출이 수락되었지만 보안상의 이유로 리디렉션 URL이 차단되었습니다.", diff --git a/ui/packages/i18n/src/locales/pt.ts b/ui/packages/i18n/src/locales/pt.ts index da93c21309a..7e45f849b69 100644 --- a/ui/packages/i18n/src/locales/pt.ts +++ b/ui/packages/i18n/src/locales/pt.ts @@ -163,7 +163,6 @@ const pt = { formInvalid: "Verifique os campos destacados: {{fields}}", formInvalidJoiner: ", ", unique: "{{field}} deve ser único", - type: "{{field}} deve ser um {{type}} válido", }, form: { noPermissionToSave: "Você não tem permissão para salvar este registro.", @@ -208,10 +207,7 @@ const pt = { conflictMessage: "Outra pessoa alterou este registro enquanto você editava. Sobrescrever substituirá as alterações dela pelas suas.", conflictLatestVersion: "Salvamento dela: {{time}}", conflictOverwrite: "Sobrescrever", - removeItem: "Remover item", fieldRequired: "Este campo é obrigatório", - invalidFormat: "Formato inválido", - saveSuccess: "Salvo com sucesso", saveError: "Falha ao salvar", unsavedChanges: "Você tem alterações não salvas. Tem certeza de que deseja sair?", stepOf: "Etapa {{current}} de {{total}}", @@ -224,7 +220,6 @@ const pt = { update: "Atualizar", createSuccess: "{{object}} criado", updateSuccess: "{{object}} atualizado", - deleteSuccess: "{{object}} excluído", created: "Criado", saved: "Salvo", savedNamed: "{{title}} salvo", @@ -406,22 +401,15 @@ const pt = { }, table: { rowsPerPage: "Linhas por página", - showing: "Mostrando {{from}} a {{to}} de {{total}}", noRows: "Nenhuma linha para exibir", sortAsc: "Ordenar crescente", sortDesc: "Ordenar decrescente", - filterColumn: "Filtrar {{column}}", columns: "Colunas", exportCSV: "Exportar CSV", - exportExcel: "Exportar Excel", selectRow: "Selecionar linha", selectAllRows: "Selecionar todas as linhas", - expandRow: "Expandir linha", - collapseRow: "Recolher linha", rowActions: "Ações da linha", hideColumn: "Ocultar coluna", - freezeColumn: "Fixar coluna", - unfreezeColumn: "Soltar coluna", pageInfo: "Página {{current}} de {{total}}", firstPage: "Primeira página", previousPage: "Página anterior", @@ -510,7 +498,6 @@ const pt = { refreshing: "Atualizando…", openRecord: "Abrir registro", recordUnavailable: "{{object}} · registro indisponível", - rowHeight: "Altura da linha: {{mode}}", openMenu: "Abrir menu", empty: "Vazio", yes: "Sim", @@ -902,8 +889,6 @@ const pt = { addCard: "Adicionar cartão", addColumn: "Adicionar coluna", moveCard: "Mover cartão", - deleteCard: "Excluir cartão", - deleteColumn: "Excluir coluna", noCards: "Sem cartões", columnCards: "Cartões da coluna {{column}}", laneCards: "Cartões de {{column}} - {{lane}}", @@ -1094,11 +1079,7 @@ const pt = { changeViewType: "Alterar tipo de visualização", deleteView: "Excluir visualização", addView: "Adicionar visualização", - unsavedChanges: "Alterações não salvas", - saveAsView: "Salvar como visualização", moreViews: "mais {{count}}", - activeFilters: "Filtros ativos", - activeSort: "Ordenação ativa", manageViews: "Gerenciar exibições", manageAllViews: "Gerenciar todas as exibições…", manageViewsDescription: "Reordenar, renomear, fixar ou excluir todas as exibições deste objeto.", @@ -1426,10 +1407,6 @@ const pt = { last_90_days: "Últimos 90 dias", }, }, - addWidget: "Adicionar widget", - editLayout: "Editar layout", - saveLayout: "Salvar layout", - resetLayout: "Redefinir layout", total: "Total", noDataSourceFor: "Sem fonte de dados para", config: { @@ -1541,8 +1518,6 @@ const pt = { }, }, appDesigner: { - createApp: "Criar aplicativo", - editApp: "Editar aplicativo", basicInfo: "Informações básicas", objects: "Objetos", navigation: "Navegação", @@ -1552,11 +1527,9 @@ const pt = { appDescription: "Descrição", appIcon: "Ícone", template: "Modelo", - selectObjects: "Selecionar objetos", searchObjects: "Pesquisar objetos…", selectAll: "Selecionar todos", deselectAll: "Desmarcar todos", - navBuilder: "Construtor de navegação", addGroup: "Adicionar grupo", addUrl: "Adicionar URL", addSeparator: "Adicionar separador", @@ -1567,25 +1540,9 @@ const pt = { complete: "Concluir", snakeCaseHint: "Deve ser snake_case (ex. my_app)", modeEdit: "Editar", - modePreview: "Pré-visualizar", - modeCode: "Código", addWidget: "Adicionar widget", widgetProperties: "Propriedades do widget", dataSource: "Fonte de dados", - valueField: "Campo de valor", - aggregate: "Agregar", - colorVariant: "Variante de cor", - addComponent: "Adicionar componente", - componentProperties: "Propriedades do componente", - viewType: "Tipo de exibição", - fields: "Campos", - toolbar: "Barra de ferramentas", - showSearch: "Mostrar pesquisa", - showFilters: "Mostrar filtros", - showSort: "Mostrar ordenação", - appearance: "Aparência", - rowHeight: "Altura da linha", - livePreview: "Pré-visualização em tempo real", stepBasicDesc: "Nome, título e ícone", stepObjectsDesc: "Selecionar objetos de negócio", stepNavigationDesc: "Construir árvore de navegação", @@ -1812,8 +1769,6 @@ const pt = { ctaTopUp: "Adicione créditos para continuar", ariaLabel: "Uso de IA: {{status}}", }, - workspaceTitle: "Workspace de IA", - workspaceSubtitle: "Pergunte, inspecione e retome conversas", openChats: "Abrir conversas", chats: "Conversas", chatsDescription: "Navegue e gerencie as conversas com a IA.", @@ -1856,8 +1811,6 @@ const pt = { saveRename: "Salvar novo nome", cancelRename: "Cancelar renomeação", loadingHistory: "Carregando o histórico da conversa…", - conversationReady: "Conversa pronta", - preparingConversation: "Preparando uma nova conversa", offlineDemoMode: "Modo de demonstração off-line — lista de agentes indisponível", sendFailedRateLimited: "Você está enviando mensagens rápido demais. Sua mensagem foi mantida abaixo — aguarde um momento e tente novamente.", sendFailedGeneric: "Não foi possível enviar sua mensagem. Ela foi mantida abaixo — tente novamente.", @@ -2052,7 +2005,6 @@ const pt = { older: "Mais antigas", }, }, - title: "Console ObjectStack", initializing: "Abrindo seu console…", skipToContent: "Pular para o conteúdo", breadcrumb: { @@ -2115,9 +2067,7 @@ const pt = { }, error: { connectionFailed: "Não é possível conectar ao servidor", - serverUnreachable: "O servidor {{url}} está inacessível.", checkServer: "Verifique sua conexão de rede ou se o backend está em execução.", - timeout: "Tempo limite de conexão esgotado após 10 segundos.", }, actions: { retry: "Tentar novamente", @@ -2170,7 +2120,6 @@ const pt = { systemTheme: "Tema do sistema", actions: "Ações", openFullSearch: "Abrir página de pesquisa completa", - createApp: "Criar novo aplicativo", searching: "Pesquisando…", records: "Registros", recentRecords: "Vistos recentemente", @@ -2304,7 +2253,6 @@ const pt = { selectOption: "Selecionar…", noEligibleFieldForType: "Nenhum campo elegível desse tipo. Adicione um ao objeto primeiro.", viewTypeUnavailable: "Este objeto não tem um campo que sirva como \"{{field}}\".", - viewTypeUnavailableShort: "Não suportado por este objeto.", chartType: "Tipo de gráfico", chartTypeHelp: "Determina como os dados são renderizados.", chartTypeBar: "Gráfico de barras", @@ -2320,7 +2268,6 @@ const pt = { chartDimensionHelp: "A dimensão do conjunto de dados pela qual os valores são agrupados (opcional).", viewTypeUnavailableDataset: "Este objeto não expõe nenhum conjunto de dados para o gráfico.", noDatasetMeasure: "Este conjunto de dados não declara nenhuma medida para o gráfico.", - ufTabs: "Abas", ufAddField: "+ Adicionar campo de filtro…", ufShowAllRecords: "Mostrar aba \"Todos os registros\"", }, @@ -2886,12 +2833,8 @@ const pt = { }, }, errors: { - networkError: "Erro de rede. Por favor, verifique sua conexão.", - serverError: "Erro no servidor. Por favor, tente novamente mais tarde.", - notFound: "Recurso não encontrado.", unauthorized: "Você não tem permissão para realizar esta ação.", forbidden: "Acesso negado.", - timeout: "Tempo limite excedido. Por favor, tente novamente.", unknown: "Ocorreu um erro inesperado.", }, refusalDetails: { @@ -2911,8 +2854,6 @@ const pt = { namePlaceholder: "p.ex., Acme Inc", slugLabel: "Slug de URL", slugHint: "Usado em URLs. Apenas letras minúsculas, números e hifens.", - invite: "Convidar membro", - members: "Membros", settings: "Configurações da organização", multiOrgDisabled: "A criação de novas organizações está desativada nesta instância.", createFailed: 'Falha ao criar a organização', @@ -2933,7 +2874,6 @@ const pt = { sidebar: { packageManagement: "Gerenciamento de pacotes", searchNavigation: "Pesquisar na navegação…", - settings: "Configurações", helpTooltip: "Ajuda e documentação", activityFeed: "Feed de atividades", notifications: "Notificações", @@ -2941,7 +2881,6 @@ const pt = { inbox: "Caixa de entrada", inboxAriaLabel: "Abrir caixa de entrada", area: "Área", - scope: "Escopo", recent: "Recente", starred: "Favoritos", removeFromFavorites: "Remover {{name}} dos favoritos", @@ -3012,9 +2951,7 @@ const pt = { publishedVerified: "Publicado e verificado — {{count}} linha(s) de exemplo ativa(s).", capabilityWarn: "Verificação de autoria: {{count}} referência(s) de capacidade não apontam para nada.", }, - title: "Início", nav: "Início", - allApps: "Todas as aplicações", yourApps: "As suas aplicações", showMoreApps: "Mais", showLess: "Mostrar menos", @@ -3032,8 +2969,6 @@ const pt = { }, loading: "Carregando…", welcome: "Bem-vindo ao ObjectUI", - welcomeDescription: "Comece criando sua primeira aplicação ou configurando as preferências do sistema.", - createFirstApp: "Crie sua primeira app", systemSettings: "Configurações do sistema", browseMarketplace: "Explorar marketplace de apps", quickActions: { @@ -3081,9 +3016,7 @@ const pt = { title: "Favoritos", }, gettingStarted: { - title: "Personalizar início", description: "Marque um aplicativo como favorito para fixá-lo aqui e acessá-lo com um clique. Tudo que você abrir aparecerá automaticamente em \"Abertos recentemente\".", - cta: "Explorar todos os aplicativos", }, }, layout: { @@ -3103,13 +3036,6 @@ const pt = { appSwitcher: { switchApplication: "Trocar de aplicativo", appsAvailable: "{{count}} aplicativos disponíveis", - editNavigation: "Editar a navegação", - home: "Início", - addApp: "Adicionar aplicativo", - editApp: "Editar aplicativo", - manageAllApps: "Gerenciar todos os aplicativos", - systemConsole: "Console do sistema", - noAppsConfigured: "Nenhum aplicativo configurado", allApps: "Todos os aplicativos", editAppInStudio: "Editar {{app}} no Studio", backToHome: "Voltar ao início", @@ -3263,7 +3189,6 @@ const pt = { // objectui#13461 — the top bar's avatar button, which holds only the avatar. accountMenu: 'Menu da conta', profile: "Perfil", - settings: "Configurações", logout: "Sair", preferences: "Preferências", theme: "Tema", @@ -3271,17 +3196,8 @@ const pt = { }, report: { total: "Total", - rowTotal: "Total da linha", - columnTotal: "Total da coluna", - grandTotal: "Total geral", totals: "Totais", - rowsLabel: "Linha", - columnsLabel: "Coluna", - allLabel: "(Tudo)", - emptyLabel: "(Vazio)", loading: "Carregando…", - failedToLoad: "Falha ao carregar a matriz: {{message}}", - needsAcross: "O relatório de matriz requer pelo menos um campo `groupingsAcross`.", forbiddenMessage: "Você não tem permissão para ver os dados deste relatório.", aggregate: { count: "Contar", @@ -3554,22 +3470,14 @@ const pt = { publicForm: { submit: "Enviar", submitting: "Enviando…", - submitAnother: "Enviar outra resposta", poweredBy: "Desenvolvido por ObjectStack", - secureNotice: "Suas informações são transmitidas com segurança e usadas apenas para processar sua solicitação.", thankYouTitle: "Obrigado!", thankYouMessage: "Seu envio foi recebido com sucesso.", redirecting: "Redirecionando em {{seconds}} segundo(s)…", redirectPending: "Redirecionando…", - unavailableTitle: "Formulário indisponível", - unavailableDescription: "Nenhum formulário público está disponível nesta URL. Certifique-se de que a exibição subjacente tenha o compartilhamento anônimo ativado.", - tryDemo: "Experimentar demo", - retry: "Tentar novamente", loading: "Carregando formulário…", requiredHint: "* Campo obrigatório", requiredFields: "Campos obrigatórios: {{fields}}", - consentLabelDefault: "Aceito a política de privacidade e consinto com o processamento dos meus dados para esta solicitação.", - consentLink: "Política de privacidade", consentRequired: "Por favor, aceite a política de privacidade para continuar.", rateLimited: "Reserve um momento para revisar suas respostas antes de enviar.", redirectBlocked: "Envio aceito, mas a URL de redirecionamento foi bloqueada por segurança.", diff --git a/ui/packages/i18n/src/locales/ru.ts b/ui/packages/i18n/src/locales/ru.ts index 3e26915e619..260190aec9c 100644 --- a/ui/packages/i18n/src/locales/ru.ts +++ b/ui/packages/i18n/src/locales/ru.ts @@ -168,7 +168,6 @@ const ru = { formInvalid: "Проверьте выделенные поля: {{fields}}", formInvalidJoiner: ", ", unique: "{{field}} должно быть уникальным", - type: "{{field}} должно быть допустимым {{type}}", }, form: { noPermissionToSave: "У вас нет прав на сохранение этой записи.", @@ -213,10 +212,7 @@ const ru = { conflictMessage: "Пока вы редактировали, эту запись изменил другой пользователь. Перезапись заменит его изменения вашими.", conflictLatestVersion: "Его сохранение: {{time}}", conflictOverwrite: "Перезаписать", - removeItem: "Удалить элемент", fieldRequired: "Это поле обязательно", - invalidFormat: "Неверный формат", - saveSuccess: "Успешно сохранено", saveError: "Ошибка сохранения", unsavedChanges: "У вас есть несохранённые изменения. Вы уверены, что хотите покинуть страницу?", stepOf: "Шаг {{current}} из {{total}}", @@ -229,7 +225,6 @@ const ru = { update: "Обновить", createSuccess: "{{object}} создан", updateSuccess: "{{object}} обновлён", - deleteSuccess: "{{object}} удалён", created: "Создано", saved: "Сохранено", savedNamed: "{{title}} сохранён", @@ -412,22 +407,15 @@ const ru = { }, table: { rowsPerPage: "Строк на странице", - showing: "Показано с {{from}} по {{to}} из {{total}}", noRows: "Нет строк для отображения", sortAsc: "Сортировка по возрастанию", sortDesc: "Сортировка по убыванию", - filterColumn: "Фильтр по {{column}}", columns: "Столбцы", exportCSV: "Экспорт в CSV", - exportExcel: "Экспорт в Excel", selectRow: "Выбрать строку", selectAllRows: "Выбрать все строки", - expandRow: "Развернуть строку", - collapseRow: "Свернуть строку", rowActions: "Действия со строкой", hideColumn: "Скрыть столбец", - freezeColumn: "Закрепить столбец", - unfreezeColumn: "Открепить столбец", pageInfo: "Страница {{current}} из {{total}}", firstPage: "Первая страница", previousPage: "Предыдущая страница", @@ -523,7 +511,6 @@ const ru = { refreshing: "Обновление…", openRecord: "Открыть запись", recordUnavailable: "{{object}} · запись недоступна", - rowHeight: "Высота строки: {{mode}}", openMenu: "Открыть меню", empty: "Пусто", yes: "Да", @@ -931,8 +918,6 @@ const ru = { addCard: "Добавить карточку", addColumn: "Добавить колонку", moveCard: "Переместить карточку", - deleteCard: "Удалить карточку", - deleteColumn: "Удалить колонку", noCards: "Нет карточек", columnCards: "Карточки в колонке {{column}}", laneCards: "Карточки: {{column}} - {{lane}}", @@ -1126,11 +1111,7 @@ const ru = { changeViewType: "Изменить тип представления", deleteView: "Удалить представление", addView: "Добавить представление", - unsavedChanges: "Несохранённые изменения", - saveAsView: "Сохранить как представление", moreViews: "ещё {{count}}", - activeFilters: "Фильтры активны", - activeSort: "Сортировка активна", manageViews: "Управление представлениями", manageAllViews: "Управление всеми представлениями…", manageViewsDescription: "Переупорядочьте, переименуйте, закрепите или удалите все представления этого объекта.", @@ -1474,10 +1455,6 @@ const ru = { last_90_days: "Последние 90 дней", }, }, - addWidget: "Добавить виджет", - editLayout: "Редактировать макет", - saveLayout: "Сохранить макет", - resetLayout: "Сбросить макет", total: "Итого", noDataSourceFor: "Нет источника данных для", config: { @@ -1589,8 +1566,6 @@ const ru = { }, }, appDesigner: { - createApp: "Создать приложение", - editApp: "Редактировать приложение", basicInfo: "Основная информация", objects: "Объекты", navigation: "Навигация", @@ -1600,11 +1575,9 @@ const ru = { appDescription: "Описание", appIcon: "Значок", template: "Шаблон", - selectObjects: "Выбрать объекты", searchObjects: "Поиск объектов…", selectAll: "Выбрать все", deselectAll: "Снять все", - navBuilder: "Конструктор навигации", addGroup: "Добавить группу", addUrl: "Добавить URL", addSeparator: "Добавить разделитель", @@ -1615,25 +1588,9 @@ const ru = { complete: "Завершить", snakeCaseHint: "Должно быть в формате snake_case (например, my_app)", modeEdit: "Редактировать", - modePreview: "Предпросмотр", - modeCode: "Код", addWidget: "Добавить виджет", widgetProperties: "Свойства виджета", dataSource: "Источник данных", - valueField: "Поле значения", - aggregate: "Агрегат", - colorVariant: "Вариант цвета", - addComponent: "Добавить компонент", - componentProperties: "Свойства компонента", - viewType: "Тип представления", - fields: "Поля", - toolbar: "Панель инструментов", - showSearch: "Показать поиск", - showFilters: "Показать фильтры", - showSort: "Показать сортировку", - appearance: "Внешний вид", - rowHeight: "Высота строки", - livePreview: "Предпросмотр в реальном времени", stepBasicDesc: "Имя, заголовок и значок", stepObjectsDesc: "Выбрать бизнес-объекты", stepNavigationDesc: "Построить дерево навигации", @@ -1866,8 +1823,6 @@ const ru = { ctaTopUp: "Добавьте кредиты, чтобы продолжить", ariaLabel: "Использование ИИ: {{status}}", }, - workspaceTitle: "Рабочее пространство ИИ", - workspaceSubtitle: "Задавайте вопросы, изучайте и возвращайтесь к диалогам", openChats: "Открыть чаты", chats: "Чаты", chatsDescription: "Просматривайте диалоги с ИИ и управляйте ими.", @@ -1910,8 +1865,6 @@ const ru = { saveRename: "Сохранить название", cancelRename: "Отменить переименование", loadingHistory: "Загрузка истории диалога…", - conversationReady: "Диалог готов", - preparingConversation: "Подготовка нового диалога", offlineDemoMode: "Автономный демонстрационный режим — список агентов недоступен", sendFailedRateLimited: "Вы отправляете сообщения слишком часто. Ваше сообщение сохранено ниже — подождите немного и попробуйте снова.", sendFailedGeneric: "Не удалось отправить сообщение. Оно сохранено ниже — попробуйте ещё раз.", @@ -2106,7 +2059,6 @@ const ru = { older: "Ранее", }, }, - title: "Консоль ObjectStack", initializing: "Открытие консоли…", skipToContent: "Перейти к содержимому", breadcrumb: { @@ -2171,9 +2123,7 @@ const ru = { }, error: { connectionFailed: "Не удаётся подключиться к серверу", - serverUnreachable: "Сервер {{url}} недоступен.", checkServer: "Проверьте сетевое подключение или работает ли бэкенд.", - timeout: "Время ожидания подключения истекло (10 секунд).", }, actions: { retry: "Повторить", @@ -2226,7 +2176,6 @@ const ru = { systemTheme: "Системная тема", actions: "Действия", openFullSearch: "Открыть полную страницу поиска", - createApp: "Создать новое приложение", searching: "Поиск…", records: "Записи", recentRecords: "Недавно просмотренные", @@ -2366,7 +2315,6 @@ const ru = { selectOption: "Выбрать…", noEligibleFieldForType: "Нет подходящих полей этого типа. Сначала добавьте его к объекту.", viewTypeUnavailable: "У этого объекта нет поля, которое может служить \"{{field}}\".", - viewTypeUnavailableShort: "Не поддерживается этим объектом.", chartType: "Тип диаграммы", chartTypeHelp: "Определяет способ отображения данных.", chartTypeBar: "Столбчатая", @@ -2382,7 +2330,6 @@ const ru = { chartDimensionHelp: "Измерение набора данных, по которому группируются значения (необязательно).", viewTypeUnavailableDataset: "Этот объект не предоставляет набора данных для диаграммы.", noDatasetMeasure: "В этом наборе данных не объявлено ни одной меры для диаграммы.", - ufTabs: "Вкладки", ufAddField: "+ Добавить поле фильтра…", ufShowAllRecords: "Показать вкладку \"Все записи\"", }, @@ -2948,12 +2895,8 @@ const ru = { }, }, errors: { - networkError: "Ошибка сети. Проверьте подключение к интернету.", - serverError: "Ошибка сервера. Попробуйте позже.", - notFound: "Ресурс не найден.", unauthorized: "У вас нет прав для выполнения этого действия.", forbidden: "Доступ запрещён.", - timeout: "Время ожидания истекло. Попробуйте снова.", unknown: "Произошла непредвиденная ошибка.", }, refusalDetails: { @@ -2973,8 +2916,6 @@ const ru = { namePlaceholder: "например, Acme Inc", slugLabel: "URL-слаг", slugHint: "Используется в URL. Только строчные буквы, цифры и дефисы.", - invite: "Пригласить участника", - members: "Участники", settings: "Настройки организации", multiOrgDisabled: "Создание новых организаций отключено в этой среде.", createFailed: 'Не удалось создать организацию', @@ -2995,7 +2936,6 @@ const ru = { sidebar: { packageManagement: "Управление пакетами", searchNavigation: "Поиск по навигации…", - settings: "Настройки", helpTooltip: "Помощь и документация", activityFeed: "Лента активности", notifications: "Уведомления", @@ -3003,7 +2943,6 @@ const ru = { inbox: "Входящие", inboxAriaLabel: "Открыть входящие", area: "Область", - scope: "Область", recent: "Недавние", starred: "Отмеченные", removeFromFavorites: "Убрать {{name}} из избранного", @@ -3076,9 +3015,7 @@ const ru = { publishedVerified: "Опубликовано и проверено — активных строк примеров данных: {{count}}.", capabilityWarn: "Проверка при создании — несуществующих ссылок на возможности: {{count}}.", }, - title: "Главная", nav: "Главная", - allApps: "Все приложения", yourApps: "Ваши приложения", showMoreApps: "Ещё", showLess: "Свернуть", @@ -3096,8 +3033,6 @@ const ru = { }, loading: "Загрузка…", welcome: "Добро пожаловать в ObjectUI", - welcomeDescription: "Начните с создания вашего первого приложения или настройки системы.", - createFirstApp: "Создайте первое приложение", systemSettings: "Системные настройки", browseMarketplace: "Обзор маркетплейса приложений", quickActions: { @@ -3146,9 +3081,7 @@ const ru = { title: "Отмеченные", }, gettingStarted: { - title: "Настройка главной", description: "Отметьте приложение как избранное, чтобы закрепить его здесь и открывать одним кликом. Всё, что вы открываете, автоматически появляется в \"Недавно открытых\".", - cta: "Просмотреть все приложения", }, }, layout: { @@ -3168,13 +3101,6 @@ const ru = { appSwitcher: { switchApplication: "Сменить приложение", appsAvailable: "Доступно приложений: {{count}}", - editNavigation: "Изменить навигацию", - home: "Главная", - addApp: "Добавить приложение", - editApp: "Редактировать приложение", - manageAllApps: "Управление приложениями", - systemConsole: "Системная консоль", - noAppsConfigured: "Нет приложений", allApps: "Все приложения", editAppInStudio: "Редактировать {{app}} в Studio", backToHome: "Вернуться на главную", @@ -3335,7 +3261,6 @@ const ru = { // objectui#13461 — the top bar's avatar button, which holds only the avatar. accountMenu: 'Меню аккаунта', profile: "Профиль", - settings: "Настройки", logout: "Выйти", preferences: "Настройки", theme: "Тема", @@ -3343,17 +3268,8 @@ const ru = { }, report: { total: "Итого", - rowTotal: "Итог строки", - columnTotal: "Итог столбца", - grandTotal: "Общий итог", totals: "Итоги", - rowsLabel: "Строка", - columnsLabel: "Столбец", - allLabel: "(Все)", - emptyLabel: "(Пусто)", loading: "Загрузка…", - failedToLoad: "Не удалось загрузить матрицу: {{message}}", - needsAcross: "Матричный отчёт требует хотя бы одно поле `groupingsAcross`.", forbiddenMessage: "У вас нет прав на просмотр данных этого отчёта.", aggregate: { count: "Количество", @@ -3632,22 +3548,14 @@ const ru = { publicForm: { submit: "Отправить", submitting: "Отправка…", - submitAnother: "Отправить ещё ответ", poweredBy: "Работает на ObjectStack", - secureNotice: "Ваши данные передаются безопасно и используются только для обработки запроса.", thankYouTitle: "Спасибо!", thankYouMessage: "Ваш ответ успешно получен.", redirecting: "Перенаправление через {{seconds}} с…", redirectPending: "Перенаправление…", - unavailableTitle: "Форма недоступна", - unavailableDescription: "По этому URL нет публичной формы. Убедитесь, что базовое представление имеет анонимный доступ.", - tryDemo: "Попробовать демо", - retry: "Повторить", loading: "Загрузка формы…", requiredHint: "* Обязательное поле", requiredFields: "Обязательные поля: {{fields}}", - consentLabelDefault: "Я принимаю политику конфиденциальности и соглашаюсь на обработку моих данных.", - consentLink: "Политика конфиденциальности", consentRequired: "Пожалуйста, примите политику конфиденциальности.", rateLimited: "Проверьте ответы перед отправкой.", redirectBlocked: "Ответ принят, но URL перенаправления заблокирован по соображениям безопасности.", diff --git a/ui/packages/i18n/src/locales/zh.ts b/ui/packages/i18n/src/locales/zh.ts index a42676bb155..81a2c3c7bac 100644 --- a/ui/packages/i18n/src/locales/zh.ts +++ b/ui/packages/i18n/src/locales/zh.ts @@ -166,7 +166,6 @@ const zh = { formInvalid: '请检查表单中标记的字段:{{fields}}', formInvalidJoiner: '、', unique: '{{field}}必须唯一', - type: '{{field}}必须是有效的{{type}}', }, form: { noPermissionToSave: '您没有权限保存这条记录。', @@ -176,10 +175,7 @@ const zh = { uploadInFlight: '请等待上传完成后再保存。', clearedOnHide: '已清空不再适用于当前取值的字段:{{fields}}', visibleWhenFaulted: '无法提交:{{fields}} 的 visibleWhen 规则无法求值。需先修正该规则,才能提交此表单。', - removeItem: '移除项目', fieldRequired: '此字段为必填项', - invalidFormat: '格式不正确', - saveSuccess: '保存成功', saveError: '保存失败', unsavedChanges: '您有未保存的更改,确定要离开吗?', discardTitle: '放弃更改?', @@ -227,7 +223,6 @@ const zh = { update: '更新', createSuccess: '{{object}}创建成功', updateSuccess: '{{object}}更新成功', - deleteSuccess: '{{object}}删除成功', // objectui#11039 — plugin-form feedback chrome; see the `en` pack. created: '已创建', saved: '已保存', @@ -412,22 +407,15 @@ const zh = { }, table: { rowsPerPage: '每页行数', - showing: '显示第{{from}}到{{to}}条,共{{total}}条', noRows: '暂无数据', sortAsc: '升序排列', sortDesc: '降序排列', - filterColumn: '筛选{{column}}', columns: '列', exportCSV: '导出CSV', - exportExcel: '导出Excel', selectRow: '选择行', selectAllRows: '选择所有行', - expandRow: '展开行', - collapseRow: '折叠行', rowActions: '行操作', hideColumn: '隐藏列', - freezeColumn: '冻结列', - unfreezeColumn: '取消冻结列', pageInfo: '第 {{current}} 页,共 {{total}} 页', firstPage: '第一页', previousPage: '上一页', @@ -482,7 +470,6 @@ const zh = { bulkSelectAllMatching: '选择全部 {{count}} 项匹配记录', bulkAllMatchingSelected: '已选择全部 {{count}} 项匹配记录。', bulkActions: '批量操作', - rowHeight: '行高: {{mode}}', empty: '空', yes: '是', no: '否', @@ -899,8 +886,6 @@ const zh = { addCard: '添加卡片', addColumn: '添加列', moveCard: '移动卡片', - deleteCard: '删除卡片', - deleteColumn: '删除列', noCards: '暂无卡片', columnCards: '{{column}} 的卡片', laneCards: '{{column}} - {{lane}} 的卡片', @@ -1091,11 +1076,7 @@ const zh = { changeViewType: '更改视图类型', deleteView: '删除视图', addView: '添加视图', - unsavedChanges: '未保存的更改', - saveAsView: '另存为视图', moreViews: '还有 {{count}} 个', - activeFilters: '筛选条件生效中', - activeSort: '排序生效中', manageViews: '管理视图', manageAllViews: '管理所有视图…', manageViewsDescription: '对该对象的所有视图进行重新排序、重命名、固定或删除。', @@ -1380,17 +1361,8 @@ const zh = { }, report: { total: '总计', - rowTotal: '行合计', - columnTotal: '列合计', - grandTotal: '总计', totals: '汇总', - rowsLabel: '行', - columnsLabel: '列', - allLabel: '(全部)', - emptyLabel: '(空)', loading: '加载中…', - failedToLoad: '矩阵报表加载失败:{{message}}', - needsAcross: '矩阵报表至少需要一个 `groupingsAcross` 字段。', forbiddenMessage: '你没有查看该报表数据的权限。', aggregate: { count: '计数', @@ -1406,10 +1378,6 @@ const zh = { }, }, dashboard: { - addWidget: '添加组件', - editLayout: '编辑布局', - saveLayout: '保存布局', - resetLayout: '重置布局', total: '总计', noDataSourceFor: '没有可用的数据源:', // objectui#7063 — the DEFAULT empty state every dashboard widget renders @@ -1568,8 +1536,6 @@ const zh = { }, }, appDesigner: { - createApp: '创建应用', - editApp: '编辑应用', basicInfo: '基本信息', objects: '业务对象', navigation: '导航', @@ -1579,11 +1545,9 @@ const zh = { appDescription: '描述', appIcon: '图标', template: '模板', - selectObjects: '选择对象', searchObjects: '搜索对象…', selectAll: '全选', deselectAll: '取消全选', - navBuilder: '导航构建器', addGroup: '添加分组', addUrl: '添加链接', addSeparator: '添加分隔线', @@ -1594,25 +1558,9 @@ const zh = { complete: '完成', snakeCaseHint: '必须使用 snake_case 格式(如 my_app)', modeEdit: '编辑', - modePreview: '预览', - modeCode: '代码', addWidget: '添加组件', widgetProperties: '组件属性', dataSource: '数据源', - valueField: '数值字段', - aggregate: '聚合', - colorVariant: '颜色', - addComponent: '添加组件', - componentProperties: '组件属性', - viewType: '视图类型', - fields: '字段', - toolbar: '工具栏', - showSearch: '显示搜索', - showFilters: '显示筛选', - showSort: '显示排序', - appearance: '外观', - rowHeight: '行高', - livePreview: '实时预览', stepBasicDesc: '名称、标题和图标', stepObjectsDesc: '选择业务对象', stepNavigationDesc: '构建导航树', @@ -1766,7 +1714,6 @@ const zh = { savedViewsUnavailable: "无法加载 {{object}} 的已保存视图", savedViewsRefused: "服务器拒绝了此请求,因此该列表为空是因为读取失败,而不是因为该对象没有已保存的视图。请重新登录,或联系管理员申请访问权限。", savedViewsUnreadable: "该列表为空是因为读取失败,而不是因为该对象没有已保存的视图。请重试;如果反复出现,请反馈此问题。", - title: 'ObjectStack 控制台', initializing: '正在打开你的控制台…', skipToContent: '跳到主要内容', search: '搜索…', @@ -1859,9 +1806,7 @@ const zh = { }, error: { connectionFailed: '无法连接到服务器', - serverUnreachable: '无法访问服务器 {{url}}。', checkServer: '请检查网络连接,或确认后端服务正在运行。', - timeout: '连接超时(10 秒)。', }, actions: { retry: '重试', @@ -1917,7 +1862,6 @@ const zh = { systemTheme: '系统主题', actions: '操作', openFullSearch: '打开完整搜索页面', - createApp: '创建新应用', }, ai: { pendingDrafts: { @@ -1945,8 +1889,6 @@ const zh = { ctaTopUp: '购买额度包以继续', ariaLabel: 'AI 用量:{{status}}', }, - workspaceTitle: 'AI 工作区', - workspaceSubtitle: '提问、查看并继续历史对话', openChats: '打开对话列表', chats: '对话', chatsDescription: '浏览和管理 AI 对话。', @@ -1989,8 +1931,6 @@ const zh = { saveRename: '保存重命名', cancelRename: '取消重命名', loadingHistory: '正在加载对话历史…', - conversationReady: '对话已就绪', - preparingConversation: '正在准备新对话', offlineDemoMode: '离线演示模式 — 无法获取助手列表', sendFailedRateLimited: '发送过于频繁,请稍候再试。你的消息已保留在输入框中。', sendFailedGeneric: '消息发送失败,请重试。你的消息已保留在输入框中。', @@ -2349,7 +2289,6 @@ const zh = { selectOption: '选择…', noEligibleFieldForType: '没有符合条件的字段。请先在对象上添加该类型字段。', viewTypeUnavailable: '该对象缺少所需的“{{field}}”字段。', - viewTypeUnavailableShort: '当前对象不支持。', chartType: '图表类型', chartTypeHelp: '决定数据如何呈现。', chartTypeBar: '柱状图', @@ -2366,7 +2305,6 @@ const zh = { viewTypeUnavailableDataset: '该对象没有可用于图表的数据集。', noDatasetMeasure: '该数据集未声明可绘制的度量。', endDateField: '结束日期字段', - ufTabs: '标签页', ufAddField: '+ 添加筛选字段…', ufShowAllRecords: '显示"全部记录"标签页', }, @@ -2916,12 +2854,8 @@ const zh = { }, }, errors: { - networkError: '网络错误,请检查网络连接。', - serverError: '服务器错误,请稍后重试。', - notFound: '资源未找到。', unauthorized: '您没有权限执行此操作。', forbidden: '访问被拒绝。', - timeout: '请求超时,请重试。', unknown: '发生未知错误。', }, refusalDetails: { @@ -2941,8 +2875,6 @@ const zh = { namePlaceholder: '例如:极客科技', slugLabel: 'URL 标识', slugHint: '用于 URL 中,仅支持小写字母、数字和连字符。', - invite: '邀请成员', - members: '成员', settings: '组织设置', multiOrgDisabled: '此实例已禁用创建新组织。', createFailed: '创建组织失败', @@ -2961,7 +2893,6 @@ const zh = { aboutBuildUnknown: '未知构建(构建此控制台时未设置版本)', }, sidebar: { - settings: '设置', helpTooltip: '帮助与文档', activityFeed: '活动动态', notifications: '通知', @@ -2969,7 +2900,6 @@ const zh = { inbox: '消息中心', inboxAriaLabel: '打开消息中心', area: '区域', - scope: '范围', packageManagement: '软件包管理', searchNavigation: '搜索导航…', recent: '最近使用', @@ -2999,9 +2929,7 @@ const zh = { }, }, home: { - title: '首页', nav: '首页', - allApps: '全部应用', yourApps: '我的应用', showMoreApps: '更多', showLess: '收起', @@ -3043,7 +2971,6 @@ const zh = { open: '打开', loading: '加载中…', welcome: '用 AI 搭建你的业务系统', - welcomeDescription: '用一句话描述你的业务,AI 帮你生成对象、界面、API 和 agent 工具。也可以手动从零开始。', welcomeAdminDescription: '用一句话描述你的业务,AI 会为你生成对象、界面、API 和智能体工具;也可以从左侧菜单自行搭建。', welcomeAdminDescriptionNoBuild: '从左侧的「管理」菜单创建你的第一个应用。有了数据后,AI 助手可以帮你查询和分析。', welcomeAdminDescriptionNoAi: '从左侧的「管理」菜单创建你的第一个应用。', @@ -3078,7 +3005,6 @@ const zh = { publishedVerified: '已发布并验证 —— {{count}} 行示例数据已生效。', capabilityWarn: '编写检查:{{count}} 个能力引用无法解析。', }, - createFirstApp: '手动创建应用', systemSettings: '系统设置', browseMarketplace: '浏览应用市场', quickActions: { @@ -3105,9 +3031,7 @@ const zh = { title: '收藏', }, gettingStarted: { - title: '把首页布置成你喜欢的样子', description: '收藏一个应用即可固定到此处,一键直达。打开过的内容会自动出现在「最近访问」中。', - cta: '浏览全部应用', }, appCard: { noDescription: '暂无描述', @@ -3116,15 +3040,8 @@ const zh = { }, layout: { appSwitcher: { - home: '首页', switchApplication: '切换应用', appsAvailable: '共 {{count}} 个可用应用', - addApp: '新增应用', - editApp: '编辑应用', - editNavigation: '编辑导航', - manageAllApps: '管理所有应用', - systemConsole: '系统控制台', - noAppsConfigured: '尚未配置任何应用', allApps: '所有应用', editAppInStudio: '在 Studio 中编辑 {{app}}', backToHome: '返回首页', @@ -3419,7 +3336,6 @@ const zh = { // objectui#13461 — the top bar's avatar button, which holds only the avatar. accountMenu: '账户菜单', profile: '个人资料', - settings: '设置', logout: '注销', preferences: '偏好设置', theme: '主题', @@ -3677,22 +3593,14 @@ const zh = { publicForm: { submit: '提交', submitting: '提交中…', - submitAnother: '再填一份', poweredBy: '由 ObjectStack 提供技术支持', - secureNotice: '您的信息将通过安全通道传输,仅用于回复您本次请求。', thankYouTitle: '感谢您的提交!', thankYouMessage: '我们已成功收到您的信息。', redirecting: '将在 {{seconds}} 秒后跳转…', redirectPending: '正在跳转…', - unavailableTitle: '表单不可用', - unavailableDescription: '该链接当前没有可用的公开表单。请确认对应视图已启用匿名分享并匹配此 slug。', - tryDemo: '试用演示', - retry: '重试', loading: '正在加载表单…', requiredHint: '* 为必填项', requiredFields: '必填项:{{fields}}', - consentLabelDefault: '我已阅读并同意隐私政策,授权对本次请求所提供的信息进行处理。', - consentLink: '隐私政策', consentRequired: '请先勾选同意隐私政策。', rateLimited: '请再花点时间核对一下您的填写内容,再点击提交。', redirectBlocked: '提交成功,但出于安全考虑,跳转链接已被阻止。', From 9e53f05ab5a8a026b57d53fb346a735cdafb7cca Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 13:14:35 +0000 Subject: [PATCH 15/19] test(i18n): move ufTabs and viewTypeUnavailableShort to the retired objectView keys Claude-Session: https://claude.ai/code/session_01FpUtgwNrSiRgWJ8GAepPeP Co-authored-by: Claude --- .changeset/25921-delete-dead-i18n-keys.md | 17 +++++++++++++++++ ...objectView-config-keys-retired-4730.test.ts | 18 ++++++++++++------ 2 files changed, 29 insertions(+), 6 deletions(-) create mode 100644 .changeset/25921-delete-dead-i18n-keys.md diff --git a/.changeset/25921-delete-dead-i18n-keys.md b/.changeset/25921-delete-dead-i18n-keys.md new file mode 100644 index 00000000000..9bdafc990b2 --- /dev/null +++ b/.changeset/25921-delete-dead-i18n-keys.md @@ -0,0 +1,17 @@ +--- +'@object-ui/i18n': patch +--- + +chore(i18n): delete 92 message keys nothing reads from all ten locale packs + +Clause-②: no (only unused message keys go; no exported type or runtime contract changes) +Metadata-protocol: none + +`ui/scripts/check-i18n-dead-keys.mjs` listed 122 keys with no call site and no textual occurrence anywhere in the repository. 92 of them are deleted from `ar`, `de`, `en`, `es`, `fr`, `ja`, `ko`, `pt`, `ru` and `zh`; no key is renamed or added, and no screen changes, because no code asked for any of them. The largest groups are `appDesigner.*` (20 keys), `report.*` (9), `publicForm.*` (8), `table.*` (7) and `layout.appSwitcher.*` (7). + +Two groups the script also listed stay: + +- 28 `_one` / `_other` plural forms (`preview.changes.typeCount.*`, `notifications.arrivalMany` / `arrivalRepeats`, `fields.relativeDate.overdue`). Their base keys are called with `count`, and i18next reads the plural form, so they are in use. +- `console.ai.agentLabels.dataChat` and `metadataAssistant`, which the pack keeps on purpose for external overrides. + +A host that imports a pack object and reads one of the deleted members by property sees it gone from the type; nothing in this repository does. diff --git a/ui/packages/i18n/src/__tests__/objectView-config-keys-retired-4730.test.ts b/ui/packages/i18n/src/__tests__/objectView-config-keys-retired-4730.test.ts index cdb42e54b38..3cd4c5e8d6c 100644 --- a/ui/packages/i18n/src/__tests__/objectView-config-keys-retired-4730.test.ts +++ b/ui/packages/i18n/src/__tests__/objectView-config-keys-retired-4730.test.ts @@ -281,6 +281,10 @@ const RETIRED_OBJECT_VIEW_KEYS = [ // objectui#12504 — the refused toolbar view-write's toast; the write and the // adapter gate that refused it retired together, and it had no other reader. 'viewConfigPermissionDenied', + // objectstack#25921 — CONFIRMED by `check-i18n-dead-keys.mjs`: no call site + // and no textual occurrence of either dotted key outside the packs. + 'ufTabs', + 'viewTypeUnavailableShort', ] as const; /** @@ -288,16 +292,17 @@ const RETIRED_OBJECT_VIEW_KEYS = [ * 104 since objectui#13052 added `objectPackageDisabled`, the object route's * "this object's package is disabled" sentence (`ObjectNotFoundState`); 105 * since objectui#13330 added `managedUsers`, Setup → Users' invite line and - * 「未授权」 marker on a cloud-managed environment (`managedEnvironmentUsers`). + * 「未授权」 marker on a cloud-managed environment (`managedEnvironmentUsers`); + * 103 since objectstack#25921 retired `ufTabs` and `viewTypeUnavailableShort`. */ -const SURVIVING_KEY_COUNT = 105; +const SURVIVING_KEY_COUNT = 103; describe('console.objectView config-panel keys are retired (objectui#4730)', () => { it('the retired list is the measured set', () => { // Guards the premise the rest of the file rests on. If this arithmetic ever // stops holding, the assertions below are checking a set nobody chose. - expect(RETIRED_OBJECT_VIEW_KEYS).toHaveLength(124); - expect(new Set(RETIRED_OBJECT_VIEW_KEYS).size).toBe(124); + expect(RETIRED_OBJECT_VIEW_KEYS).toHaveLength(126); + expect(new Set(RETIRED_OBJECT_VIEW_KEYS).size).toBe(126); // 209 at objectui#4730's landing; 210 since objectui#5232 added // `viewConfigPermissionDenied`; 211 since objectui#10636 added // `recordCountOne`, the record-count footer's singular; 217 since @@ -309,8 +314,9 @@ describe('console.objectView config-panel keys are retired (objectui#4730)', () // `viewConfigPermissionDenied`, which moved the split, not the total); 228 // since objectui#13052 added `objectPackageDisabled`, the not-found view's // package-disabled sentence; 229 since objectui#13330 added - // `managedUsers`, the managed environment's Users-list strings. The - // RETIRED half is a ratchet that never + // `managedUsers`, the managed environment's Users-list strings + // (objectstack#25921 retired `ufTabs` and `viewTypeUnavailableShort`, + // which moved the split, not the total). The RETIRED half is a ratchet that never // runs BACKWARDS — it is pinned twice above, and objectui#8754 advanced it // 116 -> 118 — while the surviving half is a live namespace that grows on a // new key and shrinks by exactly what the retired half gains. Splitting From b7bc639d8d05ad59f2c9dc0ea29029734c0cb202 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 13:36:11 +0000 Subject: [PATCH 16/19] test(app-shell): drop workspace.invite and workspace.members from the product-specific allowlist Claude-Session: https://claude.ai/code/session_01FpUtgwNrSiRgWJ8GAepPeP Co-authored-by: Claude --- .../app-shell/src/__tests__/product-specific.allowlist.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/ui/packages/app-shell/src/__tests__/product-specific.allowlist.json b/ui/packages/app-shell/src/__tests__/product-specific.allowlist.json index f72781eeb8a..06f541ddb4b 100644 --- a/ui/packages/app-shell/src/__tests__/product-specific.allowlist.json +++ b/ui/packages/app-shell/src/__tests__/product-specific.allowlist.json @@ -123,9 +123,7 @@ "createFailed", "createTitle", "default", - "invite", "label", - "members", "multiOrgDisabled", "nameLabel", "namePlaceholder", From 8bdaaff3ec237faa06416cf07efbc6040166484c Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 14:05:29 +0000 Subject: [PATCH 17/19] chore(sdui): re-record the i18n source digest Claude-Session: https://claude.ai/code/session_01FpUtgwNrSiRgWJ8GAepPeP Co-authored-by: Claude --- scripts/sdui-manifest.sources/i18n.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/sdui-manifest.sources/i18n.json b/scripts/sdui-manifest.sources/i18n.json index 873a389b2b8..8186bdbe7a6 100644 --- a/scripts/sdui-manifest.sources/i18n.json +++ b/scripts/sdui-manifest.sources/i18n.json @@ -1 +1 @@ -{"package":"ui/packages/i18n","sources":31,"sha256":"9cd75a1d5d100dec0273df9e1018574cc9188811a2af4963f04abc901ca95a9c","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} +{"package":"ui/packages/i18n","sources":31,"sha256":"28b745202e9b692dbbe3a63b30238590646c2ec034867045e8c266d8548e7f70","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} From ad1e1866c3fc1783a52aa3a8a7e0c819e9372354 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 14:46:41 +0000 Subject: [PATCH 18/19] =?UTF-8?q?chore(changeset):=20Clause-=E2=91=A1=20ye?= =?UTF-8?q?s=20for=20the=20dead=20i18n=20key=20deletion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01FpUtgwNrSiRgWJ8GAepPeP Co-authored-by: Claude --- .changeset/25921-delete-dead-i18n-keys.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/25921-delete-dead-i18n-keys.md b/.changeset/25921-delete-dead-i18n-keys.md index 9bdafc990b2..4cf1fe723a0 100644 --- a/.changeset/25921-delete-dead-i18n-keys.md +++ b/.changeset/25921-delete-dead-i18n-keys.md @@ -4,7 +4,7 @@ chore(i18n): delete 92 message keys nothing reads from all ten locale packs -Clause-②: no (only unused message keys go; no exported type or runtime contract changes) +Clause-②: yes (TranslationKeys narrows by 92 unread members; patch per the #4730 retirement precedent) Metadata-protocol: none `ui/scripts/check-i18n-dead-keys.mjs` listed 122 keys with no call site and no textual occurrence anywhere in the repository. 92 of them are deleted from `ar`, `de`, `en`, `es`, `fr`, `ja`, `ko`, `pt`, `ru` and `zh`; no key is renamed or added, and no screen changes, because no code asked for any of them. The largest groups are `appDesigner.*` (20 keys), `report.*` (9), `publicForm.*` (8), `table.*` (7) and `layout.appSwitcher.*` (7). @@ -14,4 +14,4 @@ Two groups the script also listed stay: - 28 `_one` / `_other` plural forms (`preview.changes.typeCount.*`, `notifications.arrivalMany` / `arrivalRepeats`, `fields.relativeDate.overdue`). Their base keys are called with `count`, and i18next reads the plural form, so they are in use. - `console.ai.agentLabels.dataChat` and `metadataAssistant`, which the pack keeps on purpose for external overrides. -A host that imports a pack object and reads one of the deleted members by property sees it gone from the type; nothing in this repository does. +`TranslationKeys` (`typeof en`) and the other pack objects' types lose the 92 members. A host that reads one of them by property gets a type error; nothing in this repository does, and no `t()` call anywhere asks for one. From 5e59433fc8741f7527b2be9a88f6cb0aff5d941e Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 14:54:22 +0000 Subject: [PATCH 19/19] chore(sdui): re-record components, i18n and plugin-map source digests for the i18n batch Claude-Session: https://claude.ai/code/session_01FpUtgwNrSiRgWJ8GAepPeP Co-authored-by: Claude --- scripts/sdui-manifest.sources/components.json | 2 +- scripts/sdui-manifest.sources/i18n.json | 2 +- scripts/sdui-manifest.sources/plugin-map.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/sdui-manifest.sources/components.json b/scripts/sdui-manifest.sources/components.json index b2f126ff477..f3006fdc5f9 100644 --- a/scripts/sdui-manifest.sources/components.json +++ b/scripts/sdui-manifest.sources/components.json @@ -1 +1 @@ -{"package":"ui/packages/components","sources":226,"sha256":"7c50070c8bc750d5717fce0a081038855777bfb749567757047e672262517886","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} +{"package":"ui/packages/components","sources":226,"sha256":"cd1d23767a76e6b4dcf2c0d4ca61b10ba27b336eeab990b22bfb0f7d10bb0424","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} diff --git a/scripts/sdui-manifest.sources/i18n.json b/scripts/sdui-manifest.sources/i18n.json index 01a3863646a..6fe92078785 100644 --- a/scripts/sdui-manifest.sources/i18n.json +++ b/scripts/sdui-manifest.sources/i18n.json @@ -1 +1 @@ -{"package":"ui/packages/i18n","sources":31,"sha256":"57909816bcbfa5661852ecf652392b19d6a1e971a608b07360bfa2399bca11e4","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} +{"package":"ui/packages/i18n","sources":31,"sha256":"ae5a36bc2327b985cb909eed35e099d1b2d953b7c4cb7dda463e4d8d3ef5cc86","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} diff --git a/scripts/sdui-manifest.sources/plugin-map.json b/scripts/sdui-manifest.sources/plugin-map.json index 632e105402c..00045bf6d45 100644 --- a/scripts/sdui-manifest.sources/plugin-map.json +++ b/scripts/sdui-manifest.sources/plugin-map.json @@ -1 +1 @@ -{"package":"ui/packages/plugin-map","sources":4,"sha256":"9987a8a9895284b5e3c44407a44f284462c5f522616cf9f88f9d9bdd7e0b8d90","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} +{"package":"ui/packages/plugin-map","sources":4,"sha256":"7e7bcc627a2cf9334b56fe20b1b97ab2732eec9f1e5fe77ede4e570bde95d6b1","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"}