@agent-native/core 0.144.2 → 0.144.3

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 (70) hide show
  1. package/corpus/templates/analytics/app/pages/DataSources.tsx +28 -4
  2. package/corpus/templates/analytics/server/jobs/dashboard-report.ts +7 -3
  3. package/corpus/templates/analytics/server/plugins/dashboard-report-jobs.ts +13 -3
  4. package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +64 -23
  5. package/corpus/templates/calendar/app/hooks/use-google-auth.ts +69 -46
  6. package/corpus/templates/calendar/app/hooks/use-view-preferences.ts +35 -6
  7. package/corpus/templates/calendar/app/hooks/use-zoom-auth.ts +24 -3
  8. package/corpus/templates/clips/app/components/player/transcript-panel.tsx +45 -6
  9. package/corpus/templates/clips/app/components/recorder/storage-setup-card.tsx +38 -5
  10. package/corpus/templates/clips/desktop/src/app.tsx +146 -45
  11. package/corpus/templates/clips/server/jobs/brain-export.ts +13 -6
  12. package/corpus/templates/clips/server/jobs/builder-media-compression.ts +10 -3
  13. package/corpus/templates/clips/server/jobs/meeting-reminders.ts +10 -3
  14. package/corpus/templates/clips/server/jobs/poll-calendars.ts +23 -21
  15. package/corpus/templates/clips/server/jobs/stale-meeting-sweeper.ts +10 -3
  16. package/corpus/templates/clips/server/jobs/transactional-emails.ts +10 -3
  17. package/corpus/templates/content/app/components/sidebar/NotionButton.tsx +1 -0
  18. package/corpus/templates/design/app/components/design/FusionAppBanner.tsx +15 -2
  19. package/corpus/templates/mail/app/components/GoogleConnectBanner.tsx +82 -39
  20. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +31 -9
  21. package/corpus/templates/mail/server/plugins/mail-jobs.ts +36 -17
  22. package/dist/client/AssistantChat.js +73 -43
  23. package/dist/client/MultiTabAssistantChat.d.ts +3 -1
  24. package/dist/client/MultiTabAssistantChat.js +79 -92
  25. package/dist/client/browser-session-bridge.js +97 -54
  26. package/dist/client/db-admin/useAgentSync.js +34 -43
  27. package/dist/client/dynamic-suggestions.d.ts +3 -0
  28. package/dist/client/dynamic-suggestions.js +54 -52
  29. package/dist/client/hooks/index.d.ts +1 -0
  30. package/dist/client/hooks/index.js +1 -0
  31. package/dist/client/i18n.d.ts +7 -0
  32. package/dist/client/i18n.js +12 -3
  33. package/dist/client/integrations/useIntegrationStatus.d.ts +1 -1
  34. package/dist/client/integrations/useIntegrationStatus.js +9 -22
  35. package/dist/client/notifications/NotificationsBell.js +14 -9
  36. package/dist/client/progress/RunsTray.js +6 -5
  37. package/dist/client/settings/useBuilderStatus.js +95 -73
  38. package/dist/client/transcription/BuilderTranscriptionCta.js +27 -31
  39. package/dist/client/use-agent-engine-configured.js +21 -3
  40. package/dist/client/use-pausing-interval.d.ts +6 -0
  41. package/dist/client/use-pausing-interval.js +6 -0
  42. package/dist/client/use-poll-loop.d.ts +39 -0
  43. package/dist/client/use-poll-loop.js +73 -0
  44. package/dist/client/use-run-stuck-detection.js +41 -13
  45. package/dist/collab/routes.d.ts +1 -1
  46. package/dist/integrations/google-docs-poller.js +26 -7
  47. package/dist/integrations/pending-tasks-retry-job.js +17 -23
  48. package/dist/integrations/plugin.js +16 -15
  49. package/dist/integrations/remote-push-delivery-job.js +16 -25
  50. package/dist/integrations/remote-retry-job.js +19 -23
  51. package/dist/notifications/routes.d.ts +1 -1
  52. package/dist/oauth-tokens/google-refresh.js +12 -19
  53. package/dist/provider-api/actions/custom-provider-registration.d.ts +9 -9
  54. package/dist/provider-api/actions/provider-api.d.ts +11 -11
  55. package/dist/server/agent-chat-plugin.js +26 -29
  56. package/dist/server/index.d.ts +1 -0
  57. package/dist/server/index.js +1 -0
  58. package/dist/server/interval-job.d.ts +25 -0
  59. package/dist/server/interval-job.js +19 -0
  60. package/dist/server/realtime-token.d.ts +1 -1
  61. package/dist/shared/index.d.ts +1 -0
  62. package/dist/shared/index.js +1 -0
  63. package/dist/shared/poll-engine.d.ts +49 -0
  64. package/dist/shared/poll-engine.js +148 -0
  65. package/dist/templates/default/.agents/skills/internationalization/SKILL.md +5 -0
  66. package/dist/templates/workspace-core/.agents/skills/internationalization/SKILL.md +5 -0
  67. package/dist/tracking/providers.js +10 -1
  68. package/package.json +4 -3
  69. package/src/templates/default/.agents/skills/internationalization/SKILL.md +5 -0
  70. package/src/templates/workspace-core/.agents/skills/internationalization/SKILL.md +5 -0
@@ -0,0 +1,19 @@
1
+ import { createPollEngine } from "../shared/poll-engine.js";
2
+ /**
3
+ * Run a background job on a recurring interval without hand-rolling a
4
+ * `running` overlap guard or a request timeout — see `createPollEngine` for
5
+ * the underlying guarantees. `runOnce` keeps its normal exported signature
6
+ * (serverless schedulers can still call it directly, bypassing the loop);
7
+ * this only replaces the `setInterval` + module-level flag wiring around it.
8
+ * The internal timer is `.unref()`'d so it never keeps the process alive on
9
+ * its own.
10
+ */
11
+ export function startIntervalJob(runOnce, options) {
12
+ const engine = createPollEngine(runOnce, options);
13
+ engine.start();
14
+ return {
15
+ stop() {
16
+ engine.stop();
17
+ },
18
+ };
19
+ }
@@ -26,8 +26,8 @@ export declare function createRealtimeTokenHandler(): import("h3").EventHandlerW
26
26
  expiresAt?: undefined;
27
27
  ttlSeconds?: undefined;
28
28
  } | {
29
+ error?: undefined;
29
30
  token: string;
30
31
  expiresAt: string;
31
32
  ttlSeconds: number;
32
- error?: undefined;
33
33
  }>>;
@@ -9,6 +9,7 @@ export { llmConnectionTrackingProperties, normalizeLlmConnection, type LlmConnec
9
9
  export { DISPATCH_WORKSPACE_ROOT_REDIRECTS, RESERVED_WORKSPACE_APP_IDS, assertValidWorkspaceAppId, getWorkspaceAppIdValidationError, isValidWorkspaceAppIdFormat, } from "./workspace-app-id.js";
10
10
  export { DEFAULT_WORKSPACE_APP_AUDIENCE, WORKSPACE_APP_AUDIENCES, normalizeWorkspaceAppAudience, normalizeWorkspaceAppPathList, workspaceAppAudienceFromEnv, workspaceAppAudienceFromPackageJson, workspaceAppRouteAccessFromEnv, workspaceAppRouteAccessFromPackageJson, type WorkspaceAppRouteAccess, type WorkspaceAppRouteAccessFromConfig, type WorkspaceAppAudience, } from "./workspace-app-audience.js";
11
11
  export { AGENT_NATIVE_OPEN_PATH, AGENT_SIDEBAR_QUERY_PARAM, AGENT_SIDEBAR_QUERY_VALUE_CLOSED, isAgentNativeOpenDeepLink, withCollapsedAgentSidebarParam, } from "./agent-sidebar-url.js";
12
+ export { createPollEngine, type PollEngineOptions, type PollEngineHandle, } from "./poll-engine.js";
12
13
  export { AGENT_NATIVE_DEFAULT_SOCIAL_IMAGE, AGENT_NATIVE_SOCIAL_IMAGE_CACHE_BUSTER, AGENT_NATIVE_SOCIAL_IMAGE_ALT, AGENT_NATIVE_SOCIAL_IMAGE_HEIGHT, AGENT_NATIVE_SOCIAL_IMAGE_PATH, AGENT_NATIVE_SOCIAL_IMAGE_TYPE, AGENT_NATIVE_SOCIAL_IMAGE_WIDTH, defaultSocialImageMeta, withAgentNativeSocialImageCacheBuster, withDefaultSocialImage, type SocialMetaDescriptor, } from "./social-meta.js";
13
14
  export { EMBED_MODE_QUERY_PARAM, EMBED_SESSION_COOKIE, EMBED_START_PATH, EMBED_TOKEN_QUERY_PARAM, } from "./embed-auth.js";
14
15
  export { AGENT_ACCESS_PARAM, DEFAULT_AGENT_ACCESS_TTL_SECONDS, appendAgentAccessParam, buildAgentAccessApiUrl, buildAgentAccessUrl, normalizeAgentAccessBasePath, normalizeAgentAccessOrigin, scopedAgentAccessResourceId, toAgentAccessUrl, type AgentAccessApiUrlOptions, type AgentAccessResourceScope, type AgentAccessUrlOptions, } from "./agent-access.js";
@@ -9,6 +9,7 @@ export { llmConnectionTrackingProperties, normalizeLlmConnection, } from "./llm-
9
9
  export { DISPATCH_WORKSPACE_ROOT_REDIRECTS, RESERVED_WORKSPACE_APP_IDS, assertValidWorkspaceAppId, getWorkspaceAppIdValidationError, isValidWorkspaceAppIdFormat, } from "./workspace-app-id.js";
10
10
  export { DEFAULT_WORKSPACE_APP_AUDIENCE, WORKSPACE_APP_AUDIENCES, normalizeWorkspaceAppAudience, normalizeWorkspaceAppPathList, workspaceAppAudienceFromEnv, workspaceAppAudienceFromPackageJson, workspaceAppRouteAccessFromEnv, workspaceAppRouteAccessFromPackageJson, } from "./workspace-app-audience.js";
11
11
  export { AGENT_NATIVE_OPEN_PATH, AGENT_SIDEBAR_QUERY_PARAM, AGENT_SIDEBAR_QUERY_VALUE_CLOSED, isAgentNativeOpenDeepLink, withCollapsedAgentSidebarParam, } from "./agent-sidebar-url.js";
12
+ export { createPollEngine, } from "./poll-engine.js";
12
13
  export { AGENT_NATIVE_DEFAULT_SOCIAL_IMAGE, AGENT_NATIVE_SOCIAL_IMAGE_CACHE_BUSTER, AGENT_NATIVE_SOCIAL_IMAGE_ALT, AGENT_NATIVE_SOCIAL_IMAGE_HEIGHT, AGENT_NATIVE_SOCIAL_IMAGE_PATH, AGENT_NATIVE_SOCIAL_IMAGE_TYPE, AGENT_NATIVE_SOCIAL_IMAGE_WIDTH, defaultSocialImageMeta, withAgentNativeSocialImageCacheBuster, withDefaultSocialImage, } from "./social-meta.js";
13
14
  export { EMBED_MODE_QUERY_PARAM, EMBED_SESSION_COOKIE, EMBED_START_PATH, EMBED_TOKEN_QUERY_PARAM, } from "./embed-auth.js";
14
15
  export { AGENT_ACCESS_PARAM, DEFAULT_AGENT_ACCESS_TTL_SECONDS, appendAgentAccessParam, buildAgentAccessApiUrl, buildAgentAccessUrl, normalizeAgentAccessBasePath, normalizeAgentAccessOrigin, scopedAgentAccessResourceId, toAgentAccessUrl, } from "./agent-access.js";
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Shared recurring-poll engine used by both `usePollLoop` (client) and
3
+ * `startIntervalJob` (server) so every polling loop in the codebase gets the
4
+ * same three properties for free instead of hand-rolling them per call site:
5
+ *
6
+ * 1. Never overlaps: the next attempt is scheduled via `setTimeout` only
7
+ * after the current one settles, not on a fixed `setInterval` cadence.
8
+ * This also avoids the latency `setInterval` + skip-if-busy introduces —
9
+ * a skipped tick there means waiting a full extra cadence before the next
10
+ * retry; here the gap after completion is always exactly `intervalMs`.
11
+ * 2. Never stalls silently: every attempt is bounded by a timeout (paired
12
+ * with an `AbortController` so a `fetch` can actually cancel, not just be
13
+ * abandoned), and that timeout reports through `onError` the moment it
14
+ * fires. An attempt that ignores its `signal` keeps holding the loop —
15
+ * reported, not hidden — because releasing the slot to the next attempt
16
+ * would trade a visible stall for a silent overlap.
17
+ * 3. Never double-fires: a single internal in-flight flag, owned by this
18
+ * engine rather than duplicated in every caller.
19
+ */
20
+ export interface PollEngineOptions {
21
+ /** Delay in ms before the next attempt. Evaluated fresh before each schedule. */
22
+ intervalMs: number | (() => number);
23
+ /** Per-attempt timeout. Default: `Math.max(timeoutFloorMs, intervalMs * 4)`. */
24
+ timeoutMs?: number | (() => number);
25
+ /** Floor used by the default `timeoutMs`. Default: 10_000. */
26
+ timeoutFloorMs?: number;
27
+ /** Called when an attempt throws or times out. Default: swallow. */
28
+ onError?: (err: unknown) => void;
29
+ /** Run the first attempt immediately on `start()`. Default: true. */
30
+ leading?: boolean;
31
+ }
32
+ export interface PollEngineHandle {
33
+ /** Arm the loop. No-op if already running. */
34
+ start(): void;
35
+ /** Halt the loop and abort any in-flight attempt; its eventual settlement is a no-op. */
36
+ stop(): void;
37
+ /** Cancel the pending wait and run now. No-op if not running or an attempt is already in flight. */
38
+ pollNow(): void;
39
+ /**
40
+ * Re-arm the pending wait using a freshly resolved `intervalMs`, without
41
+ * running an attempt. Use this when `intervalMs` is a function whose
42
+ * result can change externally (e.g. tab visibility) — otherwise a
43
+ * change only takes effect the next time an attempt completes. No-op if
44
+ * there's no pending wait (not running, or an attempt is in flight).
45
+ */
46
+ reschedule(): void;
47
+ readonly isRunning: boolean;
48
+ }
49
+ export declare function createPollEngine(attempt: (signal: AbortSignal) => Promise<void>, options: PollEngineOptions): PollEngineHandle;
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Shared recurring-poll engine used by both `usePollLoop` (client) and
3
+ * `startIntervalJob` (server) so every polling loop in the codebase gets the
4
+ * same three properties for free instead of hand-rolling them per call site:
5
+ *
6
+ * 1. Never overlaps: the next attempt is scheduled via `setTimeout` only
7
+ * after the current one settles, not on a fixed `setInterval` cadence.
8
+ * This also avoids the latency `setInterval` + skip-if-busy introduces —
9
+ * a skipped tick there means waiting a full extra cadence before the next
10
+ * retry; here the gap after completion is always exactly `intervalMs`.
11
+ * 2. Never stalls silently: every attempt is bounded by a timeout (paired
12
+ * with an `AbortController` so a `fetch` can actually cancel, not just be
13
+ * abandoned), and that timeout reports through `onError` the moment it
14
+ * fires. An attempt that ignores its `signal` keeps holding the loop —
15
+ * reported, not hidden — because releasing the slot to the next attempt
16
+ * would trade a visible stall for a silent overlap.
17
+ * 3. Never double-fires: a single internal in-flight flag, owned by this
18
+ * engine rather than duplicated in every caller.
19
+ */
20
+ const DEFAULT_TIMEOUT_FLOOR_MS = 10_000;
21
+ function resolve(value) {
22
+ return typeof value === "function" ? value() : value;
23
+ }
24
+ function maybeUnref(timer) {
25
+ if (timer &&
26
+ typeof timer === "object" &&
27
+ "unref" in timer &&
28
+ typeof timer.unref === "function") {
29
+ timer.unref();
30
+ }
31
+ }
32
+ export function createPollEngine(attempt, options) {
33
+ const timeoutFloorMs = options.timeoutFloorMs ?? DEFAULT_TIMEOUT_FLOOR_MS;
34
+ const getTimeoutMs = () => options.timeoutMs != null
35
+ ? resolve(options.timeoutMs)
36
+ : Math.max(timeoutFloorMs, resolve(options.intervalMs) * 4);
37
+ const onError = options.onError ?? (() => { });
38
+ const leading = options.leading ?? true;
39
+ let generation = 0;
40
+ let running = false;
41
+ let inFlight = false;
42
+ let timer = null;
43
+ let activeController = null;
44
+ function clearTimer() {
45
+ if (timer != null) {
46
+ clearTimeout(timer);
47
+ timer = null;
48
+ }
49
+ }
50
+ function schedule(gen) {
51
+ clearTimer();
52
+ timer = setTimeout(() => {
53
+ timer = null;
54
+ void tick(gen);
55
+ }, resolve(options.intervalMs));
56
+ maybeUnref(timer);
57
+ }
58
+ async function tick(gen) {
59
+ if (gen !== generation || !running)
60
+ return;
61
+ if (inFlight) {
62
+ // An attempt from a superseded generation is still settling — stop()
63
+ // aborted it but the caller has not returned yet. Its own `finally`
64
+ // sees a stale generation and will not reschedule, so re-arming here
65
+ // is what keeps a stop()/start() cycle (tab hidden then visible) from
66
+ // leaving the engine alive-but-timerless, i.e. permanently dead.
67
+ schedule(gen);
68
+ return;
69
+ }
70
+ inFlight = true;
71
+ const controller = new AbortController();
72
+ activeController = controller;
73
+ const timeoutMs = getTimeoutMs();
74
+ const abortTimer = setTimeout(() => controller.abort(), timeoutMs);
75
+ maybeUnref(abortTimer);
76
+ let reported = false;
77
+ const report = (err) => {
78
+ if (reported)
79
+ return;
80
+ reported = true;
81
+ onError(err);
82
+ };
83
+ // The attempt is retained separately from the timeout race below. The
84
+ // timeout reports a slow attempt immediately, but the in-flight slot is
85
+ // only released once the attempt itself settles: releasing on the
86
+ // timeout would let the next tick start while an attempt that ignores
87
+ // `signal` is still doing work, which is the overlap (duplicate external
88
+ // requests, racing writes) this engine exists to prevent. An attempt that
89
+ // never settles blocks further attempts by design — `onError` has already
90
+ // fired, so it is loud rather than silent.
91
+ const settled = Promise.resolve()
92
+ .then(() => attempt(controller.signal))
93
+ .then(() => { }, (err) => report(err));
94
+ try {
95
+ await Promise.race([
96
+ settled,
97
+ new Promise((_, reject) => {
98
+ controller.signal.addEventListener("abort", () => reject(new Error(`poll attempt timed out after ${timeoutMs}ms`)), { once: true });
99
+ }),
100
+ ]);
101
+ }
102
+ catch (err) {
103
+ report(err);
104
+ }
105
+ finally {
106
+ await settled;
107
+ clearTimeout(abortTimer);
108
+ if (activeController === controller)
109
+ activeController = null;
110
+ inFlight = false;
111
+ if (gen === generation && running)
112
+ schedule(gen);
113
+ }
114
+ }
115
+ return {
116
+ start() {
117
+ if (running)
118
+ return;
119
+ running = true;
120
+ generation++;
121
+ const gen = generation;
122
+ if (leading)
123
+ void tick(gen);
124
+ else
125
+ schedule(gen);
126
+ },
127
+ stop() {
128
+ running = false;
129
+ generation++;
130
+ clearTimer();
131
+ activeController?.abort();
132
+ },
133
+ pollNow() {
134
+ if (!running || inFlight)
135
+ return;
136
+ clearTimer();
137
+ void tick(generation);
138
+ },
139
+ reschedule() {
140
+ if (!running || inFlight || timer == null)
141
+ return;
142
+ schedule(generation);
143
+ },
144
+ get isRunning() {
145
+ return running;
146
+ },
147
+ };
148
+ }
@@ -29,6 +29,11 @@ Templates use `app/i18n/`:
29
29
  Use BCP-47 filenames from the supported set: `en-US`, `zh-CN`, `es-ES`,
30
30
  `fr-FR`, `de-DE`, `ja-JP`, `ko-KR`, `pt-BR`, `hi-IN`, `ar-SA`.
31
31
 
32
+ If an app only ships translations for a subset of that set, set
33
+ `supportedLocales` on the `AgentNativeI18nCatalog` to that subset. Without it,
34
+ `<LanguagePicker />` lists every framework-supported locale regardless of
35
+ whether `loadMessages` can actually resolve it.
36
+
32
37
  ## UI Pattern
33
38
 
34
39
  - Wrap apps with `AppProviders i18n={{ catalog: i18nCatalog }}`.
@@ -29,6 +29,11 @@ Templates use `app/i18n/`:
29
29
  Use BCP-47 filenames from the supported set: `en-US`, `zh-CN`, `es-ES`,
30
30
  `fr-FR`, `de-DE`, `ja-JP`, `ko-KR`, `pt-BR`, `hi-IN`, `ar-SA`.
31
31
 
32
+ If an app only ships translations for a subset of that set, set
33
+ `supportedLocales` on the `AgentNativeI18nCatalog` to that subset. Without it,
34
+ `<LanguagePicker />` lists every framework-supported locale regardless of
35
+ whether `loadMessages` can actually resolve it.
36
+
32
37
  ## UI Pattern
33
38
 
34
39
  - Wrap apps with `AppProviders i18n={{ catalog: i18nCatalog }}`.
@@ -38,7 +38,16 @@ function setTimer(t) {
38
38
  function enqueue(url, body, headers, options) {
39
39
  const queue = getQueue();
40
40
  queue.push({ url, body, headers });
41
- if (options?.flushImmediately || queue.length >= MAX_BATCH_SIZE) {
41
+ // The batch timer is unref'd so it never keeps a long-lived server alive —
42
+ // but on Netlify/Vercel/Lambda the execution environment can freeze the
43
+ // moment the event loop looks empty, which is exactly when an unref'd timer
44
+ // doesn't count. Without a signal, a warm container freezing between
45
+ // invocations never fires that timer, silently dropping every provider's
46
+ // queued events for a request that ends in a crash. Default to flushing
47
+ // synchronously with the response in that environment; a caller (e.g.
48
+ // Agent Native Analytics' flush-mode override) can still force either way.
49
+ const flushImmediately = options?.flushImmediately ?? isServerlessRuntime();
50
+ if (flushImmediately || queue.length >= MAX_BATCH_SIZE) {
42
51
  void drainQueue();
43
52
  }
44
53
  else if (!getTimer()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.144.2",
3
+ "version": "0.144.3",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -72,6 +72,7 @@
72
72
  "./server/request-context": "./dist/server/request-context.js",
73
73
  "./server/design-token-utils": "./dist/server/design-token-utils.js",
74
74
  "./server/poll": "./dist/server/poll.js",
75
+ "./server/interval-job": "./dist/server/interval-job.js",
75
76
  "./server/sse": "./dist/server/sse.js",
76
77
  "./server/short-lived-token": "./dist/server/short-lived-token.js",
77
78
  "./integrations": "./dist/integrations/index.js",
@@ -411,8 +412,8 @@
411
412
  "y-protocols": "^1.0.7",
412
413
  "yjs": "^13.6.31",
413
414
  "zod": "^4.3.6",
414
- "@agent-native/recap-cli": "0.5.3",
415
- "@agent-native/toolkit": "^0.13.3"
415
+ "@agent-native/toolkit": "^0.13.3",
416
+ "@agent-native/recap-cli": "0.5.3"
416
417
  },
417
418
  "devDependencies": {
418
419
  "@ai-sdk/anthropic": "^3.0.71",
@@ -29,6 +29,11 @@ Templates use `app/i18n/`:
29
29
  Use BCP-47 filenames from the supported set: `en-US`, `zh-CN`, `es-ES`,
30
30
  `fr-FR`, `de-DE`, `ja-JP`, `ko-KR`, `pt-BR`, `hi-IN`, `ar-SA`.
31
31
 
32
+ If an app only ships translations for a subset of that set, set
33
+ `supportedLocales` on the `AgentNativeI18nCatalog` to that subset. Without it,
34
+ `<LanguagePicker />` lists every framework-supported locale regardless of
35
+ whether `loadMessages` can actually resolve it.
36
+
32
37
  ## UI Pattern
33
38
 
34
39
  - Wrap apps with `AppProviders i18n={{ catalog: i18nCatalog }}`.
@@ -29,6 +29,11 @@ Templates use `app/i18n/`:
29
29
  Use BCP-47 filenames from the supported set: `en-US`, `zh-CN`, `es-ES`,
30
30
  `fr-FR`, `de-DE`, `ja-JP`, `ko-KR`, `pt-BR`, `hi-IN`, `ar-SA`.
31
31
 
32
+ If an app only ships translations for a subset of that set, set
33
+ `supportedLocales` on the `AgentNativeI18nCatalog` to that subset. Without it,
34
+ `<LanguagePicker />` lists every framework-supported locale regardless of
35
+ whether `loadMessages` can actually resolve it.
36
+
32
37
  ## UI Pattern
33
38
 
34
39
  - Wrap apps with `AppProviders i18n={{ catalog: i18nCatalog }}`.