This is an automated email from the ASF dual-hosted git repository.

wu-sheng pushed a commit to branch feat/infra-3d-config-i18n
in repository https://gitbox.apache.org/repos/asf/skywalking-horizon-ui.git

commit 6dab9cd7278e6e217bd91dc39d7861e42d6c6136
Author: Wu Sheng <[email protected]>
AuthorDate: Mon Jun 1 22:31:35 2026 +0800

    feat(infra-3d): internationalize the 3D-map config admin page
    
    The /admin/3d-map config editor was the only admin page still rendering
    hardcoded English in every locale — it landed after the 0.6.0 i18n pass.
    Wire it through vue-i18n like the other admin pages and ship full
    eight-locale coverage.
    
    - Infra3dAdminView.vue: 58 t() keys plus 5 <i18n-t> blocks for the prose
      hints with inline <code>/<strong>; named-arg interpolation for the
      dynamic counts, the via tag, and the Push-button state. Layer keys,
      MQE ids, and the .* regex stay verbatim.
    - en.json: 55 new source keys.
    - de/es/fr/ja/ko/pt/zh-CN: the 55 keys translated, matching each
      catalog's existing terminology.
---
 CHANGELOG.md                                       |   2 +-
 .../features/admin/infra-3d/Infra3dAdminView.vue   | 158 +++++++++++----------
 apps/ui/src/i18n/locales/de.json                   |  57 +++++++-
 apps/ui/src/i18n/locales/en.json                   |  57 +++++++-
 apps/ui/src/i18n/locales/es.json                   |  57 +++++++-
 apps/ui/src/i18n/locales/fr.json                   |  57 +++++++-
 apps/ui/src/i18n/locales/ja.json                   |  57 +++++++-
 apps/ui/src/i18n/locales/ko.json                   |  57 +++++++-
 apps/ui/src/i18n/locales/pt.json                   |  57 +++++++-
 apps/ui/src/i18n/locales/zh-CN.json                |  57 +++++++-
 10 files changed, 532 insertions(+), 84 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8e471c8..607bd9a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -25,7 +25,7 @@ per device.
 - **UI chrome.** Every routed page and every shared sub-component
   renders through `vue-i18n`; non-English locales now cover every
   admin page (Roles, Users, Auth status, Alert page setup, Global
-  defaults), every operate page (Alerting rules, DSL catalog / editor /
+  defaults, 3D-map config), every operate page (Alerting rules, DSL catalog / 
editor /
   dump, OAL catalog, Live debugger + MAL / LAL / OAL, Capture history,
   Metrics inspect, OAP config, TTL), the alarms surface, and the
   shared modals. Long lede paragraphs that previously rendered as
diff --git a/apps/ui/src/features/admin/infra-3d/Infra3dAdminView.vue 
b/apps/ui/src/features/admin/infra-3d/Infra3dAdminView.vue
index 23a627b..4636d2c 100644
--- a/apps/ui/src/features/admin/infra-3d/Infra3dAdminView.vue
+++ b/apps/ui/src/features/admin/infra-3d/Infra3dAdminView.vue
@@ -42,6 +42,7 @@
 -->
 <script setup lang="ts">
 import { computed, onMounted, ref, watch } from 'vue';
+import { useI18n } from 'vue-i18n';
 import { useLayers } from '@/shell/useLayers';
 import {
   bff,
@@ -68,6 +69,7 @@ import MonacoDiff from 
'@/features/operate/_shared/MonacoDiff.vue';
 // and an out-of-tree layer (config exists, OAP no longer reports it)
 // also shows so the admin can remove it.
 const { availableLayers } = useLayers();
+const { t } = useI18n();
 
 // ── Template-sync state ───────────────────────────────────────────────
 // The 3D-map config is a singleton template kind — ONE row,
@@ -415,8 +417,8 @@ const layersByTier = computed<Record<string, 
LayerRow[]>>(() => {
     // bucket (tagged "filtered out"), never under a tier even if pinned, so
     // the editor matches what the scene renders.
     if (resolvedLevels.value[r.key]?.via === 'filtered') continue;
-    const t = explicitTier.value[r.key];
-    if (t && out[t]) out[t].push(r);
+    const tierId = explicitTier.value[r.key];
+    if (tierId && out[tierId]) out[tierId].push(r);
   }
   return out;
 });
@@ -567,47 +569,47 @@ const stats = computed(() => {
   <div class="i3d-admin">
     <header class="hd">
       <div class="hd-text">
-        <span class="kicker">Dashboard setup · 3D Infra Map</span>
-        <h1>3D Infrastructure Map</h1>
+        <span class="kicker">{{ t('Dashboard setup · 3D Infra Map') }}</span>
+        <h1>{{ t('3D Infrastructure Map') }}</h1>
         <p class="lede">
-          Config for the <code>/3d/map</code> view, published to OAP. Levels
-          control the vertical stack; per-layer color + metrics drive each
-          cube. Edits save to a <strong>local draft in this browser</strong>;
-          <strong>Check diff &amp; push</strong> publishes to OAP — the map
-          renders the remote, with bundled defaults as fallback.
+          <i18n-t keypath="Config for the {mapPath} view, published to OAP. 
Levels control the vertical stack; per-layer color + metrics drive each cube. 
Edits save to a {localDraft}; {checkDiffPush} publishes to OAP — the map 
renders the remote, with bundled defaults as fallback." tag="span" 
scope="global">
+            <template #mapPath><code>/3d/map</code></template>
+            <template #localDraft><strong>{{ t('local draft in this browser') 
}}</strong></template>
+            <template #checkDiffPush><strong>{{ t('Check diff & push') 
}}</strong></template>
+          </i18n-t>
         </p>
       </div>
       <div class="hd-actions">
         <span class="src-pill" :data-src="editorSource">
-          editing: {{ editorSource }}
+          {{ t('editing: {source}', { source: editorSource }) }}
           <TemplateStatusBadge v-if="badge" :status="badge" />
         </span>
         <div class="reset-wrap">
-          <button class="btn" :disabled="pushing" @click="resetMenuOpen = 
!resetMenuOpen">Reset to ▾</button>
+          <button class="btn" :disabled="pushing" @click="resetMenuOpen = 
!resetMenuOpen">{{ t('Reset to') }} ▾</button>
           <div v-if="resetMenuOpen" class="reset-menu">
             <button
               class="reset-item"
               :disabled="isSynced"
-              :title="isSynced ? 'Bundled equals OAP-live — nothing to reset 
to.' : 'Discard current edits and reload the bundled (shipped) default.'"
+              :title="isSynced ? t('Bundled equals OAP-live — nothing to reset 
to.') : t('Discard current edits and reload the bundled (shipped) default.')"
               @click="resetTo('bundled')"
             >
-              Bundled<span v-if="isSynced" class="reset-suffix"> 
(synced)</span>
+              {{ t('Bundled') }}<span v-if="isSynced" class="reset-suffix"> 
({{ t('synced') }})</span>
             </button>
             <button
               class="reset-item"
               :disabled="!hasRemote"
-              :title="hasRemote ? 'Discard current edits and reload OAP\'s 
live version.' : 'OAP has no copy of this config yet.'"
+              :title="hasRemote ? t('Discard current edits and reload OAP\'s 
live version.') : t('OAP has no copy of this config yet.')"
               @click="resetTo('remote')"
             >
-              Remote
+              {{ t('Remote') }}
             </button>
           </div>
         </div>
         <button class="btn" :disabled="!dirty" @click="save">
-          {{ hasLocalDraft && !dirty ? 'Saved local' : 'Save local' }}
+          {{ hasLocalDraft && !dirty ? t('Saved local') : t('Save local') }}
         </button>
         <button class="btn primary" :disabled="readOnly || (!dirty && 
!hasLocalDraft)" @click="openPushDiff">
-          Check diff &amp; push
+          {{ t('Check diff & push') }}
         </button>
       </div>
     </header>
@@ -618,15 +620,19 @@ const stats = computed(() => {
     </ul>
 
     <div v-if="loadError" class="loading">
-      Couldn't load the 3D-map config — the BFF may be unreachable. Refresh 
the page to retry.
+      {{ t('Couldn\'t load the 3D-map config — the BFF may be unreachable. 
Refresh the page to retry.') }}
     </div>
-    <div v-else-if="!ready" class="loading">Loading config…</div>
+    <div v-else-if="!ready" class="loading">{{ t('Loading config…') }}</div>
     <template v-else-if="draft">
       <!-- ── Global filter ─────────────────────────────────────────── -->
       <section class="sect">
         <header class="sect-head">
-          <h2>Global layer filter</h2>
-          <span class="sec-hint">The one top-level gate — a layer this JS 
regex excludes is off the map (it still shows below in Unpinned, tagged 
"filtered out"). Default <code>.*</code> admits all.</span>
+          <h2>{{ t('Global layer filter') }}</h2>
+          <span class="sec-hint">
+            <i18n-t keypath='The one top-level gate — a layer this JS regex 
excludes is off the map (it still shows below in Unpinned, tagged "filtered 
out"). Default {dotStar} admits all.' tag="span" scope="global">
+              <template #dotStar><code>.*</code></template>
+            </i18n-t>
+          </span>
         </header>
         <div class="sect-body">
           <label class="field">
@@ -639,14 +645,18 @@ const stats = computed(() => {
       <!-- ── Tiers & layers ─────────────────────────────────────────── -->
       <section class="sect">
         <header class="sect-head">
-          <h2>Tiers &amp; layers (top → bottom)</h2>
+          <h2>{{ t('Tiers & layers (top → bottom)') }}</h2>
           <span class="sec-hint">
-            Every layer belongs to one tier — set it with the tier dropdown; 
order = vertical stacking.
-            <strong>{{ stats.oap }}</strong> in OAP<template 
v-if="stats.unclassified > 0"> ·
-            <strong class="warn-count">{{ stats.unclassified }}</strong> 
unpinned → fallback</template>
+            {{ t('Every layer belongs to one tier — set it with the tier 
dropdown; order = vertical stacking.') }}
+            <i18n-t keypath="{count} in OAP" tag="span" scope="global">
+              <template #count><strong>{{ stats.oap }}</strong></template>
+            </i18n-t><template v-if="stats.unclassified > 0"> ·
+            <i18n-t keypath="{count} unpinned → fallback" tag="span" 
scope="global">
+              <template #count><strong class="warn-count">{{ 
stats.unclassified }}</strong></template>
+            </i18n-t></template>
           </span>
-          <input class="inp search" v-model="layerSearch" placeholder="filter 
layers…" />
-          <button type="button" class="btn small" @click="addLevel">+ add 
tier</button>
+          <input class="inp search" v-model="layerSearch" 
:placeholder="t('filter layers…')" />
+          <button type="button" class="btn small" @click="addLevel">{{ t('+ 
add tier') }}</button>
         </header>
         <div class="sect-body">
           <article v-for="(lvl, idx) in levelsSorted" :key="lvl.id" 
class="tier-card">
@@ -654,11 +664,11 @@ const stats = computed(() => {
               <span class="lvl-order">#{{ lvl.order }}</span>
               <input class="inp lvl-id mono" v-model="lvl.id" 
placeholder="apps" />
               <input class="inp lvl-label" v-model="lvl.label" 
placeholder="Apps" />
-              <span class="tier-count">{{ layersByTier[lvl.id]?.length ?? 0 }} 
layers</span>
+              <span class="tier-count">{{ t('{count} layers', { count: 
layersByTier[lvl.id]?.length ?? 0 }) }}</span>
               <div class="lvl-spacer" />
               <button type="button" class="btn tiny" :disabled="idx === 0" 
@click="moveLevel(idx, -1)">↑</button>
               <button type="button" class="btn tiny" :disabled="idx === 
levelsSorted.length - 1" @click="moveLevel(idx, 1)">↓</button>
-              <button type="button" class="btn tiny danger" 
@click="removeLevel(lvl.id)">remove</button>
+              <button type="button" class="btn tiny danger" 
@click="removeLevel(lvl.id)">{{ t('remove') }}</button>
             </header>
             <div class="tier-body">
               <div class="layer-rows">
@@ -666,20 +676,20 @@ const stats = computed(() => {
                   <input type="color" class="color-pick" 
:value="row.spec?.color ?? '#8a8a8a'" @input="(e) => 
(ensureLayerSpec(row.key).color = (e.target as HTMLInputElement).value)" />
                   <span class="layer-key">{{ row.key }}</span>
                   <template v-if="row.spec?.metric">
-                    <input class="inp mono metric-mqe" 
v-model="row.spec.metric.mqe" placeholder="mqe e.g. service_cpm" />
-                    <input class="inp metric-lbl" 
v-model="row.spec.metric.label" placeholder="label" />
-                    <input class="inp metric-unit" 
v-model="row.spec.metric.unit" placeholder="unit" />
-                    <button class="btn tiny danger" type="button" 
title="remove metric" @click="clearMetric(row.key)">⊘</button>
+                    <input class="inp mono metric-mqe" 
v-model="row.spec.metric.mqe" :placeholder="t('mqe e.g. service_cpm')" />
+                    <input class="inp metric-lbl" 
v-model="row.spec.metric.label" :placeholder="t('label')" />
+                    <input class="inp metric-unit" 
v-model="row.spec.metric.unit" :placeholder="t('unit')" />
+                    <button class="btn tiny danger" type="button" 
:title="t('remove metric')" @click="clearMetric(row.key)">⊘</button>
                   </template>
-                  <button v-else class="btn tiny ghost" type="button" 
@click="ensureMetric(row.key)">+ metric</button>
+                  <button v-else class="btn tiny ghost" type="button" 
@click="ensureMetric(row.key)">{{ t('+ metric') }}</button>
                   <div class="row-spacer" />
                   <select class="inp tier-select" 
:value="explicitTier[row.key] ?? ''" @change="(e) => onMoveTier(row.key, e)">
-                    <option value="">— unpinned —</option>
-                    <option v-for="t in levelsSorted" :key="t.id" 
:value="t.id">{{ t.label }}</option>
+                    <option value="">{{ t('— unpinned —') }}</option>
+                    <option v-for="tier in levelsSorted" :key="tier.id" 
:value="tier.id">{{ tier.label }}</option>
                   </select>
-                  <button class="btn tiny ghost" type="button" title="Remove 
from this tier (moves to Unpinned)" @click="assignLayerToLevel(row.key, 
null)">×</button>
+                  <button class="btn tiny ghost" type="button" 
:title="t('Remove from this tier (moves to Unpinned)')" 
@click="assignLayerToLevel(row.key, null)">×</button>
                 </div>
-                <div v-if="(layersByTier[lvl.id]?.length ?? 0) === 0" 
class="tier-empty">No layers pinned — use a layer's tier dropdown to move one 
here.</div>
+                <div v-if="(layersByTier[lvl.id]?.length ?? 0) === 0" 
class="tier-empty">{{ t('No layers pinned — use a layer\'s tier dropdown to 
move one here.') }}</div>
               </div>
             </div>
           </article>
@@ -689,10 +699,11 @@ const stats = computed(() => {
       <!-- ── Unpinned → failover ────────────────────────────────────── -->
       <section class="sect">
         <header class="sect-head">
-          <h2>Unpinned layers</h2>
+          <h2>{{ t('Unpinned layers') }}</h2>
           <span class="sec-hint">
-            Pinned to no tier — they land on the failover tier
-            (<strong>{{ levelLabel(draft.unknownLayer.level) }}</strong>). 
Pick a tier to pin one.
+            <i18n-t keypath="Pinned to no tier — they land on the failover 
tier ({failoverTier}). Pick a tier to pin one." tag="span" scope="global">
+              <template #failoverTier><strong>{{ 
levelLabel(draft.unknownLayer.level) }}</strong></template>
+            </i18n-t>
           </span>
         </header>
         <div class="sect-body">
@@ -707,22 +718,22 @@ const stats = computed(() => {
               <span class="layer-key">{{ row.key }}</span>
               <template v-if="row.spec?.metric">
                 <input class="inp mono metric-mqe" 
v-model="row.spec.metric.mqe" placeholder="mqe" />
-                <input class="inp metric-lbl" v-model="row.spec.metric.label" 
placeholder="label" />
-                <input class="inp metric-unit" v-model="row.spec.metric.unit" 
placeholder="unit" />
-                <button class="btn tiny danger" type="button" title="remove 
metric" @click="clearMetric(row.key)">⊘</button>
+                <input class="inp metric-lbl" v-model="row.spec.metric.label" 
:placeholder="t('label')" />
+                <input class="inp metric-unit" v-model="row.spec.metric.unit" 
:placeholder="t('unit')" />
+                <button class="btn tiny danger" type="button" 
:title="t('remove metric')" @click="clearMetric(row.key)">⊘</button>
               </template>
-              <button v-else class="btn tiny ghost" type="button" 
@click="ensureMetric(row.key)">+ metric</button>
+              <button v-else class="btn tiny ghost" type="button" 
@click="ensureMetric(row.key)">{{ t('+ metric') }}</button>
               <div class="row-spacer" />
-              <span class="via-tag" :data-via="resolvedLevels[row.key]?.via" 
:title="`falls to ${resolvedLevels[row.key]?.via}`">
-                {{ resolvedLevels[row.key]?.via === 'filtered' ? 'filtered 
out' : '→ ' + levelLabel(resolvedLevels[row.key]?.levelId ?? null) }}
+              <span class="via-tag" :data-via="resolvedLevels[row.key]?.via" 
:title="t('falls to {via}', { via: resolvedLevels[row.key]?.via })">
+                {{ resolvedLevels[row.key]?.via === 'filtered' ? t('filtered 
out') : '→ ' + levelLabel(resolvedLevels[row.key]?.levelId ?? null) }}
               </span>
               <select class="inp tier-select" :value="explicitTier[row.key] ?? 
''" @change="(e) => onMoveTier(row.key, e)">
-                <option value="">— pin to tier —</option>
-                <option v-for="t in levelsSorted" :key="t.id" :value="t.id">{{ 
t.label }}</option>
+                <option value="">{{ t('— pin to tier —') }}</option>
+                <option v-for="tier in levelsSorted" :key="tier.id" 
:value="tier.id">{{ tier.label }}</option>
               </select>
-              <button class="btn tiny danger" type="button" title="Delete this 
layer from the config entirely" 
@click="removeLayerFromConfig(row.key)">×</button>
+              <button class="btn tiny danger" type="button" :title="t('Delete 
this layer from the config entirely')" 
@click="removeLayerFromConfig(row.key)">×</button>
             </div>
-            <div v-if="unpinnedRows.length === 0" class="empty">No unpinned 
layers.</div>
+            <div v-if="unpinnedRows.length === 0" class="empty">{{ t('No 
unpinned layers.') }}</div>
           </div>
         </div>
       </section>
@@ -730,9 +741,9 @@ const stats = computed(() => {
       <!-- ── Groups ────────────────────────────────────────────────── -->
       <section class="sect">
         <header class="sect-head">
-          <h2>Logic groups</h2>
-          <span class="sec-hint">Several layers drawn as one labelled block on 
a tier (e.g. Self-Observability). Each member keeps its own cube color.</span>
-          <button type="button" class="btn small" @click="addGroup">+ add 
group</button>
+          <h2>{{ t('Logic groups') }}</h2>
+          <span class="sec-hint">{{ t('Several layers drawn as one labelled 
block on a tier (e.g. Self-Observability). Each member keeps its own cube 
color.') }}</span>
+          <button type="button" class="btn small" @click="addGroup">{{ t('+ 
add group') }}</button>
         </header>
         <div class="sect-body">
           <div class="groups-grid">
@@ -747,40 +758,40 @@ const stats = computed(() => {
                 <input class="inp group-id mono" v-model="g.id" 
placeholder="self-observability" />
                 <input class="inp group-label" v-model="g.label" 
placeholder="Self-Observability" />
                 <div class="lvl-spacer" />
-                <button type="button" class="btn tiny danger" 
@click="removeGroup(g.id)">remove</button>
+                <button type="button" class="btn tiny danger" 
@click="removeGroup(g.id)">{{ t('remove') }}</button>
               </header>
               <div class="group-body">
                 <div class="row-2">
                   <label class="field">
-                    <span class="lbl small">level</span>
+                    <span class="lbl small">{{ t('level') }}</span>
                     <select class="inp" v-model="g.level">
                       <option v-for="lvl in levelsSorted" :key="lvl.id" 
:value="lvl.id">{{ lvl.label }} ({{ lvl.id }})</option>
                     </select>
                   </label>
                   <label class="field">
-                    <span class="lbl small">icon</span>
+                    <span class="lbl small">{{ t('icon') }}</span>
                     <select class="inp" v-model="g.icon">
                       <option v-for="ic in ICON_NAMES" :key="ic" 
:value="ic">{{ ic }}</option>
                     </select>
                   </label>
                 </div>
                 <div class="field">
-                  <span class="lbl small">member layers ({{ g.layers.length 
}})</span>
+                  <span class="lbl small">{{ t('member layers ({count})', { 
count: g.layers.length }) }}</span>
                   <div class="chips">
                     <span v-for="k in g.layers" :key="k" class="chip">
                       {{ k }}
-                      <button class="x" type="button" title="remove from 
group" @click="removeLayerFromGroup(g, k)">×</button>
+                      <button class="x" type="button" :title="t('remove from 
group')" @click="removeLayerFromGroup(g, k)">×</button>
                     </span>
                     <select class="add-layer" :value="''" @change="(e) => 
onAddLayerToGroup(g, e)">
-                      <option value="">+ add layer…</option>
+                      <option value="">{{ t('+ add layer…') }}</option>
                       <option v-for="k in groupCandidates(g)" :key="k" 
:value="k">{{ k }}</option>
                     </select>
                   </div>
-                  <p v-if="g.layers.length === 0" class="hint-sm warn">A group 
needs at least one layer — pushing will be rejected until you add one.</p>
+                  <p v-if="g.layers.length === 0" class="hint-sm warn">{{ t('A 
group needs at least one layer — pushing will be rejected until you add one.') 
}}</p>
                 </div>
               </div>
             </article>
-            <div v-if="(draft.groups ?? []).length === 0" class="empty">No 
logic groups. Add one to cluster related layers into a single block.</div>
+            <div v-if="(draft.groups ?? []).length === 0" class="empty">{{ 
t('No logic groups. Add one to cluster related layers into a single block.') 
}}</div>
           </div>
         </div>
       </section>
@@ -788,17 +799,15 @@ const stats = computed(() => {
       <!-- ── Service-map layers ─────────────────────────────────────── -->
       <section class="sect">
         <header class="sect-head">
-          <h2>Service-map layers</h2>
+          <h2>{{ t('Service-map layers') }}</h2>
           <span class="sec-hint">
-            These layers' templates provide a service map, so their cubes 
render
-            as a call graph (and seed the cross-layer hierarchy). Read-only —
-            it's a layer-template property, not a 3D-map setting.
+            {{ t('These layers\' templates provide a service map, so their 
cubes render as a call graph (and seed the cross-layer hierarchy). Read-only — 
it\'s a layer-template property, not a 3D-map setting.') }}
           </span>
         </header>
         <div class="sect-body">
           <div class="chips">
             <span v-for="k in serviceMapLayers" :key="k" class="chip">{{ k 
}}</span>
-            <span v-if="serviceMapLayers.length === 0" 
class="chips-empty">None reporting a service map.</span>
+            <span v-if="serviceMapLayers.length === 0" class="chips-empty">{{ 
t('None reporting a service map.') }}</span>
           </div>
         </div>
       </section>
@@ -806,12 +815,12 @@ const stats = computed(() => {
       <!-- ── Unknown layer fallback ─────────────────────────────────── -->
       <section class="sect">
         <header class="sect-head">
-          <h2>Failover tier</h2>
-          <span class="sec-hint">The single catch-all: any layer no tier pins 
lands here.</span>
+          <h2>{{ t('Failover tier') }}</h2>
+          <span class="sec-hint">{{ t('The single catch-all: any layer no tier 
pins lands here.') }}</span>
         </header>
         <div class="sect-body">
           <label class="field">
-            <span class="lbl">level</span>
+            <span class="lbl">{{ t('level') }}</span>
             <select class="inp fallback-level" 
v-model="draft.unknownLayer.level">
               <option v-for="lvl in levelsSorted" :key="lvl.id" 
:value="lvl.id">{{ lvl.label }} ({{ lvl.id }})</option>
             </select>
@@ -822,11 +831,10 @@ const stats = computed(() => {
     </template>
 
     <!-- ── Check diff & push ──────────────────────────────────────────── -->
-    <Modal :open="pushDiffOpen" title="Push 3D-map config to OAP" 
width="min(1100px, 94vw)" @close="pushDiffOpen = false">
+    <Modal :open="pushDiffOpen" :title="t('Push 3D-map config to OAP')" 
width="min(1100px, 94vw)" @close="pushDiffOpen = false">
       <div class="push-modal">
         <p class="push-lede">
-          Left = live on OAP (remote). Right = your local draft. Pushing
-          replaces the OAP copy — the map renders it on the next visit.
+          {{ t('Left = live on OAP (remote). Right = your local draft. Pushing 
replaces the OAP copy — the map renders it on the next visit.') }}
         </p>
         <ul v-if="pushErr && pushErr.length" class="issues">
           <li v-for="(it, i) in pushErr" :key="i"><code>{{ it }}</code></li>
@@ -834,9 +842,9 @@ const stats = computed(() => {
         <MonacoDiff :original="pushRemotePretty" :modified="pushLocalPretty" 
language="json" />
       </div>
       <template #footer>
-        <button class="btn" :disabled="pushing" @click="pushDiffOpen = 
false">Cancel</button>
+        <button class="btn" :disabled="pushing" @click="pushDiffOpen = 
false">{{ t('Cancel') }}</button>
         <button class="btn primary" :disabled="pushing || readOnly" 
@click="pushToOap">
-          {{ pushing ? 'Pushing…' : 'Push to OAP' }}
+          {{ pushing ? t('Pushing…') : t('Push to OAP') }}
         </button>
       </template>
     </Modal>
diff --git a/apps/ui/src/i18n/locales/de.json b/apps/ui/src/i18n/locales/de.json
index 013daa3..0d800f8 100644
--- a/apps/ui/src/i18n/locales/de.json
+++ b/apps/ui/src/i18n/locales/de.json
@@ -1214,5 +1214,60 @@
   "— pick a currently-running pod, or check that on-demand pod logs are 
enabled on OAP.": "— Wähle einen laufenden Pod oder prüfe, ob 
On-Demand-Pod-Logs in OAP aktiviert sind.",
   "Select a service to begin.": "Wähle einen Service, um zu beginnen.",
   "Select a pod to list its containers.": "Wähle einen Pod, um seine Container 
aufzulisten.",
-  "Select a container, then press Start to tail its logs.": "Wähle einen 
Container und drücke Start, um seine Logs live zu verfolgen."
+  "Select a container, then press Start to tail its logs.": "Wähle einen 
Container und drücke Start, um seine Logs live zu verfolgen.",
+  "Dashboard setup · 3D Infra Map": "Dashboard-Einrichtung · 3D Infra Map",
+  "3D Infrastructure Map": "3D-Infrastrukturkarte",
+  "Config for the {mapPath} view, published to OAP. Levels control the 
vertical stack; per-layer color + metrics drive each cube. Edits save to a 
{localDraft}; {checkDiffPush} publishes to OAP — the map renders the remote, 
with bundled defaults as fallback.": "Konfiguration für die Ansicht {mapPath}, 
auf OAP veröffentlicht. Ebenen steuern die vertikale Stapelung; Farbe und 
Metriken pro Ebene bestimmen jeden Würfel. Änderungen werden in einem 
{localDraft} gespeichert; {checkDiffPush} verö [...]
+  "local draft in this browser": "lokaler Entwurf in diesem Browser",
+  "editing: {source}": "Bearbeitung: {source}",
+  "Reset to": "Zurücksetzen auf",
+  "Bundled equals OAP-live — nothing to reset to.": "Mitgeliefert entspricht 
der OAP-Live-Version — nichts zum Zurücksetzen.",
+  "synced": "synchron",
+  "OAP has no copy of this config yet.": "OAP hat noch keine Kopie dieser 
Konfiguration.",
+  "Saved local": "Lokal gespeichert",
+  "Save local": "Lokal speichern",
+  "Couldn't load the 3D-map config — the BFF may be unreachable. Refresh the 
page to retry.": "3D-map-Konfiguration konnte nicht geladen werden — der BFF 
ist möglicherweise nicht erreichbar. Seite neu laden, um es erneut zu 
versuchen.",
+  "Loading config…": "Konfiguration wird geladen…",
+  "Global layer filter": "Globaler Ebenen-Filter",
+  "The one top-level gate — a layer this JS regex excludes is off the map (it 
still shows below in Unpinned, tagged \"filtered out\"). Default {dotStar} 
admits all.": "Die einzige übergeordnete Schranke — eine Ebene, die diese 
JS-regex ausschließt, erscheint nicht auf der Karte (sie wird unten weiterhin 
unter „Nicht angeheftet“ mit der Markierung „filtered out“ angezeigt). Der 
Standard {dotStar} lässt alle zu.",
+  "Tiers & layers (top → bottom)": "Stufen & Ebenen (oben → unten)",
+  "Every layer belongs to one tier — set it with the tier dropdown; order = 
vertical stacking.": "Jede Ebene gehört zu genau einer Stufe — lege sie über 
das Stufen-Dropdown fest; die Reihenfolge entspricht der vertikalen Stapelung.",
+  "{count} in OAP": "{count} in OAP",
+  "{count} unpinned → fallback": "{count} nicht angeheftet → Fallback",
+  "filter layers…": "Ebenen filtern…",
+  "+ add tier": "+ Stufe hinzufügen",
+  "{count} layers": "{count} Ebenen",
+  "mqe e.g. service_cpm": "mqe, z. B. service_cpm",
+  "label": "Bezeichnung",
+  "unit": "Einheit",
+  "remove metric": "Metrik entfernen",
+  "+ metric": "+ Metrik",
+  "— unpinned —": "— nicht angeheftet —",
+  "Remove from this tier (moves to Unpinned)": "Aus dieser Stufe entfernen 
(wandert zu „Nicht angeheftet“)",
+  "No layers pinned — use a layer's tier dropdown to move one here.": "Keine 
Ebenen angeheftet — verschiebe eine über das Stufen-Dropdown einer Ebene 
hierher.",
+  "Unpinned layers": "Nicht angeheftete Ebenen",
+  "Pinned to no tier — they land on the failover tier ({failoverTier}). Pick a 
tier to pin one.": "An keine Stufe angeheftet — sie landen auf der 
Failover-Stufe ({failoverTier}). Wähle eine Stufe aus, um eine anzuheften.",
+  "falls to {via}": "fällt auf {via} zurück",
+  "filtered out": "filtered out",
+  "— pin to tier —": "— an Stufe anheften —",
+  "Delete this layer from the config entirely": "Diese Ebene vollständig aus 
der Konfiguration löschen",
+  "No unpinned layers.": "Keine nicht angehefteten Ebenen.",
+  "Logic groups": "Logikgruppen",
+  "Several layers drawn as one labelled block on a tier (e.g. 
Self-Observability). Each member keeps its own cube color.": "Mehrere Ebenen, 
die als ein beschrifteter Block auf einer Stufe dargestellt werden (z. B. 
Self-Observability). Jedes Mitglied behält seine eigene Würfelfarbe.",
+  "+ add group": "+ Gruppe hinzufügen",
+  "level": "Ebene",
+  "icon": "Symbol",
+  "member layers ({count})": "Mitglieds-Ebenen ({count})",
+  "remove from group": "aus Gruppe entfernen",
+  "+ add layer…": "+ Ebene hinzufügen…",
+  "A group needs at least one layer — pushing will be rejected until you add 
one.": "Eine Gruppe braucht mindestens eine Ebene — die Veröffentlichung wird 
abgelehnt, bis du eine hinzufügst.",
+  "No logic groups. Add one to cluster related layers into a single block.": 
"Keine Logikgruppen. Füge eine hinzu, um zusammengehörige Ebenen zu einem 
einzigen Block zu bündeln.",
+  "Service-map layers": "Service-Map-Ebenen",
+  "These layers' templates provide a service map, so their cubes render as a 
call graph (and seed the cross-layer hierarchy). Read-only — it's a 
layer-template property, not a 3D-map setting.": "Die Vorlagen dieser Ebenen 
liefern eine Service-Map, daher werden ihre Würfel als Aufrufgraph dargestellt 
(und legen den Grundstein für die ebenenübergreifende Hierarchie). 
Schreibgeschützt — es ist eine Eigenschaft der Ebenen-Vorlage, keine 
3D-map-Einstellung.",
+  "None reporting a service map.": "Keine meldet eine Service-Map.",
+  "Failover tier": "Failover-Stufe",
+  "The single catch-all: any layer no tier pins lands here.": "Die einzige 
Auffangstufe: jede Ebene, die an keine Stufe angeheftet ist, landet hier.",
+  "Push 3D-map config to OAP": "3D-map-Konfiguration auf OAP veröffentlichen",
+  "Left = live on OAP (remote). Right = your local draft. Pushing replaces the 
OAP copy — the map renders it on the next visit.": "Links = live auf OAP 
(Remote). Rechts = dein lokaler Entwurf. Die Veröffentlichung ersetzt die 
OAP-Kopie — die Karte zeigt sie beim nächsten Besuch.",
+  "Push to OAP": "Auf OAP veröffentlichen"
 }
diff --git a/apps/ui/src/i18n/locales/en.json b/apps/ui/src/i18n/locales/en.json
index 236a926..1780696 100644
--- a/apps/ui/src/i18n/locales/en.json
+++ b/apps/ui/src/i18n/locales/en.json
@@ -1225,5 +1225,60 @@
   "— pick a currently-running pod, or check that on-demand pod logs are 
enabled on OAP.": "— pick a currently-running pod, or check that on-demand pod 
logs are enabled on OAP.",
   "Select a service to begin.": "Select a service to begin.",
   "Select a pod to list its containers.": "Select a pod to list its 
containers.",
-  "Select a container, then press Start to tail its logs.": "Select a 
container, then press Start to tail its logs."
+  "Select a container, then press Start to tail its logs.": "Select a 
container, then press Start to tail its logs.",
+  "Dashboard setup · 3D Infra Map": "Dashboard setup · 3D Infra Map",
+  "3D Infrastructure Map": "3D Infrastructure Map",
+  "Config for the {mapPath} view, published to OAP. Levels control the 
vertical stack; per-layer color + metrics drive each cube. Edits save to a 
{localDraft}; {checkDiffPush} publishes to OAP — the map renders the remote, 
with bundled defaults as fallback.": "Config for the {mapPath} view, published 
to OAP. Levels control the vertical stack; per-layer color + metrics drive each 
cube. Edits save to a {localDraft}; {checkDiffPush} publishes to OAP — the map 
renders the remote, with bundle [...]
+  "local draft in this browser": "local draft in this browser",
+  "editing: {source}": "editing: {source}",
+  "Reset to": "Reset to",
+  "Bundled equals OAP-live — nothing to reset to.": "Bundled equals OAP-live — 
nothing to reset to.",
+  "synced": "synced",
+  "OAP has no copy of this config yet.": "OAP has no copy of this config yet.",
+  "Saved local": "Saved local",
+  "Save local": "Save local",
+  "Couldn't load the 3D-map config — the BFF may be unreachable. Refresh the 
page to retry.": "Couldn't load the 3D-map config — the BFF may be unreachable. 
Refresh the page to retry.",
+  "Loading config…": "Loading config…",
+  "Global layer filter": "Global layer filter",
+  "The one top-level gate — a layer this JS regex excludes is off the map (it 
still shows below in Unpinned, tagged \"filtered out\"). Default {dotStar} 
admits all.": "The one top-level gate — a layer this JS regex excludes is off 
the map (it still shows below in Unpinned, tagged \"filtered out\"). Default 
{dotStar} admits all.",
+  "Tiers & layers (top → bottom)": "Tiers & layers (top → bottom)",
+  "Every layer belongs to one tier — set it with the tier dropdown; order = 
vertical stacking.": "Every layer belongs to one tier — set it with the tier 
dropdown; order = vertical stacking.",
+  "{count} in OAP": "{count} in OAP",
+  "{count} unpinned → fallback": "{count} unpinned → fallback",
+  "filter layers…": "filter layers…",
+  "+ add tier": "+ add tier",
+  "{count} layers": "{count} layers",
+  "mqe e.g. service_cpm": "mqe e.g. service_cpm",
+  "label": "label",
+  "unit": "unit",
+  "remove metric": "remove metric",
+  "+ metric": "+ metric",
+  "— unpinned —": "— unpinned —",
+  "Remove from this tier (moves to Unpinned)": "Remove from this tier (moves 
to Unpinned)",
+  "No layers pinned — use a layer's tier dropdown to move one here.": "No 
layers pinned — use a layer's tier dropdown to move one here.",
+  "Unpinned layers": "Unpinned layers",
+  "Pinned to no tier — they land on the failover tier ({failoverTier}). Pick a 
tier to pin one.": "Pinned to no tier — they land on the failover tier 
({failoverTier}). Pick a tier to pin one.",
+  "falls to {via}": "falls to {via}",
+  "filtered out": "filtered out",
+  "— pin to tier —": "— pin to tier —",
+  "Delete this layer from the config entirely": "Delete this layer from the 
config entirely",
+  "No unpinned layers.": "No unpinned layers.",
+  "Logic groups": "Logic groups",
+  "Several layers drawn as one labelled block on a tier (e.g. 
Self-Observability). Each member keeps its own cube color.": "Several layers 
drawn as one labelled block on a tier (e.g. Self-Observability). Each member 
keeps its own cube color.",
+  "+ add group": "+ add group",
+  "level": "level",
+  "icon": "icon",
+  "member layers ({count})": "member layers ({count})",
+  "remove from group": "remove from group",
+  "+ add layer…": "+ add layer…",
+  "A group needs at least one layer — pushing will be rejected until you add 
one.": "A group needs at least one layer — pushing will be rejected until you 
add one.",
+  "No logic groups. Add one to cluster related layers into a single block.": 
"No logic groups. Add one to cluster related layers into a single block.",
+  "Service-map layers": "Service-map layers",
+  "These layers' templates provide a service map, so their cubes render as a 
call graph (and seed the cross-layer hierarchy). Read-only — it's a 
layer-template property, not a 3D-map setting.": "These layers' templates 
provide a service map, so their cubes render as a call graph (and seed the 
cross-layer hierarchy). Read-only — it's a layer-template property, not a 
3D-map setting.",
+  "None reporting a service map.": "None reporting a service map.",
+  "Failover tier": "Failover tier",
+  "The single catch-all: any layer no tier pins lands here.": "The single 
catch-all: any layer no tier pins lands here.",
+  "Push 3D-map config to OAP": "Push 3D-map config to OAP",
+  "Left = live on OAP (remote). Right = your local draft. Pushing replaces the 
OAP copy — the map renders it on the next visit.": "Left = live on OAP 
(remote). Right = your local draft. Pushing replaces the OAP copy — the map 
renders it on the next visit.",
+  "Push to OAP": "Push to OAP"
 }
diff --git a/apps/ui/src/i18n/locales/es.json b/apps/ui/src/i18n/locales/es.json
index 304d877..4df1934 100644
--- a/apps/ui/src/i18n/locales/es.json
+++ b/apps/ui/src/i18n/locales/es.json
@@ -1214,5 +1214,60 @@
   "— pick a currently-running pod, or check that on-demand pod logs are 
enabled on OAP.": "— elige un Pod en ejecución, o verifica que los logs de Pod 
bajo demanda estén habilitados en OAP.",
   "Select a service to begin.": "Selecciona un servicio para empezar.",
   "Select a pod to list its containers.": "Selecciona un Pod para listar sus 
contenedores.",
-  "Select a container, then press Start to tail its logs.": "Selecciona un 
contenedor y pulsa Inicio para seguir sus logs en vivo."
+  "Select a container, then press Start to tail its logs.": "Selecciona un 
contenedor y pulsa Inicio para seguir sus logs en vivo.",
+  "Dashboard setup · 3D Infra Map": "Configuración de paneles · 3D Infra Map",
+  "3D Infrastructure Map": "3D Infrastructure Map",
+  "Config for the {mapPath} view, published to OAP. Levels control the 
vertical stack; per-layer color + metrics drive each cube. Edits save to a 
{localDraft}; {checkDiffPush} publishes to OAP — the map renders the remote, 
with bundled defaults as fallback.": "Configuración de la vista {mapPath}, 
publicada en OAP. Los niveles controlan el apilado vertical; el color y las 
métricas de cada capa definen cada cubo. Las ediciones se guardan en un 
{localDraft}; {checkDiffPush} publica en OAP — [...]
+  "local draft in this browser": "borrador local en este navegador",
+  "editing: {source}": "editando: {source}",
+  "Reset to": "Restablecer a",
+  "Bundled equals OAP-live — nothing to reset to.": "Bundled coincide con 
OAP-live — no hay nada a lo que restablecer.",
+  "synced": "sincronizado",
+  "OAP has no copy of this config yet.": "OAP aún no tiene ninguna copia de 
esta configuración.",
+  "Saved local": "Guardado localmente",
+  "Save local": "Guardar localmente",
+  "Couldn't load the 3D-map config — the BFF may be unreachable. Refresh the 
page to retry.": "No se pudo cargar la configuración del 3D-map — puede que el 
BFF no esté disponible. Recarga la página para reintentar.",
+  "Loading config…": "Cargando configuración…",
+  "Global layer filter": "Filtro global de capas",
+  "The one top-level gate — a layer this JS regex excludes is off the map (it 
still shows below in Unpinned, tagged \"filtered out\"). Default {dotStar} 
admits all.": "El único filtro de nivel superior — una capa que este regex de 
JS excluye queda fuera del mapa (aún aparece más abajo en No fijadas, marcada 
como \"filtered out\"). El valor predeterminado {dotStar} admite todas.",
+  "Tiers & layers (top → bottom)": "Niveles y capas (arriba → abajo)",
+  "Every layer belongs to one tier — set it with the tier dropdown; order = 
vertical stacking.": "Cada capa pertenece a un nivel — asígnalo con el menú 
desplegable de nivel; el orden = apilado vertical.",
+  "{count} in OAP": "{count} en OAP",
+  "{count} unpinned → fallback": "{count} no fijadas → respaldo",
+  "filter layers…": "filtrar capas…",
+  "+ add tier": "+ agregar nivel",
+  "{count} layers": "{count} capas",
+  "mqe e.g. service_cpm": "mqe p. ej. service_cpm",
+  "label": "etiqueta",
+  "unit": "unidad",
+  "remove metric": "quitar métrica",
+  "+ metric": "+ métrica",
+  "— unpinned —": "— no fijadas —",
+  "Remove from this tier (moves to Unpinned)": "Quitar de este nivel (pasa a 
No fijadas)",
+  "No layers pinned — use a layer's tier dropdown to move one here.": "No hay 
capas fijadas — usa el menú desplegable de nivel de una capa para mover una 
aquí.",
+  "Unpinned layers": "Capas no fijadas",
+  "Pinned to no tier — they land on the failover tier ({failoverTier}). Pick a 
tier to pin one.": "Sin nivel asignado — caen en el nivel de respaldo 
({failoverTier}). Elige un nivel para fijar una.",
+  "falls to {via}": "cae a {via}",
+  "filtered out": "filtered out",
+  "— pin to tier —": "— fijar a un nivel —",
+  "Delete this layer from the config entirely": "Eliminar esta capa por 
completo de la configuración",
+  "No unpinned layers.": "No hay capas sin fijar.",
+  "Logic groups": "Grupos lógicos",
+  "Several layers drawn as one labelled block on a tier (e.g. 
Self-Observability). Each member keeps its own cube color.": "Varias capas 
dibujadas como un único bloque con etiqueta en un nivel (p. ej. 
Self-Observability). Cada miembro conserva el color de su propio cubo.",
+  "+ add group": "+ agregar grupo",
+  "level": "nivel",
+  "icon": "icono",
+  "member layers ({count})": "capas miembro ({count})",
+  "remove from group": "quitar del grupo",
+  "+ add layer…": "+ agregar capa…",
+  "A group needs at least one layer — pushing will be rejected until you add 
one.": "Un grupo necesita al menos una capa — el push se rechazará hasta que 
agregues una.",
+  "No logic groups. Add one to cluster related layers into a single block.": 
"No hay grupos lógicos. Agrega uno para agrupar capas relacionadas en un solo 
bloque.",
+  "Service-map layers": "Capas con service map",
+  "These layers' templates provide a service map, so their cubes render as a 
call graph (and seed the cross-layer hierarchy). Read-only — it's a 
layer-template property, not a 3D-map setting.": "Las plantillas de estas capas 
proporcionan un service map, por lo que sus cubos se muestran como un call 
graph (y siembran la jerarquía entre capas). Solo lectura — es una propiedad de 
la plantilla de la capa, no un ajuste del 3D-map.",
+  "None reporting a service map.": "Ninguna reporta un service map.",
+  "Failover tier": "Nivel de respaldo",
+  "The single catch-all: any layer no tier pins lands here.": "El único nivel 
comodín: cualquier capa que ningún nivel fije termina aquí.",
+  "Push 3D-map config to OAP": "Publicar la configuración del 3D-map en OAP",
+  "Left = live on OAP (remote). Right = your local draft. Pushing replaces the 
OAP copy — the map renders it on the next visit.": "Izquierda = en vivo en OAP 
(remote). Derecha = tu borrador local. El push reemplaza la copia en OAP — el 
mapa la mostrará en la próxima visita.",
+  "Push to OAP": "Publicar en OAP"
 }
diff --git a/apps/ui/src/i18n/locales/fr.json b/apps/ui/src/i18n/locales/fr.json
index 9d97b22..d0fd337 100644
--- a/apps/ui/src/i18n/locales/fr.json
+++ b/apps/ui/src/i18n/locales/fr.json
@@ -1214,5 +1214,60 @@
   "— pick a currently-running pod, or check that on-demand pod logs are 
enabled on OAP.": "— choisissez un Pod en cours d'exécution, ou vérifiez que 
les logs de Pod à la demande sont activés sur OAP.",
   "Select a service to begin.": "Sélectionnez un service pour commencer.",
   "Select a pod to list its containers.": "Sélectionnez un Pod pour lister ses 
conteneurs.",
-  "Select a container, then press Start to tail its logs.": "Sélectionnez un 
conteneur, puis appuyez sur Début pour suivre ses logs en direct."
+  "Select a container, then press Start to tail its logs.": "Sélectionnez un 
conteneur, puis appuyez sur Début pour suivre ses logs en direct.",
+  "Dashboard setup · 3D Infra Map": "Configuration du tableau de bord · 3D 
Infra Map",
+  "3D Infrastructure Map": "Carte d'infrastructure 3D",
+  "Config for the {mapPath} view, published to OAP. Levels control the 
vertical stack; per-layer color + metrics drive each cube. Edits save to a 
{localDraft}; {checkDiffPush} publishes to OAP — the map renders the remote, 
with bundled defaults as fallback.": "Configuration de la vue {mapPath}, 
publiée sur OAP. Les niveaux contrôlent l'empilement vertical ; la couleur et 
les métriques propres à chaque couche pilotent chaque cube. Les modifications 
sont enregistrées dans un {localDraft} ; [...]
+  "local draft in this browser": "brouillon local dans ce navigateur",
+  "editing: {source}": "édition : {source}",
+  "Reset to": "Réinitialiser à",
+  "Bundled equals OAP-live — nothing to reset to.": "Bundled est identique à 
la version OAP en ligne — rien à réinitialiser.",
+  "synced": "synchronisé",
+  "OAP has no copy of this config yet.": "OAP n'a pas encore de copie de cette 
configuration.",
+  "Saved local": "Enregistré en local",
+  "Save local": "Enregistrer en local",
+  "Couldn't load the 3D-map config — the BFF may be unreachable. Refresh the 
page to retry.": "Impossible de charger la configuration de la 3D-map — le BFF 
est peut-être inaccessible. Actualisez la page pour réessayer.",
+  "Loading config…": "Chargement de la configuration…",
+  "Global layer filter": "Filtre global des couches",
+  "The one top-level gate — a layer this JS regex excludes is off the map (it 
still shows below in Unpinned, tagged \"filtered out\"). Default {dotStar} 
admits all.": "L'unique filtre de premier niveau — une couche que ce regex JS 
exclut disparaît de la carte (elle reste affichée plus bas dans les Non 
épinglées, marquée « filtré »). La valeur par défaut {dotStar} admet tout.",
+  "Tiers & layers (top → bottom)": "Paliers et couches (haut → bas)",
+  "Every layer belongs to one tier — set it with the tier dropdown; order = 
vertical stacking.": "Chaque couche appartient à un seul palier — définissez-le 
via le menu déroulant du palier ; l'ordre correspond à l'empilement vertical.",
+  "{count} in OAP": "{count} dans OAP",
+  "{count} unpinned → fallback": "{count} non épinglées → repli",
+  "filter layers…": "filtrer les couches…",
+  "+ add tier": "+ ajouter un palier",
+  "{count} layers": "{count} couches",
+  "mqe e.g. service_cpm": "mqe, p. ex. service_cpm",
+  "label": "libellé",
+  "unit": "unité",
+  "remove metric": "supprimer la métrique",
+  "+ metric": "+ métrique",
+  "— unpinned —": "— non épinglées —",
+  "Remove from this tier (moves to Unpinned)": "Retirer de ce palier (déplace 
vers Non épinglées)",
+  "No layers pinned — use a layer's tier dropdown to move one here.": "Aucune 
couche épinglée — utilisez le menu déroulant de palier d'une couche pour en 
déplacer une ici.",
+  "Unpinned layers": "Couches non épinglées",
+  "Pinned to no tier — they land on the failover tier ({failoverTier}). Pick a 
tier to pin one.": "Épinglées à aucun palier — elles atterrissent sur le palier 
de secours ({failoverTier}). Choisissez un palier pour en épingler une.",
+  "falls to {via}": "bascule vers {via}",
+  "filtered out": "filtré",
+  "— pin to tier —": "— épingler à un palier —",
+  "Delete this layer from the config entirely": "Supprimer entièrement cette 
couche de la configuration",
+  "No unpinned layers.": "Aucune couche non épinglée.",
+  "Logic groups": "Groupes logiques",
+  "Several layers drawn as one labelled block on a tier (e.g. 
Self-Observability). Each member keeps its own cube color.": "Plusieurs couches 
dessinées comme un seul bloc étiqueté sur un palier (p. ex. 
Self-Observability). Chaque membre conserve la couleur de son propre cube.",
+  "+ add group": "+ ajouter un groupe",
+  "level": "niveau",
+  "icon": "icône",
+  "member layers ({count})": "couches membres ({count})",
+  "remove from group": "retirer du groupe",
+  "+ add layer…": "+ ajouter une couche…",
+  "A group needs at least one layer — pushing will be rejected until you add 
one.": "Un groupe nécessite au moins une couche — la publication sera refusée 
tant que vous n'en ajoutez pas une.",
+  "No logic groups. Add one to cluster related layers into a single block.": 
"Aucun groupe logique. Ajoutez-en un pour regrouper des couches apparentées en 
un seul bloc.",
+  "Service-map layers": "Couches à carte de services",
+  "These layers' templates provide a service map, so their cubes render as a 
call graph (and seed the cross-layer hierarchy). Read-only — it's a 
layer-template property, not a 3D-map setting.": "Les modèles de ces couches 
fournissent une carte de services, leurs cubes s'affichent donc comme un graphe 
d'appels (et amorcent la hiérarchie inter-couches). Lecture seule — c'est une 
propriété du modèle de couche, pas un paramètre de la 3D-map.",
+  "None reporting a service map.": "Aucune ne signale de carte de services.",
+  "Failover tier": "Palier de secours",
+  "The single catch-all: any layer no tier pins lands here.": "Le palier 
fourre-tout unique : toute couche qu'aucun palier n'épingle atterrit ici.",
+  "Push 3D-map config to OAP": "Publier la configuration de la 3D-map sur OAP",
+  "Left = live on OAP (remote). Right = your local draft. Pushing replaces the 
OAP copy — the map renders it on the next visit.": "À gauche = en ligne sur OAP 
(Remote). À droite = votre brouillon local. La publication remplace la copie 
OAP — la carte l'affiche à la prochaine visite.",
+  "Push to OAP": "Publier sur OAP"
 }
diff --git a/apps/ui/src/i18n/locales/ja.json b/apps/ui/src/i18n/locales/ja.json
index 54be75d..81511ff 100644
--- a/apps/ui/src/i18n/locales/ja.json
+++ b/apps/ui/src/i18n/locales/ja.json
@@ -1214,5 +1214,60 @@
   "— pick a currently-running pod, or check that on-demand pod logs are 
enabled on OAP.": "— 実行中の Pod を選択するか、オンデマンド Pod ログが OAP で有効になっているか確認してください。",
   "Select a service to begin.": "サービスを選択して開始してください。",
   "Select a pod to list its containers.": "Pod を選択してコンテナを一覧表示します。",
-  "Select a container, then press Start to tail its logs.": 
"コンテナを選択し、「開始」を押してログをライブで追尾します。"
+  "Select a container, then press Start to tail its logs.": 
"コンテナを選択し、「開始」を押してログをライブで追尾します。",
+  "Dashboard setup · 3D Infra Map": "ダッシュボード設定 · 3D Infra Map",
+  "3D Infrastructure Map": "3D インフラマップ",
+  "Config for the {mapPath} view, published to OAP. Levels control the 
vertical stack; per-layer color + metrics drive each cube. Edits save to a 
{localDraft}; {checkDiffPush} publishes to OAP — the map renders the remote, 
with bundled defaults as fallback.": "{mapPath} ビューの設定で、OAP 
に公開されます。レベルは垂直方向の積み重ねを制御し、レイヤーごとの色とメトリックが各キューブを表現します。編集内容は {localDraft} 
に保存され、{checkDiffPush} で OAP に公開されます。マップはリモート版を表示し、バンドルのデフォルトがフォールバックとなります。",
+  "local draft in this browser": "このブラウザ内のローカル草稿",
+  "editing: {source}": "編集中: {source}",
+  "Reset to": "次にリセット",
+  "Bundled equals OAP-live — nothing to reset to.": "バンドル版が OAP の公開版と一致しています — 
リセット先はありません。",
+  "synced": "同期済み",
+  "OAP has no copy of this config yet.": "OAP にはこの設定のコピーがまだありません。",
+  "Saved local": "ローカルに保存しました",
+  "Save local": "ローカルに保存",
+  "Couldn't load the 3D-map config — the BFF may be unreachable. Refresh the 
page to retry.": "3D-map の設定を読み込めませんでした — BFF 
に接続できない可能性があります。ページを再読み込みして再試行してください。",
+  "Loading config…": "設定を読み込んでいます…",
+  "Global layer filter": "グローバルレイヤーフィルター",
+  "The one top-level gate — a layer this JS regex excludes is off the map (it 
still shows below in Unpinned, tagged \"filtered out\"). Default {dotStar} 
admits all.": "唯一の最上位ゲートです — この JS regex が除外したレイヤーはマップから外れます(「filtered 
out」のタグ付きで下の未ピン留め欄には表示されます)。デフォルトの {dotStar} はすべてを許可します。",
+  "Tiers & layers (top → bottom)": "ティアとレイヤー(上 → 下)",
+  "Every layer belongs to one tier — set it with the tier dropdown; order = 
vertical stacking.": "各レイヤーはいずれか 1 つのティアに属します — 
ティアのドロップダウンで設定します。順序は垂直方向の積み重ねを表します。",
+  "{count} in OAP": "OAP に {count} 件",
+  "{count} unpinned → fallback": "未ピン留め {count} 件 → フォールバック",
+  "filter layers…": "レイヤーを絞り込み…",
+  "+ add tier": "+ ティアを追加",
+  "{count} layers": "{count} レイヤー",
+  "mqe e.g. service_cpm": "mqe 例: service_cpm",
+  "label": "ラベル",
+  "unit": "単位",
+  "remove metric": "メトリックを削除",
+  "+ metric": "+ メトリック",
+  "— unpinned —": "— 未ピン留め —",
+  "Remove from this tier (moves to Unpinned)": "このティアから外す(未ピン留めに移動)",
+  "No layers pinned — use a layer's tier dropdown to move one here.": 
"ピン留めされたレイヤーがありません — レイヤーのティアドロップダウンを使ってここに移動してください。",
+  "Unpinned layers": "未ピン留めのレイヤー",
+  "Pinned to no tier — they land on the failover tier ({failoverTier}). Pick a 
tier to pin one.": 
"どのティアにもピン留めされておらず、フェイルオーバーティア({failoverTier})に配置されます。ティアを選択してピン留めしてください。",
+  "falls to {via}": "{via} に落ちる",
+  "filtered out": "filtered out",
+  "— pin to tier —": "— ティアにピン留め —",
+  "Delete this layer from the config entirely": "このレイヤーを設定から完全に削除",
+  "No unpinned layers.": "未ピン留めのレイヤーはありません。",
+  "Logic groups": "ロジックグループ",
+  "Several layers drawn as one labelled block on a tier (e.g. 
Self-Observability). Each member keeps its own cube color.": "複数のレイヤーを 1 
つのラベル付きブロックとしてティア上に描画します(例: Self-Observability)。各メンバーはそれぞれのキューブの色を保持します。",
+  "+ add group": "+ グループを追加",
+  "level": "レベル",
+  "icon": "アイコン",
+  "member layers ({count})": "メンバーレイヤー({count})",
+  "remove from group": "グループから外す",
+  "+ add layer…": "+ レイヤーを追加…",
+  "A group needs at least one layer — pushing will be rejected until you add 
one.": "グループには少なくとも 1 つのレイヤーが必要です — 追加するまで公開は拒否されます。",
+  "No logic groups. Add one to cluster related layers into a single block.": 
"ロジックグループがありません。追加すると、関連するレイヤーを 1 つのブロックにまとめられます。",
+  "Service-map layers": "サービスマップのレイヤー",
+  "These layers' templates provide a service map, so their cubes render as a 
call graph (and seed the cross-layer hierarchy). Read-only — it's a 
layer-template property, not a 3D-map setting.": 
"これらのレイヤーのテンプレートはサービスマップを提供するため、キューブはコールグラフとして描画されます(レイヤー横断の階層の起点にもなります)。読み取り専用です
 — これは 3D-map の設定ではなく、レイヤーテンプレートのプロパティです。",
+  "None reporting a service map.": "サービスマップを報告しているものはありません。",
+  "Failover tier": "フェイルオーバーティア",
+  "The single catch-all: any layer no tier pins lands here.": 
"唯一の受け皿です。どのティアにもピン留めされていないレイヤーはここに配置されます。",
+  "Push 3D-map config to OAP": "3D-map 設定を OAP に公開",
+  "Left = live on OAP (remote). Right = your local draft. Pushing replaces the 
OAP copy — the map renders it on the next visit.": "左 = OAP 上の公開版(リモート)。右 = 
あなたのローカル草稿。公開すると OAP 上のコピーが置き換えられ、次回アクセス時にマップへ反映されます。",
+  "Push to OAP": "OAP に公開"
 }
diff --git a/apps/ui/src/i18n/locales/ko.json b/apps/ui/src/i18n/locales/ko.json
index d549051..4b1f50b 100644
--- a/apps/ui/src/i18n/locales/ko.json
+++ b/apps/ui/src/i18n/locales/ko.json
@@ -1214,5 +1214,60 @@
   "— pick a currently-running pod, or check that on-demand pod logs are 
enabled on OAP.": "— 실행 중인 Pod를 선택하거나, OAP에서 온디맨드 Pod 로그가 활성화되어 있는지 확인하세요.",
   "Select a service to begin.": "시작하려면 서비스를 선택하세요.",
   "Select a pod to list its containers.": "컨테이너를 나열하려면 Pod를 선택하세요.",
-  "Select a container, then press Start to tail its logs.": "컨테이너를 선택한 다음 시작을 
눌러 로그를 실시간으로 확인하세요."
+  "Select a container, then press Start to tail its logs.": "컨테이너를 선택한 다음 시작을 
눌러 로그를 실시간으로 확인하세요.",
+  "Dashboard setup · 3D Infra Map": "대시보드 설정 · 3D Infra Map",
+  "3D Infrastructure Map": "3D 인프라 맵",
+  "Config for the {mapPath} view, published to OAP. Levels control the 
vertical stack; per-layer color + metrics drive each cube. Edits save to a 
{localDraft}; {checkDiffPush} publishes to OAP — the map renders the remote, 
with bundled defaults as fallback.": "OAP에 게시되는 {mapPath} 화면의 설정입니다. 레벨은 수직 적층 
순서를 정하고, 각 큐브는 레이어별 색상과 메트릭으로 표현됩니다. 편집 내용은 {localDraft}에 저장되며, 
{checkDiffPush}로 OAP에 게시합니다 — 맵은 원격 버전을 렌더링하고 번들 기본값을 폴백으로 사용합니다.",
+  "local draft in this browser": "이 브라우저의 로컬 초안",
+  "editing: {source}": "편집 중: {source}",
+  "Reset to": "재설정 대상",
+  "Bundled equals OAP-live — nothing to reset to.": "번들과 OAP 라이브가 동일 — 재설정할 
대상이 없습니다.",
+  "synced": "동기화됨",
+  "OAP has no copy of this config yet.": "OAP에 아직 이 설정의 복사본이 없습니다.",
+  "Saved local": "로컬에 저장됨",
+  "Save local": "로컬에 저장",
+  "Couldn't load the 3D-map config — the BFF may be unreachable. Refresh the 
page to retry.": "3D-map 설정을 불러오지 못했습니다 — BFF에 연결할 수 없을 수 있습니다. 페이지를 새로 고쳐 다시 
시도하세요.",
+  "Loading config…": "설정을 불러오는 중…",
+  "Global layer filter": "전역 레이어 필터",
+  "The one top-level gate — a layer this JS regex excludes is off the map (it 
still shows below in Unpinned, tagged \"filtered out\"). Default {dotStar} 
admits all.": "최상위 단일 관문입니다 — 이 JS regex가 제외하는 레이어는 맵에서 빠집니다(아래 미고정 목록에는 
\"filtered out\"으로 표시되어 그대로 남습니다). 기본값 {dotStar}은 모두 허용합니다.",
+  "Tiers & layers (top → bottom)": "티어 및 레이어 (위 → 아래)",
+  "Every layer belongs to one tier — set it with the tier dropdown; order = 
vertical stacking.": "모든 레이어는 하나의 티어에 속합니다 — 티어 드롭다운으로 지정하며, 순서가 곧 수직 적층 
순서입니다.",
+  "{count} in OAP": "OAP에 {count}개",
+  "{count} unpinned → fallback": "미고정 {count}개 → 폴백",
+  "filter layers…": "레이어 필터…",
+  "+ add tier": "+ 티어 추가",
+  "{count} layers": "레이어 {count}개",
+  "mqe e.g. service_cpm": "mqe 예: service_cpm",
+  "label": "라벨",
+  "unit": "단위",
+  "remove metric": "메트릭 제거",
+  "+ metric": "+ 메트릭",
+  "— unpinned —": "— 미고정 —",
+  "Remove from this tier (moves to Unpinned)": "이 티어에서 제거 (미고정으로 이동)",
+  "No layers pinned — use a layer's tier dropdown to move one here.": "고정된 
레이어가 없습니다 — 레이어의 티어 드롭다운으로 여기로 옮기세요.",
+  "Unpinned layers": "미고정 레이어",
+  "Pinned to no tier — they land on the failover tier ({failoverTier}). Pick a 
tier to pin one.": "어느 티어에도 고정되지 않아 폴백 티어({failoverTier})에 배치됩니다. 고정하려면 티어를 
선택하세요.",
+  "falls to {via}": "{via}(으)로 폴백",
+  "filtered out": "필터로 제외됨",
+  "— pin to tier —": "— 티어에 고정 —",
+  "Delete this layer from the config entirely": "이 레이어를 설정에서 완전히 삭제",
+  "No unpinned layers.": "미고정 레이어가 없습니다.",
+  "Logic groups": "로직 그룹",
+  "Several layers drawn as one labelled block on a tier (e.g. 
Self-Observability). Each member keeps its own cube color.": "여러 레이어를 티어 위에 라벨이 
붙은 하나의 블록으로 그립니다(예: Self-Observability). 각 멤버는 자신의 큐브 색상을 그대로 유지합니다.",
+  "+ add group": "+ 그룹 추가",
+  "level": "레벨",
+  "icon": "아이콘",
+  "member layers ({count})": "멤버 레이어 ({count}개)",
+  "remove from group": "그룹에서 제거",
+  "+ add layer…": "+ 레이어 추가…",
+  "A group needs at least one layer — pushing will be rejected until you add 
one.": "그룹에는 레이어가 최소 하나 필요합니다 — 하나를 추가하기 전까지 게시가 거부됩니다.",
+  "No logic groups. Add one to cluster related layers into a single block.": 
"로직 그룹이 없습니다. 관련 레이어를 하나의 블록으로 묶으려면 추가하세요.",
+  "Service-map layers": "서비스 맵 레이어",
+  "These layers' templates provide a service map, so their cubes render as a 
call graph (and seed the cross-layer hierarchy). Read-only — it's a 
layer-template property, not a 3D-map setting.": "이 레이어들의 템플릿은 서비스 맵을 제공하므로, 해당 
큐브는 호출 그래프로 렌더링되고 레이어 간 계층 구조의 기반이 됩니다. 읽기 전용입니다 — 이는 레이어 템플릿의 속성이며 3D-map 설정이 
아닙니다.",
+  "None reporting a service map.": "서비스 맵을 보고하는 레이어가 없습니다.",
+  "Failover tier": "폴백 티어",
+  "The single catch-all: any layer no tier pins lands here.": "단일 포괄 티어입니다: 어느 
티어에도 고정되지 않은 레이어가 모두 여기에 배치됩니다.",
+  "Push 3D-map config to OAP": "3D-map 설정을 OAP에 게시",
+  "Left = live on OAP (remote). Right = your local draft. Pushing replaces the 
OAP copy — the map renders it on the next visit.": "왼쪽 = OAP 라이브(원격). 오른쪽 = 내 
로컬 초안. 게시하면 OAP 복사본을 교체하며, 다음 방문 시 맵이 이를 렌더링합니다.",
+  "Push to OAP": "OAP에 게시"
 }
diff --git a/apps/ui/src/i18n/locales/pt.json b/apps/ui/src/i18n/locales/pt.json
index 007a655..7228384 100644
--- a/apps/ui/src/i18n/locales/pt.json
+++ b/apps/ui/src/i18n/locales/pt.json
@@ -1214,5 +1214,60 @@
   "— pick a currently-running pod, or check that on-demand pod logs are 
enabled on OAP.": "— escolha um Pod em execução, ou verifique se os logs de Pod 
sob demanda estão habilitados no OAP.",
   "Select a service to begin.": "Selecione um serviço para começar.",
   "Select a pod to list its containers.": "Selecione um Pod para listar seus 
contêineres.",
-  "Select a container, then press Start to tail its logs.": "Selecione um 
contêiner e pressione Início para acompanhar seus logs ao vivo."
+  "Select a container, then press Start to tail its logs.": "Selecione um 
contêiner e pressione Início para acompanhar seus logs ao vivo.",
+  "Dashboard setup · 3D Infra Map": "Configuração de painéis · 3D Infra Map",
+  "3D Infrastructure Map": "3D Infrastructure Map",
+  "Config for the {mapPath} view, published to OAP. Levels control the 
vertical stack; per-layer color + metrics drive each cube. Edits save to a 
{localDraft}; {checkDiffPush} publishes to OAP — the map renders the remote, 
with bundled defaults as fallback.": "Configuração da visão {mapPath}, 
publicada no OAP. Os níveis controlam o empilhamento vertical; a cor e as 
métricas de cada camada definem cada cubo. As edições são salvas em um 
{localDraft}; {checkDiffPush} publica no OAP — o mapa [...]
+  "local draft in this browser": "rascunho local neste navegador",
+  "editing: {source}": "editando: {source}",
+  "Reset to": "Redefinir para",
+  "Bundled equals OAP-live — nothing to reset to.": "Bundled coincide com a 
versão em produção do OAP — nada para redefinir.",
+  "synced": "sincronizado",
+  "OAP has no copy of this config yet.": "O OAP ainda não tem uma cópia desta 
configuração.",
+  "Saved local": "Salvo localmente",
+  "Save local": "Salvar localmente",
+  "Couldn't load the 3D-map config — the BFF may be unreachable. Refresh the 
page to retry.": "Não foi possível carregar a configuração do 3D-map — o BFF 
pode estar indisponível. Atualize a página para tentar de novo.",
+  "Loading config…": "Carregando a configuração…",
+  "Global layer filter": "Filtro global de camadas",
+  "The one top-level gate — a layer this JS regex excludes is off the map (it 
still shows below in Unpinned, tagged \"filtered out\"). Default {dotStar} 
admits all.": "O único filtro de nível superior — uma camada excluída por este 
regex JS fica fora do mapa (ainda aparece abaixo em Não fixadas, marcada como 
\"filtered out\"). O padrão {dotStar} admite todas.",
+  "Tiers & layers (top → bottom)": "Níveis e camadas (topo → base)",
+  "Every layer belongs to one tier — set it with the tier dropdown; order = 
vertical stacking.": "Cada camada pertence a um nível — defina-o pela lista 
suspensa de nível; a ordem = empilhamento vertical.",
+  "{count} in OAP": "{count} no OAP",
+  "{count} unpinned → fallback": "{count} não fixadas → fallback",
+  "filter layers…": "filtrar camadas…",
+  "+ add tier": "+ adicionar nível",
+  "{count} layers": "{count} camadas",
+  "mqe e.g. service_cpm": "mqe ex.: service_cpm",
+  "label": "rótulo",
+  "unit": "unidade",
+  "remove metric": "remover métrica",
+  "+ metric": "+ métrica",
+  "— unpinned —": "— não fixadas —",
+  "Remove from this tier (moves to Unpinned)": "Remover deste nível (move para 
Não fixadas)",
+  "No layers pinned — use a layer's tier dropdown to move one here.": "Nenhuma 
camada fixada — use a lista de nível de uma camada para mover uma para cá.",
+  "Unpinned layers": "Camadas não fixadas",
+  "Pinned to no tier — they land on the failover tier ({failoverTier}). Pick a 
tier to pin one.": "Sem nível fixado — caem no nível de failover 
({failoverTier}). Escolha um nível para fixar uma delas.",
+  "falls to {via}": "cai para {via}",
+  "filtered out": "filtered out",
+  "— pin to tier —": "— fixar em nível —",
+  "Delete this layer from the config entirely": "Excluir esta camada 
completamente da configuração",
+  "No unpinned layers.": "Nenhuma camada não fixada.",
+  "Logic groups": "Grupos lógicos",
+  "Several layers drawn as one labelled block on a tier (e.g. 
Self-Observability). Each member keeps its own cube color.": "Várias camadas 
desenhadas como um único bloco rotulado em um nível (p.ex. Self-Observability). 
Cada membro mantém a cor do próprio cubo.",
+  "+ add group": "+ adicionar grupo",
+  "level": "nível",
+  "icon": "ícone",
+  "member layers ({count})": "camadas membro ({count})",
+  "remove from group": "remover do grupo",
+  "+ add layer…": "+ adicionar camada…",
+  "A group needs at least one layer — pushing will be rejected until you add 
one.": "Um grupo precisa de pelo menos uma camada — a publicação será rejeitada 
até você adicionar uma.",
+  "No logic groups. Add one to cluster related layers into a single block.": 
"Nenhum grupo lógico. Adicione um para agrupar camadas relacionadas em um único 
bloco.",
+  "Service-map layers": "Camadas com mapa de serviços",
+  "These layers' templates provide a service map, so their cubes render as a 
call graph (and seed the cross-layer hierarchy). Read-only — it's a 
layer-template property, not a 3D-map setting.": "Os templates destas camadas 
fornecem um mapa de serviços, então seus cubos são renderizados como um grafo 
de chamadas (e alimentam a hierarquia entre camadas). Somente leitura — é uma 
propriedade do template da camada, não uma configuração do 3D-map.",
+  "None reporting a service map.": "Nenhuma reportando um mapa de serviços.",
+  "Failover tier": "Nível de failover",
+  "The single catch-all: any layer no tier pins lands here.": "O único nível 
coringa: toda camada que nenhum nível fixa cai aqui.",
+  "Push 3D-map config to OAP": "Publicar a configuração do 3D-map no OAP",
+  "Left = live on OAP (remote). Right = your local draft. Pushing replaces the 
OAP copy — the map renders it on the next visit.": "Esquerda = em produção no 
OAP (remote). Direita = seu rascunho local. Publicar substitui a cópia do OAP — 
o mapa passa a renderizá-la na próxima visita.",
+  "Push to OAP": "Publicar no OAP"
 }
diff --git a/apps/ui/src/i18n/locales/zh-CN.json 
b/apps/ui/src/i18n/locales/zh-CN.json
index 883551a..9ba66c1 100644
--- a/apps/ui/src/i18n/locales/zh-CN.json
+++ b/apps/ui/src/i18n/locales/zh-CN.json
@@ -1214,5 +1214,60 @@
   "— pick a currently-running pod, or check that on-demand pod logs are 
enabled on OAP.": "— 请选择一个正在运行的 Pod,或确认 OAP 已启用按需 Pod 日志。",
   "Select a service to begin.": "请选择服务以开始。",
   "Select a pod to list its containers.": "选择一个 Pod 以列出其容器。",
-  "Select a container, then press Start to tail its logs.": 
"选择一个容器,然后点击“开始”以实时查看其日志。"
+  "Select a container, then press Start to tail its logs.": 
"选择一个容器,然后点击“开始”以实时查看其日志。",
+  "Dashboard setup · 3D Infra Map": "仪表板设置 · 3D Infra Map",
+  "3D Infrastructure Map": "3D 基础设施地图",
+  "Config for the {mapPath} view, published to OAP. Levels control the 
vertical stack; per-layer color + metrics drive each cube. Edits save to a 
{localDraft}; {checkDiffPush} publishes to OAP — the map renders the remote, 
with bundled defaults as fallback.": "{mapPath} 视图的配置,发布到 
OAP。层级(level)控制垂直堆叠;每个 layer 
的颜色与指标决定对应方块的呈现。修改会保存到{localDraft};{checkDiffPush}将其发布到 OAP —— 
地图渲染的是远端版本,内置默认值作为兜底。",
+  "local draft in this browser": "当前浏览器中的本地草稿",
+  "editing: {source}": "正在编辑:{source}",
+  "Reset to": "重置为",
+  "Bundled equals OAP-live — nothing to reset to.": "内置版与 OAP 实时版本一致 —— 无需重置。",
+  "synced": "已同步",
+  "OAP has no copy of this config yet.": "OAP 还没有此配置的副本。",
+  "Saved local": "已保存到本地",
+  "Save local": "保存到本地",
+  "Couldn't load the 3D-map config — the BFF may be unreachable. Refresh the 
page to retry.": "无法加载 3D-map 配置 —— BFF 可能不可达。请刷新页面重试。",
+  "Loading config…": "正在加载配置…",
+  "Global layer filter": "全局 layer 过滤",
+  "The one top-level gate — a layer this JS regex excludes is off the map (it 
still shows below in Unpinned, tagged \"filtered out\"). Default {dotStar} 
admits all.": "唯一的顶级开关 —— 被这段 JS regex 排除的 layer 
不会出现在地图上(它仍会显示在下方的“未固定”中,并标记为“filtered out”)。默认 {dotStar} 放行所有 layer。",
+  "Tiers & layers (top → bottom)": "层组与 layer(从上到下)",
+  "Every layer belongs to one tier — set it with the tier dropdown; order = 
vertical stacking.": "每个 layer 都归属于一个层组 —— 通过层组下拉框设置;顺序即垂直堆叠次序。",
+  "{count} in OAP": "OAP 中 {count} 个",
+  "{count} unpinned → fallback": "{count} 个未固定 → 兜底",
+  "filter layers…": "过滤 layer…",
+  "+ add tier": "+ 添加层组",
+  "{count} layers": "{count} 个 layer",
+  "mqe e.g. service_cpm": "mqe,例如 service_cpm",
+  "label": "标签",
+  "unit": "单位",
+  "remove metric": "移除指标",
+  "+ metric": "+ 指标",
+  "— unpinned —": "— 未固定 —",
+  "Remove from this tier (moves to Unpinned)": "从该层组移除(移至“未固定”)",
+  "No layers pinned — use a layer's tier dropdown to move one here.": "尚无固定的 
layer —— 使用某个 layer 的层组下拉框将其移到这里。",
+  "Unpinned layers": "未固定的 layer",
+  "Pinned to no tier — they land on the failover tier ({failoverTier}). Pick a 
tier to pin one.": "未固定到任何层组 —— 它们会落到兜底层组({failoverTier})。选择一个层组即可固定。",
+  "falls to {via}": "落到 {via}",
+  "filtered out": "已过滤",
+  "— pin to tier —": "— 固定到层组 —",
+  "Delete this layer from the config entirely": "将该 layer 从配置中彻底删除",
+  "No unpinned layers.": "没有未固定的 layer。",
+  "Logic groups": "逻辑分组",
+  "Several layers drawn as one labelled block on a tier (e.g. 
Self-Observability). Each member keeps its own cube color.": "将多个 layer 
在某个层组上绘制为一个带标签的整块(例如 Self-Observability)。每个成员仍保留各自方块的颜色。",
+  "+ add group": "+ 添加分组",
+  "level": "层级",
+  "icon": "图标",
+  "member layers ({count})": "成员 layer({count})",
+  "remove from group": "从分组移除",
+  "+ add layer…": "+ 添加 layer…",
+  "A group needs at least one layer — pushing will be rejected until you add 
one.": "分组至少需要一个 layer —— 添加之前推送会被拒绝。",
+  "No logic groups. Add one to cluster related layers into a single block.": 
"暂无逻辑分组。添加一个,即可将相关 layer 聚合为同一整块。",
+  "Service-map layers": "服务地图 layer",
+  "These layers' templates provide a service map, so their cubes render as a 
call graph (and seed the cross-layer hierarchy). Read-only — it's a 
layer-template property, not a 3D-map setting.": "这些 layer 
的模板提供服务地图,因此其方块会渲染为调用关系图(并作为跨 layer 层级关系的起点)。只读 —— 这是 layer 模板的属性,而非 3D-map 
的设置。",
+  "None reporting a service map.": "没有 layer 上报服务地图。",
+  "Failover tier": "兜底层组",
+  "The single catch-all: any layer no tier pins lands here.": 
"唯一的兜底层组:任何未被固定到具体层组的 layer 都会落到这里。",
+  "Push 3D-map config to OAP": "将 3D-map 配置推送至 OAP",
+  "Left = live on OAP (remote). Right = your local draft. Pushing replaces the 
OAP copy — the map renders it on the next visit.": "左侧 = OAP 上的线上版本(远端)。右侧 = 
你的本地草稿。推送会替换 OAP 上的副本 —— 下次访问时地图便以其渲染。",
+  "Push to OAP": "推送至 OAP"
 }

Reply via email to