This is an automated email from the ASF dual-hosted git repository.
wu-sheng pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/skywalking-horizon-ui.git
The following commit(s) were added to refs/heads/main by this push:
new d43d73f fix(admin): default editor to remote + reset truly discards
local draft (#18)
d43d73f is described below
commit d43d73fe8545bcc41dedd716b1f46c944e3f21a1
Author: 吴晟 Wu Sheng <[email protected]>
AuthorDate: Tue May 26 22:15:12 2026 +0800
fix(admin): default editor to remote + reset truly discards local draft
(#18)
Operator-facing fixes across the three admin editors (Layer
Dashboards, Overview Templates, Translations):
1. **Editor source default = remote** (Layer + Overview). The runtime
bundle serves remote content to end users for synced / diverged /
remote-only rows; the editor now opens from remote on every
(re-)mount, with local draft winning when present and bundled
only on bundled-fallback rows.
2. **Source pill — three distinct states** (Layer + Overview).
- Remote (default) → no pill.
- After Reset to bundled (no local edits yet) → "from bundled".
- After save / typed local edits → "from local".
The bundled-vs-local distinction matters: reset stages bundled
for inspection / push, local edits are typed content not on disk
anywhere.
3. **Source pill on Translations follows the same vocabulary.** As
soon as the operator has a staged local draft for the current
(template, locale), a "from local" chip surfaces in the editor
header. Discard puts the editor back on the default (OAP overlay
row) and the chip disappears.
4. **Reset → Bundled stays visible even when synced** (Layer +
Overview). Previously hidden via `v-if="!isSynced"`; now always
rendered and disabled with a "(synced)" tail when bundled equals
remote so the affordance stays visible while clearly signalling
nothing meaningful to reset to.
5. **Reset really discards** (Layer + Overview). "Reset to bundled" /
"Reset to remote" advertised "Discard current edits …", but the
implementation called `persistLocal(new_content)` after
`loadFrom(src)` — writing the freshly-loaded content into
localEdits as a brand-new draft and triggering the
TemplateConflictPrompt right after every reset. Now calls
`localEdits.remove(editName)`; subsequent edits re-create a
draft naturally on the next change.
6. **Picker filters auto-uncheck when their set empties** (Layer +
Overview). Reset OAP to bundled / Push / Sync on the last row
matching the active filter (e.g. the last Diverged layer) used to
leave the operator stranded — the filter stuck on Diverged, the
list went empty, and the checkbox itself was disabled because
`divergedCount === 0`, so the operator couldn't uncheck to
recover. Reactive guard watches both diverged + local counts and
clears the matching filter when it goes to zero.
Adds the new pill / dropdown labels + tooltips to all eight locale
catalogs.
---
.../admin/layer-templates/LayerDashboardsAdmin.vue | 146 ++++++++++++----
.../overview-templates/OverviewTemplatesAdmin.vue | 136 ++++++++++-----
.../admin/translations/TranslationsView.vue | 190 +++++++++++++++++++--
apps/ui/src/i18n/locales/de.json | 24 ++-
apps/ui/src/i18n/locales/en.json | 24 ++-
apps/ui/src/i18n/locales/es.json | 24 ++-
apps/ui/src/i18n/locales/fr.json | 24 ++-
apps/ui/src/i18n/locales/ja.json | 24 ++-
apps/ui/src/i18n/locales/ko.json | 24 ++-
apps/ui/src/i18n/locales/pt.json | 24 ++-
apps/ui/src/i18n/locales/zh-CN.json | 24 ++-
11 files changed, 562 insertions(+), 102 deletions(-)
diff --git
a/apps/ui/src/features/admin/layer-templates/LayerDashboardsAdmin.vue
b/apps/ui/src/features/admin/layer-templates/LayerDashboardsAdmin.vue
index 3cd07d6..98c5284 100644
--- a/apps/ui/src/features/admin/layer-templates/LayerDashboardsAdmin.vue
+++ b/apps/ui/src/features/admin/layer-templates/LayerDashboardsAdmin.vue
@@ -171,6 +171,15 @@ function isDivergedRow(key: string): boolean {
}
const divergedCount = computed(() => templates.value.filter((t) =>
isDivergedRow(t.key)).length);
const localCount = computed(() => templates.value.filter((t) =>
localEdits.has(layerEditName(t.key))).length);
+
+// Auto-uncheck the picker filter whenever its underlying set goes to
+// zero. Without this the operator can hit Reset / Push / Sync — which
+// drops the matching row out of the filter — and then be stranded on
+// an empty picker with no way to navigate to another layer (the
+// filter checkbox would also be disabled since divergedCount === 0).
+// Catches every path that depletes the set, not just reset.
+watch(divergedCount, (n) => { if (n === 0) divergedOnly.value = false; });
+watch(localCount, (n) => { if (n === 0) localOnly.value = false; });
const filteredTemplates = computed<AdminLayerTemplate[]>(() => {
const q = layerSearch.value.trim().toLowerCase();
return templates.value.filter((t) => {
@@ -192,6 +201,13 @@ const localEdits = useLocalTemplateEdits();
// Server-side bundled + remote content for the Reset-to / Preview editor
// sources. Local (browser draft) comes from `localEdits`.
const sources = useTemplateSources('layer');
+// Settled-state flag for the source pill: until the config-bundle
+// fetch resolves, `hasRemote(name)` returns false for every name, so
+// any pill we render reflects the boot fallback (bundled) and then
+// flips to its real value when the bundle lands — a visual flash on
+// every page open. The pill v-if's on `sourcesReady` so it stays
+// hidden until the data is real.
+const sourcesReady = computed(() => !sources.isLoading.value);
const previewOverride = usePreviewOverride();
async function loadAll(): Promise<void> {
@@ -214,7 +230,14 @@ async function loadAll(): Promise<void> {
if (SCOPES.includes(queryScope as AdminScope)) {
activeScope.value = queryScope as AdminScope;
}
- syncDraft();
+ // Seed the editor only once the config-bundle (and hence
+ // `remoteAvailable` / `hasLocalDraft`) has resolved — otherwise
+ // syncDraft falls through to bundled and the editor visibly
+ // re-loads as the bundle lands. Two paths: (a) sources are
+ // already ready (warm cache) → run synchronously; (b) still
+ // loading → handed off to the corrective watcher below, which
+ // fires syncDraft on the false → true transition.
+ if (sourcesReady.value) syncDraft();
} catch (err) {
error.value = err instanceof Error ? err.message : String(err);
} finally {
@@ -253,7 +276,14 @@ watch(
* is the baseline for `dirty` (= unsaved edits since the last load/save).
* Saving always writes LOCAL; you then publish LOCAL → OAP.
* ─────────────────────────────────────────────────────────────────── */
-const editorSource = ref<'local' | 'bundled' | 'remote'>('bundled');
+// Remote is the canonical baseline (what the runtime menu renders for
+// synced + diverged + remote-only rows). The editor opens from remote
+// on every (re-)mount and stays there until the operator explicitly
+// loads bundled (Reset to bundled) or saves a local draft. The default
+// `'remote'` here matches what `syncDraft()` will pick on the first
+// pass — pre-seeding the same value lets the source pill stay hidden
+// (labelling the default would be noise).
+const editorSource = ref<'local' | 'bundled' | 'remote'>('remote');
const loadedSnapshot = ref<string>('');
const editName = computed(() => layerEditName(selectedKey.value));
const hasLocalDraft = computed(() => localEdits.has(editName.value));
@@ -293,22 +323,22 @@ function loadFrom(src: 'local' | 'bundled' | 'remote'):
void {
editorSource.value = src;
saveMsg.value = null;
}
-/** Seed the editor when the selected layer changes. Priority mirrors
- * what the operator actually sees in the live menu / dashboards:
+/** Seed the editor when the selected layer changes. Remote is the
+ * canonical baseline — it's what `pickLayerContent` in the runtime
+ * bundle serves to end users for synced / diverged / remote-only
+ * rows, so the editor opens from remote whenever remote is reachable.
+ * Priority:
* 1. Local draft — unpublished in-progress edits in this browser.
- * 2. Remote — when bundled and remote diverged (or the layer is
- * remote-only), OAP is the source of truth and the runtime
- * bundle loads it via `pickLayerContent` (bundle.ts). Showing
- * bundled here would silently disagree with the live UI.
- * 3. Bundled — synced rows are byte-equal anyway; bundled-fallback
- * is the bundle's only source. */
+ * 2. Remote — the default for every re-mount when remote exists.
+ * 3. Bundled — only when remote is absent (bundled-fallback). The
+ * operator can also hit "Reset to bundled" explicitly to swap;
+ * that path goes through `resetTo`, not this seed function. */
function syncDraft(): void {
if (hasLocalDraft.value) {
loadFrom('local');
return;
}
- const badge = sync.badgeFor(editName.value);
- if ((badge === 'diverged' || badge === 'remote-only') &&
remoteAvailable.value) {
+ if (remoteAvailable.value) {
loadFrom('remote');
return;
}
@@ -329,12 +359,17 @@ function persistLocal(content: AdminLayerTemplate): void {
loadedSnapshot.value = JSON.stringify(content);
}
-// "Reset to ▾" dropdown — reload the editor from a source AND adopt it as
-// the local draft (so the choice persists + Push/diff status updates).
+// "Reset to ▾" dropdown — discard the current local draft and reload the
+// editor from the picked source. The op-facing tooltip says "Discard
+// current edits and reload …", so the action must DROP the local draft
+// rather than re-stage the new content as a fresh draft (which the
+// previous implementation did via persistLocal, triggering the
+// TemplateConflictPrompt right after every reset). Subsequent edits in
+// the editor re-create a local draft naturally on the next change.
const resetDropdownOpen = ref(false);
function resetTo(src: 'bundled' | 'remote'): void {
loadFrom(src);
- if (draft.template) persistLocal(draft.template);
+ localEdits.remove(editName.value);
resetDropdownOpen.value = false;
}
@@ -387,7 +422,21 @@ function previewLive(src: 'local' | 'bundled' | 'remote'):
void {
window.open(href, '_blank', 'noopener');
}
-watch(selectedKey, syncDraft);
+watch(selectedKey, () => {
+ if (sourcesReady.value) syncDraft();
+});
+
+// First-mount deferred sync. When the page opens on a cold cache
+// `loadAll()` returns before the config-bundle has settled, so the
+// initial syncDraft is skipped (see loadAll). The moment sources
+// transition to ready, run syncDraft for the currently-selected
+// layer — once. No clobber on later transitions: this only fires on
+// the false → true edge.
+watch(sourcesReady, (ready, wasReady) => {
+ if (ready && !wasReady && selectedKey.value) {
+ syncDraft();
+ }
+});
onMounted(loadAll);
// Force-refresh the cached config bundle on mount so per-row badges
// (`synced` / `diverged` / `disabled`) reflect actual OAP state. Without
@@ -1599,13 +1648,29 @@ const namingTest = computed<NamingTestResult>(() => {
</div>
<div class="actions">
<span v-if="saveMsg" class="save-msg">{{ saveMsg }}</span>
- <!-- Where the editor content was loaded from — distinct from
- the sync-status chip by the title (prefixed "from "). -->
+ <!-- Source pill — three visible states, one per
+ `editorSource`. The pill is gated on
+ `sourcesReady` to suppress the flash on initial
+ load: until the config-bundle settles we can't tell
+ whether the row is remote-backed or bundled-only,
+ so showing anything is a guess that flips moments
+ later. Once the bundle resolves the right pill
+ renders directly. -->
<span
- v-if="editorSource === 'local' || !isSynced"
- class="src-tag"
- :title="`Editing from: ${editorSource}`"
- >from {{ editorSource }}</span>
+ v-if="sourcesReady && editorSource === 'local'"
+ class="src-tag is-local"
+ :title="t('Unpublished local edits — Push to publish to OAP.')"
+ >{{ t('from local') }}</span>
+ <span
+ v-else-if="sourcesReady && editorSource === 'bundled'"
+ class="src-tag is-bundled"
+ :title="t('Showing the shipped bundled default — Push to
overwrite OAP with bundled.')"
+ >{{ t('from bundled') }}</span>
+ <span
+ v-else-if="sourcesReady && editorSource === 'remote'"
+ class="src-tag is-remote"
+ :title="t('Showing the OAP-live version. End users render the
same bytes.')"
+ >{{ t('from remote') }}</span>
<!-- Reset the editor to a source (discard current content). -->
<div class="reset-dd">
<button class="sw-btn" type="button" @click="resetDropdownOpen
= !resetDropdownOpen">
@@ -1614,23 +1679,32 @@ const namingTest = computed<NamingTestResult>(() => {
<template v-if="resetDropdownOpen">
<div class="reset-dd-backdrop" @click="resetDropdownOpen =
false" />
<div class="reset-dd-pop">
+ <!-- "Reset to → Bundled" stays visible even when
+ the row is synced (bundled === remote). The
+ button is disabled in that case with a
+ "(synced)" tail so the operator can tell the
+ option still exists but there's nothing
+ meaningful to reset to — Bundled equals what
+ Remote would already give them. -->
<button
- v-if="!isSynced"
class="reset-dd-item"
type="button"
- title="Discard current edits and reload the bundled
(shipped) default."
+ :disabled="isSynced"
+ :title="isSynced
+ ? t('Bundled equals OAP-live for this row — nothing to
reset to.')
+ : t('Discard current edits and reload the bundled
(shipped) default.')"
@click="resetTo('bundled')"
>
- Bundled
+ {{ t('Bundled') }}<span v-if="isSynced"
class="reset-dd-suffix"> {{ t('(synced)') }}</span>
</button>
<button
class="reset-dd-item"
type="button"
:disabled="!remoteAvailable"
- :title="remoteAvailable ? 'Discard current edits and
reload OAP\'s live version.' : 'OAP has no copy of this template yet.'"
+ :title="remoteAvailable ? t('Discard current edits and
reload OAP\'s live version.') : t('OAP has no copy of this template yet.')"
@click="resetTo('remote')"
>
- Remote
+ {{ t('Remote') }}
</button>
</div>
</template>
@@ -3155,16 +3229,24 @@ const namingTest = computed<NamingTestResult>(() => {
gap: 8px;
}
.actions .sw-btn { font-size: 11.5px; }
+/* Source pill — per-state theme matched across all three editors
+ * (Layer Dashboards, Overview Templates, Translations):
+ * local → warn : unpublished edits, action recommended
+ * bundled → info : informational (showing disk default)
+ * remote → dim : canonical baseline, quiet
+ * Geometry stays from the original `.src-tag` so widget metrics
+ * don't shift; only colour swaps per modifier class. */
.src-tag {
- font-size: 9.5px;
+ font-size: var(--sw-fs-xs);
+ font-weight: var(--sw-fw-semibold);
text-transform: uppercase;
- letter-spacing: 0.06em;
- color: var(--sw-fg-3);
- background: var(--sw-bg-2);
- border: 1px solid var(--sw-line-2);
+ letter-spacing: var(--sw-ls-caps);
border-radius: 4px;
padding: 2px 7px;
}
+.src-tag.is-local { color: var(--sw-warn); background: var(--sw-warn-soft);
border: 1px solid transparent; }
+.src-tag.is-bundled { color: var(--sw-info); background: var(--sw-info-soft);
border: 1px solid transparent; }
+.src-tag.is-remote { color: var(--sw-fg-3); background: var(--sw-bg-2);
border: 1px solid var(--sw-line-2); }
/* "Reset to ▾" merged dropdown (Bundled / Remote sources). */
.reset-dd { position: relative; display: inline-flex; }
.reset-dd .caret { display: inline-block; transform: rotate(90deg); font-size:
11px; }
diff --git
a/apps/ui/src/features/admin/overview-templates/OverviewTemplatesAdmin.vue
b/apps/ui/src/features/admin/overview-templates/OverviewTemplatesAdmin.vue
index 353665e..039ee56 100644
--- a/apps/ui/src/features/admin/overview-templates/OverviewTemplatesAdmin.vue
+++ b/apps/ui/src/features/admin/overview-templates/OverviewTemplatesAdmin.vue
@@ -83,6 +83,10 @@ const router = useRouter();
const localEdits = useLocalTemplateEdits();
const previewOverride = usePreviewOverride();
const sources = useTemplateSources('overview');
+// See LayerDashboardsAdmin: the source pill stays hidden until the
+// config-bundle settles so the initial pill state isn't a guess that
+// flips moments later.
+const sourcesReady = computed(() => !sources.isLoading.value);
const OV_PREFIX = 'horizon.overview.';
function summaryFrom(
@@ -165,6 +169,15 @@ function isDivergedRow(id: string): boolean {
}
const divergedCount = computed(() => dashboards.value.filter((d) =>
isDivergedRow(d.id)).length);
const localDraftCount = computed(() => dashboards.value.filter((d) =>
hasLocalDraftFor(d.id)).length);
+
+// Auto-uncheck the picker filter when its underlying set drops to
+// zero. Without this the operator can hit Reset / Push / Sync — which
+// removes the matching row from the filtered set — and end up stuck
+// on an empty list with the checkbox itself disabled (since
+// divergedCount / localDraftCount === 0). Same guard as the Layer
+// Dashboards admin page.
+watch(divergedCount, (n) => { if (n === 0) divergedOnly.value = false; });
+watch(localDraftCount, (n) => { if (n === 0) localOnly.value = false; });
const filteredDashboards = computed(() => {
const q = listSearch.value.trim().toLowerCase();
return dashboards.value.filter((d) => {
@@ -553,7 +566,13 @@ const isSynced = computed<boolean>(
);
const draft = ref<OverviewDashboard | null>(null);
-const editorSource = ref<'local' | 'bundled' | 'remote'>('bundled');
+// Remote is the canonical baseline. The editor opens from remote on
+// every (re-)mount (`defaultEditorSource` returns 'remote' whenever
+// remote is reachable + no local draft exists) and stays there until
+// the operator explicitly hits "Reset to bundled" or saves a local
+// draft. The initial ref matches so the source pill can stay hidden
+// on the default path.
+const editorSource = ref<'local' | 'bundled' | 'remote'>('remote');
const loadedSnapshot = ref<string>('');
function bundledContent(): OverviewDashboard | null {
@@ -587,12 +606,15 @@ function persistLocal(content: OverviewDashboard): void {
loadedSnapshot.value = JSON.stringify(content);
}
-// "Reset to ▾" dropdown — reload from a source AND adopt it as the local
-// draft (so the choice persists + Push/diff status updates).
+// "Reset to ▾" dropdown — discard the current local draft and reload
+// the editor from the picked source. Symmetric with the layer
+// dashboards editor: reset is "discard, reload", not "re-stage as a
+// new draft" (which would trigger TemplateConflictPrompt right after
+// every reset). Subsequent edits re-create a local draft naturally.
const resetDropdownOpen = ref(false);
function resetTo(src: 'bundled' | 'remote'): void {
loadFrom(src);
- if (draft.value) persistLocal(draft.value);
+ if (editName.value) localEdits.remove(editName.value);
resetDropdownOpen.value = false;
}
@@ -667,39 +689,43 @@ const isDirty = computed<boolean>(() =>
);
// Which source to seed the editor from for the current selection.
-// Priority mirrors what the operator sees on the live overview pages:
+// Remote is the canonical baseline — it's what `pickOverviewContent`
+// in the runtime bundle serves to end users — so the editor opens
+// from remote on every re-mount when remote is reachable. Priority:
// 1. Local draft — unpublished in-progress edits in this browser.
-// 2. Remote — when bundled and remote diverged (or the row is
-// remote-only), OAP is the source of truth and the runtime bundle
-// loads remote via `pickOverviewContent` (bundle.ts). Loading
-// bundled here silently disagrees with the live UI.
-// 3. Bundled — synced rows are byte-equal anyway; bundled-fallback is
-// the bundle's only source.
-// 4. Last resort: remote when there's no bundled at all
-// (defensive — every overview ships a bundled default today).
+// 2. Remote — the default for every re-entry when remote exists.
+// 3. Bundled — fallback for bundled-only overviews (no OAP row).
function defaultEditorSource(): 'local' | 'bundled' | 'remote' {
if (hasLocalDraft.value) return 'local';
- if (editorSource.value === 'remote' && remoteAvailable.value) return
'remote';
- const badge = editName.value ? sync.badgeFor(editName.value) : null;
- if ((badge === 'diverged' || badge === 'remote-only') &&
remoteAvailable.value) {
- return 'remote';
- }
- if (bundledContent()) return 'bundled';
if (remoteAvailable.value) return 'remote';
+ if (bundledContent()) return 'bundled';
return 'bundled';
}
// Seed the editor when the selected overview (or a background refetch)
-// changes — but never clobber unsaved keystrokes.
+// changes — but never clobber unsaved keystrokes OR an operator-driven
+// load (Reset to bundled / local draft). Defer the initial seed until
+// `sourcesReady` so the editor doesn't visibly flip from bundled to
+// remote on first mount. Once sources are ready the watcher runs
+// every time the operator picks a different overview.
watch(
- [selectedId, () => detailQuery.data.value, hasLocalDraft, remoteAvailable],
+ [selectedId, () => detailQuery.data.value],
() => {
+ if (!sourcesReady.value) return;
if (isDirty.value) return;
loadFrom(defaultEditorSource());
},
{ immediate: true },
);
+// First-mount deferred seed. The moment sources transition to ready,
+// run the seed for the currently-selected overview once.
+watch(sourcesReady, (ready, wasReady) => {
+ if (ready && !wasReady && selectedId.value && !isDirty.value) {
+ loadFrom(defaultEditorSource());
+ }
+});
+
/** Save the editor to the browser local draft. Never writes OAP. */
function onSave(): void {
if (!draft.value || !editName.value) return;
@@ -1058,24 +1084,26 @@ function widgetKindLabel(type: OverviewWidget['type']):
string {
<!-- Source / save / publish actions, right-aligned (same row as
the title + tabs, mirroring the layer dashboards editor). -->
<div class="ot__head-actions">
- <!-- Source pill. Mirrors the layer dashboards editor wording
- ("from <source>") so operators get the same hint about
- which bytes are currently in the buffer:
- - local = unpublished browser draft
- - remote = OAP-live (default for diverged / remote-
- only rows so the editor agrees with what
- the live menu renders)
- - bundled = shipped default (only when the row is
- bundled-fallback or the operator just
- hit "Reset to bundled")
- Hidden when the row is "synced" (bundled === remote) and
- no local draft exists, because in that case "from
- anything" is unambiguous noise. -->
+ <!-- Source pill — three visible states gated on
+ `sourcesReady` so the initial render doesn't flash
+ "from bundled" while the config bundle is still
+ resolving (see Layer Dashboards admin for the
+ parallel fix). -->
<span
- v-if="editorSource === 'local' || !isSynced"
- class="ot__src"
- :title="`Editing from: ${editorSource}`"
- >from {{ editorSource }}</span>
+ v-if="sourcesReady && editorSource === 'local'"
+ class="ot__src is-local"
+ :title="t('Unpublished local edits — Push to publish to OAP.')"
+ >{{ t('from local') }}</span>
+ <span
+ v-else-if="sourcesReady && editorSource === 'bundled'"
+ class="ot__src is-bundled"
+ :title="t('Showing the shipped bundled default — Push to
overwrite OAP with bundled.')"
+ >{{ t('from bundled') }}</span>
+ <span
+ v-else-if="sourcesReady && editorSource === 'remote'"
+ class="ot__src is-remote"
+ :title="t('Showing the OAP-live version. End users render the
same bytes.')"
+ >{{ t('from remote') }}</span>
<div class="reset-dd">
<button type="button" class="ot__btn"
@click="resetDropdownOpen = !resetDropdownOpen">
reset to <span class="caret" :class="{ open:
resetDropdownOpen }">›</span>
@@ -1083,8 +1111,20 @@ function widgetKindLabel(type: OverviewWidget['type']):
string {
<template v-if="resetDropdownOpen">
<div class="reset-dd-backdrop" @click="resetDropdownOpen =
false" />
<div class="reset-dd-pop">
- <button v-if="!isSynced" class="reset-dd-item"
type="button" title="Discard current edits and reload the bundled default."
@click="resetTo('bundled')">Bundled</button>
- <button class="reset-dd-item" type="button"
:disabled="!remoteAvailable" :title="remoteAvailable ? 'Discard current edits
and reload OAP\'s live version.' : 'OAP has no copy yet.'"
@click="resetTo('remote')">Remote</button>
+ <!-- Bundled stays visible when synced; disabled
+ with "(synced)" tail so the option is
+ recognizable but the operator knows there's
+ nothing meaningful to reset to. -->
+ <button
+ class="reset-dd-item"
+ type="button"
+ :disabled="isSynced"
+ :title="isSynced
+ ? t('Bundled equals OAP-live for this row — nothing to
reset to.')
+ : t('Discard current edits and reload the bundled
default.')"
+ @click="resetTo('bundled')"
+ >{{ t('Bundled') }}<span v-if="isSynced"
class="reset-dd-suffix"> {{ t('(synced)') }}</span></button>
+ <button class="reset-dd-item" type="button"
:disabled="!remoteAvailable" :title="remoteAvailable ? t('Discard current edits
and reload OAP\'s live version.') : t('OAP has no copy yet.')"
@click="resetTo('remote')">{{ t('Remote') }}</button>
</div>
</template>
</div>
@@ -2222,16 +2262,22 @@ function widgetKindLabel(type: OverviewWidget['type']):
string {
.ot__flash { font-size: 11px; color: var(--sw-ok); }
.ot__dirty { font-size: 11px; color: var(--sw-warn); }
+/* Source pill — per-state theme matched across all three editors
+ * (Layer Dashboards, Overview Templates, Translations):
+ * local → warn : unpublished edits, action recommended
+ * bundled → info : informational (showing disk default)
+ * remote → dim : canonical baseline, quiet */
.ot__src {
- font-size: 9.5px;
+ font-size: var(--sw-fs-xs);
+ font-weight: var(--sw-fw-semibold);
text-transform: uppercase;
- letter-spacing: 0.06em;
- color: var(--sw-fg-3);
- background: var(--sw-bg-2);
- border: 1px solid var(--sw-line-2);
+ letter-spacing: var(--sw-ls-caps);
border-radius: 4px;
padding: 2px 7px;
}
+.ot__src.is-local { color: var(--sw-warn); background: var(--sw-warn-soft);
border: 1px solid transparent; }
+.ot__src.is-bundled { color: var(--sw-info); background: var(--sw-info-soft);
border: 1px solid transparent; }
+.ot__src.is-remote { color: var(--sw-fg-3); background: var(--sw-bg-2);
border: 1px solid var(--sw-line-2); }
.ot__clean { font-size: 11px; color: var(--sw-fg-3); }
.ot__btn {
background: var(--sw-bg-1); border: 1px solid var(--sw-line-2);
diff --git a/apps/ui/src/features/admin/translations/TranslationsView.vue
b/apps/ui/src/features/admin/translations/TranslationsView.vue
index 108d64d..4f7cc40 100644
--- a/apps/ui/src/features/admin/translations/TranslationsView.vue
+++ b/apps/ui/src/features/admin/translations/TranslationsView.vue
@@ -266,6 +266,18 @@ async function ensureOverlayFetched(name: string, loc:
Locale, eff: NonNullable<
if (staged) applyOverlayToDraft(name, loc, staged, eff);
}
+// `target` MUST be declared above the `watch(effective, ...)` below
+// because that watch uses `{ immediate: true }` and reads `target.value`
+// in its callback — which fires DURING setup. Declaring `target` after
+// the watch leaves it in the TDZ at the moment the immediate callback
+// runs, producing a silent ReferenceError that aborts setup and renders
+// the page blank with no console trace (CLAUDE.md flags this as a
+// recurring failure mode for `immediate: true` watchers).
+const target = ref<Locale>(
+ (SUPPORTED_LOCALES.find((l) => l !== 'en') as Locale) ?? 'zh-CN',
+);
+const targetLocales = SUPPORTED_LOCALES.filter((l) => l !== 'en');
+
watch(
effective,
(eff) => {
@@ -279,11 +291,6 @@ watch(
{ immediate: true },
);
-const target = ref<Locale>(
- (SUPPORTED_LOCALES.find((l) => l !== 'en') as Locale) ?? 'zh-CN',
-);
-const targetLocales = SUPPORTED_LOCALES.filter((l) => l !== 'en');
-
// When the operator switches target language, lazy-fetch its overlays.
watch([target, effective], ([loc, eff]) => {
if (!eff || !selectedName.value) return;
@@ -503,6 +510,58 @@ const dirty = computed<boolean>(() => {
const saveMsg = ref<string | null>(null);
const saving = ref(false);
+/* ── Editor source tracking ────────────────────────────────────────
+ * Matches the Layer Dashboards + Overview Templates admin editors:
+ * the pill always shows one of three states (`from local` / `from
+ * bundled` / `from remote`) and the dropdown lets the operator reset
+ * to the disk-shipped overlay only ("bundled") or to whatever OAP
+ * currently has ("remote"). Local edits flip the pill to "from
+ * local"; discard flips it back to "from remote". */
+const editorSource = ref<'local' | 'bundled' | 'remote'>('remote');
+
+// Switching template or target locale recomputes the source from
+// whether the operator has unstaged local edits for that (name, loc).
+// Don't clobber an explicitly-set source (bundled) inside the same
+// locale — the watcher only fires when name/locale changes.
+watch([selectedName, target], () => {
+ editorSource.value = hasStagedLocal.value ? 'local' : 'remote';
+});
+
+/** Rebuild the draft for one (name, locale) from a specific source.
+ * - `remote` reproduces the default layering (disk overlay first, OAP
+ * overlay wins per leaf) — the canonical baseline the operator
+ * sees in the live UI.
+ * - `bundled` shows ONLY the disk-shipped overlay, ignoring OAP. Use
+ * this to compare against the on-disk seed without OAP overrides. */
+function rebuildDraftForLocale(name: string, loc: string, src: 'remote' |
'bundled'): void {
+ const eff = effective.value;
+ if (!eff) return;
+ const tplMap = { ...(draft.value[name] ?? {}) };
+ delete tplMap[loc];
+ draft.value = { ...draft.value, [name]: tplMap };
+ const snap = fetchedOverlays.value[overlayKey(name, loc)];
+ if (!snap) return;
+ applyOverlayToDraft(name, loc, snap.disk, eff);
+ if (src === 'remote') {
+ applyOverlayToDraft(name, loc, snap.oap, eff);
+ }
+}
+
+/** Reset to dropdown — discard the current local draft and reload the
+ * draft from the picked source. Symmetric with the layer / overview
+ * editors. */
+const resetDropdownOpen = ref(false);
+function resetTo(src: 'remote' | 'bundled'): void {
+ const name = selectedName.value;
+ const loc = target.value;
+ if (!name) return;
+ localEdits.remove(name, loc);
+ rebuildDraftForLocale(name, loc, src);
+ editorSource.value = src;
+ resetDropdownOpen.value = false;
+ closePanel();
+}
+
/** Persist the current draft to localStorage for the active locale.
* Survives reloads, doesn't reach other users, doesn't touch OAP. */
function stageLocal(): void {
@@ -512,6 +571,7 @@ function stageLocal(): void {
if (!name) return;
if (overlay === null) localEdits.remove(name, loc);
else localEdits.set(name, loc, overlay);
+ editorSource.value = 'local';
saveMsg.value = t('Staged {locale} locally.', { locale:
LOCALE_NATIVE_LABEL[loc] });
closePanel();
setTimeout(() => (saveMsg.value = null), 4000);
@@ -589,17 +649,8 @@ function discardLocal(): void {
const loc = target.value;
if (!name) return;
localEdits.remove(name, loc);
- // Re-seed the active locale's draft from OAP + disk.
- const tplMap = { ...(draft.value[name] ?? {}) };
- delete tplMap[loc];
- draft.value = { ...draft.value, [name]: tplMap };
- // Force the snapshot to be re-applied (it was already fetched but
- // we just dropped the draft for this locale).
- const snap = fetchedOverlays.value[overlayKey(name, loc)];
- if (snap && effective.value) {
- applyOverlayToDraft(name, loc, snap.disk, effective.value);
- applyOverlayToDraft(name, loc, snap.oap, effective.value);
- }
+ rebuildDraftForLocale(name, loc, 'remote');
+ editorSource.value = 'remote';
saveMsg.value = t('Discarded {locale} local draft.', { locale:
LOCALE_NATIVE_LABEL[loc] });
closePanel();
setTimeout(() => (saveMsg.value = null), 3000);
@@ -700,6 +751,56 @@ function leafLabel(segments: Array<string | number>):
string {
<span class="tv__progress">{{ t('{filled} / {total} translated', {
filled: filledCount, total: allFields.length }) }}</span>
<div class="tv__actions">
+ <!-- Source pill — sits at the right edge next to the
+ "reset to" dropdown so the operator's eye reads
+ `state pill → reset to source` left-to-right. Per-state
+ colour: local = warn (action needed), bundled = info
+ (informational), remote = dim (baseline / quiet). -->
+ <span
+ v-if="editorSource === 'local'"
+ class="tv__src is-local"
+ :title="t('Unpublished local edits — Push to publish to OAP.')"
+ >{{ t('from local') }}</span>
+ <span
+ v-else-if="editorSource === 'bundled'"
+ class="tv__src is-bundled"
+ :title="t('Showing the shipped bundled default — Push to overwrite
OAP with bundled.')"
+ >{{ t('from bundled') }}</span>
+ <span
+ v-else-if="editorSource === 'remote'"
+ class="tv__src is-remote"
+ :title="t('Showing the OAP-live version. End users render the same
bytes.')"
+ >{{ t('from remote') }}</span>
+ <!-- Reset to ▾ dropdown — matches the layer / overview
+ editors. Discards local edits and re-seeds the draft from
+ the picked source. -->
+ <div class="reset-dd">
+ <button
+ type="button"
+ class="sw-btn"
+ :disabled="readOnly || !selectedName"
+ @click="resetDropdownOpen = !resetDropdownOpen"
+ >
+ {{ t('reset to') }} <span class="caret" :class="{ open:
resetDropdownOpen }">›</span>
+ </button>
+ <template v-if="resetDropdownOpen">
+ <div class="reset-dd-backdrop" @click="resetDropdownOpen = false"
/>
+ <div class="reset-dd-pop">
+ <button
+ class="reset-dd-item"
+ type="button"
+ :title="t('Discard local edits and reload only the
disk-shipped overlay (ignore OAP overlay).')"
+ @click="resetTo('bundled')"
+ >{{ t('Bundled') }}</button>
+ <button
+ class="reset-dd-item"
+ type="button"
+ :title="t('Discard local edits and reload from the OAP overlay
row.')"
+ @click="resetTo('remote')"
+ >{{ t('Remote') }}</button>
+ </div>
+ </template>
+ </div>
<button
v-if="hasStagedLocal"
type="button"
@@ -825,7 +926,62 @@ function leafLabel(segments: Array<string | number>):
string {
padding: 3px 8px; background: var(--sw-bg-2); border-radius: 3px;
font-variant-numeric: tabular-nums;
}
-.tv__actions { margin-left: auto; display: inline-flex; gap: 6px; }
+/* Source pill — same shape and vocabulary as the layer + overview
+ * editors. Per-state colour:
+ * - local → warn (yellow), background warn-soft : "you have
+ * unpublished changes, action recommended"
+ * - bundled → info (cyan), background info-soft : "showing the
+ * shipped default — informational"
+ * - remote → fg-3 (dim), background bg-2 : "you're on the
+ * canonical baseline — nothing to call out"
+ */
+.tv__src {
+ font-size: var(--sw-fs-xs);
+ font-weight: var(--sw-fw-semibold);
+ letter-spacing: var(--sw-ls-caps);
+ text-transform: uppercase;
+ padding: 3px 8px;
+ border-radius: 3px;
+}
+.tv__src.is-local { color: var(--sw-warn); background: var(--sw-warn-soft); }
+.tv__src.is-bundled { color: var(--sw-info); background: var(--sw-info-soft); }
+.tv__src.is-remote { color: var(--sw-fg-3); background: var(--sw-bg-2); }
+.tv__actions { margin-left: auto; display: inline-flex; gap: 6px; align-items:
center; }
+
+/* Reset-to dropdown — vocabulary matches the layer + overview admin
+ * editors so the affordance reads the same across all three pages. */
+.reset-dd { position: relative; }
+.reset-dd .caret {
+ display: inline-block; margin-left: 4px;
+ transition: transform 0.1s ease;
+}
+.reset-dd .caret.open { transform: rotate(90deg); }
+.reset-dd-backdrop {
+ position: fixed; inset: 0; z-index: 50;
+}
+.reset-dd-pop {
+ position: absolute; top: calc(100% + 4px); right: 0;
+ z-index: 51;
+ min-width: 160px;
+ background: var(--sw-bg-1);
+ border: 1px solid var(--sw-line-2);
+ border-radius: 4px;
+ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.35);
+ padding: 4px;
+ display: flex; flex-direction: column; gap: 2px;
+}
+.reset-dd-item {
+ text-align: left;
+ padding: 6px 10px;
+ background: transparent;
+ border: 0;
+ border-radius: 3px;
+ color: var(--sw-fg-1);
+ font-size: var(--sw-fs-sm);
+ cursor: pointer;
+}
+.reset-dd-item:hover:not(:disabled) { background: var(--sw-bg-2); color:
var(--sw-fg-0); }
+.reset-dd-item:disabled { opacity: 0.4; cursor: not-allowed; }
.tv__msg {
padding: 6px 10px; font-size: 12px; color: var(--sw-fg-2);
background: var(--sw-bg-1); border: 1px solid var(--sw-line-2);
border-radius: 4px;
diff --git a/apps/ui/src/i18n/locales/de.json b/apps/ui/src/i18n/locales/de.json
index aef836c..af41a71 100644
--- a/apps/ui/src/i18n/locales/de.json
+++ b/apps/ui/src/i18n/locales/de.json
@@ -1157,5 +1157,27 @@
"Window exceeds {h}h cap": "Fenster überschreitet Obergrenze von {h} h",
"last {window}": "letzte {window}",
"Metrics Analysis Language - …": "Metrics Analysis Language - …",
- "Operate · OAP configuration": "Betrieb · OAP-Konfiguration"
+ "Operate · OAP configuration": "Betrieb · OAP-Konfiguration",
+ "from {source}": "aus {source}",
+ "local": "lokal",
+ "Local": "Lokal",
+ "Editor buffer differs from OAP — not the live version.": "Editor-Inhalt
weicht von OAP ab — nicht die Live-Version.",
+ "from local": "aus lokal",
+ "from bundled": "aus mitgeliefert",
+ "(synced)": "(synchron)",
+ "Unpublished local edits — Push to publish to OAP.": "Unveröffentlichte
lokale Änderungen — auf OAP per Push veröffentlichen.",
+ "Showing the shipped bundled default — Push to overwrite OAP with bundled.":
"Zeigt den mitgelieferten Standard — per Push OAP mit dem mitgelieferten Stand
überschreiben.",
+ "Bundled equals OAP-live for this row — nothing to reset to.": "Mitgeliefert
entspricht der OAP-Live-Version — nichts zum Zurücksetzen.",
+ "Discard current edits and reload the bundled (shipped) default.": "Aktuelle
Änderungen verwerfen und mitgelieferten Standard neu laden.",
+ "Discard current edits and reload the bundled default.": "Aktuelle
Änderungen verwerfen und mitgelieferten Standard neu laden.",
+ "Discard current edits and reload OAP's live version.": "Aktuelle Änderungen
verwerfen und OAP-Live-Version neu laden.",
+ "OAP has no copy of this template yet.": "OAP hat noch keine Kopie dieses
Templates.",
+ "OAP has no copy yet.": "OAP hat noch keine Kopie.",
+ "Bundled": "Mitgeliefert",
+ "Remote": "Remote",
+ "from remote": "aus remote",
+ "Showing the OAP-live version. End users render the same bytes.": "Zeigt die
OAP-Live-Version — exakt was Endnutzer sehen.",
+ "reset to": "zurücksetzen auf",
+ "Discard local edits and reload only the disk-shipped overlay (ignore OAP
overlay).": "Lokale Änderungen verwerfen und nur das mitgelieferte Disk-Overlay
neu laden (OAP-Overlay ignorieren).",
+ "Discard local edits and reload from the OAP overlay row.": "Lokale
Änderungen verwerfen und aus der OAP-Overlay-Zeile neu laden."
}
diff --git a/apps/ui/src/i18n/locales/en.json b/apps/ui/src/i18n/locales/en.json
index 3cc5141..7f478ce 100644
--- a/apps/ui/src/i18n/locales/en.json
+++ b/apps/ui/src/i18n/locales/en.json
@@ -1157,5 +1157,27 @@
"Window exceeds {h}h cap": "Window exceeds {h}h cap",
"last {window}": "last {window}",
"Metrics Analysis Language - …": "Metrics Analysis Language - …",
- "Operate · OAP configuration": "Operate · OAP configuration"
+ "Operate · OAP configuration": "Operate · OAP configuration",
+ "from {source}": "from {source}",
+ "local": "local",
+ "Local": "Local",
+ "Editor buffer differs from OAP — not the live version.": "Editor buffer
differs from OAP — not the live version.",
+ "from local": "from local",
+ "from bundled": "from bundled",
+ "(synced)": "(synced)",
+ "Unpublished local edits — Push to publish to OAP.": "Unpublished local
edits — Push to publish to OAP.",
+ "Showing the shipped bundled default — Push to overwrite OAP with bundled.":
"Showing the shipped bundled default — Push to overwrite OAP with bundled.",
+ "Bundled equals OAP-live for this row — nothing to reset to.": "Bundled
equals OAP-live for this row — nothing to reset to.",
+ "Discard current edits and reload the bundled (shipped) default.": "Discard
current edits and reload the bundled (shipped) default.",
+ "Discard current edits and reload the bundled default.": "Discard current
edits and reload the bundled default.",
+ "Discard current edits and reload OAP's live version.": "Discard current
edits and reload OAP's live version.",
+ "OAP has no copy of this template yet.": "OAP has no copy of this template
yet.",
+ "OAP has no copy yet.": "OAP has no copy yet.",
+ "Bundled": "Bundled",
+ "Remote": "Remote",
+ "from remote": "from remote",
+ "Showing the OAP-live version. End users render the same bytes.": "Showing
the OAP-live version. End users render the same bytes.",
+ "reset to": "reset to",
+ "Discard local edits and reload only the disk-shipped overlay (ignore OAP
overlay).": "Discard local edits and reload only the disk-shipped overlay
(ignore OAP overlay).",
+ "Discard local edits and reload from the OAP overlay row.": "Discard local
edits and reload from the OAP overlay row."
}
diff --git a/apps/ui/src/i18n/locales/es.json b/apps/ui/src/i18n/locales/es.json
index 7f9a23d..45bad9b 100644
--- a/apps/ui/src/i18n/locales/es.json
+++ b/apps/ui/src/i18n/locales/es.json
@@ -1157,5 +1157,27 @@
"Window exceeds {h}h cap": "La ventana supera el tope de {h}h",
"last {window}": "últimos {window}",
"Metrics Analysis Language - …": "Metrics Analysis Language - …",
- "Operate · OAP configuration": "Operar · Configuración de OAP"
+ "Operate · OAP configuration": "Operar · Configuración de OAP",
+ "from {source}": "desde {source}",
+ "local": "local",
+ "Local": "Local",
+ "Editor buffer differs from OAP — not the live version.": "El buffer del
editor difiere de OAP — no es la versión en vivo.",
+ "from local": "desde local",
+ "from bundled": "desde bundled",
+ "(synced)": "(sincronizado)",
+ "Unpublished local edits — Push to publish to OAP.": "Cambios locales sin
publicar — usa Push para publicar en OAP.",
+ "Showing the shipped bundled default — Push to overwrite OAP with bundled.":
"Mostrando el valor predeterminado bundled — Push sobrescribe OAP con bundled.",
+ "Bundled equals OAP-live for this row — nothing to reset to.": "En esta
fila, bundled coincide con OAP-live — no hay nada que restablecer.",
+ "Discard current edits and reload the bundled (shipped) default.": "Descarta
los cambios actuales y recarga el valor predeterminado bundled (de fábrica).",
+ "Discard current edits and reload the bundled default.": "Descarta los
cambios actuales y recarga el valor predeterminado bundled.",
+ "Discard current edits and reload OAP's live version.": "Descarta los
cambios actuales y recarga la versión en vivo de OAP.",
+ "OAP has no copy of this template yet.": "OAP aún no tiene una copia de esta
plantilla.",
+ "OAP has no copy yet.": "OAP aún no tiene una copia.",
+ "Bundled": "Bundled",
+ "Remote": "Remote",
+ "from remote": "desde remote",
+ "Showing the OAP-live version. End users render the same bytes.": "Mostrando
la versión en vivo de OAP — los usuarios finales ven los mismos bytes.",
+ "reset to": "restablecer a",
+ "Discard local edits and reload only the disk-shipped overlay (ignore OAP
overlay).": "Descarta los cambios locales y recarga solo el overlay de disco
(ignora el overlay de OAP).",
+ "Discard local edits and reload from the OAP overlay row.": "Descarta los
cambios locales y recarga desde la fila de overlay de OAP."
}
diff --git a/apps/ui/src/i18n/locales/fr.json b/apps/ui/src/i18n/locales/fr.json
index 5471b28..cd03348 100644
--- a/apps/ui/src/i18n/locales/fr.json
+++ b/apps/ui/src/i18n/locales/fr.json
@@ -1157,5 +1157,27 @@
"Window exceeds {h}h cap": "La fenêtre dépasse le plafond de {h} h",
"last {window}": "dernier(s) {window}",
"Metrics Analysis Language - …": "Metrics Analysis Language - …",
- "Operate · OAP configuration": "Exploitation · Configuration OAP"
+ "Operate · OAP configuration": "Exploitation · Configuration OAP",
+ "from {source}": "depuis {source}",
+ "local": "local",
+ "Local": "Local",
+ "Editor buffer differs from OAP — not the live version.": "Le contenu de
l'éditeur diffère d'OAP — ce n'est pas la version en ligne.",
+ "from local": "depuis local",
+ "from bundled": "depuis bundled",
+ "(synced)": "(synchronisé)",
+ "Unpublished local edits — Push to publish to OAP.": "Modifications locales
non publiées — Push pour publier sur OAP.",
+ "Showing the shipped bundled default — Push to overwrite OAP with bundled.":
"Affiche la valeur par défaut bundled livrée — Push pour écraser OAP avec
bundled.",
+ "Bundled equals OAP-live for this row — nothing to reset to.": "Pour cette
ligne, bundled est identique à la version OAP en ligne — rien à réinitialiser.",
+ "Discard current edits and reload the bundled (shipped) default.":
"Abandonner les modifications actuelles et recharger la valeur par défaut
bundled (livrée).",
+ "Discard current edits and reload the bundled default.": "Abandonner les
modifications actuelles et recharger la valeur par défaut bundled.",
+ "Discard current edits and reload OAP's live version.": "Abandonner les
modifications actuelles et recharger la version OAP en ligne.",
+ "OAP has no copy of this template yet.": "OAP n'a pas encore de copie de ce
template.",
+ "OAP has no copy yet.": "OAP n'a pas encore de copie.",
+ "Bundled": "Bundled",
+ "Remote": "Remote",
+ "from remote": "depuis remote",
+ "Showing the OAP-live version. End users render the same bytes.": "Affiche
la version OAP en ligne — exactement ce que voient les utilisateurs finaux.",
+ "reset to": "réinitialiser à",
+ "Discard local edits and reload only the disk-shipped overlay (ignore OAP
overlay).": "Abandonner les modifications locales et recharger uniquement
l'overlay livré sur disque (ignorer l'overlay OAP).",
+ "Discard local edits and reload from the OAP overlay row.": "Abandonner les
modifications locales et recharger depuis la ligne d'overlay OAP."
}
diff --git a/apps/ui/src/i18n/locales/ja.json b/apps/ui/src/i18n/locales/ja.json
index 0864c8a..d5aefd9 100644
--- a/apps/ui/src/i18n/locales/ja.json
+++ b/apps/ui/src/i18n/locales/ja.json
@@ -1157,5 +1157,27 @@
"Window exceeds {h}h cap": "範囲が {h} 時間の上限を超えています",
"last {window}": "直近 {window}",
"Metrics Analysis Language - …": "Metrics Analysis Language - …",
- "Operate · OAP configuration": "運用 · OAP 設定"
+ "Operate · OAP configuration": "運用 · OAP 設定",
+ "from {source}": "{source} から",
+ "local": "ローカル",
+ "Local": "ローカル",
+ "Editor buffer differs from OAP — not the live version.": "エディタの内容は OAP
と異なります — ライブ版ではありません。",
+ "from local": "ローカルから",
+ "from bundled": "バンドルから",
+ "(synced)": "(同期済み)",
+ "Unpublished local edits — Push to publish to OAP.": "未公開のローカル編集 — Push で
OAP に公開します。",
+ "Showing the shipped bundled default — Push to overwrite OAP with bundled.":
"同梱されたバンドルのデフォルトを表示中 — Push でバンドルを OAP に上書きします。",
+ "Bundled equals OAP-live for this row — nothing to reset to.": "この行はバンドルと
OAP のライブが一致しています — リセット先がありません。",
+ "Discard current edits and reload the bundled (shipped) default.":
"現在の編集を破棄し、同梱されたバンドルのデフォルトを再読み込みします。",
+ "Discard current edits and reload the bundled default.":
"現在の編集を破棄し、バンドルのデフォルトを再読み込みします。",
+ "Discard current edits and reload OAP's live version.": "現在の編集を破棄し、OAP
のライブ版を再読み込みします。",
+ "OAP has no copy of this template yet.": "OAP にはまだこのテンプレートのコピーがありません。",
+ "OAP has no copy yet.": "OAP にはまだコピーがありません。",
+ "Bundled": "バンドル",
+ "Remote": "リモート",
+ "from remote": "リモートから",
+ "Showing the OAP-live version. End users render the same bytes.": "OAP
のライブ版を表示中 — エンドユーザーが見るものと同じです。",
+ "reset to": "リセット先",
+ "Discard local edits and reload only the disk-shipped overlay (ignore OAP
overlay).": "ローカル編集を破棄し、同梱のディスクオーバーレイのみを再読み込み(OAP オーバーレイは無視)。",
+ "Discard local edits and reload from the OAP overlay row.": "ローカル編集を破棄し、OAP
オーバーレイ行から再読み込み。"
}
diff --git a/apps/ui/src/i18n/locales/ko.json b/apps/ui/src/i18n/locales/ko.json
index 3d08e58..6f7613b 100644
--- a/apps/ui/src/i18n/locales/ko.json
+++ b/apps/ui/src/i18n/locales/ko.json
@@ -1157,5 +1157,27 @@
"Window exceeds {h}h cap": "범위가 {h}시간 상한을 초과합니다",
"last {window}": "최근 {window}",
"Metrics Analysis Language - …": "Metrics Analysis Language - …",
- "Operate · OAP configuration": "운영 · OAP 구성"
+ "Operate · OAP configuration": "운영 · OAP 구성",
+ "from {source}": "{source}에서",
+ "local": "로컬",
+ "Local": "로컬",
+ "Editor buffer differs from OAP — not the live version.": "에디터 내용이 OAP와 다릅니다
— 라이브 버전이 아닙니다.",
+ "from local": "로컬에서",
+ "from bundled": "번들에서",
+ "(synced)": "(동기화됨)",
+ "Unpublished local edits — Push to publish to OAP.": "게시되지 않은 로컬 편집 — Push로
OAP에 게시합니다.",
+ "Showing the shipped bundled default — Push to overwrite OAP with bundled.":
"번들된 기본값을 표시 중 — Push로 번들을 OAP에 덮어씁니다.",
+ "Bundled equals OAP-live for this row — nothing to reset to.": "이 행은 번들과 OAP
라이브가 동일 — 재설정할 대상이 없습니다.",
+ "Discard current edits and reload the bundled (shipped) default.": "현재 편집을
버리고 번들(배포) 기본값을 다시 불러옵니다.",
+ "Discard current edits and reload the bundled default.": "현재 편집을 버리고 번들 기본값을
다시 불러옵니다.",
+ "Discard current edits and reload OAP's live version.": "현재 편집을 버리고 OAP 라이브
버전을 다시 불러옵니다.",
+ "OAP has no copy of this template yet.": "OAP에 아직 이 템플릿 복사본이 없습니다.",
+ "OAP has no copy yet.": "OAP에 아직 복사본이 없습니다.",
+ "Bundled": "번들",
+ "Remote": "원격",
+ "from remote": "원격에서",
+ "Showing the OAP-live version. End users render the same bytes.": "OAP 라이브
버전 표시 중 — 최종 사용자가 보는 것과 동일합니다.",
+ "reset to": "재설정 대상",
+ "Discard local edits and reload only the disk-shipped overlay (ignore OAP
overlay).": "로컬 편집을 버리고 디스크에 동봉된 오버레이만 다시 불러옵니다(OAP 오버레이 무시).",
+ "Discard local edits and reload from the OAP overlay row.": "로컬 편집을 버리고 OAP
오버레이 행에서 다시 불러옵니다."
}
diff --git a/apps/ui/src/i18n/locales/pt.json b/apps/ui/src/i18n/locales/pt.json
index 52a347a..e12ea9a 100644
--- a/apps/ui/src/i18n/locales/pt.json
+++ b/apps/ui/src/i18n/locales/pt.json
@@ -1157,5 +1157,27 @@
"Window exceeds {h}h cap": "A janela excede o limite de {h}h",
"last {window}": "últimos {window}",
"Metrics Analysis Language - …": "Metrics Analysis Language - …",
- "Operate · OAP configuration": "Operar · Configuração do OAP"
+ "Operate · OAP configuration": "Operar · Configuração do OAP",
+ "from {source}": "a partir de {source}",
+ "local": "local",
+ "Local": "Local",
+ "Editor buffer differs from OAP — not the live version.": "O conteúdo do
editor difere do OAP — não é a versão em produção.",
+ "from local": "a partir de local",
+ "from bundled": "a partir de bundled",
+ "(synced)": "(sincronizado)",
+ "Unpublished local edits — Push to publish to OAP.": "Edições locais não
publicadas — use Push para publicar no OAP.",
+ "Showing the shipped bundled default — Push to overwrite OAP with bundled.":
"Exibindo o padrão bundled — Push sobrescreve o OAP com bundled.",
+ "Bundled equals OAP-live for this row — nothing to reset to.": "Nesta linha,
bundled coincide com a versão em produção do OAP — nada para redefinir.",
+ "Discard current edits and reload the bundled (shipped) default.":
"Descartar edições atuais e recarregar o padrão bundled (de fábrica).",
+ "Discard current edits and reload the bundled default.": "Descartar edições
atuais e recarregar o padrão bundled.",
+ "Discard current edits and reload OAP's live version.": "Descartar edições
atuais e recarregar a versão em produção do OAP.",
+ "OAP has no copy of this template yet.": "OAP ainda não tem uma cópia deste
template.",
+ "OAP has no copy yet.": "OAP ainda não tem uma cópia.",
+ "Bundled": "Bundled",
+ "Remote": "Remote",
+ "from remote": "a partir de remote",
+ "Showing the OAP-live version. End users render the same bytes.": "Exibindo
a versão em produção do OAP — os usuários finais veem exatamente isto.",
+ "reset to": "redefinir para",
+ "Discard local edits and reload only the disk-shipped overlay (ignore OAP
overlay).": "Descartar edições locais e recarregar apenas o overlay do disco
(ignorar o overlay do OAP).",
+ "Discard local edits and reload from the OAP overlay row.": "Descartar
edições locais e recarregar a partir da linha de overlay do OAP."
}
diff --git a/apps/ui/src/i18n/locales/zh-CN.json
b/apps/ui/src/i18n/locales/zh-CN.json
index 86ce28c..bc44031 100644
--- a/apps/ui/src/i18n/locales/zh-CN.json
+++ b/apps/ui/src/i18n/locales/zh-CN.json
@@ -1157,5 +1157,27 @@
"Window exceeds {h}h cap": "窗口超过 {h} 小时上限",
"last {window}": "最近 {window}",
"Metrics Analysis Language - …": "Metrics Analysis Language - …",
- "Operate · OAP configuration": "运维 · OAP 配置"
+ "Operate · OAP configuration": "运维 · OAP 配置",
+ "from {source}": "来自 {source}",
+ "local": "本地",
+ "Local": "本地",
+ "Editor buffer differs from OAP — not the live version.": "编辑器内容与 OAP 不同 —
不是线上版本。",
+ "from local": "来自本地",
+ "from bundled": "来自内置",
+ "(synced)": "(已同步)",
+ "Unpublished local edits — Push to publish to OAP.": "未发布的本地编辑 — 推送以发布到
OAP。",
+ "Showing the shipped bundled default — Push to overwrite OAP with bundled.":
"当前显示内置默认值 — 推送会用内置版覆盖 OAP。",
+ "Bundled equals OAP-live for this row — nothing to reset to.": "该行的内置版与 OAP
实时版本一致 — 无需重置。",
+ "Discard current edits and reload the bundled (shipped) default.":
"丢弃当前编辑,重新加载内置(发行)默认值。",
+ "Discard current edits and reload the bundled default.": "丢弃当前编辑,重新加载内置默认值。",
+ "Discard current edits and reload OAP's live version.": "丢弃当前编辑,重新加载 OAP
实时版本。",
+ "OAP has no copy of this template yet.": "OAP 还没有此模板的副本。",
+ "OAP has no copy yet.": "OAP 还没有副本。",
+ "Bundled": "内置",
+ "Remote": "远端",
+ "from remote": "来自远端",
+ "Showing the OAP-live version. End users render the same bytes.": "显示 OAP
实时版本 — 与终端用户看到的内容一致。",
+ "reset to": "重置为",
+ "Discard local edits and reload only the disk-shipped overlay (ignore OAP
overlay).": "丢弃本地编辑,只重新加载随包的磁盘覆盖层(忽略 OAP 覆盖层)。",
+ "Discard local edits and reload from the OAP overlay row.": "丢弃本地编辑,从 OAP
覆盖行重新加载。"
}