diff --git a/.changeset/30665-clear-saved-beneath-env-pin.md b/.changeset/30665-clear-saved-beneath-env-pin.md
new file mode 100644
index 00000000000..b9ddf1b131a
--- /dev/null
+++ b/.changeset/30665-clear-saved-beneath-env-pin.md
@@ -0,0 +1,12 @@
+---
+'@object-ui/console': patch
+'@object-ui/i18n': patch
+---
+
+feat(console): a settings field an environment variable pins offers to clear the value saved beneath the pin
+
+Clause-②: yes (widening)
+Metadata-protocol: none
+
+- **`@object-ui/console`:** on a Setup settings page, a field an `OS_*` environment variable pins still shows the lock badge and stays read-only. When the namespace read lists a value stored beneath the pin at the scope the page writes (a secret's as the read mask), the row also says that a value is saved there and that the environment value stays in force, and offers "Clear the saved value". The control asks first, then writes `null` for that one key, the one write the server lets through a pin; the page's unsaved edits are left as they are. After the clear the row still reads the environment value, and the saved value no longer comes back when the variable is removed. A pinned field with nothing stored at that scope, a field no variable pins, and the first-run checklist show no control.
+- **`@object-ui/i18n`:** four new `console.settingsView` keys in all eleven packs: `savedBeneathPin`, `clearSaved`, `clearSavedConfirm` and `savedCleared`.
diff --git a/ui/apps/console/src/pages/settings/SettingsField.tsx b/ui/apps/console/src/pages/settings/SettingsField.tsx
index 175cd9029a6..dd5cca2959a 100644
--- a/ui/apps/console/src/pages/settings/SettingsField.tsx
+++ b/ui/apps/console/src/pages/settings/SettingsField.tsx
@@ -128,6 +128,63 @@ export interface SettingsFieldProps {
* not (the first-run checklist) keeps the badges a lower scope would see.
*/
writeScope?: SpecifierScope;
+ /**
+ * Clears the value stored beneath an env pin (objectstack#30665): the page
+ * writes `null` for this key, the one write the server lets through a pin
+ * (ADR-0007 D6). Offered only where {@link storedBeneathPin} holds, so a
+ * host that does not pass it, or a row with nothing stored, shows nothing.
+ */
+ onClearSaved?: () => void;
+}
+
+/**
+ * Whether a value is stored beneath the env pin at the scope this row writes,
+ * the one a clear would null (objectstack#30665). Read off the server's
+ * `cascadeChain` alone: the namespace read lists the stored cascade beneath an
+ * in-force `env` entry, a secret's entries as the read mask or `null`. Nothing
+ * stored, or stored only at another scope, is no control: a clear here could
+ * not remove it.
+ */
+export function storedBeneathPin(
+ resolved: ResolvedSettingValue | undefined,
+ writeScope: SpecifierScope | undefined,
+): boolean {
+ if (!resolved?.locked || resolved.source !== 'env' || !writeScope) return false;
+ return (resolved.cascadeChain ?? []).some(
+ (entry) => entry.scope === writeScope && entry.value !== null && entry.value !== undefined,
+ );
+}
+
+/**
+ * The line and control under a pinned row that has a value stored beneath the
+ * pin (objectstack#30665). The wording clears the saved value, never deletes
+ * it, and says the environment value stays in force; the clear asks first,
+ * because it writes at once rather than through the page's draft.
+ */
+function ClearSavedBeneathPin({ onClear, disabled }: { onClear: () => void; disabled?: boolean }) {
+ const { t } = useObjectTranslation();
+ return (
+
+ );
}
type SourceBadge =
@@ -519,8 +576,20 @@ function ToggleSwitch({
}
export function SettingsField(props: SettingsFieldProps) {
- const { spec, resolved, value, onChange, onAction, locked, saving, labels, error, note, writeScope } =
- props;
+ const {
+ spec,
+ resolved,
+ value,
+ onChange,
+ onAction,
+ locked,
+ saving,
+ labels,
+ error,
+ note,
+ writeScope,
+ onClearSaved,
+ } = props;
const id = useId();
const { t, language } = useObjectTranslation();
const disabled = Boolean(locked || saving);
@@ -691,6 +760,11 @@ export function SettingsField(props: SettingsFieldProps) {
// control is a group that names itself from `labelId` instead.
// A value already cleared in the draft has nothing left to reset.
const onReset = value === null ? undefined : () => onChange(null);
+ // The clear beneath an env pin, where a value is stored at this row's scope.
+ const clearSaved =
+ onClearSaved && storedBeneathPin(resolved, writeScope) ? (
+
+ ) : null;
// `data-settings-key` lets the page find this row to focus it when a save is
// blocked on it (objectui#13239).
const wrapper = (children: React.ReactNode, labelFor: string | undefined = id) => (
@@ -723,6 +797,7 @@ export function SettingsField(props: SettingsFieldProps) {
{note}
) : null}
+ {clearSaved}
);
@@ -832,6 +907,7 @@ export function SettingsField(props: SettingsFieldProps) {
) : (
)}
+ {clearSaved}
, tracks dirty state, and saves through PUT.
* A successful save or action reads the namespace again, manifest included.
- * Env-locked fields are read-only and show .
+ * Env-locked fields are read-only and show ; one with a value
+ * stored beneath the pin offers to clear that saved value.
*/
import { useEffect, useMemo, useRef, useState } from 'react';
@@ -596,6 +597,30 @@ export function SettingsView() {
}
};
+ /**
+ * Clear the value stored beneath an env pin (objectstack#30665): `null` for
+ * this one key, the write the server lets through a pin (ADR-0007 D6). It
+ * writes at once, after the row's own confirm, and leaves the draft alone: the
+ * row cannot be edited, so the clear is not one of the page's edits. The pin
+ * still answers the key, so the row reads the env value before and after.
+ */
+ const onClearSaved = async (key: string) => {
+ setSaving(true);
+ try {
+ const res = await saveSettingsNamespace(namespace, { [key]: null });
+ setPayload({ ...payload, values: { ...values, ...res.values } });
+ toast.success(t('console.settingsView.savedCleared'));
+ void reread(namespace);
+ } catch (err: any) {
+ // The pin itself never refuses a clear, so a refusal here is something
+ // else (a lock a stored row carries, a 403): the server's sentence says
+ // which, and `lockedByEnv` would misname it.
+ toast.error(err?.message ?? t('console.settingsView.saveFailed'));
+ } finally {
+ setSaving(false);
+ }
+ };
+
const onAction = async (actionId: string) => {
setSaving(true);
try {
@@ -734,6 +759,7 @@ export function SettingsView() {
// The scope the server writes this key at: the specifier's
// own, else the manifest's, else the spec's default `tenant`.
writeScope={spec.scope ?? manifest.scope ?? 'tenant'}
+ onClearSaved={key ? () => onClearSaved(key) : undefined}
/>
);
})}
diff --git a/ui/apps/console/src/pages/settings/__tests__/SettingsView.clearBeneathPin-30665.test.tsx b/ui/apps/console/src/pages/settings/__tests__/SettingsView.clearBeneathPin-30665.test.tsx
new file mode 100644
index 00000000000..87ae254d262
--- /dev/null
+++ b/ui/apps/console/src/pages/settings/__tests__/SettingsView.clearBeneathPin-30665.test.tsx
@@ -0,0 +1,282 @@
+/**
+ * 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.
+ */
+
+/**
+ * A field an env variable pins can have its SAVED value cleared from Setup
+ * (objectstack#30665), so removing the variable later does not bring that
+ * value back.
+ *
+ * The namespace read lists, beneath an in-force `env` entry, the stored
+ * cascade the key falls back to (objectstack#30823), a secret's entries as the
+ * read mask or `null`; and the server lets a clear (`null`) through the pin
+ * while refusing every other write (objectstack#30385). The control appears
+ * only where a value is stored at the scope this page writes, the one the
+ * clear nulls.
+ *
+ * Driven over a stubbed `fetch` and the REAL `../api` module, so the PUT body
+ * is read as it leaves the client.
+ */
+
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
+import { render, screen, cleanup, fireEvent, waitFor, within } from '@testing-library/react';
+import { MemoryRouter, Route, Routes } from 'react-router-dom';
+import { I18nProvider } from '@object-ui/i18n';
+
+vi.mock('sonner', () => ({ toast: { error: vi.fn(), success: vi.fn() } }));
+
+import { toast } from 'sonner';
+import { SettingsView } from '../SettingsView';
+
+const MASK = '•'.repeat(8);
+const ENV = (value: unknown, name: string) => ({
+ scope: 'env',
+ value,
+ locked: true,
+ lockedReason: `Set via env: ${name}`,
+ effective: true,
+});
+
+/** Pinned, with a value stored at the page's write scope beneath the pin. */
+function pinned(value: unknown, name: string, beneath: Array<{ scope: string; value: unknown }>) {
+ return {
+ value,
+ source: 'env',
+ locked: true,
+ lockedReason: `Set via env: ${name}`,
+ cascadeChain: [ENV(value, name), ...beneath],
+ };
+}
+
+const PAYLOAD = {
+ manifest: {
+ namespace: 'auth',
+ version: 1,
+ label: 'Authentication',
+ scope: 'global',
+ specifiers: [
+ { type: 'text', key: 'allowed_ip_ranges', label: 'Allowed IP ranges' },
+ { type: 'password', key: 'webhook_secret', label: 'Webhook secret' },
+ { type: 'toggle', key: 'require_mfa', label: 'Require MFA', default: false },
+ { type: 'text', key: 'sign_in_message', label: 'Sign-in message', default: 'Welcome' },
+ { type: 'text', key: 'org_banner', label: 'Organization banner', scope: 'tenant' },
+ { type: 'text', key: 'support_email', label: 'Support email' },
+ ],
+ },
+ values: {
+ allowed_ip_ranges: pinned('', 'OS_AUTH_ALLOWED_IP_RANGES', [
+ { scope: 'global', value: '10.0.0.0/8' },
+ { scope: 'default', value: null },
+ ]),
+ webhook_secret: pinned(MASK, 'OS_AUTH_WEBHOOK_SECRET', [
+ { scope: 'global', value: MASK },
+ { scope: 'default', value: null },
+ ]),
+ require_mfa: pinned(true, 'OS_AUTH_REQUIRE_MFA', [
+ { scope: 'global', value: false },
+ { scope: 'default', value: false },
+ ]),
+ // Pinned, nothing stored beneath: only the default.
+ sign_in_message: pinned('Hi', 'OS_AUTH_SIGN_IN_MESSAGE', [
+ { scope: 'global', value: null },
+ { scope: 'default', value: 'Welcome' },
+ ]),
+ // Pinned, stored only ABOVE the page's write scope: a tenant clear cannot remove it.
+ org_banner: pinned('Hello', 'OS_AUTH_ORG_BANNER', [
+ { scope: 'global', value: 'Old banner' },
+ { scope: 'default', value: null },
+ ]),
+ // Not pinned at all.
+ support_email: { value: 'help@example.com', source: 'global', locked: false },
+ },
+};
+
+/** The server's answer to the clear: the pin still answers, nothing stored beneath. */
+const CLEARED_IP_RANGES = pinned('', 'OS_AUTH_ALLOWED_IP_RANGES', []);
+const REREAD_IP_RANGES = pinned('', 'OS_AUTH_ALLOWED_IP_RANGES', [
+ { scope: 'global', value: null },
+ { scope: 'default', value: null },
+]);
+
+const fetchMock = vi.fn();
+
+function ok(body: unknown): Response {
+ return { ok: true, status: 200, statusText: 'OK', json: async () => body } as unknown as Response;
+}
+
+/** GET answers the page; a PUT answers `putValues` and the next GET reads `rereadValues`. */
+function serve(putValues: Record, rereadValues: Record = putValues) {
+ const stored = structuredClone(PAYLOAD) as { values: Record };
+ fetchMock.mockImplementation(async (_url: string, init?: RequestInit) => {
+ if (init?.method === 'PUT') {
+ Object.assign(stored.values, structuredClone(rereadValues));
+ return ok({ success: true, data: { values: putValues } });
+ }
+ return ok({ success: true, data: structuredClone(stored) });
+ });
+}
+
+function puts(): string[] {
+ return fetchMock.mock.calls
+ .filter(([, init]) => (init as RequestInit | undefined)?.method === 'PUT')
+ .map(([, init]) => String((init as RequestInit).body));
+}
+
+function renderView() {
+ return render(
+
+
+
+ } />
+
+
+ ,
+ );
+}
+
+/** The row a field label belongs to. */
+function rowOf(label: string): HTMLElement {
+ const row = screen.getByText(label).closest('[data-settings-key]');
+ expect(row).not.toBeNull();
+ return row as HTMLElement;
+}
+
+const CLEAR = /^clear the saved value$/i;
+
+async function confirmClear(label: string) {
+ fireEvent.click(within(rowOf(label)).getByRole('button', { name: CLEAR }));
+ const dialog = await screen.findByRole('alertdialog');
+ fireEvent.click(within(dialog).getByRole('button', { name: CLEAR }));
+}
+
+beforeEach(() => {
+ fetchMock.mockReset();
+ vi.mocked(toast.success).mockReset();
+ vi.mocked(toast.error).mockReset();
+ vi.stubGlobal('fetch', fetchMock);
+ window.localStorage.clear();
+});
+afterEach(() => {
+ cleanup();
+ vi.unstubAllGlobals();
+});
+
+describe('clear the value saved beneath an env pin (objectstack#30665)', () => {
+ it('offers the clear only on a pinned row with a value stored at the scope the page writes', async () => {
+ serve({});
+ renderView();
+ await screen.findByText('Allowed IP ranges');
+
+ for (const label of ['Allowed IP ranges', 'Webhook secret', 'Require MFA']) {
+ expect(within(rowOf(label)).getByRole('button', { name: CLEAR })).toBeTruthy();
+ }
+ for (const label of ['Sign-in message', 'Organization banner', 'Support email']) {
+ expect(within(rowOf(label)).queryByRole('button', { name: CLEAR })).toBeNull();
+ expect(within(rowOf(label)).queryByTestId('settings-saved-beneath-pin')).toBeNull();
+ }
+ expect(screen.getAllByRole('button', { name: CLEAR })).toHaveLength(3);
+ });
+
+ it('says the environment value stays in force, and never says delete', async () => {
+ serve({});
+ renderView();
+ await screen.findByText('Allowed IP ranges');
+ const line = within(rowOf('Allowed IP ranges')).getByTestId('settings-saved-beneath-pin');
+ expect(line.textContent).toMatch(/environment value stays in force/i);
+
+ fireEvent.click(within(rowOf('Allowed IP ranges')).getByRole('button', { name: CLEAR }));
+ const dialog = await screen.findByRole('alertdialog');
+ expect(dialog.textContent).toMatch(/environment value stays in force/i);
+ for (const text of [line.textContent, dialog.textContent]) expect(text).not.toMatch(/delet/i);
+ });
+
+ it('sends null for that one key after the confirm, and the row follows the server answer', async () => {
+ serve({ allowed_ip_ranges: CLEARED_IP_RANGES }, { allowed_ip_ranges: REREAD_IP_RANGES });
+ renderView();
+ await screen.findByText('Allowed IP ranges');
+
+ await confirmClear('Allowed IP ranges');
+ await waitFor(() => expect(puts()).toHaveLength(1));
+ expect(puts()[0]).toBe('{"allowed_ip_ranges":null}');
+ await waitFor(() => expect(toast.success).toHaveBeenCalledWith('Saved value cleared'));
+
+ // Still pinned, still read-only, and nothing left to clear.
+ await waitFor(() =>
+ expect(within(rowOf('Allowed IP ranges')).queryByRole('button', { name: CLEAR })).toBeNull(),
+ );
+ const row = rowOf('Allowed IP ranges');
+ expect(within(row).getByText('Locked by env')).toBeTruthy();
+ expect((row.querySelector('input') as HTMLInputElement).disabled).toBe(true);
+ // The other pinned rows keep theirs.
+ expect(screen.getAllByRole('button', { name: CLEAR })).toHaveLength(2);
+ });
+
+ it('a masked secret beneath the pin is a stored value, and its clear sends null', async () => {
+ serve({ webhook_secret: pinned(MASK, 'OS_AUTH_WEBHOOK_SECRET', []) });
+ renderView();
+ await screen.findByText('Webhook secret');
+ await confirmClear('Webhook secret');
+ await waitFor(() => expect(puts()).toEqual(['{"webhook_secret":null}']));
+ });
+
+ it('CONTROL: Cancel sends nothing', async () => {
+ serve({});
+ renderView();
+ await screen.findByText('Allowed IP ranges');
+ fireEvent.click(within(rowOf('Allowed IP ranges')).getByRole('button', { name: CLEAR }));
+ const dialog = await screen.findByRole('alertdialog');
+ fireEvent.click(within(dialog).getByRole('button', { name: /cancel/i }));
+ await waitFor(() => expect(screen.queryByRole('alertdialog')).toBeNull());
+ expect(puts()).toEqual([]);
+ });
+
+ it('leaves the page draft alone: only the cleared key is written, the edit stays unsaved', async () => {
+ serve({ allowed_ip_ranges: CLEARED_IP_RANGES });
+ renderView();
+ fireEvent.change(await screen.findByDisplayValue('help@example.com'), {
+ target: { value: 'ops@example.com' },
+ });
+ expect(screen.getByTestId('settings-unsaved-count')).toBeTruthy();
+
+ await confirmClear('Allowed IP ranges');
+ await waitFor(() => expect(puts()).toEqual(['{"allowed_ip_ranges":null}']));
+ await waitFor(() => expect(toast.success).toHaveBeenCalled());
+ expect(screen.getByTestId('settings-unsaved-count')).toBeTruthy();
+ expect(screen.getByDisplayValue('ops@example.com')).toBeTruthy();
+ });
+
+ it("a refused clear shows the server's sentence", async () => {
+ fetchMock.mockImplementation(async (_url: string, init?: RequestInit) => {
+ if (init?.method === 'PUT') {
+ return {
+ ok: false,
+ status: 409,
+ statusText: 'Conflict',
+ json: async () => ({
+ success: false,
+ error: {
+ code: 'SETTINGS_LOCKED',
+ message: "Setting 'auth.allowed_ip_ranges' is locked (locked-by-global).",
+ details: { key: 'allowed_ip_ranges' },
+ },
+ }),
+ } as unknown as Response;
+ }
+ return ok({ success: true, data: structuredClone(PAYLOAD) });
+ });
+ renderView();
+ await screen.findByText('Allowed IP ranges');
+ await confirmClear('Allowed IP ranges');
+ await waitFor(() =>
+ expect(toast.error).toHaveBeenCalledWith(
+ "Setting 'auth.allowed_ip_ranges' is locked (locked-by-global).",
+ ),
+ );
+ expect(toast.success).not.toHaveBeenCalled();
+ expect(within(rowOf('Allowed IP ranges')).getByRole('button', { name: CLEAR })).toBeTruthy();
+ });
+});
diff --git a/ui/packages/i18n/src/locales/ar.ts b/ui/packages/i18n/src/locales/ar.ts
index 64ff81699bd..753e5a119b2 100644
--- a/ui/packages/i18n/src/locales/ar.ts
+++ b/ui/packages/i18n/src/locales/ar.ts
@@ -2439,6 +2439,10 @@ const ar = {
defaultValueHint: "الافتراضي: {{value}}",
envLockBadge: "مقفل بواسطة البيئة",
envLockBadgeTitle: "مثبّت بواسطة متغير بيئة — لا يمكن تعديله من الواجهة.",
+ savedBeneathPin: "توجد أيضًا قيمة محفوظة هنا. تظل قيمة متغير البيئة سارية، وتعود القيمة المحفوظة إذا أُزيل المتغير.",
+ clearSaved: "مسح القيمة المحفوظة",
+ clearSavedConfirm: "ستُمسح القيمة المحفوظة هنا، فلا تعود عند إزالة متغير البيئة. تظل قيمة متغير البيئة سارية.",
+ savedCleared: "تم مسح القيمة المحفوظة",
deprecatedBadge: "مهمل",
deprecatedBadgeReplacedBy: "مهمل ← {{replacedBy}}",
unsupportedSpecifierType: "نوع إعداد غير مدعوم: {{type}}",
diff --git a/ui/packages/i18n/src/locales/de.ts b/ui/packages/i18n/src/locales/de.ts
index 8ebaeea7693..a717db9dcb4 100644
--- a/ui/packages/i18n/src/locales/de.ts
+++ b/ui/packages/i18n/src/locales/de.ts
@@ -2200,6 +2200,10 @@ const de = {
defaultValueHint: "Standard: {{value}}",
envLockBadge: "Per Umgebung gesperrt",
envLockBadgeTitle: "Durch eine Umgebungsvariable festgelegt — kann nicht in der Oberfläche bearbeitet werden.",
+ savedBeneathPin: "Hier ist zusätzlich ein Wert gespeichert. Der Wert der Umgebungsvariable bleibt wirksam; der gespeicherte gilt wieder, sobald die Variable entfernt wird.",
+ clearSaved: "Gespeicherten Wert leeren",
+ clearSavedConfirm: "Der hier gespeicherte Wert wird geleert und gilt nicht wieder, wenn die Umgebungsvariable entfernt wird. Der Wert der Umgebungsvariable bleibt wirksam.",
+ savedCleared: "Gespeicherter Wert geleert",
deprecatedBadge: "Veraltet",
deprecatedBadgeReplacedBy: "Veraltet → {{replacedBy}}",
unsupportedSpecifierType: "Nicht unterstützter Einstellungstyp: {{type}}",
diff --git a/ui/packages/i18n/src/locales/en.ts b/ui/packages/i18n/src/locales/en.ts
index a777f1fffd3..9df794c790b 100644
--- a/ui/packages/i18n/src/locales/en.ts
+++ b/ui/packages/i18n/src/locales/en.ts
@@ -2355,6 +2355,14 @@ const en = {
// page has no control for, `{{type}}` that type.
envLockBadge: 'Locked by env',
envLockBadgeTitle: 'Pinned by environment variable — cannot be edited from the UI.',
+ // The clear under a pinned row that has a value stored beneath the pin
+ // (objectstack#30665): it clears the saved value, never "deletes" it, and
+ // says the environment value stays in force. `clearSaved` is the button and
+ // the confirm's title and action; `savedCleared` the toast once it is done.
+ savedBeneathPin: 'A value is also saved here. The environment value stays in force; the saved one returns if the variable is removed.',
+ clearSaved: 'Clear the saved value',
+ clearSavedConfirm: 'The value saved here is cleared, so it does not return when the environment variable is removed. The environment value stays in force.',
+ savedCleared: 'Saved value cleared',
deprecatedBadge: 'Deprecated',
deprecatedBadgeReplacedBy: 'Deprecated → {{replacedBy}}',
unsupportedSpecifierType: 'Unsupported specifier type: {{type}}',
diff --git a/ui/packages/i18n/src/locales/es.ts b/ui/packages/i18n/src/locales/es.ts
index e9bd10c0758..64b5245bf47 100644
--- a/ui/packages/i18n/src/locales/es.ts
+++ b/ui/packages/i18n/src/locales/es.ts
@@ -2262,6 +2262,10 @@ const es = {
defaultValueHint: "Predeterminado: {{value}}",
envLockBadge: "Bloqueado por el entorno",
envLockBadgeTitle: "Fijado por una variable de entorno: no se puede editar desde la interfaz.",
+ savedBeneathPin: "También hay un valor guardado aquí. El valor de la variable de entorno sigue vigente; el guardado vuelve a aplicarse si se quita la variable.",
+ clearSaved: "Borrar el valor guardado",
+ clearSavedConfirm: "Se borra el valor guardado aquí, por lo que no vuelve a aplicarse al quitar la variable de entorno. El valor de la variable de entorno sigue vigente.",
+ savedCleared: "Valor guardado borrado",
deprecatedBadge: "Obsoleto",
deprecatedBadgeReplacedBy: "Obsoleto → {{replacedBy}}",
unsupportedSpecifierType: "Tipo de ajuste no admitido: {{type}}",
diff --git a/ui/packages/i18n/src/locales/fr.ts b/ui/packages/i18n/src/locales/fr.ts
index 174624fdbc9..902d95f8d81 100644
--- a/ui/packages/i18n/src/locales/fr.ts
+++ b/ui/packages/i18n/src/locales/fr.ts
@@ -2258,6 +2258,10 @@ const fr = {
defaultValueHint: "Par défaut : {{value}}",
envLockBadge: "Verrouillé par l'env.",
envLockBadgeTitle: "Fixé par une variable d'environnement — modification impossible depuis l'interface.",
+ savedBeneathPin: "Une valeur est aussi enregistrée ici. La valeur de la variable d'environnement reste en vigueur ; la valeur enregistrée s'appliquera de nouveau si la variable est retirée.",
+ clearSaved: "Effacer la valeur enregistrée",
+ clearSavedConfirm: "La valeur enregistrée ici est effacée : elle ne s'appliquera plus quand la variable d'environnement sera retirée. La valeur de la variable d'environnement reste en vigueur.",
+ savedCleared: "Valeur enregistrée effacée",
deprecatedBadge: "Obsolète",
deprecatedBadgeReplacedBy: "Obsolète → {{replacedBy}}",
unsupportedSpecifierType: "Type de paramètre non pris en charge : {{type}}",
diff --git a/ui/packages/i18n/src/locales/ja.ts b/ui/packages/i18n/src/locales/ja.ts
index 04231f2632b..94fada9b614 100644
--- a/ui/packages/i18n/src/locales/ja.ts
+++ b/ui/packages/i18n/src/locales/ja.ts
@@ -2202,6 +2202,10 @@ const ja = {
defaultValueHint: "デフォルト: {{value}}",
envLockBadge: "環境変数でロック",
envLockBadgeTitle: "環境変数で固定されているため、画面から編集できません。",
+ savedBeneathPin: "ここにも値が保存されています。環境変数の値が引き続き有効で、変数を削除すると保存された値が再び適用されます。",
+ clearSaved: "保存された値をクリア",
+ clearSavedConfirm: "ここに保存された値をクリアします。環境変数を削除しても、この値には戻りません。環境変数の値は引き続き有効です。",
+ savedCleared: "保存された値をクリアしました",
deprecatedBadge: "非推奨",
deprecatedBadgeReplacedBy: "非推奨 → {{replacedBy}}",
unsupportedSpecifierType: "サポートされていない設定項目の種類: {{type}}",
diff --git a/ui/packages/i18n/src/locales/ko.ts b/ui/packages/i18n/src/locales/ko.ts
index 3e191edf4fd..116c4cc9a2b 100644
--- a/ui/packages/i18n/src/locales/ko.ts
+++ b/ui/packages/i18n/src/locales/ko.ts
@@ -2200,6 +2200,10 @@ const ko = {
defaultValueHint: "기본값: {{value}}",
envLockBadge: "환경 변수로 잠김",
envLockBadgeTitle: "환경 변수로 고정되어 UI에서 편집할 수 없습니다.",
+ savedBeneathPin: "여기에도 값이 저장되어 있습니다. 환경 변수 값이 계속 적용되며, 변수를 제거하면 저장된 값이 다시 적용됩니다.",
+ clearSaved: "저장된 값 지우기",
+ clearSavedConfirm: "여기에 저장된 값을 지웁니다. 환경 변수를 제거해도 이 값으로 돌아가지 않습니다. 환경 변수 값은 계속 적용됩니다.",
+ savedCleared: "저장된 값을 지웠습니다",
deprecatedBadge: "지원 중단됨",
deprecatedBadgeReplacedBy: "지원 중단됨 → {{replacedBy}}",
unsupportedSpecifierType: "지원되지 않는 설정 항목 유형: {{type}}",
diff --git a/ui/packages/i18n/src/locales/pt-BR.ts b/ui/packages/i18n/src/locales/pt-BR.ts
index 93107e3c97c..93856feea24 100644
--- a/ui/packages/i18n/src/locales/pt-BR.ts
+++ b/ui/packages/i18n/src/locales/pt-BR.ts
@@ -2257,6 +2257,10 @@ const pt = {
defaultValueHint: "Padrão: {{value}}",
envLockBadge: "Bloqueado pelo ambiente",
envLockBadgeTitle: "Fixado por uma variável de ambiente — não pode ser editado pela interface.",
+ savedBeneathPin: "Também há um valor salvo aqui. O valor da variável de ambiente continua em vigor; o valor salvo volta a valer se a variável for removida.",
+ clearSaved: "Limpar o valor salvo",
+ clearSavedConfirm: "O valor salvo aqui é limpo e não volta a valer quando a variável de ambiente for removida. O valor da variável de ambiente continua em vigor.",
+ savedCleared: "Valor salvo limpo",
deprecatedBadge: "Obsoleto",
deprecatedBadgeReplacedBy: "Obsoleto → {{replacedBy}}",
unsupportedSpecifierType: "Tipo de configuração não suportado: {{type}}",
diff --git a/ui/packages/i18n/src/locales/ru.ts b/ui/packages/i18n/src/locales/ru.ts
index 82e7c3e8bf5..643cf78cf0f 100644
--- a/ui/packages/i18n/src/locales/ru.ts
+++ b/ui/packages/i18n/src/locales/ru.ts
@@ -2326,6 +2326,10 @@ const ru = {
defaultValueHint: "По умолчанию: {{value}}",
envLockBadge: "Задано окружением",
envLockBadgeTitle: "Закреплено переменной окружения — изменить в интерфейсе нельзя.",
+ savedBeneathPin: "Здесь также сохранено значение. Значение переменной окружения остаётся в силе; сохранённое снова начнёт действовать, если переменную убрать.",
+ clearSaved: "Очистить сохранённое значение",
+ clearSavedConfirm: "Сохранённое здесь значение будет очищено и не вернётся, когда переменную окружения уберут. Значение переменной окружения остаётся в силе.",
+ savedCleared: "Сохранённое значение очищено",
deprecatedBadge: "Устарело",
deprecatedBadgeReplacedBy: "Устарело → {{replacedBy}}",
unsupportedSpecifierType: "Неподдерживаемый тип настройки: {{type}}",
diff --git a/ui/packages/i18n/src/locales/zh-Hans.ts b/ui/packages/i18n/src/locales/zh-Hans.ts
index c28cb7cb350..4c2ee2b08b8 100644
--- a/ui/packages/i18n/src/locales/zh-Hans.ts
+++ b/ui/packages/i18n/src/locales/zh-Hans.ts
@@ -1974,6 +1974,10 @@ const zh = {
defaultValueHint: '默认:{{value}}',
envLockBadge: '环境变量锁定',
envLockBadgeTitle: '由环境变量固定,无法在界面中编辑。',
+ savedBeneathPin: '此处还保存着一个值。环境变量的值仍然生效;移除该变量后,保存的值会重新生效。',
+ clearSaved: '清除保存的值',
+ clearSavedConfirm: '此处保存的值将被清除,移除环境变量后不会再恢复。环境变量的值仍然生效。',
+ savedCleared: '已清除保存的值',
deprecatedBadge: '已弃用',
deprecatedBadgeReplacedBy: '已弃用 → {{replacedBy}}',
unsupportedSpecifierType: '不支持的设置项类型:{{type}}',
diff --git a/ui/packages/i18n/src/locales/zh-Hant.ts b/ui/packages/i18n/src/locales/zh-Hant.ts
index 668ea0b36d0..ee5ef8f61ef 100644
--- a/ui/packages/i18n/src/locales/zh-Hant.ts
+++ b/ui/packages/i18n/src/locales/zh-Hant.ts
@@ -1974,6 +1974,10 @@ const zhHant = {
defaultValueHint: '預設:{{value}}',
envLockBadge: '環境變數鎖定',
envLockBadgeTitle: '由環境變數固定,無法在介面中編輯。',
+ savedBeneathPin: '此處還儲存著一個值。環境變數的值仍然生效;移除該變數後,儲存的值會重新生效。',
+ clearSaved: '清除儲存的值',
+ clearSavedConfirm: '此處儲存的值將被清除,移除環境變數後不會再恢復。環境變數的值仍然生效。',
+ savedCleared: '已清除儲存的值',
deprecatedBadge: '已棄用',
deprecatedBadgeReplacedBy: '已棄用 → {{replacedBy}}',
unsupportedSpecifierType: '不支援的設定項型別:{{type}}',