@gradio/core 1.1.3 → 1.3.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 (52) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/dist/src/Blocks.svelte +33 -2
  3. package/dist/src/_init.js +2 -2
  4. package/dist/src/api_docs/ApiBanner.svelte +6 -2
  5. package/dist/src/api_docs/ApiBanner.svelte.d.ts +1 -1
  6. package/dist/src/api_docs/ApiDocs.svelte +41 -25
  7. package/dist/src/api_docs/CodeSnippet.svelte +67 -170
  8. package/dist/src/api_docs/CodeSnippet.svelte.d.ts +2 -6
  9. package/dist/src/api_docs/CopyMarkdown.svelte +7 -2
  10. package/dist/src/api_docs/CopyMarkdown.svelte.d.ts +1 -1
  11. package/dist/src/api_docs/InstallSnippet.svelte +6 -1
  12. package/dist/src/api_docs/InstallSnippet.svelte.d.ts +1 -1
  13. package/dist/src/api_docs/ParametersSnippet.svelte +6 -1
  14. package/dist/src/api_docs/ParametersSnippet.svelte.d.ts +1 -1
  15. package/dist/src/api_docs/RecordingSnippet.svelte +6 -1
  16. package/dist/src/api_docs/RecordingSnippet.svelte.d.ts +1 -1
  17. package/dist/src/api_docs/ResponseSnippet.svelte +6 -1
  18. package/dist/src/api_docs/ResponseSnippet.svelte.d.ts +1 -1
  19. package/dist/src/api_docs/Settings.svelte +2 -7
  20. package/dist/src/api_docs/SettingsBanner.svelte +0 -3
  21. package/dist/src/api_docs/SkillSnippet.svelte +125 -0
  22. package/dist/src/api_docs/SkillSnippet.svelte.d.ts +20 -0
  23. package/dist/src/api_docs/img/skill.svg +10 -0
  24. package/dist/src/api_docs/utils.d.ts +0 -1
  25. package/dist/src/api_docs/utils.js +0 -22
  26. package/dist/src/dependency.d.ts +3 -1
  27. package/dist/src/dependency.js +39 -1
  28. package/dist/src/gradio_helper.js +5 -13
  29. package/dist/src/i18n.d.ts +1 -3
  30. package/dist/src/i18n.js +6 -53
  31. package/dist/src/init.svelte.js +81 -82
  32. package/package.json +49 -49
  33. package/src/Blocks.svelte +33 -2
  34. package/src/_init.ts +2 -2
  35. package/src/api_docs/ApiBanner.svelte +6 -2
  36. package/src/api_docs/ApiDocs.svelte +41 -25
  37. package/src/api_docs/CodeSnippet.svelte +67 -170
  38. package/src/api_docs/CopyMarkdown.svelte +7 -2
  39. package/src/api_docs/InstallSnippet.svelte +6 -1
  40. package/src/api_docs/ParametersSnippet.svelte +6 -1
  41. package/src/api_docs/RecordingSnippet.svelte +6 -1
  42. package/src/api_docs/ResponseSnippet.svelte +6 -1
  43. package/src/api_docs/Settings.svelte +2 -7
  44. package/src/api_docs/SettingsBanner.svelte +0 -3
  45. package/src/api_docs/SkillSnippet.svelte +125 -0
  46. package/src/api_docs/img/skill.svg +10 -0
  47. package/src/api_docs/utils.ts +0 -25
  48. package/src/dependency.ts +39 -1
  49. package/src/gradio_helper.ts +5 -17
  50. package/src/i18n.test.ts +41 -9
  51. package/src/i18n.ts +9 -62
  52. package/src/init.svelte.ts +95 -99
@@ -1,7 +1,7 @@
1
- import { all_common_keys } from "./i18n";
2
1
  import { _ } from "svelte-i18n";
3
2
  import { get, derived } from "svelte/store";
4
3
  export { Gradio } from "@gradio/utils";
4
+ import { I18N_MARKER, translate_i18n_marker } from "@gradio/utils";
5
5
 
6
6
  export type I18nFormatter = typeof formatter;
7
7
 
@@ -12,27 +12,15 @@ export function formatter(value: string | null | undefined): string {
12
12
  const string_value = String(value);
13
13
  const translate = get(_);
14
14
 
15
- let direct_translation = translate(string_value);
15
+ if (string_value.includes(I18N_MARKER)) {
16
+ return translate_i18n_marker(string_value, translate);
17
+ }
16
18
 
19
+ const direct_translation = translate(string_value);
17
20
  if (direct_translation !== string_value) {
18
21
  return direct_translation;
19
22
  }
20
23
 
21
- const lower_value = string_value.toLowerCase();
22
-
23
- for (const common_key of all_common_keys) {
24
- const key_name = common_key.substring(common_key.indexOf(".") + 1);
25
-
26
- if (lower_value === key_name) {
27
- const translation = translate(common_key);
28
-
29
- if (translation !== common_key) {
30
- return translation;
31
- }
32
- break;
33
- }
34
- }
35
-
36
24
  return string_value;
37
25
  }
38
26
 
package/src/i18n.test.ts CHANGED
@@ -11,12 +11,12 @@ import { Lang, process_langs } from "./i18n";
11
11
  import languagesByAnyCode from "wikidata-lang/indexes/by_any_code";
12
12
  import BCP47 from "./lang/BCP47_codes";
13
13
  import {
14
- translate_if_needed,
15
14
  get_initial_locale,
16
15
  load_translations,
17
16
  changeLocale,
18
17
  is_translation_metadata
19
18
  } from "./i18n";
19
+ import { formatter } from "./gradio_helper";
20
20
  import { loading } from "./lang/loading";
21
21
 
22
22
  const loading_count = Object.keys(loading).length;
@@ -28,8 +28,16 @@ vi.mock("svelte-i18n", () => ({
28
28
  init: vi.fn().mockResolvedValue(undefined)
29
29
  }));
30
30
 
31
+ const mock_translations: Record<string, string> = {
32
+ "common.submit": "Submit",
33
+ "common.name": "Name",
34
+ "common.greeting": "Hello",
35
+ "common.submit_es": "Enviar",
36
+ "common.name_es": "Nombre"
37
+ };
38
+
31
39
  vi.mock("svelte/store", () => ({
32
- get: vi.fn((store) => store),
40
+ get: vi.fn(() => (key: string) => mock_translations[key] ?? key),
33
41
  derived: vi.fn()
34
42
  }));
35
43
 
@@ -66,13 +74,6 @@ describe("i18n", () => {
66
74
  );
67
75
  });
68
76
 
69
- describe("basic functions", () => {
70
- test("translate_if_needed handles regular strings", () => {
71
- const regularString = "hello world";
72
- expect(translate_if_needed(regularString)).toBe(regularString);
73
- });
74
- });
75
-
76
77
  describe("locale management", () => {
77
78
  test("get_initial_locale returns browser locale when available", () => {
78
79
  const result = get_initial_locale("fr", ["en", "fr", "de"]);
@@ -155,4 +156,35 @@ describe("i18n", () => {
155
156
  expect(Boolean(is_translation_metadata("not an object"))).toBe(false);
156
157
  });
157
158
  });
159
+
160
+ describe("formatter", () => {
161
+ test("translates i18n markers", () => {
162
+ expect(formatter('__i18n__{"key":"common.submit"}')).toBe("Submit");
163
+ expect(formatter('Click: __i18n__{"key":"common.submit"}')).toBe(
164
+ "Click: Submit"
165
+ );
166
+ expect(formatter('__i18n__{"key":"common.name"} field')).toBe(
167
+ "Name field"
168
+ );
169
+ expect(formatter('__i18n__{"key":"common.submit_es"}')).toBe("Enviar");
170
+ });
171
+
172
+ test("returns key when no translation exists", () => {
173
+ expect(formatter('__i18n__{"key":"unknown.key"}')).toBe("unknown.key");
174
+ });
175
+
176
+ test("handles null, undefined, and plain text", () => {
177
+ expect(formatter(null)).toBe("");
178
+ expect(formatter(undefined)).toBe("");
179
+ expect(formatter("Hello world")).toBe("Hello world");
180
+ });
181
+
182
+ test("handles malformed markers", () => {
183
+ expect(formatter("__i18n__")).toBe("__i18n__");
184
+ expect(formatter('__i18n__{"key":"test.key"')).toBe(
185
+ '__i18n__{"key":"test.key"'
186
+ );
187
+ expect(formatter("__i18n__{invalid}")).toBe("__i18n__{invalid}");
188
+ });
189
+ });
158
190
  });
package/src/i18n.ts CHANGED
@@ -6,7 +6,6 @@ import {
6
6
  register,
7
7
  waitLocale
8
8
  } from "svelte-i18n";
9
- import { formatter } from "./gradio_helper";
10
9
  import { loading } from "./lang/loading";
11
10
 
12
11
  const lang_map = {
@@ -71,63 +70,7 @@ export function is_translation_metadata(obj: any): obj is I18nData {
71
70
  return result;
72
71
  }
73
72
 
74
- export const i18n_marker = "__i18n__";
75
-
76
- // handles strings with embedded JSON metadata of shape "__i18n__{"key": "some.key"}"
77
- export function translate_if_needed(value: any): string {
78
- if (typeof value !== "string") {
79
- return value;
80
- }
81
-
82
- const marker_index = value.indexOf(i18n_marker);
83
-
84
- if (marker_index === -1) {
85
- return value;
86
- }
87
-
88
- try {
89
- const before_marker =
90
- marker_index > 0 ? value.substring(0, marker_index) : "";
91
-
92
- const after_marker_index = marker_index + i18n_marker.length;
93
- const json_start = value.indexOf("{", after_marker_index);
94
- let json_end = -1;
95
- let bracket_count = 0;
96
-
97
- for (let i = json_start; i < value.length; i++) {
98
- if (value[i] === "{") bracket_count++;
99
- if (value[i] === "}") bracket_count--;
100
- if (bracket_count === 0) {
101
- json_end = i + 1;
102
- break;
103
- }
104
- }
105
-
106
- if (json_end === -1) {
107
- console.error("Could not find end of JSON in i18n string");
108
- return value;
109
- }
110
-
111
- const metadata_json = value.substring(json_start, json_end);
112
- const after_json = json_end < value.length ? value.substring(json_end) : "";
113
-
114
- try {
115
- const metadata = JSON.parse(metadata_json);
116
-
117
- if (metadata && metadata.key) {
118
- const translated = formatter(metadata.key);
119
- return before_marker + translated + after_json;
120
- }
121
- } catch (jsonError) {
122
- console.error("Error parsing i18n JSON:", jsonError);
123
- }
124
-
125
- return value;
126
- } catch (e) {
127
- console.error("Error processing translation:", e);
128
- return value;
129
- }
130
- }
73
+ export { I18N_MARKER as i18n_marker } from "@gradio/utils";
131
74
 
132
75
  export function process_langs(): LangsRecord {
133
76
  const lazy_langs = Object.fromEntries(
@@ -150,8 +93,6 @@ export const language_choices: [string, string][] = Object.entries(
150
93
  processed_langs
151
94
  ).map(([code]) => [lang_map[code as keyof typeof lang_map] || code, code]);
152
95
 
153
- export let all_common_keys: Set<string> = new Set();
154
-
155
96
  let i18n_initialized = false;
156
97
  let previous_translations: Record<string, Record<string, string>> | undefined;
157
98
 
@@ -183,15 +124,21 @@ export async function setupi18n(
183
124
  return;
184
125
  }
185
126
 
186
- previous_translations = custom_translations;
127
+ const translations_to_use =
128
+ custom_translations ?? previous_translations ?? {};
129
+
130
+ if (custom_translations !== undefined) {
131
+ previous_translations = custom_translations;
132
+ }
187
133
 
188
134
  load_translations({
189
135
  processed_langs,
190
- custom_translations: custom_translations ?? {}
136
+ custom_translations: translations_to_use
191
137
  });
192
138
 
193
139
  let initial_locale: string | null = null;
194
140
  const browser_locale = getLocaleFromNavigator();
141
+
195
142
  if (preferred_locale) {
196
143
  initial_locale = get_lang_from_preferred_locale(preferred_locale);
197
144
  } else {
@@ -3,7 +3,6 @@ import {
3
3
  get_component,
4
4
  get_inputs_outputs
5
5
  } from "./init_utils";
6
- import { translate_if_needed } from "./i18n";
7
6
  import { tick } from "svelte";
8
7
  import { dequal } from "dequal";
9
8
 
@@ -68,6 +67,7 @@ export class AppTree {
68
67
 
69
68
  #get_callbacks = new Map<number, get_data_type>();
70
69
  #set_callbacks = new Map<number, set_data_type>();
70
+ #pending_updates = new Map<number, Record<string, unknown>>();
71
71
  #event_dispatcher: (id: number, event: string, data: unknown) => void;
72
72
  component_ids: number[];
73
73
  initial_tabs: Record<number, Tab[]> = {};
@@ -196,6 +196,21 @@ export class AppTree {
196
196
  this.#set_callbacks.set(id, _set_data);
197
197
  this.#get_callbacks.set(id, _get_data);
198
198
  this.components_to_register.delete(id);
199
+
200
+ // Apply any pending updates that were stored while the component
201
+ // was not yet mounted (e.g. hidden in an inactive tab).
202
+ // We must apply AFTER tick() so that the Gradio class's $effect
203
+ // (which syncs from node props) has already run. Otherwise the
204
+ // $effect would overwrite the values we set here.
205
+ const pending = this.#pending_updates.get(id);
206
+ if (pending) {
207
+ this.#pending_updates.delete(id);
208
+ tick().then(() => {
209
+ const _set = this.#set_callbacks.get(id);
210
+ if (_set) _set(pending);
211
+ });
212
+ }
213
+
199
214
  if (this.components_to_register.size === 0 && !this.resolved) {
200
215
  this.resolved = true;
201
216
  this.ready_resolve();
@@ -222,8 +237,6 @@ export class AppTree {
222
237
  node,
223
238
  this.components_to_register
224
239
  ),
225
- (node) => handle_empty_forms(node, this.components_to_register),
226
- (node) => translate_props(node),
227
240
  (node) => apply_initial_tabs(node, this.initial_tabs),
228
241
  (node) => this.find_attached_events(node, this.#dependency_payload),
229
242
  (node) =>
@@ -422,7 +435,6 @@ export class AppTree {
422
435
  //@ts-ignore
423
436
  (n) => set_visibility_for_updated_node(n, id, new_state.visible),
424
437
  //@ts-ignore
425
- (n) => update_parent_visibility(n, id, new_state.visible),
426
438
  (n) => handle_visibility(n, this.#config.api_url)
427
439
  ]);
428
440
  await tick();
@@ -433,11 +445,24 @@ export class AppTree {
433
445
  const old_value = node?.props.props.value;
434
446
  // @ts-ignore
435
447
  const new_props = create_props_shared_props(new_state);
436
- node!.props.shared_props = {
437
- ...node?.props.shared_props,
438
- ...new_props.shared_props
439
- };
440
- node!.props.props = { ...node?.props.props, ...new_props.props };
448
+ // Modify props in-place instead of replacing the entire object.
449
+ // Replacing with a new object via spread can cause Svelte 5's
450
+ // deep $state proxy to lose track of the values during async
451
+ // component mounting/revival.
452
+ for (const key in new_props.shared_props) {
453
+ // @ts-ignore
454
+ node!.props.shared_props[key] = new_props.shared_props[key];
455
+ }
456
+ for (const key in new_props.props) {
457
+ // @ts-ignore
458
+ node!.props.props[key] = new_props.props[key];
459
+ }
460
+
461
+ // Also store as pending so the value can be applied via _set_data
462
+ // when the component eventually mounts and registers
463
+ const existing = this.#pending_updates.get(id) || {};
464
+ this.#pending_updates.set(id, { ...existing, ...new_state });
465
+
441
466
  if ("value" in new_state && !dequal(old_value, new_state.value)) {
442
467
  this.#event_dispatcher(id, "change", null);
443
468
  }
@@ -453,10 +478,6 @@ export class AppTree {
453
478
  // any values currently in the UI.
454
479
  // @ts-ignore
455
480
  await this.update_visibility(node, new_state);
456
- const parent_node = find_parent(this.root!, id);
457
- if (parent_node)
458
- // @ts-ignore
459
- update_parent_visibility(parent_node, id, new_state.visible);
460
481
  }
461
482
 
462
483
  /**
@@ -477,28 +498,73 @@ export class AppTree {
477
498
  }
478
499
 
479
500
  async render_previously_invisible_children(id: number) {
480
- this.root = this.traverse(this.root!, [
481
- (node) => {
482
- if (node.id === id) {
483
- make_visible_if_not_rendered(node, this.#hidden_on_startup);
484
- }
485
- return node;
486
- },
487
- (node) => handle_visibility(node, this.#config.api_url)
488
- ]);
501
+ const node = find_node_by_id(this.root!, id);
502
+ if (!node) return;
503
+
504
+ // Check if this node or any of its descendants need to be made visible.
505
+ // If not, skip entirely to avoid unnecessary reactive updates
506
+ // from mutating the tree through the $state proxy.
507
+ if (
508
+ !this.#hidden_on_startup.has(node.id) &&
509
+ !has_hidden_descendants(node, this.#hidden_on_startup)
510
+ ) {
511
+ return;
512
+ }
513
+
514
+ make_visible_if_not_rendered(node, this.#hidden_on_startup, true);
515
+ load_components(node, this.#config.api_url);
489
516
  }
490
517
  }
491
518
 
492
519
  function make_visible_if_not_rendered(
493
520
  node: ProcessedComponentMeta,
494
- hidden_on_startup: Set<number>
521
+ hidden_on_startup: Set<number>,
522
+ is_target_node = false
495
523
  ): void {
496
524
  node.props.shared_props.visible = hidden_on_startup.has(node.id)
497
525
  ? true
498
526
  : node.props.shared_props.visible;
499
- node.children.forEach((child) => {
500
- make_visible_if_not_rendered(child, hidden_on_startup);
501
- });
527
+
528
+ if (node.type === "tabs") {
529
+ const selectedId =
530
+ node.props.props.selected ?? node.props.props.initial_tabs?.[0]?.id;
531
+ node.children.forEach((child) => {
532
+ if (
533
+ child.type === "tabitem" &&
534
+ (child.props.props.id === selectedId || child.id === selectedId)
535
+ ) {
536
+ make_visible_if_not_rendered(child, hidden_on_startup, false);
537
+ }
538
+ });
539
+ } else if (
540
+ node.type === "accordion" &&
541
+ node.props.props.open === false &&
542
+ !is_target_node
543
+ ) {
544
+ // Don't recurse into closed accordion content
545
+ } else {
546
+ node.children.forEach((child) => {
547
+ make_visible_if_not_rendered(child, hidden_on_startup, false);
548
+ });
549
+ }
550
+ }
551
+
552
+ function has_hidden_descendants(
553
+ node: ProcessedComponentMeta,
554
+ hidden_on_startup: Set<number>
555
+ ): boolean {
556
+ for (const child of node.children) {
557
+ if (hidden_on_startup.has(child.id)) return true;
558
+ if (has_hidden_descendants(child, hidden_on_startup)) return true;
559
+ }
560
+ return false;
561
+ }
562
+
563
+ function load_components(node: ProcessedComponentMeta, api_url: string): void {
564
+ if (node.props.shared_props.visible && !node.component) {
565
+ node.component = get_component(node.type, node.component_class_id, api_url);
566
+ }
567
+ node.children.forEach((child) => load_components(child, api_url));
502
568
  }
503
569
 
504
570
  /**
@@ -701,78 +767,6 @@ function untrack_children_of_closed_accordions_or_inactive_tabs(
701
767
  return node;
702
768
  }
703
769
 
704
- function handle_empty_forms(
705
- node: ProcessedComponentMeta,
706
- components_to_register: Set<number>
707
- ): ProcessedComponentMeta {
708
- // Check if the node is visible
709
- if (node.type === "form") {
710
- const all_children_invisible = node.children.every(
711
- (child) => child.props.shared_props.visible === false
712
- );
713
-
714
- if (all_children_invisible) {
715
- node.props.shared_props.visible = false;
716
- components_to_register.delete(node.id);
717
- return node;
718
- }
719
- }
720
-
721
- return node;
722
- }
723
-
724
- function update_parent_visibility(
725
- node: ProcessedComponentMeta,
726
- child_made_visible: number,
727
- visibility_state: boolean | "hidden"
728
- ): ProcessedComponentMeta {
729
- // This function was added to address a tricky situation:
730
- // Form components are wrapped in a Form component automatically.
731
- // If all the children of the Form are invisible, the Form itself is marked invisible.
732
- // in AppTree.postprocess -> handle_empty_forms
733
- // This is to avoid rendering empty forms in the UI. They look ugly.
734
- // So what happens when a child inside the Form is made visible again?
735
- // The Form needs to become visible again too.
736
- // If the child is made invisible, the form should be too if all other children are invisible.
737
- // However, we are not doing this now since what we want to do is fetch the latest visibility of all
738
- // the children from the UI. However, get_data only returns the props, not the shared props.
739
- if (
740
- node.type === "form" &&
741
- node.children.length &&
742
- node.children.some((child) => child.id === child_made_visible)
743
- ) {
744
- if (visibility_state === true) node.props.shared_props.visible = true;
745
- else if (!visibility_state && node.children.length === 1)
746
- node.props.shared_props.visible = "hidden";
747
- }
748
- return node;
749
- }
750
-
751
- function translate_props(node: ProcessedComponentMeta): ProcessedComponentMeta {
752
- const supported_props = [
753
- "description",
754
- "info",
755
- "title",
756
- "placeholder",
757
- "value",
758
- "label"
759
- ];
760
- for (const attr of Object.keys(node.props.shared_props)) {
761
- if (supported_props.includes(attr as string)) {
762
- // @ts-ignore
763
- node.props.shared_props[attr] = translate_if_needed(
764
- node.props.shared_props[attr as keyof SharedProps]
765
- );
766
- }
767
- }
768
- for (const attr of Object.keys(node.props.props)) {
769
- if (supported_props.includes(attr as string)) {
770
- node.props.props[attr] = translate_if_needed(node.props.props[attr]);
771
- }
772
- }
773
- return node;
774
- }
775
-
776
770
  function apply_initial_tabs(
777
771
  node: ProcessedComponentMeta,
778
772
  initial_tabs: Record<number, Tab[]>
@@ -799,8 +793,10 @@ function _gather_initial_tabs(
799
793
  if (!("id" in node.props.props)) {
800
794
  node.props.props.id = node.id;
801
795
  }
796
+ const i18n = node.props.props.i18n as ((str: string) => string) | undefined;
797
+ const raw_label = node.props.shared_props.label as string;
802
798
  initial_tabs[parent_tab_id].push({
803
- label: node.props.shared_props.label as string,
799
+ label: i18n ? i18n(raw_label) : raw_label,
804
800
  id: node.props.props.id as string,
805
801
  elem_id: node.props.shared_props.elem_id,
806
802
  visible: node.props.shared_props.visible as boolean,