@isi-ui7/bos7-shared 0.4.5 → 0.4.7

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@isi-ui7/bos7-shared",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
4
4
  "description": "Shared auth7 and layout primitives for bos7 applications.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -70,10 +70,10 @@
70
70
  "peerDependencies": {
71
71
  "@carbon/icons-react": "^11.71.0",
72
72
  "@carbon/react": "^1.97.0",
73
- "@isi-ui7/corporate-themes": ">=0.2.15",
73
+ "@isi-ui7/corporate-themes": ">=0.2.17",
74
74
  "@isi-ui7/data-table": ">=0.2.5",
75
75
  "@isi-ui7/editable-table": ">=0.2.3",
76
- "@isi-ui7/i18n": ">=0.2.3",
76
+ "@isi-ui7/i18n": ">=0.2.4",
77
77
  "@isi-ui7/lookup-input": ">=0.2.4",
78
78
  "@isi-ui7/modal-manager": ">=0.2.0",
79
79
  "@isi-ui7/realtime": ">=0.2.0",
@@ -106,14 +106,14 @@
106
106
  "vite": "^5.0.0",
107
107
  "vite-plugin-dts": "^3.6.0",
108
108
  "vitest": "^1.0.0",
109
- "@isi-ui7/corporate-themes": "0.2.15",
109
+ "@isi-ui7/corporate-themes": "0.2.17",
110
110
  "@isi-ui7/data-table": "0.2.5",
111
- "@isi-ui7/editable-table": "0.2.3",
112
111
  "@isi-ui7/lookup-input": "0.2.4",
113
112
  "@isi-ui7/realtime": "0.2.3",
114
113
  "@isi-ui7/modal-manager": "0.2.2",
115
- "@isi-ui7/ui-shell": "0.2.8",
116
- "@isi-ui7/i18n": "0.2.3"
114
+ "@isi-ui7/i18n": "0.2.4",
115
+ "@isi-ui7/editable-table": "0.2.3",
116
+ "@isi-ui7/ui-shell": "0.2.8"
117
117
  },
118
118
  "dependencies": {
119
119
  "@carbon/icons-react": "^11.71.0",
@@ -126,9 +126,9 @@
126
126
  "jose": "^5.9.6",
127
127
  "jspdf": "^4.2.1",
128
128
  "jspdf-autotable": "^5.0.7",
129
- "@isi-ui7/data-table": "0.2.5",
129
+ "@isi-ui7/lookup-input": "0.2.4",
130
130
  "@isi-ui7/editable-table": "0.2.3",
131
- "@isi-ui7/lookup-input": "0.2.4"
131
+ "@isi-ui7/data-table": "0.2.5"
132
132
  },
133
133
  "scripts": {
134
134
  "build": "vite build",
@@ -0,0 +1,125 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { render, screen, cleanup } from "@testing-library/react";
3
+ import { afterEach } from "vitest";
4
+ import { SchemaFormRenderer } from "./form-renderer";
5
+ import type { FormSection } from "./form-types";
6
+
7
+ afterEach(cleanup);
8
+
9
+ type D = Record<string, unknown>;
10
+
11
+ // Medan `detail-rows` NOL meneruskan `invalidText` sampai 2026-09-06, sementara
12
+ // saudaranya `detail-modal` SUDAH (detail-modal-field.tsx:277).
13
+ //
14
+ // 🔴 Akibatnya: sebuah aturan validasi yang keyed ke medan detail-rows
15
+ // DIHITUNG, MENAHAN SUBMIT (use-crud-form.ts:167 `if (Object.keys(errs).length)
16
+ // return`), lalu NOL PERNAH DITAMPILKAN. Petugas melihat spanduk generik
17
+ // "Check required fields" dan tabel yang bersih — pesannya sudah ditulis, sudah
18
+ // benar, dan invisible.
19
+ //
20
+ // Terukur pada Produk Pembiayaan (bos7-financing, errors.costs). Radius ledak
21
+ // lintas estate: 5 formulir yang benar-benar menyetel error pada kunci
22
+ // detail-rows (financing ×2 `costs`, treasury ×3 `items`/`items`/`mappings`);
23
+ // 12 dari 17 berkas ber-detail-rows lainnya nol menyetelnya dan karena itu nol
24
+ // dapat melihat perbedaan.
25
+
26
+ const PESAN = "Setiap baris biaya harus memilih Element Biaya";
27
+
28
+ const section: FormSection<D> = {
29
+ title: "Parameter Biaya",
30
+ fields: [
31
+ {
32
+ key: "costs",
33
+ label: "Daftar Biaya",
34
+ type: "detail-rows",
35
+ span: 12,
36
+ detailRows: {
37
+ columns: [{ key: "cost_element_id", label: "Element Biaya", type: "text" }],
38
+ newRowFactory: () => ({ cost_element_id: "" }),
39
+ },
40
+ },
41
+ ],
42
+ };
43
+
44
+ // 🔴 Menghitung SEMUA [role=alert] mengukur populasi yang salah: Carbon's
45
+ // TextInput selalu merender <span class="cds--text-input__counter-alert"> yang
46
+ // KOSONG, jadi asersi "tepat satu alert" gagal karena alasan yang nol
47
+ // berhubungan. Terukur saat menulis berkas ini — uji "galat nol bocor" berbunyi
48
+ // 2 padahal cuma satu medan bergalat.
49
+ //
50
+ // Yang dihitung: alert yang BERISI teks. Itu populasi yang pertanyaannya
51
+ // maksud.
52
+ function alertsBerteks(root: HTMLElement) {
53
+ return Array.from(root.querySelectorAll('[role="alert"]')).filter(
54
+ (el) => (el.textContent ?? "").trim() !== "",
55
+ );
56
+ }
57
+
58
+ function renderWith(errors: Record<string, string>) {
59
+ return render(
60
+ <SchemaFormRenderer<D>
61
+ mode="create"
62
+ value={{ costs: [] }}
63
+ sections={[section]}
64
+ errors={errors}
65
+ onChange={() => {}}
66
+ />,
67
+ );
68
+ }
69
+
70
+ describe("detail-rows menampilkan galat medannya", () => {
71
+ // ARAH 1 — merah sebelum perbaikan: pesannya nol pernah muncul.
72
+ it("error DISETEL ⇒ pesannya MUNCUL, dan isinya menyebut biaya", () => {
73
+ renderWith({ costs: PESAN });
74
+ const el = screen.getByText(PESAN);
75
+ expect(el).toBeTruthy();
76
+ // 🔴 Diasersi ISINYA, bukan "pesannya berubah". Generik yang diganti
77
+ // generik lain akan lulus asersi "bukan lagi Check required fields"
78
+ // sambil nol menutup AC-nya.
79
+ expect(el.textContent).toContain("baris biaya");
80
+ // Bentuk yang sama dgn DetailModalField: role=alert supaya pembaca layar
81
+ // mengumumkannya, bukan sekadar teks merah.
82
+ expect(el.getAttribute("role")).toBe("alert");
83
+ });
84
+
85
+ // ARAH 2 — dan ini yang menjaga treasury: perubahan yang cuma dapat MENAMBAH
86
+ // nol dapat meregresi. Formulir yang nol menyetel error nol boleh melihat
87
+ // apa pun yang baru.
88
+ it("error KOSONG ⇒ NOL ada yang muncul, dan nol ada elemen alert", () => {
89
+ const { container } = renderWith({});
90
+ expect(screen.queryByText(PESAN)).toBeNull();
91
+ expect(alertsBerteks(container).length).toBe(0);
92
+ });
93
+
94
+ // KONTROL POSITIF utk instrumen: tabelnya memang dirender di kedua keadaan,
95
+ // jadi "nol ada alert" di atas berarti "nol ada galat", bukan "nol ada apa-apa".
96
+ it("KONTROL: tabelnya dirender di KEDUA keadaan", () => {
97
+ const a = renderWith({ costs: PESAN });
98
+ expect(a.container.querySelector("table")).toBeTruthy();
99
+ cleanup();
100
+ const b = renderWith({});
101
+ expect(b.container.querySelector("table")).toBeTruthy();
102
+ });
103
+
104
+ // Medan LAIN di section yang sama nol boleh ikut menampilkan galat medan ini.
105
+ it("galat detail-rows nol bocor ke medan lain", () => {
106
+ const dua: FormSection<D> = {
107
+ ...section,
108
+ fields: [
109
+ ...section.fields,
110
+ { key: "catatan", label: "Catatan", type: "text", span: 6 },
111
+ ],
112
+ };
113
+ const { container } = render(
114
+ <SchemaFormRenderer<D>
115
+ mode="create"
116
+ value={{ costs: [], catatan: "" }}
117
+ sections={[dua]}
118
+ errors={{ costs: PESAN }}
119
+ onChange={() => {}}
120
+ />,
121
+ );
122
+ expect(alertsBerteks(container).length).toBe(1);
123
+ expect(alertsBerteks(container)[0].textContent).toBe(PESAN);
124
+ });
125
+ });
@@ -0,0 +1,176 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { render, screen, cleanup } from "@testing-library/react";
3
+ import { afterEach } from "vitest";
4
+ import { SchemaFormRenderer } from "./form-renderer";
5
+ import type { FormSection } from "./form-types";
6
+
7
+ afterEach(cleanup);
8
+
9
+ type D = Record<string, unknown>;
10
+
11
+ // Medan `detail-rows` MENAMPILKAN baris yang sudah ada — dan sampai berkas ini,
12
+ // NOL ADA satu uji pun di paket ini yang mengasersinya.
13
+ //
14
+ // ── Celah yang ditutup ───────────────────────────────────────────────────────
15
+ //
16
+ // Satu-satunya uji yang pernah merender `detail-rows` dengan nilai TAK-KOSONG
17
+ // adalah `detail-modal-field.test.tsx:444` (`value={{ limits: [{kode:
18
+ // "CASH_IN"}] }}`), dan asersinya:
19
+ //
20
+ // getByText("Limit Transaksi") <- LABEL medannya
21
+ // getByRole("button", {name:"Tambah Ahli Waris"}) <- tombol medan LAIN
22
+ //
23
+ // `CASH_IN` sendiri nol pernah disentuh. Uji itu jujur — ia berjudul "tetap
24
+ // jalan berdampingan" dan memang mengukur koeksistensi. Yang salah adalah nol
25
+ // ada yang lain. `detail-rows-invalid-text.test.tsx` (2026-09-06) juga nol
26
+ // menutupnya: ia merender `value={{ costs: [] }}` — nol baris.
27
+ //
28
+ // ── Kenapa kelas ini mahal ───────────────────────────────────────────────────
29
+ //
30
+ // form-renderer.tsx:623
31
+ //
32
+ // const rows = Array.isArray(rawValue) ? rawValue : [];
33
+ //
34
+ // Fallback `[]` itu DIAM. Dan setiap konsumen `detail-rows` di estate ini
35
+ // menulis dengan semantik GANTI-SEMUA — backend menghapus seluruh anak lalu
36
+ // menyisipkan ulang apa yang dikirim. Gabungannya:
37
+ //
38
+ // nilai non-array (atau nol ter-bind) ⇒ tabel render KOSONG
39
+ // ⇒ Save mengirim []
40
+ // ⇒ ganti-semua MENGHAPUS SEMUANYA
41
+ // ⇒ dan layarnya SUKSES
42
+ //
43
+ // ── Siapa yang dijaga berkas ini ─────────────────────────────────────────────
44
+ //
45
+ // Uji yang nol menyebut konsumennya akan dihapus oleh orang yang mengira ia
46
+ // mengukur satu instans. Keempatnya:
47
+ //
48
+ // holidays bos7-enterprise calendar/_schema.ts (DeleteCalendarHolidays + Insert)
49
+ // costs bos7-financing produk pembiayaan (2 layar)
50
+ // items bos7-accounting controlling/budgets (DeleteBudgetItemsByBudget + Insert)
51
+ // bos7-treasury journal-template, pof-account-group
52
+ // mappings bos7-treasury pof
53
+ //
54
+ // ⚠️ `detail-modal` (form-renderer.tsx:737) punya fallback `[]` yang IDENTIK dan
55
+ // tetap tanpa penjaga. Di luar lingkup berkas ini — sengaja disebut, bukan
56
+ // diam-diam dilewati.
57
+
58
+ const BARIS = [
59
+ { description: "Idul Fitri", holiday_type: "NATIONAL" },
60
+ { description: "Natal", holiday_type: "NATIONAL" },
61
+ ];
62
+
63
+ const section: FormSection<D> = {
64
+ title: "Hari Libur",
65
+ fields: [
66
+ {
67
+ key: "holidays",
68
+ label: "Daftar Hari Libur",
69
+ type: "detail-rows",
70
+ span: 12,
71
+ detailRows: {
72
+ columns: [
73
+ { field: "description", header: "Keterangan", columnType: { type: "text" } },
74
+ { field: "holiday_type", header: "Tipe", columnType: { type: "text" } },
75
+ ],
76
+ newRowFactory: () => ({ description: "", holiday_type: "" }),
77
+ },
78
+ },
79
+ ],
80
+ };
81
+
82
+ function renderRows(rows: unknown, mode: "edit" | "view" = "edit") {
83
+ return render(
84
+ <SchemaFormRenderer<D>
85
+ mode={mode}
86
+ value={{ holidays: rows }}
87
+ sections={[section]}
88
+ onChange={() => {}}
89
+ />,
90
+ );
91
+ }
92
+
93
+ /** Sel-sel `<td>` sebuah tabel sebagai teks — populasi yang pertanyaannya maksud. */
94
+ function selTabel(root: HTMLElement): string[] {
95
+ return Array.from(root.querySelectorAll("tbody td")).map((td) =>
96
+ (td.textContent ?? "").trim(),
97
+ );
98
+ }
99
+
100
+ describe("detail-rows menampilkan baris yang sudah ada", () => {
101
+ // ── ARAH 1 — yang merah kalau perendernya berhenti mem-bind nilainya.
102
+ //
103
+ // 🔴 Diasersi ISI SELNYA, bukan JUMLAH BARIS. "2 baris" dapat dipenuhi dua
104
+ // baris KOSONG — tepat kelas yang sama dengan uji lama yang mengasersi label,
105
+ // satu tingkat lebih halus.
106
+ it("dua baris terisi ⇒ isi tiap selnya muncul", () => {
107
+ const { container } = renderRows(BARIS);
108
+
109
+ // Nilai yang UNIK per baris: membuktikan KEDUANYA dirender, bukan satu
110
+ // baris yang kebetulan cocok dua kali.
111
+ expect(screen.getByText("Idul Fitri")).toBeTruthy();
112
+ expect(screen.getByText("Natal")).toBeTruthy();
113
+
114
+ // Kolom kedua sengaja bernilai SAMA di kedua baris — jadi ia hanya lulus
115
+ // kalau perendernya benar-benar mengulang per baris.
116
+ expect(screen.getAllByText("NATIONAL").length).toBe(2);
117
+
118
+ // Dan keduanya harus duduk di <tr> yang BERBEDA. Tanpa ini, sebuah
119
+ // perender yang menumpuk semua nilai di satu baris tetap lulus di atas.
120
+ const barisIdul = screen.getByText("Idul Fitri").closest("tr");
121
+ const barisNatal = screen.getByText("Natal").closest("tr");
122
+ expect(barisIdul).toBeTruthy();
123
+ expect(barisNatal).toBeTruthy();
124
+ expect(barisIdul).not.toBe(barisNatal);
125
+
126
+ expect(container.querySelectorAll("tbody tr").length).toBe(2);
127
+ });
128
+
129
+ // ── ARAH 2 — KONTROL yang membedakan "menampilkan yang ada" dari
130
+ // "selalu menampilkan sesuatu".
131
+ //
132
+ // Tanpa arah ini, sebuah perender yang MENGARANG baris (mis. selalu
133
+ // menyisipkan satu baris kosong dari newRowFactory) akan lulus arah 1 dan
134
+ // nol pernah merah. Dan nol-baris BUKAN kegagalan — ia keadaan sah sebuah
135
+ // kalender tanpa hari libur.
136
+ it("KONTROL: nol baris ⇒ nol sel, dan itu BUKAN kegagalan", () => {
137
+ const { container } = renderRows([]);
138
+ expect(selTabel(container)).toEqual([]);
139
+ expect(container.querySelectorAll("tbody tr").length).toBe(0);
140
+ expect(screen.queryByText("Idul Fitri")).toBeNull();
141
+ // Tabelnya sendiri TETAP dirender — jadi "nol ada sel" di atas berarti
142
+ // "nol ada baris", bukan "medannya nol ada".
143
+ expect(container.querySelector("table")).toBeTruthy();
144
+ expect(screen.getByText("Keterangan")).toBeTruthy(); // header kolom
145
+ });
146
+
147
+ // ── ARAH 3 — jalur yang mengubah fallback diam jadi TERUKUR.
148
+ //
149
+ // `Array.isArray(rawValue) ? rawValue : []` memperlakukan nilai non-array
150
+ // sebagai nol-baris tanpa satu galat pun. Itu perilaku yang DISENGAJA
151
+ // (perender nol boleh melempar atas data cacat), tapi ia harus punya uji —
152
+ // kalau suatu hari ia berubah jadi melempar, atau jadi merender sesuatu,
153
+ // seseorang harus melihatnya di sini.
154
+ it("nilai non-array diperlakukan sebagai nol-baris, tanpa melempar", () => {
155
+ for (const buruk of [undefined, null, "Idul Fitri", 42, { description: "Idul Fitri" }]) {
156
+ const { container } = renderRows(buruk);
157
+ expect(container.querySelector("table")).toBeTruthy();
158
+ expect(container.querySelectorAll("tbody tr").length).toBe(0);
159
+ // 🔴 Ini kalimat yang harus dibaca orang berikutnya: string
160
+ // "Idul Fitri" dan objek ber-`description` TIDAK muncul. Digabung
161
+ // ganti-semua, keadaan ini = hapus senyap.
162
+ expect(screen.queryByText("Idul Fitri")).toBeNull();
163
+ cleanup();
164
+ }
165
+ });
166
+
167
+ // ── ARAH 4 — layar `view` memakai <EditableTable> yang SAMA (readOnly).
168
+ // Kalau binding-nya putus hanya di jalur readonly, checker melihat tabel
169
+ // kosong pada layar approval sementara datanya utuh.
170
+ it("mode view: baris tetap muncul (readOnly, bukan kosong)", () => {
171
+ const { container } = renderRows(BARIS, "view");
172
+ expect(screen.getByText("Idul Fitri")).toBeTruthy();
173
+ expect(screen.getByText("Natal")).toBeTruthy();
174
+ expect(container.querySelectorAll("tbody tr").length).toBe(2);
175
+ });
176
+ });
@@ -186,12 +186,21 @@ function FieldShell({
186
186
  required,
187
187
  children,
188
188
  helperText,
189
+ invalidText,
189
190
  htmlFor,
190
191
  }: {
191
192
  label: ReactNode;
192
193
  required?: boolean;
193
194
  children: ReactNode;
194
195
  helperText?: ReactNode;
196
+ /**
197
+ * Galat tingkat-medan. Bentuknya SAMA PERSIS dgn DetailModalField
198
+ * (detail-modal-field.tsx:277) — role="alert", kelas helper, token warna
199
+ * galat, dirender DI ATAS helperText. Tipe medan itu SUDAH menampilkan
200
+ * galatnya dan `detail-rows` NOL; ini menyamakan, bukan menciptakan bentuk
201
+ * kedua.
202
+ */
203
+ invalidText?: ReactNode;
195
204
  htmlFor?: string;
196
205
  }) {
197
206
  const labelStyle = { color: "var(--cds-text-secondary, #525252)", display: "flex", alignItems: "center", gap: "0.25rem" } as const;
@@ -211,6 +220,15 @@ function FieldShell({
211
220
  <div className={UI7_FORM_VISUAL_CLASSNAMES.label} style={labelStyle}>{labelContent}</div>
212
221
  )}
213
222
  {children}
223
+ {invalidText ? (
224
+ <div
225
+ role="alert"
226
+ className={UI7_FORM_VISUAL_CLASSNAMES.helper}
227
+ style={{ color: "var(--cds-text-error, #da1e28)" }}
228
+ >
229
+ {invalidText}
230
+ </div>
231
+ ) : null}
214
232
  {helperText ? (
215
233
  <div className={UI7_FORM_VISUAL_CLASSNAMES.helper}>{helperText}</div>
216
234
  ) : null}
@@ -622,6 +640,13 @@ export function SchemaFormRenderer<TData extends Record<string, unknown>>({
622
640
  label={field.label}
623
641
  required={effectiveRequired}
624
642
  helperText={field.helperText}
643
+ // 🔴 `detail-rows` NOL meneruskan ini sampai 2026-09-06, sementara
644
+ // saudaranya `detail-modal` SUDAH. Akibatnya aturan validasi yang
645
+ // keyed ke medan detail-rows DIHITUNG, MENAHAN SUBMIT, lalu NOL
646
+ // PERNAH DITAMPILKAN — petugas melihat spanduk generik "Check
647
+ // required fields" dan tabel yang bersih. Terukur pada Produk
648
+ // Pembiayaan (errors.costs, bos7-financing#200).
649
+ invalidText={invalidText}
625
650
  >
626
651
  <EditableTable
627
652
  columns={detail.columns}
@@ -0,0 +1,52 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { SHARED_DICTIONARIES } from "@isi-ui7/i18n";
3
+
4
+ // Butir 1.8 (DG-00): satu medan boolean menampilkan DUA kosakata di modul yang
5
+ // sama, karena dua widget membaca dua kunci —
6
+ //
7
+ // LIST is_printed -> "Ya" / "Tidak" (checkbox -> boolYes/boolNo)
8
+ // VIEW is_printed -> "On" / "Off" (toggle -> toggleOn/toggleOff)
9
+ //
10
+ // Locale bawaan estate `id`, dan "Ya"/"Tidak" SUDAH ada di kamus itu, nol
11
+ // terpakai oleh toggle.
12
+ //
13
+ // ⚠️ Kaidah user (ditekan 4x): "sel menampilkan DESKRIPSI, bukan nilai
14
+ // mentah". "On" memenuhi hurufnya dan meleset dari maksudnya — deskripsi yang
15
+ // berbahasa lain dari layarnya nol lebih terbaca dari `true`.
16
+ describe("kamus toggle — bahasa mengikuti locale-nya", () => {
17
+ const id = SHARED_DICTIONARIES.id;
18
+ const en = SHARED_DICTIONARIES.en;
19
+
20
+ it("KONTROL POSITIF: kedua kamus terbaca dan berisi", () => {
21
+ // Tanpa ini, setiap asersi di bawah bisa lulus atas objek kosong.
22
+ expect(Object.keys(id).length).toBeGreaterThan(20);
23
+ expect(Object.keys(en).length).toBeGreaterThan(20);
24
+ });
25
+
26
+ it("ID: toggle memakai Ya/Tidak, bukan On/Off", () => {
27
+ expect(id["bos7.toggleOn"]).toBe("Ya");
28
+ expect(id["bos7.toggleOff"]).toBe("Tidak");
29
+ });
30
+
31
+ it("KONTROL (+): boolYes/boolNo NOL berubah — LIST tetap Ya/Tidak", () => {
32
+ // Perbaikan ini menyentuh toggle saja. Kalau checkbox ikut berubah, yang
33
+ // terjadi bukan penyeragaman melainkan kerusakan di tempat lain.
34
+ expect(id["bos7.boolYes"]).toBe("Ya");
35
+ expect(id["bos7.boolNo"]).toBe("Tidak");
36
+ });
37
+
38
+ it("KONTROL (−): kamus EN TETAP On/Off — di sana ia benar", () => {
39
+ expect(en["bos7.toggleOn"]).toBe("On");
40
+ expect(en["bos7.toggleOff"]).toBe("Off");
41
+ expect(en["bos7.boolYes"]).toBe("Yes");
42
+ expect(en["bos7.boolNo"]).toBe("No");
43
+ });
44
+
45
+ it("ID nol memuat kata Inggris On/Off di keempat kunci boolean", () => {
46
+ // Asersi yang MEMISAHKAN: ia merah pada keadaan lama, dan tetap merah
47
+ // kalau seseorang "memperbaiki" satu kunci dan melupakan pasangannya.
48
+ for (const k of ["bos7.toggleOn", "bos7.toggleOff", "bos7.boolYes", "bos7.boolNo"]) {
49
+ expect(["On", "Off", "Yes", "No"], `${k} berbahasa Inggris di kamus ID`).not.toContain(id[k]);
50
+ }
51
+ });
52
+ });
package/src/i18n.tsx CHANGED
@@ -81,8 +81,14 @@ export function useBosSharedI18n(): BosSharedLabels {
81
81
 
82
82
  boolYes: t("bos7.boolYes", "Ya"),
83
83
  boolNo: t("bos7.boolNo", "Tidak"),
84
- toggleOn: t("bos7.toggleOn", "On"),
85
- toggleOff: t("bos7.toggleOff", "Off"),
84
+ // Fallback Indonesia, sama dengan boolYes/boolNo dua baris di atas.
85
+ //
86
+ // 🔴 Sebelumnya "On"/"Off" — fallback berbahasa INGGRIS di blok yang tiap
87
+ // baris lainnya berbahasa Indonesia. Ia cacat KEDUA dari temuan yang
88
+ // sama: memperbaiki kamus saja meninggalkan jalur ini, dan jalur ini yang
89
+ // dipakai kapan pun kuncinya nol ada di kamus locale aktif.
90
+ toggleOn: t("bos7.toggleOn", "Ya"),
91
+ toggleOff: t("bos7.toggleOff", "Tidak"),
86
92
  emptyValue: t("bos7.emptyValue", "—"),
87
93
  showInactive: t("bos7.showInactive", "Tampilkan Nonaktif"),
88
94