@gradio/core 0.16.0 → 0.17.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.
Files changed (63) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/dist/src/Blocks.svelte +6 -2
  3. package/dist/src/Render.svelte +2 -2
  4. package/dist/src/RenderComponent.svelte +21 -1
  5. package/dist/src/api_docs/ApiDocs.svelte +18 -3
  6. package/dist/src/gradio_helper.d.ts +1 -11
  7. package/dist/src/gradio_helper.js +3 -19
  8. package/dist/src/i18n.d.ts +12 -5
  9. package/dist/src/i18n.js +93 -12
  10. package/dist/src/init.js +36 -20
  11. package/dist/src/lang/en.json +2 -1
  12. package/dist/src/lang/es.json +2 -1
  13. package/dist/src/lang/fr.json +2 -1
  14. package/dist/src/lang/ja.json +2 -1
  15. package/dist/src/lang/ko.json +2 -1
  16. package/dist/src/lang/lt.json +2 -1
  17. package/dist/src/lang/nb.json +2 -1
  18. package/dist/src/lang/nl.json +2 -1
  19. package/dist/src/lang/pl.json +2 -1
  20. package/dist/src/lang/pt-BR.json +1 -0
  21. package/dist/src/lang/pt.json +2 -1
  22. package/dist/src/lang/ro.json +2 -1
  23. package/dist/src/lang/ru.json +2 -1
  24. package/dist/src/lang/sv.json +2 -1
  25. package/dist/src/lang/ta.json +2 -1
  26. package/dist/src/lang/th.json +2 -1
  27. package/dist/src/lang/tr.json +2 -1
  28. package/dist/src/lang/uk.json +2 -1
  29. package/dist/src/lang/ur.json +2 -1
  30. package/dist/src/lang/uz.json +2 -1
  31. package/dist/src/lang/zh-CN.json +2 -1
  32. package/dist/src/lang/zh-TW.json +2 -1
  33. package/package.json +53 -53
  34. package/src/Blocks.svelte +7 -3
  35. package/src/Render.svelte +2 -2
  36. package/src/RenderComponent.svelte +21 -1
  37. package/src/api_docs/ApiDocs.svelte +19 -3
  38. package/src/gradio_helper.ts +5 -21
  39. package/src/i18n.test.ts +120 -1
  40. package/src/i18n.ts +126 -24
  41. package/src/init.ts +47 -26
  42. package/src/lang/en.json +2 -1
  43. package/src/lang/es.json +2 -1
  44. package/src/lang/fr.json +2 -1
  45. package/src/lang/ja.json +2 -1
  46. package/src/lang/ko.json +2 -1
  47. package/src/lang/lt.json +2 -1
  48. package/src/lang/nb.json +2 -1
  49. package/src/lang/nl.json +2 -1
  50. package/src/lang/pl.json +2 -1
  51. package/src/lang/pt-BR.json +1 -0
  52. package/src/lang/pt.json +2 -1
  53. package/src/lang/ro.json +2 -1
  54. package/src/lang/ru.json +2 -1
  55. package/src/lang/sv.json +2 -1
  56. package/src/lang/ta.json +2 -1
  57. package/src/lang/th.json +2 -1
  58. package/src/lang/tr.json +2 -1
  59. package/src/lang/uk.json +2 -1
  60. package/src/lang/ur.json +2 -1
  61. package/src/lang/uz.json +2 -1
  62. package/src/lang/zh-CN.json +2 -1
  63. package/src/lang/zh-TW.json +2 -1
@@ -97,7 +97,8 @@
97
97
  "runtime_error": "இயக்க நேர பிழை உள்ளது",
98
98
  "space_not_working": "\"Space செயல்படவில்லை ஏனெனில்\" {0}",
99
99
  "space_paused": "Space இடைநிறுத்தப்பட்டுள்ளது",
100
- "use_via_api": "API மூலம் பயன்படுத்தவும்"
100
+ "use_via_api": "API மூலம் பயன்படுத்தவும்",
101
+ "use_via_api_or_mcp": "API அல்லது MCP மூலம் பயன்படுத்தவும்"
101
102
  },
102
103
  "file": {
103
104
  "uploading": "பதிவேற்றுகிறது..."
@@ -87,7 +87,8 @@
87
87
  "runtime_error": "เกิดข้อผิดพลาดขณะทำงาน",
88
88
  "space_not_working": "\"Space ใช้งานไม่ได้เนื่องจาก\" {0}",
89
89
  "space_paused": "Space ถูกหยุดชั่วคราว",
90
- "use_via_api": "ใช้งานผ่าน API"
90
+ "use_via_api": "ใช้งานผ่าน API",
91
+ "use_via_api_or_mcp": "ใช้งานผ่าน API หรือ MCP"
91
92
  },
92
93
  "file": {
93
94
  "uploading": "กำลังอัปโหลด..."
@@ -97,7 +97,8 @@
97
97
  "runtime_error": "Bir çalışma zamanı hatası var",
98
98
  "space_not_working": "\"Space çalışmıyor çünkü\" {0}",
99
99
  "space_paused": "Space duraklatıldı",
100
- "use_via_api": "API üzerinden kullan"
100
+ "use_via_api": "API üzerinden kullan",
101
+ "use_via_api_or_mcp": "API veya MCP üzerinden kullan"
101
102
  },
102
103
  "file": {
103
104
  "uploading": "Yükleniyor..."
@@ -97,7 +97,8 @@
97
97
  "runtime_error": "Є помилка виконання",
98
98
  "space_not_working": "\"Space не працює, оскільки\" {0}",
99
99
  "space_paused": "Space призупинено",
100
- "use_via_api": "Використовувати через API"
100
+ "use_via_api": "Використовувати через API",
101
+ "use_via_api_or_mcp": "Використовувати через API або MCP"
101
102
  },
102
103
  "file": {
103
104
  "uploading": "Завантаження..."
@@ -97,7 +97,8 @@
97
97
  "runtime_error": "رن ٹائم میں خطا ہے",
98
98
  "space_not_working": "\"Space کام نہیں کر رہا ہے کیونکہ\" {0}",
99
99
  "space_paused": "Space موقوف ہے",
100
- "use_via_api": "API کے ذریعے استعمال کریں"
100
+ "use_via_api": "API کے ذریعے استعمال کریں",
101
+ "use_via_api_or_mcp": "API یا MCP کے ذریعے استعمال کریں"
101
102
  },
102
103
  "file": {
103
104
  "uploading": "اپلوڈ ہو رہا ہے..."
@@ -97,7 +97,8 @@
97
97
  "runtime_error": "Bajarilish vaqti xatosi mavjud",
98
98
  "space_not_working": "\"Space ishlamayapti, chunki\" {0}",
99
99
  "space_paused": "Space to'xtatilgan",
100
- "use_via_api": "API orqali foydalaning"
100
+ "use_via_api": "API orqali foydalaning",
101
+ "use_via_api_or_mcp": "API yoki MCP orqali foydalaning"
101
102
  },
102
103
  "file": {
103
104
  "uploading": "Yuklanmoqda..."
@@ -87,7 +87,8 @@
87
87
  "runtime_error": "存在运行时错误",
88
88
  "space_not_working": "\"Space 无法工作,原因:\" {0}",
89
89
  "space_paused": "Space 已暂停",
90
- "use_via_api": "通过 API 使用"
90
+ "use_via_api": "通过 API 使用",
91
+ "use_via_api_or_mcp": "通过 API 或 MCP 使用"
91
92
  },
92
93
  "file": {
93
94
  "uploading": "正在上传..."
@@ -97,7 +97,8 @@
97
97
  "runtime_error": "有執行時錯誤",
98
98
  "space_not_working": "\"Space 無法運作,因為\" {0}",
99
99
  "space_paused": "Space 已暫停",
100
- "use_via_api": "透過 API 使用"
100
+ "use_via_api": "透過 API 使用",
101
+ "use_via_api_or_mcp": "透過 API 或 MCP 使用"
101
102
  },
102
103
  "file": {
103
104
  "uploading": "上傳中..."
package/package.json CHANGED
@@ -1,68 +1,68 @@
1
1
  {
2
2
  "name": "@gradio/core",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "type": "module",
5
5
  "devDependencies": {
6
- "@gradio/accordion": "^0.5.14",
7
- "@gradio/atoms": "^0.16.0",
8
- "@gradio/audio": "^0.17.12",
9
- "@gradio/annotatedimage": "^0.9.17",
10
- "@gradio/box": "^0.2.18",
11
- "@gradio/button": "^0.4.17",
12
- "@gradio/chatbot": "^0.26.4",
13
- "@gradio/checkboxgroup": "^0.6.20",
14
- "@gradio/checkbox": "^0.4.20",
15
- "@gradio/code": "^0.14.2",
16
- "@gradio/client": "^1.14.2",
17
- "@gradio/colorpicker": "^0.4.20",
6
+ "@gradio/accordion": "^0.5.15",
7
+ "@gradio/annotatedimage": "^0.9.19",
8
+ "@gradio/atoms": "^0.16.1",
9
+ "@gradio/audio": "^0.17.14",
10
+ "@gradio/box": "^0.2.19",
11
+ "@gradio/chatbot": "^0.26.7",
12
+ "@gradio/button": "^0.5.0",
13
+ "@gradio/checkboxgroup": "^0.6.21",
14
+ "@gradio/client": "^1.15.0",
15
+ "@gradio/checkbox": "^0.4.21",
16
+ "@gradio/code": "^0.14.4",
17
+ "@gradio/colorpicker": "^0.4.21",
18
18
  "@gradio/column": "^0.2.0",
19
- "@gradio/dataframe": "^0.17.10",
20
- "@gradio/dataset": "^0.4.16",
21
- "@gradio/datetime": "^0.3.12",
22
- "@gradio/dropdown": "^0.9.20",
23
- "@gradio/downloadbutton": "^0.3.17",
24
- "@gradio/fallback": "^0.4.20",
25
- "@gradio/file": "^0.12.16",
26
- "@gradio/form": "^0.2.18",
27
- "@gradio/fileexplorer": "^0.5.27",
28
- "@gradio/gallery": "^0.15.17",
29
- "@gradio/highlightedtext": "^0.9.3",
30
- "@gradio/html": "^0.6.11",
31
- "@gradio/icons": "^0.12.0",
19
+ "@gradio/dataframe": "^0.17.12",
20
+ "@gradio/dataset": "^0.4.19",
21
+ "@gradio/datetime": "^0.3.13",
22
+ "@gradio/downloadbutton": "^0.4.0",
23
+ "@gradio/dropdown": "^0.9.21",
24
+ "@gradio/file": "^0.12.18",
25
+ "@gradio/form": "^0.2.19",
26
+ "@gradio/fallback": "^0.4.21",
27
+ "@gradio/fileexplorer": "^0.5.29",
28
+ "@gradio/highlightedtext": "^0.9.4",
29
+ "@gradio/gallery": "^0.15.19",
30
+ "@gradio/html": "^0.6.13",
32
31
  "@gradio/group": "^0.2.0",
33
- "@gradio/imageeditor": "^0.14.4",
34
- "@gradio/image": "^0.22.4",
35
- "@gradio/imageslider": "^0.2.0",
36
- "@gradio/json": "^0.5.20",
32
+ "@gradio/icons": "^0.12.0",
33
+ "@gradio/image": "^0.22.6",
34
+ "@gradio/imageeditor": "^0.15.0",
35
+ "@gradio/imageslider": "^0.2.2",
36
+ "@gradio/label": "^0.5.13",
37
+ "@gradio/json": "^0.5.22",
37
38
  "@gradio/browserstate": "^0.3.2",
38
- "@gradio/markdown": "^0.13.10",
39
- "@gradio/label": "^0.5.12",
40
- "@gradio/model3d": "^0.14.11",
41
- "@gradio/paramviewer": "^0.7.8",
42
- "@gradio/number": "^0.5.20",
43
- "@gradio/nativeplot": "^0.5.14",
44
- "@gradio/plot": "^0.9.15",
45
- "@gradio/multimodaltextbox": "^0.10.4",
46
- "@gradio/radio": "^0.7.3",
39
+ "@gradio/markdown": "^0.13.12",
40
+ "@gradio/multimodaltextbox": "^0.10.6",
41
+ "@gradio/nativeplot": "^0.5.16",
42
+ "@gradio/paramviewer": "^0.7.9",
43
+ "@gradio/number": "^0.5.21",
44
+ "@gradio/model3d": "^0.14.13",
45
+ "@gradio/plot": "^0.9.16",
46
+ "@gradio/radio": "^0.7.4",
47
47
  "@gradio/row": "^0.2.1",
48
- "@gradio/sidebar": "^0.1.12",
49
- "@gradio/simpledropdown": "^0.3.20",
50
- "@gradio/simpleimage": "^0.8.27",
51
- "@gradio/simpletextbox": "^0.3.20",
52
- "@gradio/sketchbox": "^0.6.7",
53
- "@gradio/slider": "^0.6.8",
48
+ "@gradio/sidebar": "^0.1.13",
49
+ "@gradio/simpledropdown": "^0.3.21",
50
+ "@gradio/sketchbox": "^0.6.8",
51
+ "@gradio/simpletextbox": "^0.3.21",
52
+ "@gradio/simpleimage": "^0.8.29",
53
+ "@gradio/slider": "^0.6.9",
54
+ "@gradio/statustracker": "^0.10.11",
54
55
  "@gradio/state": "^0.1.2",
55
- "@gradio/statustracker": "^0.10.10",
56
- "@gradio/tabitem": "^0.4.3",
57
- "@gradio/tabs": "^0.4.3",
56
+ "@gradio/tabitem": "^0.4.4",
57
+ "@gradio/tabs": "^0.4.4",
58
+ "@gradio/textbox": "^0.10.11",
58
59
  "@gradio/theme": "^0.4.0",
59
- "@gradio/textbox": "^0.10.10",
60
- "@gradio/upload": "^0.16.3",
61
- "@gradio/uploadbutton": "^0.8.17",
62
60
  "@gradio/timer": "^0.4.5",
61
+ "@gradio/upload": "^0.16.5",
62
+ "@gradio/uploadbutton": "^0.9.0",
63
63
  "@gradio/utils": "^0.10.2",
64
- "@gradio/video": "^0.14.12",
65
- "@gradio/wasm": "^0.18.1"
64
+ "@gradio/wasm": "^0.18.1",
65
+ "@gradio/video": "^0.14.14"
66
66
  },
67
67
  "msw": {
68
68
  "workerDirectory": "public"
package/src/Blocks.svelte CHANGED
@@ -26,8 +26,6 @@
26
26
  StatusMessage
27
27
  } from "@gradio/client";
28
28
 
29
- setupi18n();
30
-
31
29
  export let root: string;
32
30
  export let components: ComponentMeta[];
33
31
  export let layout: LayoutNode;
@@ -52,6 +50,8 @@
52
50
  export let initial_layout: ComponentMeta | undefined = undefined;
53
51
  export let css: string | null | undefined = null;
54
52
 
53
+ setupi18n(app.config?.i18n_translations ?? undefined);
54
+
55
55
  let {
56
56
  layout: _layout,
57
57
  targets,
@@ -822,7 +822,11 @@
822
822
  }}
823
823
  class="show-api"
824
824
  >
825
- {$_("errors.use_via_api")}
825
+ {#if app.config?.mcp_server}
826
+ {$_("errors.use_via_api_or_mcp")}
827
+ {:else}
828
+ {$_("errors.use_via_api")}
829
+ {/if}
826
830
  <img src={api_logo} alt={$_("common.logo")} />
827
831
  </button>
828
832
  <div class="divider show-api-divider">·</div>
package/src/Render.svelte CHANGED
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { Gradio, formatter } from "./gradio_helper";
2
+ import { Gradio, reactive_formatter } from "./gradio_helper";
3
3
  import { onMount, createEventDispatcher, setContext } from "svelte";
4
4
  import type { ComponentMeta, ThemeMode } from "./types";
5
5
  import type { Client } from "@gradio/client";
@@ -70,7 +70,7 @@
70
70
  root,
71
71
  autoscroll,
72
72
  max_file_size,
73
- formatter,
73
+ $reactive_formatter,
74
74
  client,
75
75
  load_component
76
76
  );
@@ -1,9 +1,9 @@
1
1
  <svelte:options immutable={true} />
2
2
 
3
3
  <script lang="ts">
4
- import type { Gradio } from "./gradio_helper";
5
4
  import type { ComponentMeta, ThemeMode } from "./types";
6
5
  import type { SvelteComponent, ComponentType } from "svelte";
6
+ import { translate_if_needed } from "./i18n";
7
7
  // @ts-ignore
8
8
  import { bind, binding_callbacks } from "svelte/internal";
9
9
 
@@ -50,6 +50,26 @@
50
50
  }
51
51
 
52
52
  const _component = wrap(component);
53
+
54
+ const supported_props = [
55
+ "description",
56
+ "info",
57
+ "title",
58
+ "placeholder",
59
+ "value",
60
+ "label"
61
+ ];
62
+
63
+ function translate_prop(obj: SvelteRestProps): void {
64
+ for (const key in obj) {
65
+ if (supported_props.includes(key as string)) {
66
+ obj[key] = translate_if_needed(obj[key]);
67
+ }
68
+ }
69
+ }
70
+
71
+ $: translate_prop($$restProps);
72
+ $: value = translate_if_needed(value);
53
73
  </script>
54
74
 
55
75
  <!-- {#if visible} -->
@@ -35,6 +35,8 @@
35
35
  const bash_docs =
36
36
  "https://www.gradio.app/guides/querying-gradio-apps-with-curl";
37
37
  const spaces_docs_suffix = "#connecting-to-a-hugging-face-space";
38
+ const mcp_docs =
39
+ "https://www.gradio.app/guides/building-mcp-server-with-gradio";
38
40
 
39
41
  let api_count = dependencies.filter(
40
42
  (dependency) => dependency.show_api
@@ -343,7 +345,12 @@
343
345
  mcpServers: {
344
346
  gradio: {
345
347
  command: "npx",
346
- args: ["mcp-remote", mcp_server_url]
348
+ args: [
349
+ "mcp-remote",
350
+ mcp_server_url,
351
+ "--transport",
352
+ "sse-only"
353
+ ]
347
354
  }
348
355
  }
349
356
  },
@@ -358,7 +365,12 @@
358
365
  mcpServers: {
359
366
  gradio: {
360
367
  command: "npx",
361
- arguments: ["mcp-remote", mcp_server_url]
368
+ args: [
369
+ "mcp-remote",
370
+ mcp_server_url,
371
+ "--transport",
372
+ "sse-only"
373
+ ]
362
374
  }
363
375
  }
364
376
  },
@@ -369,7 +381,11 @@
369
381
  </code>
370
382
  </Block>
371
383
  <p>&nbsp;</p>
372
- <p>&nbsp;</p>
384
+ <p>
385
+ <a href={mcp_docs} target="_blank">
386
+ Read more about MCP in the Gradio docs
387
+ </a>
388
+ </p>
373
389
  {:else}
374
390
  This Gradio app can also serve as an MCP server, with an MCP
375
391
  tool corresponding to each API endpoint. To enable this, launch
@@ -1,28 +1,16 @@
1
- import { format, _ } from "svelte-i18n";
2
- import { get } from "svelte/store";
3
1
  import { all_common_keys } from "./i18n";
4
-
2
+ import { _ } from "svelte-i18n";
3
+ import { get, derived } from "svelte/store";
5
4
  export { Gradio } from "@gradio/utils";
5
+
6
6
  export type I18nFormatter = typeof formatter;
7
7
 
8
- /**
9
- * i18n formatter with fallback to svelte-i18n's format function.
10
- *
11
- * @param value - The string to translate or format
12
- * @returns The translated string
13
- *
14
- * This formatter attempts translation in the following order:
15
- * 1. Direct translation of the input string
16
- * 2. Checks if input matches any common key names
17
- * 3. Falls back to svelte-i18n's format function
18
- */
19
8
  export function formatter(value: string | null | undefined): string {
20
9
  if (value == null) {
21
10
  return "";
22
11
  }
23
12
  const string_value = String(value);
24
13
  const translate = get(_);
25
- const initial_formatter = get(format);
26
14
 
27
15
  let direct_translation = translate(string_value);
28
16
 
@@ -45,11 +33,7 @@ export function formatter(value: string | null | undefined): string {
45
33
  }
46
34
  }
47
35
 
48
- // fall back to the svelte-i18n formatter to maintain compatibility
49
- const formatted = initial_formatter(string_value);
50
- if (formatted !== string_value) {
51
- return formatted;
52
- }
53
-
54
36
  return string_value;
55
37
  }
38
+
39
+ export const reactive_formatter = derived(_, () => formatter);
package/src/i18n.test.ts CHANGED
@@ -1,7 +1,36 @@
1
- import { describe, test, assert } from "vitest";
1
+ import {
2
+ describe,
3
+ test,
4
+ expect,
5
+ assert,
6
+ vi,
7
+ beforeEach,
8
+ afterEach
9
+ } from "vitest";
2
10
  import { process_langs } from "./i18n";
3
11
  import languagesByAnyCode from "wikidata-lang/indexes/by_any_code";
4
12
  import BCP47 from "./lang/BCP47_codes";
13
+ import {
14
+ translate_if_needed,
15
+ get_initial_locale,
16
+ load_translations,
17
+ changeLocale,
18
+ is_translation_metadata
19
+ } from "./i18n";
20
+
21
+ vi.mock("svelte-i18n", () => ({
22
+ locale: { set: vi.fn() },
23
+ _: vi.fn((key) => `translated_${key}`),
24
+ addMessages: vi.fn(),
25
+ init: vi.fn().mockResolvedValue(undefined)
26
+ }));
27
+
28
+ vi.mock("svelte/store", () => ({
29
+ get: vi.fn((store) => store),
30
+ derived: vi.fn()
31
+ }));
32
+
33
+ import { locale, init, addMessages } from "svelte-i18n";
5
34
 
6
35
  describe("i18n", () => {
7
36
  test("languages are loaded correctly", () => {
@@ -24,4 +53,94 @@ describe("i18n", () => {
24
53
  assert.ok(translation.common);
25
54
  });
26
55
  });
56
+
57
+ describe("basic functions", () => {
58
+ test("translate_if_needed handles regular strings", () => {
59
+ const regularString = "hello world";
60
+ expect(translate_if_needed(regularString)).toBe(regularString);
61
+ });
62
+ });
63
+
64
+ describe("locale management", () => {
65
+ test("get_initial_locale returns browser locale when available", () => {
66
+ const result = get_initial_locale("fr", ["en", "fr", "de"]);
67
+ expect(result).toBe("fr");
68
+ });
69
+
70
+ test("get_initial_locale falls back to fallback locale when browser locale not available", () => {
71
+ const result = get_initial_locale("es", ["en", "fr", "de"]);
72
+ expect(result).toBe("en");
73
+ });
74
+
75
+ test("get_initial_locale falls back to fallback locale when browser locale is null", () => {
76
+ const result = get_initial_locale(null, ["en", "fr", "de"]);
77
+ expect(result).toBe("en");
78
+ });
79
+
80
+ test("get_initial_locale uses custom fallback locale when provided", () => {
81
+ const result = get_initial_locale("es", ["en", "fr", "de"], "de");
82
+ expect(result).toBe("de");
83
+ });
84
+ });
85
+
86
+ describe("i18n setup and initialization", () => {
87
+ beforeEach(() => {
88
+ vi.clearAllMocks();
89
+ });
90
+
91
+ afterEach(() => {
92
+ vi.resetAllMocks();
93
+ });
94
+
95
+ test("load_translations adds messages for each language", () => {
96
+ const mockAddMessages = addMessages as unknown as ReturnType<
97
+ typeof vi.fn
98
+ >;
99
+
100
+ const custom_translations = {
101
+ en: {
102
+ greeting: "Hello"
103
+ },
104
+ fr: {
105
+ greeting: "Bonjour"
106
+ }
107
+ };
108
+
109
+ load_translations(custom_translations);
110
+
111
+ expect(mockAddMessages).toHaveBeenCalledTimes(2);
112
+ expect(mockAddMessages).toHaveBeenCalledWith(
113
+ "en",
114
+ custom_translations.en
115
+ );
116
+ expect(mockAddMessages).toHaveBeenCalledWith(
117
+ "fr",
118
+ custom_translations.fr
119
+ );
120
+ });
121
+
122
+ test("changeLocale sets the locale correctly", () => {
123
+ const mockSetLocale = locale.set as unknown as ReturnType<typeof vi.fn>;
124
+
125
+ changeLocale("fr");
126
+
127
+ expect(mockSetLocale).toHaveBeenCalledWith("fr");
128
+ });
129
+ });
130
+
131
+ describe("translation metadata handling", () => {
132
+ test("is_translation_metadata identifies valid metadata objects", () => {
133
+ expect(
134
+ is_translation_metadata({
135
+ __type__: "translation_metadata" as const,
136
+ key: "test.key"
137
+ })
138
+ ).toBe(true);
139
+ expect(is_translation_metadata({ key: "test.key" })).toBe(false);
140
+
141
+ expect(Boolean(is_translation_metadata(null))).toBe(false);
142
+ expect(Boolean(is_translation_metadata(undefined))).toBe(false);
143
+ expect(Boolean(is_translation_metadata("not an object"))).toBe(false);
144
+ });
145
+ });
27
146
  });