@cosmicdrift/kumiko-renderer-web 0.206.0 → 0.208.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-renderer-web",
3
- "version": "0.206.0",
3
+ "version": "0.208.0",
4
4
  "description": "Web-platform bindings for @cosmicdrift/kumiko-renderer. HTML default-primitives, browser history-based navigation, EventSource-backed live events, and a one-call createKumikoApp that mounts the whole stack via react-dom.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -16,9 +16,9 @@
16
16
  "./styles.css": "./src/styles.css"
17
17
  },
18
18
  "dependencies": {
19
- "@cosmicdrift/kumiko-dispatcher-live": "0.206.0",
20
- "@cosmicdrift/kumiko-headless": "0.206.0",
21
- "@cosmicdrift/kumiko-renderer": "0.206.0",
19
+ "@cosmicdrift/kumiko-dispatcher-live": "0.208.0",
20
+ "@cosmicdrift/kumiko-headless": "0.208.0",
21
+ "@cosmicdrift/kumiko-renderer": "0.208.0",
22
22
  "@radix-ui/react-dialog": "^1.1.15",
23
23
  "@radix-ui/react-dropdown-menu": "^2.1.16",
24
24
  "@radix-ui/react-label": "^2.1.8",
@@ -63,7 +63,8 @@
63
63
  "@types/react": "^19.2.14",
64
64
  "@types/react-dom": "^19.2.3",
65
65
  "jsdom": "^29.1.1",
66
- "tailwindcss": "^4.3.0"
66
+ "tailwindcss": "^4.3.0",
67
+ "@cosmicdrift/kumiko-locale-de": "0.208.0"
67
68
  },
68
69
  "repository": {
69
70
  "type": "git",
@@ -59,11 +59,10 @@ describe("DefaultField / errors.validation.* i18n", () => {
59
59
  "invalid_option",
60
60
  "failed",
61
61
  ];
62
- for (const locale of ["de", "en"] as const) {
63
- const bundle = kumikoDefaultTranslations[locale];
64
- for (const code of codes) {
65
- expect(bundle?.[`errors.validation.${code}`]).toBeString();
66
- }
62
+ const { localeDeBundle } = await import("@cosmicdrift/kumiko-locale-de");
63
+ for (const code of codes) {
64
+ expect(kumikoDefaultTranslations["en"]?.[`errors.validation.${code}`]).toBeString();
65
+ expect(localeDeBundle[`errors.validation.${code}`]).toBeString();
67
66
  }
68
67
  });
69
68
  });
@@ -1,15 +1,20 @@
1
1
  import { describe, expect, mock, test } from "bun:test";
2
2
  import type { LocaleResolver } from "@cosmicdrift/kumiko-headless";
3
- import { createStaticLocaleResolver, LocaleProvider } from "@cosmicdrift/kumiko-renderer";
3
+ import { localeDeBundle } from "@cosmicdrift/kumiko-locale-de";
4
+ import {
5
+ createStaticLocaleResolver,
6
+ kumikoDefaultTranslations,
7
+ LocaleProvider,
8
+ } from "@cosmicdrift/kumiko-renderer";
4
9
  import { render as _render, screen, waitFor } from "@testing-library/react";
5
10
  import userEvent from "@testing-library/user-event";
6
11
  import type { ReactNode } from "react";
7
12
  import { LanguageSwitcher } from "../layout/language-switcher";
8
13
 
9
- // Tests greifen den LanguageSwitcher mit einem stateful Stub-Resolver
10
- // (setLocale + subscribe) UND einem stateless Resolver, um die zwei
11
- // Verzweigungen abzudecken: Switcher rendert nur wenn setLocale da ist.
12
- // Radix-DropdownMenu öffnet auf pointerdown, daher userEvent statt
14
+ // Tests exercise the LanguageSwitcher with both a stateful stub resolver
15
+ // (setLocale + subscribe) and a stateless resolver, covering the two
16
+ // branches: the switcher only renders when setLocale is present.
17
+ // Radix DropdownMenu opens on pointerdown, so userEvent is used instead of
13
18
  // fireEvent.click.
14
19
 
15
20
  function makeStatefulResolver(initial: string): LocaleResolver {
@@ -33,7 +38,14 @@ function makeStatefulResolver(initial: string): LocaleResolver {
33
38
  }
34
39
 
35
40
  function renderWithResolver(resolver: LocaleResolver, ui: ReactNode) {
36
- return _render(<LocaleProvider resolver={resolver}>{ui}</LocaleProvider>);
41
+ return _render(
42
+ <LocaleProvider
43
+ resolver={resolver}
44
+ fallbackBundles={[{ de: localeDeBundle }, kumikoDefaultTranslations]}
45
+ >
46
+ {ui}
47
+ </LocaleProvider>,
48
+ );
37
49
  }
38
50
 
39
51
  const locales = [
@@ -54,9 +66,9 @@ describe("LanguageSwitcher", () => {
54
66
  test("active locale shown via shorthand", () => {
55
67
  const resolver = makeStatefulResolver("de");
56
68
  renderWithResolver(resolver, <LanguageSwitcher locales={locales} testId="lang" />);
57
- // Trigger zeigt den Locale-Code im DOM (Tailwind uppercased ihn nur
58
- // visuell via CSS der Text-Knoten bleibt lowercase). Das passt:
59
- // getByText sieht den DOM-Text.
69
+ // The trigger shows the locale code in the DOM. Tailwind only
70
+ // uppercases it visually via CSS, the text node stays lowercase.
71
+ // That is fine, getByText sees the DOM text.
60
72
  expect(screen.getByText("de")).toBeTruthy();
61
73
  });
62
74
 
@@ -78,18 +90,23 @@ describe("LanguageSwitcher", () => {
78
90
  expect(resolver.setLocale).toHaveBeenCalledWith("en");
79
91
  });
80
92
 
93
+ test("trigger accessible name follows the active locale (en → Language)", () => {
94
+ const resolver = makeStatefulResolver("en");
95
+ renderWithResolver(resolver, <LanguageSwitcher locales={locales} testId="lang" />);
96
+ expect(screen.getByRole("button", { name: "Language" })).toBeTruthy();
97
+ });
98
+
81
99
  test("matches active locale via language-root (de-AT → de)", async () => {
82
100
  const user = userEvent.setup();
83
101
  const resolver = makeStatefulResolver("de-AT");
84
102
  renderWithResolver(resolver, <LanguageSwitcher locales={locales} testId="lang" />);
85
- // Trigger zeigt "DE" (aus de-AT abgeleitet) — der active marker im
86
- // Dropdown muss bei "Deutsch" sitzen, nicht bei "English".
103
+ // The trigger shows "DE", derived from de-AT. The active marker in the
104
+ // dropdown must sit on "Deutsch", not "English".
87
105
  await user.click(screen.getByRole("button", { name: "Sprache" }));
88
106
  await waitFor(() => {
89
- // Radix-CheckboxItem markiert active via aria-checked="true". Der
90
- // Check-Icon (lucide) sitzt im ItemIndicator und ist nur sichtbar
91
- // wenn checked die ARIA-Variante ist robust gegen Rendering-
92
- // Tricks.
107
+ // Radix CheckboxItem marks active via aria-checked="true". The check
108
+ // icon (lucide) lives in the ItemIndicator and is only visible when
109
+ // checked, so the ARIA variant is robust against rendering quirks.
93
110
  const deItem = screen.getByText("Deutsch").closest('[role="menuitemcheckbox"]');
94
111
  expect(deItem?.getAttribute("aria-checked")).toBe("true");
95
112
  const enItem = screen.getByText("English").closest('[role="menuitemcheckbox"]');
@@ -511,10 +511,11 @@ describe("DataTable", () => {
511
511
  test("de-Locale: Status-Text + aria-Labels kommen aus i18n statt hartcodiertem Englisch (issue #2007)", async () => {
512
512
  const { LocaleProvider, createStaticLocaleResolver, kumikoDefaultTranslations } =
513
513
  await import("@cosmicdrift/kumiko-renderer");
514
+ const { localeDeBundle } = await import("@cosmicdrift/kumiko-locale-de");
514
515
  render(
515
516
  <LocaleProvider
516
517
  resolver={createStaticLocaleResolver({ locale: "de" })}
517
- fallbackBundles={[kumikoDefaultTranslations]}
518
+ fallbackBundles={[{ de: localeDeBundle }, kumikoDefaultTranslations]}
518
519
  >
519
520
  <DataTable
520
521
  columns={cols}
@@ -655,10 +656,11 @@ describe("DataTable", () => {
655
656
  test("hasMore=false + de-Locale: Marker kommt aus i18n statt hartcodiert", async () => {
656
657
  const { LocaleProvider, createStaticLocaleResolver, kumikoDefaultTranslations } =
657
658
  await import("@cosmicdrift/kumiko-renderer");
659
+ const { localeDeBundle } = await import("@cosmicdrift/kumiko-locale-de");
658
660
  render(
659
661
  <LocaleProvider
660
662
  resolver={createStaticLocaleResolver({ locale: "de" })}
661
- fallbackBundles={[kumikoDefaultTranslations]}
663
+ fallbackBundles={[{ de: localeDeBundle }, kumikoDefaultTranslations]}
662
664
  >
663
665
  <DataTable
664
666
  columns={cols}
@@ -38,7 +38,7 @@ function renderRL(props: {
38
38
  readonly onSearchChange?: (next: string) => void;
39
39
  }) {
40
40
  return render(
41
- <LocaleProvider resolver={createStaticLocaleResolver({ locale: "de" })}>
41
+ <LocaleProvider resolver={createStaticLocaleResolver({ locale: "en" })}>
42
42
  <PrimitivesProvider value={defaultPrimitives}>
43
43
  <RenderList
44
44
  screen={screenDef}
@@ -96,7 +96,7 @@ describe("RenderList — Search-Debounce", () => {
96
96
  // Externe Quelle (Browser-Back, Cross-Component-Reset) ändert
97
97
  // searchValue → RenderList soll den Input-Wert spiegeln.
98
98
  rerender(
99
- <LocaleProvider resolver={createStaticLocaleResolver({ locale: "de" })}>
99
+ <LocaleProvider resolver={createStaticLocaleResolver({ locale: "en" })}>
100
100
  <PrimitivesProvider value={defaultPrimitives}>
101
101
  <RenderList
102
102
  screen={screenDef}
@@ -1,17 +1,17 @@
1
- // LanguageSwitcher — Dropdown der die App-Locale via
2
- // LocaleResolver.setLocale umschaltet. Auf Radix-DropdownMenu, gleicher
3
- // Stack wie UserMenu/TenantSwitcher.
1
+ // LanguageSwitcher — dropdown that switches the app locale via
2
+ // LocaleResolver.setLocale. Built on Radix DropdownMenu, same stack
3
+ // as UserMenu/TenantSwitcher.
4
4
  //
5
- // Rendert gar nix wenn der Resolver keine setLocale-Methode anbietet
6
- // (statischer Resolver) — App-Dev sieht dann sofort dass er einen
7
- // stateful Resolver verdrahten muss, bevor der Switcher UI-sichtbar
8
- // wird.
5
+ // Renders nothing if the resolver doesn't offer a setLocale method
6
+ // (static resolver) — the app dev then immediately sees they need to
7
+ // wire up a stateful resolver before the switcher becomes visible in
8
+ // the UI.
9
9
  //
10
- // Icon-Slot optional: das Framework zieht lucide-react nicht selbst
11
- // rein; eine App die keinen Icon-Import will, kriegt den Globe-
12
- // Unicode-Glyph (🌐) als Default.
10
+ // Icon slot is optional: the framework doesn't pull in lucide-react
11
+ // itself; an app that doesn't want an icon import gets the globe
12
+ // unicode glyph (🌐) as default.
13
13
 
14
- import { useLocale } from "@cosmicdrift/kumiko-renderer";
14
+ import { useLocale, useTranslation } from "@cosmicdrift/kumiko-renderer";
15
15
  import { type ReactNode, useMemo } from "react";
16
16
  import { cn } from "../lib/cn";
17
17
  import {
@@ -22,19 +22,19 @@ import {
22
22
  } from "../primitives/dropdown-menu";
23
23
 
24
24
  export type LocaleOption = {
25
- /** BCP-47-Code, z.B. "de", "en-US", "fr-CA". Wird 1:1 an
26
- * resolver.setLocale() weitergereicht. */
25
+ /** BCP-47 code, e.g. "de", "en-US", "fr-CA". Passed through 1:1 to
26
+ * resolver.setLocale(). */
27
27
  readonly code: string;
28
- /** Menschenlesbare Anzeige im Dropdown. */
28
+ /** Human-readable label shown in the dropdown. */
29
29
  readonly label: string;
30
30
  };
31
31
 
32
32
  export type LanguageSwitcherProps = {
33
- /** Auswählbare Locales. Reihenfolge = Anzeige-Reihenfolge im Menü. */
33
+ /** Selectable locales. Order = display order in the menu. */
34
34
  readonly locales: readonly LocaleOption[];
35
- /** Icon-Slot links neben dem Button-Label. Default: 🌐. */
35
+ /** Icon slot left of the button label. Default: 🌐. */
36
36
  readonly icon?: ReactNode;
37
- /** aria-label + title des Triggers. Default: "Sprache". */
37
+ /** aria-label + title of the trigger. Default: translated "kumiko.nav.language". */
38
38
  readonly label?: string;
39
39
  readonly testId?: string;
40
40
  };
@@ -42,15 +42,17 @@ export type LanguageSwitcherProps = {
42
42
  export function LanguageSwitcher({
43
43
  locales,
44
44
  icon = "🌐",
45
- label = "Sprache",
45
+ label,
46
46
  testId,
47
47
  }: LanguageSwitcherProps): ReactNode {
48
48
  const resolver = useLocale();
49
+ const t = useTranslation();
50
+ const resolvedLabel = label ?? t("kumiko.nav.language");
49
51
 
50
52
  const activeLocale = resolver.locale();
51
- // Match entweder exact ("de-DE") oder Language-Root ("de") gegen die
52
- // verfügbaren Optionen. So zeigt der Switcher "Deutsch" aktiv wenn
53
- // der Browser "de-AT" liefert und die Option nur "de" heißt.
53
+ // Matches either exact ("de-DE") or the language root ("de") against
54
+ // the available options. So the switcher shows "German" active when
55
+ // the browser reports "de-AT" but the option is just "de".
54
56
  const activeOption = useMemo(() => {
55
57
  const exact = locales.find((o) => o.code === activeLocale);
56
58
  if (exact) return exact;
@@ -59,7 +61,7 @@ export function LanguageSwitcher({
59
61
  }, [locales, activeLocale]);
60
62
 
61
63
  if (resolver.setLocale === undefined) {
62
- // Stateless-Resolverkein Wechsel möglich. Kein Noise im Topbar.
64
+ // Stateless resolver no switching possible. No noise in the topbar.
63
65
  return null;
64
66
  }
65
67
 
@@ -70,8 +72,8 @@ export function LanguageSwitcher({
70
72
  <DropdownMenuTrigger asChild>
71
73
  <button
72
74
  type="button"
73
- aria-label={label}
74
- title={label}
75
+ aria-label={resolvedLabel}
76
+ title={resolvedLabel}
75
77
  data-testid={testId}
76
78
  className={cn(
77
79
  "inline-flex h-8 items-center gap-1.5 rounded-md border bg-background px-2 text-sm",
@@ -85,7 +87,7 @@ export function LanguageSwitcher({
85
87
  </span>
86
88
  </button>
87
89
  </DropdownMenuTrigger>
88
- <DropdownMenuContent align="end" className="min-w-[10rem]" aria-label={label}>
90
+ <DropdownMenuContent align="end" className="min-w-[10rem]" aria-label={resolvedLabel}>
89
91
  {locales.map((opt) => (
90
92
  <DropdownMenuCheckboxItem
91
93
  key={opt.code}
@@ -5,6 +5,7 @@
5
5
  // green there).
6
6
 
7
7
  import { describe, expect, test } from "bun:test";
8
+ import { localeDeBundle } from "@cosmicdrift/kumiko-locale-de";
8
9
  import {
9
10
  createStaticLocaleResolver,
10
11
  kumikoDefaultTranslations,
@@ -17,7 +18,11 @@ function renderWithLocale(locale: string) {
17
18
  return render(
18
19
  <LocaleProvider
19
20
  resolver={createStaticLocaleResolver({ locale })}
20
- fallbackBundles={[kumikoDefaultTranslations]}
21
+ fallbackBundles={
22
+ locale.split("-")[0] === "de"
23
+ ? [{ de: localeDeBundle }, kumikoDefaultTranslations]
24
+ : [kumikoDefaultTranslations]
25
+ }
21
26
  >
22
27
  <DateField id="date" name="date" value="" onChange={() => {}} locale={locale} />
23
28
  </LocaleProvider>,
@@ -5,6 +5,7 @@
5
5
  // rejection.
6
6
 
7
7
  import { describe, expect, test } from "bun:test";
8
+ import { localeDeBundle } from "@cosmicdrift/kumiko-locale-de";
8
9
  import { kumikoDefaultTranslations } from "@cosmicdrift/kumiko-renderer";
9
10
  import { Temporal } from "temporal-polyfill";
10
11
  import {
@@ -18,7 +19,7 @@ import {
18
19
  // Derived from kumikoDefaultTranslations instead of hardcoded so a drift in
19
20
  // the i18n defaults (e.g. renaming "T" → "D") fails this test too.
20
21
  function placeholderLetters(locale: "de" | "en"): { year: string; month: string; day: string } {
21
- const bundle = kumikoDefaultTranslations[locale];
22
+ const bundle = locale === "de" ? localeDeBundle : kumikoDefaultTranslations[locale];
22
23
  return {
23
24
  year: bundle?.["kumiko.field.dateField.placeholderYear"] ?? "",
24
25
  month: bundle?.["kumiko.field.dateField.placeholderMonth"] ?? "",