@azure-id/orc 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,119 @@
1
+ {
2
+ "hookui.title": "Antarmuka CLI Hook",
3
+ "hookui.sub": "Bangun baris status ORC. Tiga baris. Tiap baris memuat 1 sampai 5 bagian.",
4
+ "hookui.noData": "Baris status tidak dapat dibaca.",
5
+ "hookui.gate": "Status",
6
+ "hookui.on": "hidup",
7
+ "hookui.off": "mati",
8
+ "hookui.saved": "tata letak tersimpan",
9
+ "hookui.gateOnNote": "Baris Anda yang tampil. Dua baris bawaan ORC dimatikan.",
10
+ "hookui.gateOffNote": "ORC menampilkan dua baris bawaannya. Bangun tata letak di bawah, lalu aktifkan ini. Tidak ada yang berubah sampai Anda melakukannya.",
11
+ "hookui.turnOn": "Aktifkan",
12
+ "hookui.turnOff": "Nonaktifkan",
13
+ "hookui.switchFailed": "Saklar tidak dapat diubah.",
14
+ "hookui.cautions": "Peringatan",
15
+ "hookui.preview": "Pratinjau",
16
+ "hookui.previewUnavailable": "Pratinjau tidak dapat digambar sampai tata letak sah.",
17
+ "hookui.degrades": "Tata letak yang sama, dalam tiga bentuk yang dapat dipaksakan terminal.",
18
+ "hookui.degNoColor": "tanpa warna",
19
+ "hookui.degAscii": "teks polos",
20
+ "hookui.degNoMotion": "tanpa gerak",
21
+ "hookui.cells": "lebar {n} sel",
22
+ "hookui.cellsPlural": "lebar {n} sel",
23
+ "hookui.board": "Papan",
24
+ "hookui.lineN": "Baris {n}",
25
+ "hookui.lineNPlural": "Baris {n}",
26
+ "hookui.add": "+ tambah bagian",
27
+ "hookui.addTo": "+ baris {n}",
28
+ "hookui.addToPlural": "+ baris {n}",
29
+ "hookui.toLine": "baris {n}",
30
+ "hookui.toLinePlural": "baris {n}",
31
+ "hookui.fillFirst": "Isi baris {n} dulu.",
32
+ "hookui.lineFull": "Baris {n} sudah penuh.",
33
+ "hookui.separator": "Antar bagian",
34
+ "hookui.sepChange": "pemisah baris {n} = \"{s}\"",
35
+ "hookui.moveLeft": "Pindah ke kiri",
36
+ "hookui.moveRight": "Pindah ke kanan",
37
+ "hookui.edit": "Ubah bagian ini",
38
+ "hookui.remove": "Lepas bagian ini",
39
+ "hookui.removed": "{id} dilepas",
40
+ "hookui.moved": "{id} pindah ke baris {n}",
41
+ "hookui.added": "{id} ditambahkan ke baris {n}",
42
+ "hookui.unknownComponent": "Versi ORC ini tidak punya bagian tersebut.",
43
+ "hookui.shape": "Bentuk",
44
+ "hookui.shapeChange": "bentuk {id} = {r}",
45
+ "hookui.glyphs": "Simbol",
46
+ "hookui.glyphChange": "simbol {id} = {v}",
47
+ "hookui.words": "Kata",
48
+ "hookui.label": "Nama",
49
+ "hookui.labelPlaceholder": "ketik nama apa saja",
50
+ "hookui.labelChange": "nama {id} = \"{v}\"",
51
+ "hookui.case": "Huruf",
52
+ "hookui.caseChange": "huruf {id} = {v}",
53
+ "hookui.colour": "Warna",
54
+ "hookui.labelColour": "Warna nama",
55
+ "hookui.valueColour": "Warna nilai",
56
+ "hookui.colourChange": "warna {id} = {v}",
57
+ "hookui.ramp": "Warna menurut nilai",
58
+ "hookui.rampChange": "warna menurut nilai {id} = {v}",
59
+ "hookui.stateColourWarn": "Bagian ini menyampaikan sesuatu lewat warnanya ({s}). Warna tetap menyembunyikan itu.",
60
+ "hookui.emphasis": "Ketebalan",
61
+ "hookui.emphasisChange": "ketebalan {id} = {v}",
62
+ "hookui.fontWhy": "Terminal Anda yang menentukan ukuran huruf. ORC tidak dapat mengubahnya. Tebal terlihat lebih besar dan redup terlihat lebih kecil. Agar sebuah bagian memakai ruang lebih, beri lebih banyak sel.",
63
+ "hookui.numbers": "Angka",
64
+ "hookui.format": "Bentuk",
65
+ "hookui.formatChange": "bentuk {id} = {v}",
66
+ "hookui.compact": "Bentuk singkat",
67
+ "hookui.compactChange": "bentuk singkat {id} = {v}",
68
+ "hookui.minWidthWarn": "Angka yang berubah lebar mendorong semua yang di kanannya. Tetapkan lebar minimum untuk mencegahnya.",
69
+ "hookui.whenToShow": "Kapan ditampilkan",
70
+ "hookui.hideChange": "sembunyikan {id} saat = {v}",
71
+ "hookui.explain": "Asal nilai ini",
72
+ "hookui.explainFailed": "Itu tidak dapat dibaca sekarang.",
73
+ "hookui.palette": "Bagian yang dapat ditambahkan",
74
+ "hookui.searchPlaceholder": "cari",
75
+ "hookui.matches": "{n} cocok",
76
+ "hookui.matchesPlural": "{n} cocok",
77
+ "hookui.noMatch": "Tidak ada yang cocok.",
78
+ "hookui.presets": "Tata letak siap pakai",
79
+ "hookui.active": "dipakai",
80
+ "hookui.apply": "Pakai ini",
81
+ "hookui.applyTitle": "Pakai \"{name}\"?",
82
+ "hookui.applyWarn": "Ini MENGGANTI seluruh tata letak Anda. Setiap bagian yang Anda pasang dilepas dan diganti yang siap pakai. Ini tidak dapat dibatalkan.",
83
+ "hookui.applyFailed": "Tata letak itu tidak dapat dipakai.",
84
+ "hookui.resetLayout": "Mulai lagi",
85
+ "hookui.resetTitle": "Mulai lagi?",
86
+ "hookui.resetWarn": "Ini MENGGANTI tata letak Anda dengan bawaan ORC. Setiap bagian yang Anda pasang dilepas. Ini tidak dapat dibatalkan.",
87
+ "hookui.advanced": "Lainnya",
88
+ "hookui.theme": "Set warna",
89
+ "hookui.themeChange": "set warna = {v}",
90
+ "hookui.recompile": "Bangun ulang",
91
+ "hookui.recompiled": "Sudah dibangun ulang.",
92
+ "hookui.recompileFailed": "Pembangunan ulang gagal.",
93
+ "hookui.compileNote": "ORC membangun ulang tata letak setelah setiap perubahan, jadi Anda tidak perlu. Tombol ini untuk setelah Anda memperbarui ORC.",
94
+ "hookui.clone": "Salin",
95
+ "hookui.cloned": "{id} disalin",
96
+ "hookui.expand": "Pecah",
97
+ "hookui.expanded": "{id} dipecah jadi bagiannya",
98
+ "hookui.minWidth": "Lebar minimal",
99
+ "hookui.minWidthChange": "lebar minimal {id} = {v}",
100
+ "hookui.precision": "Desimal",
101
+ "hookui.precisionChange": "desimal {id} = {v}",
102
+ "hookui.width": "Sel",
103
+ "hookui.widthChange": "sel {id} = {v}",
104
+ "hookui.widthWhy": "Lebih banyak sel adalah satu-satunya cara membesar di terminal.",
105
+ "hookui.before": "Sebelum",
106
+ "hookui.beforeChange": "sebelum {id} = \"{v}\"",
107
+ "hookui.after": "Sesudah",
108
+ "hookui.afterChange": "sesudah {id} = \"{v}\"",
109
+ "hookui.narrow": "Di terminal sempit",
110
+ "hookui.minCols": "Tampil di atas",
111
+ "hookui.minColsChange": "{id} tampil di atas {v} kolom",
112
+ "hookui.priority": "Urutan dilepas",
113
+ "hookui.priorityChange": "urutan dilepas {id} = {v}",
114
+ "hookui.priorityWhy": "Saat baris tidak muat, 1 dilepas dulu dan 5 terakhir. Anda yang memilih apa yang bertahan, bukan tepi kanan.",
115
+ "hookui.boardStatus": "Baris status",
116
+ "hookui.boardSubagent": "Satu baris per agen",
117
+ "hookui.boardStatusSub": "Dua baris di bawah terminal Anda.",
118
+ "hookui.boardSubagentSub": "Baris yang digambar Claude Code untuk tiap subagen di panel agen. Satu baris, dan menunjukkan agen itu berjalan di model apa dan sudah memakai berapa."
119
+ }
@@ -1,21 +1,22 @@
1
- {
2
- "nav.overview": "Ringkasan",
3
- "nav.settings": "Pengaturan",
4
- "nav.lanes": "Lane",
5
- "nav.runs": "Run",
6
- "nav.knowledge": "Pengetahuan",
7
- "nav.stats": "Statistik",
8
- "nav.flow": "Flow",
9
- "nav.crosslink": "Crosslink",
10
- "nav.learn": "Belajar",
11
- "nav.mockrun": "Contoh Penggunaan Skill",
12
- "nav.experiment": "Eksperimen",
13
- "nav.maintenance": "Pemeliharaan",
14
- "nav.pact": "Janji",
15
- "nav.boundary": "Batas",
16
- "nav.handoff": "Ubah Sendiri",
17
- "nav.challenge": "Challenge",
18
- "nav.extra": "Extra",
19
- "nav.wait": "Tunggu",
20
- "nav.docs": "Dokumen"
21
- }
1
+ {
2
+ "nav.overview": "Ringkasan",
3
+ "nav.settings": "Pengaturan",
4
+ "nav.lanes": "Lane",
5
+ "nav.runs": "Run",
6
+ "nav.knowledge": "Pengetahuan",
7
+ "nav.stats": "Statistik",
8
+ "nav.flow": "Flow",
9
+ "nav.crosslink": "Crosslink",
10
+ "nav.learn": "Belajar",
11
+ "nav.mockrun": "Contoh Penggunaan Skill",
12
+ "nav.experiment": "Eksperimen",
13
+ "nav.maintenance": "Pemeliharaan",
14
+ "nav.pact": "Janji",
15
+ "nav.boundary": "Batas",
16
+ "nav.handoff": "Ubah Sendiri",
17
+ "nav.challenge": "Challenge",
18
+ "nav.extra": "Extra",
19
+ "nav.wait": "Tunggu",
20
+ "nav.docs": "Dokumen",
21
+ "nav.hookui": "Antarmuka CLI Hook"
22
+ }
@@ -88,5 +88,8 @@
88
88
  "overview.extraOrphan.chip": "{n} pengiriman",
89
89
  "overview.extraOrphan.chipPlural": "{n} pengiriman",
90
90
  "overview.extraOrphan.slug": "tidak pernah melapor kembali",
91
- "overview.extraOrphan.where": "Extra · Pemulihan"
91
+ "overview.extraOrphan.where": "Extra · Pemulihan",
92
+ "overview.item.statuslineOrphaned.cta": "Buka Antarmuka CLI Hook",
93
+ "overview.item.statuslineInvalid.cta": "Buka Antarmuka CLI Hook",
94
+ "overview.item.statuslineUnreadable.cta": "Buka Antarmuka CLI Hook"
92
95
  }
@@ -29,5 +29,7 @@
29
29
  "tour.13.title": "Dokumen panjang, dalam satu gambar",
30
30
  "tour.13.text": "Setiap blok adalah satu bagian, seukuran panjangnya dan diberi warna sesuai keadaannya. Tidak ada yang memuat dokumen itu sendiri di sini - ORC bekerja dari peta ini, dan itulah sebabnya dokumen panjang tidak menghabiskan sesi.",
31
31
  "tour.14.title": "Pekerjaan yang berjalan di tempat lain",
32
- "tour.14.text": "Satu garis dari nol sampai seratus: seberapa berat skor sebuah tugas. Setiap bagiannya menunjukkan di mana pekerjaan itu benar-benar berjalan — Claude berwarna biru, penyedia yang Anda hubungkan berwarna hijau. Rentang yang belum Anda hubungkan tetap punya tempatnya, jadi \"saya sengaja meninggalkan pekerjaan berat di Claude\" tidak pernah terlihat seperti \"tidak ada pekerjaan berat\". Tidak ada yang keluar dari mesin ini sampai Anda menghubungkan sesuatu, mengujinya, dan menekan Apply."
32
+ "tour.14.text": "Satu garis dari nol sampai seratus: seberapa berat skor sebuah tugas. Setiap bagiannya menunjukkan di mana pekerjaan itu benar-benar berjalan — Claude berwarna biru, penyedia yang Anda hubungkan berwarna hijau. Rentang yang belum Anda hubungkan tetap punya tempatnya, jadi \"saya sengaja meninggalkan pekerjaan berat di Claude\" tidak pernah terlihat seperti \"tidak ada pekerjaan berat\". Tidak ada yang keluar dari mesin ini sampai Anda menghubungkan sesuatu, mengujinya, dan menekan Apply.",
33
+ "tour.15.title": "Bangun baris status Anda sendiri",
34
+ "tour.15.text": "Tiga baris, tiap baris memuat sampai lima bagian. Baris 1 harus terisi sebelum baris 2 bisa. Pratinjau di atas persis seperti yang akan tampil di terminal Anda - digambar oleh kode yang sama dengan yang dijalankan bilah itu."
33
35
  }
@@ -1,151 +1,152 @@
1
- "use strict";
2
- /* 01-i18n.js — orc ui client
3
- LANGS, loadLang, setLang, t, applyStaticText, cycleLang.
4
-
5
- Keys are written out IN FULL, never assembled from a fragment. The v0.48.1
6
- file split is by key PREFIX only; `docs.ship.confirm` is still spelled
7
- `docs.ship.confirm` at every call site.
8
-
9
- Loaded by app.html in the order its numeric prefix names. Classic
10
- script, no import/export: an ES module import carries no query string,
11
- and every static request here needs the per-launch session token. */
12
-
13
- /* ------------------------------------------------------------------- i18n -- */
14
- /*
15
- THE SCOPE RULE, and it is deliberately narrow: only the panel's OWN prose is
16
- translated. Everything that arrives from `bin/cli.js --json` stays exactly as
17
- the CLI wrote it — config keys and their descriptions, values, agent names,
18
- model ids, file paths, commands, `orc doctor` messages, command output. Those
19
- are identifiers, not sentences: a translated config key is a key that does not
20
- exist, and a translated command is a command you cannot type. So `t()` is
21
- never applied to a value read out of a payload, and neither string table
22
- contains a single ORC key name.
23
-
24
- The tables are plain JSON served as static assets. Adding a language is a
25
- FOLDER in i18n/ plus one row in LANGS — no build step, no library, no
26
- dependency, and no server change (serve.js walks i18n/ at boot).
27
- */
28
- const LANGS = [
29
- { code: "en", label: "English" },
30
- { code: "id", label: "Bahasa Indonesia" },
31
- ];
32
- const LANG_KEY = "orc-ui-lang";
33
-
34
- // v0.48.1 — one file per namespace instead of one 53 KB table, so changing the
35
- // Docs panel's wording means opening i18n/en/docs.json rather than paging
36
- // through every string in the panel.
37
- //
38
- // A plain array, and deliberately NOT an index file to fetch: an index would be
39
- // one more round trip and one more thing to forget to update. A namespace owns
40
- // one or more key PREFIXES (`stats` owns `stats.*` and `cost.*`), which the
41
- // test suite asserts — a key whose prefix belongs to no namespace would load
42
- // nowhere and render as a raw dotted string.
43
- const NAMESPACES = [
44
- "common",
45
- "nav",
46
- "banner",
47
- "overview",
48
- "settings",
49
- "lanes",
50
- "runs",
51
- "knowledge",
52
- "stats",
53
- "flow",
54
- "crosslink",
55
- "learn",
56
- "mockrun",
57
- "maintenance",
58
- "pact",
59
- "boundary",
60
- "wait",
61
- "handoff",
62
- "challenge",
63
- "docs",
64
- "extra",
65
- "experiment",
66
- "tour",
67
- ];
68
-
69
- let lang = "en";
70
- // English is loaded first and kept as the FALLBACK table: a key the other
71
- // language has not translated yet renders in English rather than as a raw
72
- // dotted key. A half-finished translation degrades to mixed prose, never to
73
- // debug output on somebody's screen.
74
- let DICT = {};
75
- let DICT_EN = {};
76
-
77
- // The namespaces are fetched in parallel and merged into ONE flat table, so
78
- // `t()` is unchanged and no call site knows the split happened.
79
- //
80
- // A namespace that fails to load falls back to ENGLISH for its keys rather than
81
- // rejecting the whole language: a network hiccup on one file should cost that
82
- // file's prose, not the entire panel. English itself has no fallback below it,
83
- // so a failure there rejects and boot() lands on raw keys ugly, never blank.
84
- function loadLang(code) {
85
- return Promise.all(
86
- NAMESPACES.map((ns) =>
87
- fetch(`/i18n/${code}/${ns}.json`, { headers: { "x-orc-token": TOKEN }, cache: "no-store" })
88
- .then((r) => {
89
- if (!r.ok) throw new Error(`i18n ${code}/${ns} ${r.status}`);
90
- return r.json();
91
- })
92
- .catch((e) => {
93
- if (code === "en") throw e;
94
- return null;
95
- })
96
- )
97
- ).then((parts) => Object.assign({}, ...parts.filter(Boolean)));
98
- }
99
-
100
- // A missing or broken table must never blank the UI. Every failure path here
101
- // lands on English, so the worst case is untranslated text never empty text.
102
- async function setLang(code, opts) {
103
- const rerender = !opts || opts.rerender !== false;
104
- const want = LANGS.some((l) => l.code === code) ? code : "en";
105
- try {
106
- DICT = want === "en" ? DICT_EN : await loadLang(want);
107
- lang = want;
108
- } catch (_) {
109
- DICT = DICT_EN;
110
- lang = "en";
111
- }
112
- try {
113
- localStorage.setItem(LANG_KEY, lang);
114
- } catch (_) {}
115
- document.documentElement.setAttribute("lang", lang);
116
- applyStaticText();
117
- if (rerender) route();
118
- }
119
-
120
- function t(key, vars) {
121
- let s = DICT[key];
122
- if (s === undefined) s = DICT_EN[key];
123
- if (s === undefined) s = key;
124
- if (vars) for (const k of Object.keys(vars)) s = s.split("{" + k + "}").join(String(vars[k]));
125
- return s;
126
- }
127
-
128
- // English has one plural form and Indonesian has none, so count-aware lookup is
129
- // a second key rather than a plural-rules engine: `n === 1` picks the singular.
130
- const tn = (n, key, vars) => t(n === 1 ? key : key + "Plural", Object.assign({ n }, vars || {}));
131
-
132
- // The parts of the shell that are markup rather than a render function. Called
133
- // on every language change; panels re-render themselves through `route()`.
134
- function applyStaticText() {
135
- for (const node of document.querySelectorAll("[data-i18n]")) node.textContent = t(node.dataset.i18n);
136
- const tt = $("#theme-toggle");
137
- if (tt) tt.textContent = document.documentElement.getAttribute("data-theme") === "light" ? t("rail.dark") : t("rail.light");
138
- const hint = $("#shortcut-hint");
139
- if (hint) hint.title = t("rail.shortcuts");
140
- const label = $("#lang-label");
141
- if (label) label.textContent = (LANGS.find((l) => l.code === lang) || LANGS[0]).label;
142
- const btn = $("#lang-toggle");
143
- if (btn) btn.title = t("lang.title");
144
- }
145
-
146
- // Two languages, so the control is a TOGGLE rather than a menu: one click, no
147
- // dropdown to open. A third language would make this a <select>; two do not.
148
- function cycleLang() {
149
- const i = LANGS.findIndex((l) => l.code === lang);
150
- setLang(LANGS[(i + 1) % LANGS.length].code);
151
- }
1
+ "use strict";
2
+ /* 01-i18n.js — orc ui client
3
+ LANGS, loadLang, setLang, t, applyStaticText, cycleLang.
4
+
5
+ Keys are written out IN FULL, never assembled from a fragment. The v0.48.1
6
+ file split is by key PREFIX only; `docs.ship.confirm` is still spelled
7
+ `docs.ship.confirm` at every call site.
8
+
9
+ Loaded by app.html in the order its numeric prefix names. Classic
10
+ script, no import/export: an ES module import carries no query string,
11
+ and every static request here needs the per-launch session token. */
12
+
13
+ /* ------------------------------------------------------------------- i18n -- */
14
+ /*
15
+ THE SCOPE RULE, and it is deliberately narrow: only the panel's OWN prose is
16
+ translated. Everything that arrives from `bin/cli.js --json` stays exactly as
17
+ the CLI wrote it — config keys and their descriptions, values, agent names,
18
+ model ids, file paths, commands, `orc doctor` messages, command output. Those
19
+ are identifiers, not sentences: a translated config key is a key that does not
20
+ exist, and a translated command is a command you cannot type. So `t()` is
21
+ never applied to a value read out of a payload, and neither string table
22
+ contains a single ORC key name.
23
+
24
+ The tables are plain JSON served as static assets. Adding a language is a
25
+ FOLDER in i18n/ plus one row in LANGS — no build step, no library, no
26
+ dependency, and no server change (serve.js walks i18n/ at boot).
27
+ */
28
+ const LANGS = [
29
+ { code: "en", label: "English" },
30
+ { code: "id", label: "Bahasa Indonesia" },
31
+ ];
32
+ const LANG_KEY = "orc-ui-lang";
33
+
34
+ // v0.48.1 — one file per namespace instead of one 53 KB table, so changing the
35
+ // Docs panel's wording means opening i18n/en/docs.json rather than paging
36
+ // through every string in the panel.
37
+ //
38
+ // A plain array, and deliberately NOT an index file to fetch: an index would be
39
+ // one more round trip and one more thing to forget to update. A namespace owns
40
+ // one or more key PREFIXES (`stats` owns `stats.*` and `cost.*`), which the
41
+ // test suite asserts — a key whose prefix belongs to no namespace would load
42
+ // nowhere and render as a raw dotted string.
43
+ const NAMESPACES = [
44
+ "common",
45
+ "nav",
46
+ "banner",
47
+ "overview",
48
+ "settings",
49
+ "lanes",
50
+ "runs",
51
+ "knowledge",
52
+ "stats",
53
+ "flow",
54
+ "crosslink",
55
+ "learn",
56
+ "mockrun",
57
+ "maintenance",
58
+ "pact",
59
+ "boundary",
60
+ "wait",
61
+ "hookui",
62
+ "handoff",
63
+ "challenge",
64
+ "docs",
65
+ "extra",
66
+ "experiment",
67
+ "tour",
68
+ ];
69
+
70
+ let lang = "en";
71
+ // English is loaded first and kept as the FALLBACK table: a key the other
72
+ // language has not translated yet renders in English rather than as a raw
73
+ // dotted key. A half-finished translation degrades to mixed prose, never to
74
+ // debug output on somebody's screen.
75
+ let DICT = {};
76
+ let DICT_EN = {};
77
+
78
+ // The namespaces are fetched in parallel and merged into ONE flat table, so
79
+ // `t()` is unchanged and no call site knows the split happened.
80
+ //
81
+ // A namespace that fails to load falls back to ENGLISH for its keys rather than
82
+ // rejecting the whole language: a network hiccup on one file should cost that
83
+ // file's prose, not the entire panel. English itself has no fallback below it,
84
+ // so a failure there rejects and boot() lands on raw keys — ugly, never blank.
85
+ function loadLang(code) {
86
+ return Promise.all(
87
+ NAMESPACES.map((ns) =>
88
+ fetch(`/i18n/${code}/${ns}.json`, { headers: { "x-orc-token": TOKEN }, cache: "no-store" })
89
+ .then((r) => {
90
+ if (!r.ok) throw new Error(`i18n ${code}/${ns} ${r.status}`);
91
+ return r.json();
92
+ })
93
+ .catch((e) => {
94
+ if (code === "en") throw e;
95
+ return null;
96
+ })
97
+ )
98
+ ).then((parts) => Object.assign({}, ...parts.filter(Boolean)));
99
+ }
100
+
101
+ // A missing or broken table must never blank the UI. Every failure path here
102
+ // lands on English, so the worst case is untranslated text — never empty text.
103
+ async function setLang(code, opts) {
104
+ const rerender = !opts || opts.rerender !== false;
105
+ const want = LANGS.some((l) => l.code === code) ? code : "en";
106
+ try {
107
+ DICT = want === "en" ? DICT_EN : await loadLang(want);
108
+ lang = want;
109
+ } catch (_) {
110
+ DICT = DICT_EN;
111
+ lang = "en";
112
+ }
113
+ try {
114
+ localStorage.setItem(LANG_KEY, lang);
115
+ } catch (_) {}
116
+ document.documentElement.setAttribute("lang", lang);
117
+ applyStaticText();
118
+ if (rerender) route();
119
+ }
120
+
121
+ function t(key, vars) {
122
+ let s = DICT[key];
123
+ if (s === undefined) s = DICT_EN[key];
124
+ if (s === undefined) s = key;
125
+ if (vars) for (const k of Object.keys(vars)) s = s.split("{" + k + "}").join(String(vars[k]));
126
+ return s;
127
+ }
128
+
129
+ // English has one plural form and Indonesian has none, so count-aware lookup is
130
+ // a second key rather than a plural-rules engine: `n === 1` picks the singular.
131
+ const tn = (n, key, vars) => t(n === 1 ? key : key + "Plural", Object.assign({ n }, vars || {}));
132
+
133
+ // The parts of the shell that are markup rather than a render function. Called
134
+ // on every language change; panels re-render themselves through `route()`.
135
+ function applyStaticText() {
136
+ for (const node of document.querySelectorAll("[data-i18n]")) node.textContent = t(node.dataset.i18n);
137
+ const tt = $("#theme-toggle");
138
+ if (tt) tt.textContent = document.documentElement.getAttribute("data-theme") === "light" ? t("rail.dark") : t("rail.light");
139
+ const hint = $("#shortcut-hint");
140
+ if (hint) hint.title = t("rail.shortcuts");
141
+ const label = $("#lang-label");
142
+ if (label) label.textContent = (LANGS.find((l) => l.code === lang) || LANGS[0]).label;
143
+ const btn = $("#lang-toggle");
144
+ if (btn) btn.title = t("lang.title");
145
+ }
146
+
147
+ // Two languages, so the control is a TOGGLE rather than a menu: one click, no
148
+ // dropdown to open. A third language would make this a <select>; two do not.
149
+ function cycleLang() {
150
+ const i = LANGS.findIndex((l) => l.code === lang);
151
+ setLang(LANGS[(i + 1) % LANGS.length].code);
152
+ }
@@ -419,6 +419,12 @@ const TOUR_STEPS = [
419
419
  unconditionally; the header strip sits outside the tabs entirely and is the
420
420
  fallback. Same rule a fourth time: point at something with a SIZE. */
421
421
  { panel: "extra", selector: ".ex-boundary, .ex-strip", title: "tour.14.title", text: "tour.14.text" },
422
+ /* v1.3.0 — the board. Same rule a fifth time: it must point at something with
423
+ a SIZE. `.hk-zone` is the one element on this panel that renders in EVERY
424
+ state, including the first run — an empty line 1 still draws its zone,
425
+ because a drop target that only appears once something is in it is a drop
426
+ target nobody finds. The gate card is the fallback. */
427
+ { panel: "hookui", selector: ".hk-zone, .card", title: "tour.15.title", text: "tour.15.text" },
422
428
  ];
423
429
 
424
430
  function startFirstRunTour(root) {