@celestia-island/hikari 0.41.0 → 0.41.1

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": "@celestia-island/hikari",
3
- "version": "0.41.0",
3
+ "version": "0.41.1",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Hikari Vue 3 component library — production-grade UI components based on shittim-chest design system",
@@ -134,6 +134,11 @@
134
134
  &[data-unset] {
135
135
  font-style: italic;
136
136
  color: rgb(var(--color-muted));
137
+ /* Italic final strokes lean past the glyph advance; overflow:hidden
138
+ * (the ellipsis pair above) shaves them at the content edge. The
139
+ * inline-end cushion is clipping room, and because it sits INSIDE
140
+ * the padding box it never triggers the ellipsis for fitting text. */
141
+ padding-inline-end: 0.2em;
137
142
  }
138
143
  }
139
144
 
@@ -0,0 +1,40 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { setLocale, useI18n } from "./context";
4
+
5
+ /**
6
+ * Regression (2026-09-08 user report): the language tag's "Not set"
7
+ * placeholder stayed ENGLISH on a Chinese UI. Root cause —
8
+ * buildLocaleMessages only merged nested domain sections and silently
9
+ * DROPPED every top-level flat key ("hikari::x.y": "…"), so all
10
+ * post-0.3 flat additions (affixPicker.*, messageBox.*,
11
+ * localizedInput.noMatches, …) fell back to the inline English default
12
+ * at runtime on every locale. These tests pin flat-key loading.
13
+ */
14
+ describe("i18n flat-key loading", () => {
15
+ it("serves top-level flat keys from the locale bundle", async () => {
16
+ await setLocale("zh-Hans");
17
+ const { t } = useI18n();
18
+ expect(t("hikari::affixPicker.unset", "Not set")).toBe("未设置");
19
+ expect(t("hikari::affixPicker.remove", "Remove")).toBe("移除");
20
+ expect(t("hikari::affixPicker.removeConfirmTitle", "Remove entry")).toBe("移除条目");
21
+ expect(t("hikari::messageBox.confirm", "Confirm")).toBe("确认");
22
+ });
23
+
24
+ it("keeps nested legacy sections loading beside the flat keys", async () => {
25
+ await setLocale("zh-Hans");
26
+ const { t } = useI18n();
27
+ expect(t("hikari::localizedInput.chooseLanguage", "Choose editing language")).toBe(
28
+ "选择编辑语言",
29
+ );
30
+ expect(t("hikari::modal.close", "Close")).toBe("关闭");
31
+ });
32
+
33
+ it("other locales serve their own flat translations, not English", async () => {
34
+ await setLocale("ja");
35
+ const { t } = useI18n();
36
+ expect(t("hikari::affixPicker.unset", "Not set")).toBe("未設定");
37
+ await setLocale("ko");
38
+ expect(t("hikari::affixPicker.unset", "Not set")).toBe("미설정");
39
+ });
40
+ });
@@ -3,7 +3,7 @@ import { reactive } from "vue";
3
3
  type Messages = Record<string, string>;
4
4
 
5
5
  interface LocaleModule {
6
- default: Record<string, Messages>;
6
+ default: Record<string, unknown>;
7
7
  }
8
8
 
9
9
  const localeModules = import.meta.glob<LocaleModule>(
@@ -18,9 +18,18 @@ function buildLocaleMessages(locale: string): Messages {
18
18
  for (const [path, mod] of Object.entries(localeModules)) {
19
19
  if (!path.startsWith(prefix)) continue;
20
20
  const domain = mod.default;
21
- for (const [_section, keys] of Object.entries(domain)) {
22
- if (typeof keys === "object" && keys !== null) {
23
- Object.assign(merged, keys);
21
+ for (const [key, value] of Object.entries(domain)) {
22
+ if (typeof value === "string") {
23
+ // Flat leaf — the appended-at-the-bottom style
24
+ // ("hikari::x.y": "…") every post-0.3 locale addition uses
25
+ // (affixPicker.*, messageBox.*, localizedInput.noMatches, …).
26
+ // The old object-only merge SILENTLY DROPPED these, so every
27
+ // flat key fell back to English at runtime no matter the
28
+ // locale (2026-09-08 user report: "Not set" stayed English).
29
+ merged[key] = value;
30
+ } else if (value !== null && typeof value === "object") {
31
+ // Legacy nested domain section ("components", "chat", …).
32
+ Object.assign(merged, value as Messages);
24
33
  }
25
34
  }
26
35
  }
@@ -0,0 +1,45 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ /**
4
+ * Flat-key parity guard (2026-09-10, follow-up to the flat-loader fix):
5
+ * buildLocaleMessages now serves top-level flat keys ("hikari::x.y": "…")
6
+ * beside the legacy nested sections — but a key missing from a locale's
7
+ * components.json still silently falls back to English for that locale.
8
+ * Every flat key in the en bundle must therefore exist in ALL 11 locale
9
+ * bundles. (This is the guard that would have caught the affixPicker/
10
+ * messageBox flat family missing from 8 locales for months.)
11
+ */
12
+ const modules = import.meta.glob<{ default: Record<string, unknown> }>(
13
+ "./locales/*/components.json",
14
+ { eager: true },
15
+ );
16
+
17
+ const EXPECTED_LOCALES = [
18
+ "ar", "de", "en", "es", "fr", "ja", "ko", "pt", "ru", "zh-Hans", "zh-Hant",
19
+ ];
20
+
21
+ describe("components.json flat-key parity", () => {
22
+ const bundles = Object.entries(modules).map(([path, mod]) => ({
23
+ locale: path.match(/locales\/([^/]+)\/components\.json/)?.[1] ?? "",
24
+ flat: Object.fromEntries(
25
+ Object.entries(mod.default).filter(([, v]) => typeof v === "string"),
26
+ ) as Record<string, string>,
27
+ }));
28
+
29
+ it("covers all 11 locales", () => {
30
+ expect(bundles.map((b) => b.locale).sort()).toEqual(EXPECTED_LOCALES);
31
+ });
32
+
33
+ it("defines every en flat key in every locale, non-empty", () => {
34
+ const en = bundles.find((b) => b.locale === "en");
35
+ expect(en, "en bundle renders").toBeTruthy();
36
+ const keys = Object.keys(en!.flat);
37
+ expect(keys.length, "en has flat keys").toBeGreaterThan(0);
38
+ for (const { locale, flat } of bundles) {
39
+ for (const key of keys) {
40
+ expect(typeof flat[key], `${locale} must define ${key}`).toBe("string");
41
+ expect((flat[key] ?? "").length, `${locale} ${key} must not be empty`).toBeGreaterThan(0);
42
+ }
43
+ }
44
+ });
45
+ });
@@ -182,5 +182,23 @@
182
182
  "hikari::zoomToolbar.reset": "إعادة تعيين التكبير",
183
183
  "hikari::zoomToolbar.zoomIn": "تكبير",
184
184
  "hikari::zoomToolbar.zoomOut": "تصغير"
185
- }
185
+ },
186
+ "hikari::affixPicker.unset": "غير محدد",
187
+ "hikari::affixPicker.title": "الخيارات",
188
+ "hikari::affixPicker.search": "بحث",
189
+ "hikari::affixPicker.empty": "لا تطابقات",
190
+ "hikari::affixPicker.selected": "المحدد",
191
+ "hikari::affixPicker.selectedCount": "تم اختيار {count}",
192
+ "hikari::affixPicker.switchTo": "التبديل إلى",
193
+ "hikari::affixPicker.remove": "إزالة",
194
+ "hikari::affixPicker.useCustom": "استخدام \"{query}\"",
195
+ "hikari::affixPicker.removeConfirmTitle": "إزالة العنصر",
196
+ "hikari::affixPicker.removeConfirm": "إزالة \"{label}\"؟ لا يمكن التراجع عن هذا الإجراء.",
197
+ "hikari::localizedInput.noMatches": "لا توجد لغة مطابقة",
198
+ "hikari::messageBox.confirm": "تأكيد",
199
+ "hikari::messageBox.cancel": "إلغاء",
200
+ "hikari::messageBox.ok": "موافق",
201
+ "hikari::messageBox.alertTitle": "تنبيه",
202
+ "hikari::messageBox.confirmTitle": "يرجى التأكيد",
203
+ "hikari::messageBox.promptTitle": "إدخال"
186
204
  }
@@ -182,5 +182,23 @@
182
182
  "hikari::zoomToolbar.reset": "Zoom zurücksetzen",
183
183
  "hikari::zoomToolbar.zoomIn": "Vergrößern",
184
184
  "hikari::zoomToolbar.zoomOut": "Verkleinern"
185
- }
185
+ },
186
+ "hikari::affixPicker.unset": "Nicht festgelegt",
187
+ "hikari::affixPicker.title": "Optionen",
188
+ "hikari::affixPicker.search": "Suchen",
189
+ "hikari::affixPicker.empty": "Keine Treffer",
190
+ "hikari::affixPicker.selected": "Ausgewählt",
191
+ "hikari::affixPicker.selectedCount": "{count} ausgewählt",
192
+ "hikari::affixPicker.switchTo": "Wechseln zu",
193
+ "hikari::affixPicker.remove": "Entfernen",
194
+ "hikari::affixPicker.useCustom": "„{query}“ verwenden",
195
+ "hikari::affixPicker.removeConfirmTitle": "Eintrag entfernen",
196
+ "hikari::affixPicker.removeConfirm": "„{label}“ entfernen? Dies kann nicht rückgängig gemacht werden.",
197
+ "hikari::localizedInput.noMatches": "Keine passende Sprache",
198
+ "hikari::messageBox.confirm": "Bestätigen",
199
+ "hikari::messageBox.cancel": "Abbrechen",
200
+ "hikari::messageBox.ok": "OK",
201
+ "hikari::messageBox.alertTitle": "Hinweis",
202
+ "hikari::messageBox.confirmTitle": "Bitte bestätigen",
203
+ "hikari::messageBox.promptTitle": "Eingabe"
186
204
  }
@@ -182,5 +182,23 @@
182
182
  "hikari::zoomToolbar.reset": "Restablecer zoom",
183
183
  "hikari::zoomToolbar.zoomIn": "Acercar",
184
184
  "hikari::zoomToolbar.zoomOut": "Alejar"
185
- }
185
+ },
186
+ "hikari::affixPicker.unset": "Sin establecer",
187
+ "hikari::affixPicker.title": "Opciones",
188
+ "hikari::affixPicker.search": "Buscar",
189
+ "hikari::affixPicker.empty": "Sin coincidencias",
190
+ "hikari::affixPicker.selected": "Seleccionados",
191
+ "hikari::affixPicker.selectedCount": "{count} seleccionados",
192
+ "hikari::affixPicker.switchTo": "Cambiar a",
193
+ "hikari::affixPicker.remove": "Quitar",
194
+ "hikari::affixPicker.useCustom": "Usar «{query}»",
195
+ "hikari::affixPicker.removeConfirmTitle": "Quitar elemento",
196
+ "hikari::affixPicker.removeConfirm": "¿Quitar «{label}»? Esto no se puede deshacer.",
197
+ "hikari::localizedInput.noMatches": "Ningún idioma coincidente",
198
+ "hikari::messageBox.confirm": "Confirmar",
199
+ "hikari::messageBox.cancel": "Cancelar",
200
+ "hikari::messageBox.ok": "Aceptar",
201
+ "hikari::messageBox.alertTitle": "Aviso",
202
+ "hikari::messageBox.confirmTitle": "Confirme",
203
+ "hikari::messageBox.promptTitle": "Entrada"
186
204
  }
@@ -182,5 +182,23 @@
182
182
  "hikari::zoomToolbar.reset": "Réinitialiser le zoom",
183
183
  "hikari::zoomToolbar.zoomIn": "Agrandir",
184
184
  "hikari::zoomToolbar.zoomOut": "Réduire"
185
- }
185
+ },
186
+ "hikari::affixPicker.unset": "Non défini",
187
+ "hikari::affixPicker.title": "Options",
188
+ "hikari::affixPicker.search": "Rechercher",
189
+ "hikari::affixPicker.empty": "Aucun résultat",
190
+ "hikari::affixPicker.selected": "Sélectionnés",
191
+ "hikari::affixPicker.selectedCount": "{count} sélectionnés",
192
+ "hikari::affixPicker.switchTo": "Passer à",
193
+ "hikari::affixPicker.remove": "Retirer",
194
+ "hikari::affixPicker.useCustom": "Utiliser « {query} »",
195
+ "hikari::affixPicker.removeConfirmTitle": "Retirer l’entrée",
196
+ "hikari::affixPicker.removeConfirm": "Retirer « {label} » ? Cette action est irréversible.",
197
+ "hikari::localizedInput.noMatches": "Aucune langue correspondante",
198
+ "hikari::messageBox.confirm": "Confirmer",
199
+ "hikari::messageBox.cancel": "Annuler",
200
+ "hikari::messageBox.ok": "OK",
201
+ "hikari::messageBox.alertTitle": "Avis",
202
+ "hikari::messageBox.confirmTitle": "Veuillez confirmer",
203
+ "hikari::messageBox.promptTitle": "Saisie"
186
204
  }
@@ -182,5 +182,23 @@
182
182
  "hikari::zoomToolbar.reset": "ズームをリセット",
183
183
  "hikari::zoomToolbar.zoomIn": "拡大",
184
184
  "hikari::zoomToolbar.zoomOut": "縮小"
185
- }
185
+ },
186
+ "hikari::affixPicker.unset": "未設定",
187
+ "hikari::affixPicker.title": "オプション",
188
+ "hikari::affixPicker.search": "検索",
189
+ "hikari::affixPicker.empty": "一致なし",
190
+ "hikari::affixPicker.selected": "選択中",
191
+ "hikari::affixPicker.selectedCount": "{count} 件選択中",
192
+ "hikari::affixPicker.switchTo": "切り替え",
193
+ "hikari::affixPicker.remove": "削除",
194
+ "hikari::affixPicker.useCustom": "「{query}」を使用",
195
+ "hikari::affixPicker.removeConfirmTitle": "項目を削除",
196
+ "hikari::affixPicker.removeConfirm": "「{label}」を削除しますか?この操作は元に戻せません。",
197
+ "hikari::localizedInput.noMatches": "一致する言語がありません",
198
+ "hikari::messageBox.confirm": "確認",
199
+ "hikari::messageBox.cancel": "キャンセル",
200
+ "hikari::messageBox.ok": "OK",
201
+ "hikari::messageBox.alertTitle": "お知らせ",
202
+ "hikari::messageBox.confirmTitle": "確認してください",
203
+ "hikari::messageBox.promptTitle": "入力"
186
204
  }
@@ -182,5 +182,23 @@
182
182
  "hikari::zoomToolbar.reset": "줌 초기화",
183
183
  "hikari::zoomToolbar.zoomIn": "확대",
184
184
  "hikari::zoomToolbar.zoomOut": "축소"
185
- }
185
+ },
186
+ "hikari::affixPicker.unset": "미설정",
187
+ "hikari::affixPicker.title": "옵션",
188
+ "hikari::affixPicker.search": "검색",
189
+ "hikari::affixPicker.empty": "일치 항목 없음",
190
+ "hikari::affixPicker.selected": "선택됨",
191
+ "hikari::affixPicker.selectedCount": "{count}개 선택됨",
192
+ "hikari::affixPicker.switchTo": "전환",
193
+ "hikari::affixPicker.remove": "제거",
194
+ "hikari::affixPicker.useCustom": "\"{query}\" 사용",
195
+ "hikari::affixPicker.removeConfirmTitle": "항목 제거",
196
+ "hikari::affixPicker.removeConfirm": "\"{label}\"을(를) 제거할까요? 이 작업은 되돌릴 수 없습니다.",
197
+ "hikari::localizedInput.noMatches": "일치하는 언어가 없습니다",
198
+ "hikari::messageBox.confirm": "확인",
199
+ "hikari::messageBox.cancel": "취소",
200
+ "hikari::messageBox.ok": "확인",
201
+ "hikari::messageBox.alertTitle": "알림",
202
+ "hikari::messageBox.confirmTitle": "확인 필요",
203
+ "hikari::messageBox.promptTitle": "입력"
186
204
  }
@@ -182,5 +182,23 @@
182
182
  "hikari::zoomToolbar.reset": "Redefinir zoom",
183
183
  "hikari::zoomToolbar.zoomIn": "Ampliar",
184
184
  "hikari::zoomToolbar.zoomOut": "Reduzir"
185
- }
185
+ },
186
+ "hikari::affixPicker.unset": "Não definido",
187
+ "hikari::affixPicker.title": "Opções",
188
+ "hikari::affixPicker.search": "Pesquisar",
189
+ "hikari::affixPicker.empty": "Sem correspondências",
190
+ "hikari::affixPicker.selected": "Selecionados",
191
+ "hikari::affixPicker.selectedCount": "{count} selecionados",
192
+ "hikari::affixPicker.switchTo": "Mudar para",
193
+ "hikari::affixPicker.remove": "Remover",
194
+ "hikari::affixPicker.useCustom": "Usar \"{query}\"",
195
+ "hikari::affixPicker.removeConfirmTitle": "Remover item",
196
+ "hikari::affixPicker.removeConfirm": "Remover \"{label}\"? Isso não pode ser desfeito.",
197
+ "hikari::localizedInput.noMatches": "Nenhum idioma correspondente",
198
+ "hikari::messageBox.confirm": "Confirmar",
199
+ "hikari::messageBox.cancel": "Cancelar",
200
+ "hikari::messageBox.ok": "OK",
201
+ "hikari::messageBox.alertTitle": "Aviso",
202
+ "hikari::messageBox.confirmTitle": "Confirme",
203
+ "hikari::messageBox.promptTitle": "Entrada"
186
204
  }
@@ -182,5 +182,23 @@
182
182
  "hikari::zoomToolbar.reset": "Сбросить масштаб",
183
183
  "hikari::zoomToolbar.zoomIn": "Увеличить",
184
184
  "hikari::zoomToolbar.zoomOut": "Уменьшить"
185
- }
185
+ },
186
+ "hikari::affixPicker.unset": "Не задано",
187
+ "hikari::affixPicker.title": "Параметры",
188
+ "hikari::affixPicker.search": "Поиск",
189
+ "hikari::affixPicker.empty": "Нет совпадений",
190
+ "hikari::affixPicker.selected": "Выбрано",
191
+ "hikari::affixPicker.selectedCount": "Выбрано: {count}",
192
+ "hikari::affixPicker.switchTo": "Переключиться на",
193
+ "hikari::affixPicker.remove": "Удалить",
194
+ "hikari::affixPicker.useCustom": "Использовать «{query}»",
195
+ "hikari::affixPicker.removeConfirmTitle": "Удалить запись",
196
+ "hikari::affixPicker.removeConfirm": "Удалить «{label}»? Это действие нельзя отменить.",
197
+ "hikari::localizedInput.noMatches": "Нет подходящего языка",
198
+ "hikari::messageBox.confirm": "Подтвердить",
199
+ "hikari::messageBox.cancel": "Отмена",
200
+ "hikari::messageBox.ok": "ОК",
201
+ "hikari::messageBox.alertTitle": "Уведомление",
202
+ "hikari::messageBox.confirmTitle": "Подтвердите",
203
+ "hikari::messageBox.promptTitle": "Ввод"
186
204
  }