diff --git a/.changeset/29186-form-load-refusal-text.md b/.changeset/29186-form-load-refusal-text.md
new file mode 100644
index 00000000000..62af35a9126
--- /dev/null
+++ b/.changeset/29186-form-load-refusal-text.md
@@ -0,0 +1,17 @@
+---
+'@object-ui/console': patch
+'@object-ui/i18n': patch
+---
+
+fix(console): a refused edit-form record read says a localized line, never the response body
+
+Clause-②: yes (widening)
+Metadata-protocol: none
+
+An edit form (`/forms/NAME?recordId=ID`, and the in-app edit route) reads its record before it draws. When that read was refused, the banner said `Could not open record "ID" of OBJECT for editing (403): {"error":…}`: the record id, the object's machine name, the status and the server's JSON, in English. The load now reads its refusal through the same `WriteRefusal` the submit uses, and the banner says one localized line:
+
+- a denial (`403`, `PERMISSION_DENIED`, `FORBIDDEN`) as "You don't have access to this record.";
+- a `404` as "This record doesn't exist, or you don't have access to it." Row-level security answers a hidden row with the same `404`, so the line never tells the two apart;
+- anything else as "Could not open this record. Please try again."
+
+The response body stays on the thrown error for a debugger and never reaches the page. The three lines are new `form` keys (`noPermissionToOpenRecord`, `recordNotFoundOrHidden`, `openRecordFailed`) in all ten packs.
diff --git a/ui/apps/console/src/components/FormPage.loadRefusal-29186.test.tsx b/ui/apps/console/src/components/FormPage.loadRefusal-29186.test.tsx
new file mode 100644
index 00000000000..064ed963256
--- /dev/null
+++ b/ui/apps/console/src/components/FormPage.loadRefusal-29186.test.tsx
@@ -0,0 +1,175 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+/**
+ * objectstack-ai/objectstack#29186 — a refused record read on an edit form says
+ * a sentence in the reader's language, never the response body.
+ *
+ * `/forms/:name?recordId=ID` reads the record before it draws the form. When
+ * that read was refused, the banner said
+ * `Could not open record "ID" of OBJECT for editing (403): {"error":…}` — the
+ * record id, the object's machine name, the status and the server's JSON.
+ *
+ * The load now reads its refusal through the same `WriteRefusal` the submit
+ * uses, and the banner draws one fixed localized line per outcome: a denial is
+ * "you don't have access", a `404` is "doesn't exist, or you don't have access"
+ * (row-level security answers a hidden row with the same `404`, so the line
+ * never tells the two apart), and anything else is the generic line.
+ */
+
+import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
+import { render, screen } from '@testing-library/react';
+import { MemoryRouter, Route, Routes } from 'react-router-dom';
+import { createI18n } from '@object-ui/i18n';
+import { FormPage } from './FormPage';
+
+vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
+
+const VIEW = {
+ name: 'showcase_task.edit',
+ object: 'showcase_task',
+ viewKind: 'form',
+ label: 'Log Time',
+ config: { type: 'simple', sections: [{ fields: ['title', 'hours'] }] },
+};
+
+const OBJECT = {
+ name: 'showcase_task',
+ label: 'Task',
+ fields: {
+ title: { type: 'text', label: 'Title' },
+ hours: { type: 'text', label: 'Hours' },
+ },
+};
+
+const RECORD_ID = 'rec_7Hq2Kx9';
+
+/**
+ * A `GET /data/:object/:id` on an object the caller has no read right on, as
+ * a showcase member measured it on `showcase_contact`.
+ */
+const PERMISSION_DENIED = {
+ status: 403,
+ body: {
+ error: 'You do not have permission to perform this action. Contact your administrator if you need access.',
+ code: 'PERMISSION_DENIED',
+ object: 'showcase_task',
+ },
+};
+
+let writes = 0;
+
+/** One answer of the stubbed server; a string body goes out as it is. */
+function answer(ok: boolean, status: number, body: unknown): Response {
+ const text = typeof body === 'string' ? body : JSON.stringify(body);
+ return {
+ ok,
+ status,
+ statusText: ok ? 'OK' : 'Refused',
+ json: async () => JSON.parse(text),
+ text: async () => text,
+ } as unknown as Response;
+}
+
+/** The internal edit route, its record read answered with `refusal`. */
+function openEdit(refusal: { status: number; body: unknown }) {
+ vi.stubGlobal(
+ 'fetch',
+ vi.fn(async (url: string, init?: RequestInit) => {
+ const method = (init?.method ?? 'GET').toUpperCase();
+ if (method !== 'GET') {
+ writes += 1;
+ return answer(true, 200, {});
+ }
+ if (String(url).includes('/meta/view/')) return answer(true, 200, VIEW);
+ if (String(url).includes('/meta/object/')) return answer(true, 200, OBJECT);
+ if (String(url).includes(`/data/showcase_task/${RECORD_ID}`)) {
+ return answer(false, refusal.status, refusal.body);
+ }
+ throw new Error(`unstubbed fetch: ${method} ${url}`);
+ }),
+ );
+ return render(
+
+
+ } />
+
+ ,
+ );
+}
+
+/**
+ * The banner reads exactly `line`, the page says nothing of the response, and
+ * no editable form is left behind it.
+ */
+async function expectLoadBanner(line: string, status: number) {
+ const banner = await screen.findByText(line);
+ expect(banner.textContent).toBe(line);
+ const page = document.body.textContent ?? '';
+ expect(page).not.toContain('{');
+ expect(page).not.toContain(String(status));
+ expect(page).not.toContain(RECORD_ID);
+ expect(page).not.toContain('showcase_task');
+ expect(screen.queryByLabelText(/Title/)).not.toBeInTheDocument();
+ expect(screen.queryByRole('button', { name: /Submit/ })).not.toBeInTheDocument();
+ expect(writes).toBe(0);
+}
+
+beforeEach(() => {
+ writes = 0;
+ createI18n({ defaultLanguage: 'en', detectBrowserLanguage: false });
+});
+
+afterEach(() => {
+ vi.unstubAllGlobals();
+});
+
+describe('a refused edit-form record read draws a localized line (#29186)', () => {
+ it('a 403 PERMISSION_DENIED read says "you don\'t have access", not the body', async () => {
+ openEdit(PERMISSION_DENIED);
+ await expectLoadBanner("You don't have access to this record.", 403);
+ });
+
+ it('says the denial in the reader\'s language', async () => {
+ createI18n({ defaultLanguage: 'zh', detectBrowserLanguage: false });
+ openEdit(PERMISSION_DENIED);
+ await expectLoadBanner('您没有权限查看这条记录。', 403);
+ });
+
+ it('a 404 says "doesn\'t exist, or you don\'t have access"', async () => {
+ // The body a showcase member measured for an inquiry the private OWD hides
+ // from them — the same shape a missing id answers.
+ openEdit({
+ status: 404,
+ body: {
+ error: `Record ${RECORD_ID} not found in showcase_task`,
+ code: 'RECORD_NOT_FOUND',
+ object: 'showcase_task',
+ },
+ });
+ await expectLoadBanner("This record doesn't exist, or you don't have access to it.", 404);
+ });
+
+ it('a 404 whose text names an access rule still reads as not-found-or-hidden', async () => {
+ // Row-level security answers a hidden row with 404; whatever its text
+ // says, the line must not tell "hidden" from "missing".
+ openEdit({
+ status: 404,
+ body: { error: 'Access denied by row-level security policy', code: 'RECORD_NOT_FOUND' },
+ });
+ await expectLoadBanner("This record doesn't exist, or you don't have access to it.", 404);
+ });
+
+ it('a 500 says the generic line, not the driver text', async () => {
+ openEdit({
+ status: 500,
+ body: { error: 'SQLITE_BUSY: database is locked', code: 'INTERNAL_ERROR' },
+ });
+ await expectLoadBanner('Could not open this record. Please try again.', 500);
+ expect(document.body.textContent).not.toContain('SQLITE_BUSY');
+ });
+
+ it('a proxy\'s markup page says the generic line', async () => {
+ openEdit({ status: 502, body: '
502 Bad Gateway
' });
+ await expectLoadBanner('Could not open this record. Please try again.', 502);
+ });
+});
diff --git a/ui/apps/console/src/components/FormPage.recordId.test.tsx b/ui/apps/console/src/components/FormPage.recordId.test.tsx
index 104ae331ed2..256df603402 100644
--- a/ui/apps/console/src/components/FormPage.recordId.test.tsx
+++ b/ui/apps/console/src/components/FormPage.recordId.test.tsx
@@ -286,7 +286,7 @@ describe('a recordId this route cannot honour FAILS CLOSED (ruling point 2)', ()
]),
);
renderInternal('?recordId=task-404');
- await expectRefusal(/Could not open record "task-404".*404/s);
+ await expectRefusal(/^This record doesn't exist, or you don't have access to it\.$/);
});
/**
@@ -305,7 +305,7 @@ describe('a recordId this route cannot honour FAILS CLOSED (ruling point 2)', ()
]),
);
renderInternal('?recordId=task-42');
- await expectRefusal(/Could not open record "task-42".*403/s);
+ await expectRefusal(/^You don't have access to this record\.$/);
});
it("an object mismatch in the payload ⇒ refuses to open the form", async () => {
@@ -342,7 +342,7 @@ describe('a recordId this route cannot honour FAILS CLOSED (ruling point 2)', ()
]),
);
renderInternal('?recordId=task-404');
- await screen.findByText(/Could not open record/);
+ await screen.findByText(/This record doesn't exist/);
expect(calls.filter((c) => c.method === 'POST' || c.method === 'PATCH')).toHaveLength(0);
});
});
diff --git a/ui/apps/console/src/components/FormPage.tsx b/ui/apps/console/src/components/FormPage.tsx
index f5afeaa3fec..0c59879d2a4 100644
--- a/ui/apps/console/src/components/FormPage.tsx
+++ b/ui/apps/console/src/components/FormPage.tsx
@@ -1902,11 +1902,10 @@ async function loadInternalForm(
channel,
);
if (!recRes.ok) {
- const detail = await recRes.text().catch(() => '');
- throw new Error(
- `Could not open record "${recordId}" of ${objectName} for editing (${recRes.status})` +
- `${detail ? `: ${detail}` : recRes.statusText ? `: ${recRes.statusText}` : ''}`,
- );
+ // The body rides on the refusal for a debugger; the banner draws only
+ // the localized line {@link refusalText} picks for a load.
+ const body = await recRes.text().catch(() => '');
+ throw new WriteRefusal('Load', recRes.status, body);
}
record = readLoadedRecord(await recRes.json(), objectName, recordId);
}
@@ -1982,7 +1981,8 @@ function firstSentence(...vals: unknown[]): string | null {
}
/**
- * The error a refused submit throws (objectstack-ai/objectstack#27417): the
+ * The error a refused submit throws (objectstack-ai/objectstack#27417), and
+ * the one a refused edit-form record read throws (`verb` `'Load'`): the
* response attached the way `@objectstack/client` attaches it to its own
* errors — the HTTP `status`, the semantic `code`, `details` from the wrapped
* envelope's `error.details` (else the whole flat body), and the producer's
@@ -2008,11 +2008,13 @@ class WriteRefusal extends Error {
readonly body: string;
/** The server's sentence for this refusal, or `null` when it gave none. */
readonly reason: string | null;
+ /** What was refused: a write (`Submit`/`Create`/`Update`) or the record read. */
+ readonly verb: 'Submit' | 'Create' | 'Update' | 'Load';
code?: string;
details?: unknown;
userMessage?: string;
- constructor(verb: 'Submit' | 'Create' | 'Update', status: number, body: string) {
+ constructor(verb: 'Submit' | 'Create' | 'Update' | 'Load', status: number, body: string) {
let parsed: unknown;
try {
parsed = JSON.parse(body);
@@ -2044,6 +2046,7 @@ class WriteRefusal extends Error {
this.status = status;
this.body = body;
this.reason = reason;
+ this.verb = verb;
if (!envelope) return;
const code = firstSentence(envelope.code, wrapped?.code);
if (code) this.code = code;
@@ -2056,6 +2059,16 @@ class WriteRefusal extends Error {
}
/**
+ * What the banner says for a refused record read (`verb` `'Load'`): one fixed
+ * localized line, never the server's text, the status or the record id —
+ *
+ * - a `404` as "not found, or you can't see it": row-level security answers a
+ * hidden row with the same `404` as a missing one, so the line must not
+ * tell the two apart. Asked first, before any text-sniffing denial reader;
+ * - any other denial (`403`, `PERMISSION_DENIED`, `FORBIDDEN`) as "you don't
+ * have access to this record";
+ * - anything else as the localized generic line.
+ *
* What the banner and the toast say for a refused write — the record dialog's
* reading (`@object-ui/components`' form renderer), so a refusal reads the same
* on both surfaces:
@@ -2076,6 +2089,11 @@ function refusalText(
unmarked: string[] | null,
t: (key: string) => string,
): string | string[] {
+ if (err.verb === 'Load') {
+ if (err.status === 404) return t('form.recordNotFoundOrHidden');
+ if (isPermissionError(err)) return t('form.noPermissionToOpenRecord');
+ return t('form.openRecordFailed');
+ }
const marked = declaredUserMessage(err) ?? delegationScopeMessage(err);
if (marked) return marked;
if (isPermissionError(err)) return t('form.noPermissionToSave');
@@ -2085,7 +2103,7 @@ function refusalText(
}
/**
- * A submit refusal as the banner and the toast draw it: one string as itself,
+ * A refusal as the banner and the toast draw it: one string as itself,
* several reasons as one list item each.
*/
function refusalContent(refusal: string | string[]): ReactNode {
@@ -2665,6 +2683,7 @@ export function FormPage({ mode, recordPath, cancelPath }: FormPageProps) {
setLoaded(null);
if (e instanceof FormNotFoundError) setNotFound(true);
else if (e instanceof PublicFormUnavailableError) setPublicUnavailable(true);
+ else if (e instanceof WriteRefusal) setError(refusalText(e, null, t));
else setError(e?.message ?? String(e));
}
})
diff --git a/ui/packages/i18n/src/locales/ar.ts b/ui/packages/i18n/src/locales/ar.ts
index 5e6c959294d..689818cb122 100644
--- a/ui/packages/i18n/src/locales/ar.ts
+++ b/ui/packages/i18n/src/locales/ar.ts
@@ -189,6 +189,9 @@ const ar = {
noPermissionToEdit: "ليس لديك إذن لتعديل سجلات {{object}}. الحقول للقراءة فقط.",
noPermissionToEditRecord: "ليس لديك إذن لتعديل هذا السجل. الحقول للقراءة فقط.",
submitFailed: "تعذّر الحفظ. يرجى المحاولة مرة أخرى.",
+ noPermissionToOpenRecord: "ليس لديك إذن للوصول إلى هذا السجل.",
+ recordNotFoundOrHidden: "هذا السجل غير موجود، أو ليس لديك إذن للوصول إليه.",
+ openRecordFailed: "تعذّر فتح هذا السجل. يرجى المحاولة مرة أخرى.",
uploadInFlight: "انتظر حتى ينتهي الرفع قبل الحفظ.",
clearedOnHide: "تم مسح ما لم يعد ينطبق على القيم الحالية: {{fields}}",
visibleWhenFaulted: "تعذّر الإرسال: تعذّر تقييم قاعدة visibleWhen لـ {{fields}}. يجب إصلاح القاعدة قبل أن يصبح إرسال هذا النموذج ممكنًا.",
diff --git a/ui/packages/i18n/src/locales/de.ts b/ui/packages/i18n/src/locales/de.ts
index 45392e2e5f3..6b507f2ee27 100644
--- a/ui/packages/i18n/src/locales/de.ts
+++ b/ui/packages/i18n/src/locales/de.ts
@@ -175,6 +175,9 @@ const de = {
noPermissionToEdit: "Sie haben keine Berechtigung, „{{object}}“-Datensätze zu bearbeiten. Die Felder sind schreibgeschützt.",
noPermissionToEditRecord: "Sie haben keine Berechtigung, diesen Datensatz zu bearbeiten. Die Felder sind schreibgeschützt.",
submitFailed: "Speichern fehlgeschlagen. Bitte versuchen Sie es erneut.",
+ noPermissionToOpenRecord: "Sie haben keinen Zugriff auf diesen Datensatz.",
+ recordNotFoundOrHidden: "Dieser Datensatz existiert nicht, oder Sie haben keinen Zugriff darauf.",
+ openRecordFailed: "Der Datensatz konnte nicht geöffnet werden. Bitte versuchen Sie es erneut.",
uploadInFlight: "Warten Sie, bis der Upload abgeschlossen ist, bevor Sie speichern.",
clearedOnHide: "Geleert, was für die aktuellen Werte nicht mehr gilt: {{fields}}",
visibleWhenFaulted: "Absenden nicht möglich: Die visibleWhen-Regel von {{fields}} konnte nicht ausgewertet werden. Die Regel muss korrigiert werden, bevor dieses Formular abgesendet werden kann.",
diff --git a/ui/packages/i18n/src/locales/en.ts b/ui/packages/i18n/src/locales/en.ts
index ac6957dc30b..bccf0c6b696 100644
--- a/ui/packages/i18n/src/locales/en.ts
+++ b/ui/packages/i18n/src/locales/en.ts
@@ -232,6 +232,12 @@ const en = {
// allows it: sharing, ownership or row-level security (objectstack#27138).
noPermissionToEditRecord: "You don't have permission to edit this record. The fields are read-only.",
submitFailed: 'Could not save. Please try again.',
+ // The banner an edit form shows when its record read is refused: a
+ // denial, a not-found (which row-level security answers for a hidden row,
+ // so the line never tells the two apart), and any other failure.
+ noPermissionToOpenRecord: "You don't have access to this record.",
+ recordNotFoundOrHidden: "This record doesn't exist, or you don't have access to it.",
+ openRecordFailed: 'Could not open this record. Please try again.',
uploadInFlight: 'Wait for the upload to finish before saving.',
// The notice the record form raises when a field's own `visibleWhen` turns
// it invisible and the form clears the value it held (objectui#8070 names
diff --git a/ui/packages/i18n/src/locales/es.ts b/ui/packages/i18n/src/locales/es.ts
index 5b2e4ab9ac9..a257c01adec 100644
--- a/ui/packages/i18n/src/locales/es.ts
+++ b/ui/packages/i18n/src/locales/es.ts
@@ -182,6 +182,9 @@ const es = {
noPermissionToEdit: "No tienes permiso para editar registros de {{object}}. Los campos son de solo lectura.",
noPermissionToEditRecord: "No tienes permiso para editar este registro. Los campos son de solo lectura.",
submitFailed: "No se pudo guardar. Inténtalo de nuevo.",
+ noPermissionToOpenRecord: "No tienes acceso a este registro.",
+ recordNotFoundOrHidden: "Este registro no existe o no tienes acceso a él.",
+ openRecordFailed: "No se pudo abrir este registro. Inténtalo de nuevo.",
uploadInFlight: "Espere a que finalice la carga antes de guardar.",
clearedOnHide: "Se borró lo que ya no corresponde a los valores actuales: {{fields}}",
visibleWhenFaulted: "No se puede enviar: no se pudo evaluar la regla visibleWhen de {{fields}}. Hay que corregir la regla antes de poder enviar este formulario.",
diff --git a/ui/packages/i18n/src/locales/fr.ts b/ui/packages/i18n/src/locales/fr.ts
index 65e1a65d3a8..d7ba96cb0d7 100644
--- a/ui/packages/i18n/src/locales/fr.ts
+++ b/ui/packages/i18n/src/locales/fr.ts
@@ -177,6 +177,9 @@ const fr = {
noPermissionToEdit: "Vous n'avez pas l'autorisation de modifier les enregistrements {{object}}. Les champs sont en lecture seule.",
noPermissionToEditRecord: "Vous n'avez pas l'autorisation de modifier cet enregistrement. Les champs sont en lecture seule.",
submitFailed: "Échec de l'enregistrement. Veuillez réessayer.",
+ noPermissionToOpenRecord: "Vous n'avez pas accès à cet enregistrement.",
+ recordNotFoundOrHidden: "Cet enregistrement n'existe pas, ou vous n'y avez pas accès.",
+ openRecordFailed: "Impossible d'ouvrir cet enregistrement. Veuillez réessayer.",
uploadInFlight: "Attendez la fin du téléversement avant de sauvegarder.",
clearedOnHide: "Ce qui ne s'applique plus aux valeurs actuelles a été effacé : {{fields}}",
visibleWhenFaulted: "Envoi impossible : la règle visibleWhen de {{fields}} n'a pas pu être évaluée. La règle doit être corrigée avant que ce formulaire puisse être envoyé.",
diff --git a/ui/packages/i18n/src/locales/ja.ts b/ui/packages/i18n/src/locales/ja.ts
index 72046c7d8d6..8ee0b876fbf 100644
--- a/ui/packages/i18n/src/locales/ja.ts
+++ b/ui/packages/i18n/src/locales/ja.ts
@@ -175,6 +175,9 @@ const ja = {
noPermissionToEdit: "{{object}}を編集する権限がありません。フィールドは読み取り専用です。",
noPermissionToEditRecord: "このレコードを編集する権限がありません。フィールドは読み取り専用です。",
submitFailed: "保存できませんでした。もう一度お試しください。",
+ noPermissionToOpenRecord: "このレコードにアクセスする権限がありません。",
+ recordNotFoundOrHidden: "このレコードは存在しないか、アクセスする権限がありません。",
+ openRecordFailed: "このレコードを開けませんでした。もう一度お試しください。",
uploadInFlight: "アップロードが完了してから保存してください。",
clearedOnHide: "現在の値に該当しなくなった項目をクリアしました: {{fields}}",
visibleWhenFaulted: "送信できません: {{fields}} の visibleWhen ルールを評価できませんでした。このフォームを送信するには、先にルールを修正する必要があります。",
diff --git a/ui/packages/i18n/src/locales/ko.ts b/ui/packages/i18n/src/locales/ko.ts
index 3683dc1afc0..afdd74b9a35 100644
--- a/ui/packages/i18n/src/locales/ko.ts
+++ b/ui/packages/i18n/src/locales/ko.ts
@@ -175,6 +175,9 @@ const ko = {
noPermissionToEdit: "{{object}} 편집 권한이 없습니다. 필드는 읽기 전용입니다.",
noPermissionToEditRecord: "이 레코드를 편집할 권한이 없습니다. 필드는 읽기 전용입니다.",
submitFailed: "저장하지 못했습니다. 다시 시도해 주세요.",
+ noPermissionToOpenRecord: "이 레코드에 접근할 권한이 없습니다.",
+ recordNotFoundOrHidden: "이 레코드가 존재하지 않거나 접근할 권한이 없습니다.",
+ openRecordFailed: "이 레코드를 열지 못했습니다. 다시 시도해 주세요.",
uploadInFlight: "업로드가 완료된 후에 저장하세요.",
clearedOnHide: "현재 값에 더 이상 해당하지 않는 항목을 비웠습니다: {{fields}}",
visibleWhenFaulted: "제출할 수 없습니다: {{fields}}의 visibleWhen 규칙을 평가할 수 없습니다. 이 양식을 제출하려면 먼저 규칙을 수정해야 합니다.",
diff --git a/ui/packages/i18n/src/locales/pt.ts b/ui/packages/i18n/src/locales/pt.ts
index 45c539ca5b0..fbac65c0444 100644
--- a/ui/packages/i18n/src/locales/pt.ts
+++ b/ui/packages/i18n/src/locales/pt.ts
@@ -177,6 +177,9 @@ const pt = {
noPermissionToEdit: "Você não tem permissão para editar registros de {{object}}. Os campos são somente leitura.",
noPermissionToEditRecord: "Você não tem permissão para editar este registro. Os campos são somente leitura.",
submitFailed: "Não foi possível salvar. Tente novamente.",
+ noPermissionToOpenRecord: "Você não tem acesso a este registro.",
+ recordNotFoundOrHidden: "Este registro não existe ou você não tem acesso a ele.",
+ openRecordFailed: "Não foi possível abrir este registro. Tente novamente.",
uploadInFlight: "Aguarde a conclusão do upload antes de salvar.",
clearedOnHide: "O que não se aplica mais aos valores atuais foi limpo: {{fields}}",
visibleWhenFaulted: "Não é possível enviar: não foi possível avaliar a regra visibleWhen de {{fields}}. A regra precisa ser corrigida antes que este formulário possa ser enviado.",
diff --git a/ui/packages/i18n/src/locales/ru.ts b/ui/packages/i18n/src/locales/ru.ts
index cd07fda656e..bb5de86cf48 100644
--- a/ui/packages/i18n/src/locales/ru.ts
+++ b/ui/packages/i18n/src/locales/ru.ts
@@ -182,6 +182,9 @@ const ru = {
noPermissionToEdit: "У вас нет прав на редактирование записей «{{object}}». Поля доступны только для чтения.",
noPermissionToEditRecord: "У вас нет прав на редактирование этой записи. Поля доступны только для чтения.",
submitFailed: "Не удалось сохранить. Попробуйте ещё раз.",
+ noPermissionToOpenRecord: "У вас нет доступа к этой записи.",
+ recordNotFoundOrHidden: "Эта запись не существует, или у вас нет к ней доступа.",
+ openRecordFailed: "Не удалось открыть эту запись. Попробуйте ещё раз.",
uploadInFlight: "Дождитесь завершения загрузки, прежде чем сохранять.",
clearedOnHide: "Очищено то, что больше не применимо к текущим значениям: {{fields}}",
visibleWhenFaulted: "Невозможно отправить: не удалось вычислить правило visibleWhen для {{fields}}. Правило нужно исправить, прежде чем эту форму можно будет отправить.",
diff --git a/ui/packages/i18n/src/locales/zh.ts b/ui/packages/i18n/src/locales/zh.ts
index f6c37f2232f..435ad254782 100644
--- a/ui/packages/i18n/src/locales/zh.ts
+++ b/ui/packages/i18n/src/locales/zh.ts
@@ -180,6 +180,9 @@ const zh = {
noPermissionToEdit: '您没有编辑{{object}}的权限,字段均为只读。',
noPermissionToEditRecord: '您没有编辑此记录的权限,字段均为只读。',
submitFailed: '保存失败,请重试。',
+ noPermissionToOpenRecord: '您没有权限查看这条记录。',
+ recordNotFoundOrHidden: '这条记录不存在,或者您没有权限查看它。',
+ openRecordFailed: '无法打开这条记录,请重试。',
uploadInFlight: '请等待上传完成后再保存。',
clearedOnHide: '已清空不再适用于当前取值的字段:{{fields}}',
visibleWhenFaulted: '无法提交:{{fields}} 的 visibleWhen 规则无法求值。需先修正该规则,才能提交此表单。',