@objectstack/runtime 3.2.5 → 3.2.7

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/dist/index.d.cts CHANGED
@@ -83,6 +83,7 @@ declare class DriverPlugin implements Plugin {
83
83
  * Responsibilities:
84
84
  * 1. Register App Manifest as a service (for ObjectQL discovery)
85
85
  * 2. Execute Runtime `onEnable` hook (for code logic)
86
+ * 3. Auto-load i18n translation bundles into the kernel's i18n service
86
87
  */
87
88
  declare class AppPlugin implements Plugin {
88
89
  name: string;
@@ -92,6 +93,15 @@ declare class AppPlugin implements Plugin {
92
93
  constructor(bundle: any);
93
94
  init: (ctx: PluginContext) => Promise<void>;
94
95
  start: (ctx: PluginContext) => Promise<void>;
96
+ /**
97
+ * Auto-load i18n translation bundles from the app config into the
98
+ * kernel's i18n service. Handles both `translations` (array of
99
+ * TranslationBundle) and `i18n` config (default locale, etc.).
100
+ *
101
+ * Gracefully skips when the i18n service is not registered —
102
+ * this keeps AppPlugin resilient across server/dev/mock environments.
103
+ */
104
+ private loadTranslations;
95
105
  }
96
106
 
97
107
  interface Logger {
@@ -155,7 +165,7 @@ interface DispatcherPluginConfig {
155
165
  * - /graphql (GraphQL)
156
166
  * - /analytics (BI queries)
157
167
  * - /packages (package management)
158
-
168
+ * - /i18n (internationalization — locales, translations, field labels)
159
169
  * - /storage (file storage)
160
170
  * - /automation (CRUD + triggers + runs)
161
171
  *
@@ -279,9 +289,13 @@ declare class HttpDispatcher {
279
289
  private error;
280
290
  private ensureBroker;
281
291
  /**
282
- * Generates the discovery JSON response for the API root
292
+ * Generates the discovery JSON response for the API root.
293
+ *
294
+ * Uses the same async `resolveService()` fallback chain that request
295
+ * handlers use, so the reported service status is always consistent
296
+ * with the actual runtime availability.
283
297
  */
284
- getDiscoveryInfo(prefix: string): {
298
+ getDiscoveryInfo(prefix: string): Promise<{
285
299
  name: string;
286
300
  version: string;
287
301
  environment: string | undefined;
@@ -498,7 +512,7 @@ declare class HttpDispatcher {
498
512
  supported: string[];
499
513
  timezone: string;
500
514
  };
501
- };
515
+ }>;
502
516
  /**
503
517
  * Handles GraphQL requests
504
518
  */
package/dist/index.d.ts CHANGED
@@ -83,6 +83,7 @@ declare class DriverPlugin implements Plugin {
83
83
  * Responsibilities:
84
84
  * 1. Register App Manifest as a service (for ObjectQL discovery)
85
85
  * 2. Execute Runtime `onEnable` hook (for code logic)
86
+ * 3. Auto-load i18n translation bundles into the kernel's i18n service
86
87
  */
87
88
  declare class AppPlugin implements Plugin {
88
89
  name: string;
@@ -92,6 +93,15 @@ declare class AppPlugin implements Plugin {
92
93
  constructor(bundle: any);
93
94
  init: (ctx: PluginContext) => Promise<void>;
94
95
  start: (ctx: PluginContext) => Promise<void>;
96
+ /**
97
+ * Auto-load i18n translation bundles from the app config into the
98
+ * kernel's i18n service. Handles both `translations` (array of
99
+ * TranslationBundle) and `i18n` config (default locale, etc.).
100
+ *
101
+ * Gracefully skips when the i18n service is not registered —
102
+ * this keeps AppPlugin resilient across server/dev/mock environments.
103
+ */
104
+ private loadTranslations;
95
105
  }
96
106
 
97
107
  interface Logger {
@@ -155,7 +165,7 @@ interface DispatcherPluginConfig {
155
165
  * - /graphql (GraphQL)
156
166
  * - /analytics (BI queries)
157
167
  * - /packages (package management)
158
-
168
+ * - /i18n (internationalization — locales, translations, field labels)
159
169
  * - /storage (file storage)
160
170
  * - /automation (CRUD + triggers + runs)
161
171
  *
@@ -279,9 +289,13 @@ declare class HttpDispatcher {
279
289
  private error;
280
290
  private ensureBroker;
281
291
  /**
282
- * Generates the discovery JSON response for the API root
292
+ * Generates the discovery JSON response for the API root.
293
+ *
294
+ * Uses the same async `resolveService()` fallback chain that request
295
+ * handlers use, so the reported service status is always consistent
296
+ * with the actual runtime availability.
283
297
  */
284
- getDiscoveryInfo(prefix: string): {
298
+ getDiscoveryInfo(prefix: string): Promise<{
285
299
  name: string;
286
300
  version: string;
287
301
  environment: string | undefined;
@@ -498,7 +512,7 @@ declare class HttpDispatcher {
498
512
  supported: string[];
499
513
  timezone: string;
500
514
  };
501
- };
515
+ }>;
502
516
  /**
503
517
  * Handles GraphQL requests
504
518
  */
package/dist/index.js CHANGED
@@ -76,6 +76,7 @@ var DriverPlugin = class {
76
76
  };
77
77
 
78
78
  // src/seed-loader.ts
79
+ import { SeedLoaderConfigSchema } from "@objectstack/spec/data";
79
80
  var DEFAULT_EXTERNAL_ID_FIELD = "name";
80
81
  var SeedLoaderService = class {
81
82
  constructor(engine, metadata, logger) {
@@ -160,7 +161,6 @@ var SeedLoaderService = class {
160
161
  return { nodes, insertOrder, circularDependencies };
161
162
  }
162
163
  async validate(datasets, config) {
163
- const { SeedLoaderConfigSchema } = await import("@objectstack/spec/data");
164
164
  const parsedConfig = SeedLoaderConfigSchema.parse({ ...config, dryRun: true });
165
165
  return this.load({ datasets, config: parsedConfig });
166
166
  }
@@ -606,7 +606,11 @@ var AppPlugin = class {
606
606
  this.start = async (ctx) => {
607
607
  const sys = this.bundle.manifest || this.bundle;
608
608
  const appId = sys.id || sys.name;
609
- const ql = ctx.getService("objectql");
609
+ let ql;
610
+ try {
611
+ ql = ctx.getService("objectql");
612
+ } catch {
613
+ }
610
614
  if (!ql) {
611
615
  ctx.logger.warn("ObjectQL engine service not found", {
612
616
  appName: this.name,
@@ -640,6 +644,7 @@ var AppPlugin = class {
640
644
  } else {
641
645
  ctx.logger.debug("No runtime.onEnable function found", { appId });
642
646
  }
647
+ this.loadTranslations(ctx, appId);
643
648
  const seedDatasets = [];
644
649
  if (Array.isArray(this.bundle.data)) {
645
650
  seedDatasets.push(...this.bundle.data);
@@ -710,10 +715,72 @@ var AppPlugin = class {
710
715
  this.name = `plugin.app.${appId}`;
711
716
  this.version = sys.version;
712
717
  }
718
+ /**
719
+ * Auto-load i18n translation bundles from the app config into the
720
+ * kernel's i18n service. Handles both `translations` (array of
721
+ * TranslationBundle) and `i18n` config (default locale, etc.).
722
+ *
723
+ * Gracefully skips when the i18n service is not registered —
724
+ * this keeps AppPlugin resilient across server/dev/mock environments.
725
+ */
726
+ loadTranslations(ctx, appId) {
727
+ let i18nService;
728
+ try {
729
+ i18nService = ctx.getService("i18n");
730
+ } catch {
731
+ }
732
+ const bundles = [];
733
+ if (Array.isArray(this.bundle.translations)) {
734
+ bundles.push(...this.bundle.translations);
735
+ }
736
+ const manifest = this.bundle.manifest || this.bundle;
737
+ if (manifest && Array.isArray(manifest.translations) && manifest.translations !== this.bundle.translations) {
738
+ bundles.push(...manifest.translations);
739
+ }
740
+ if (!i18nService) {
741
+ if (bundles.length > 0) {
742
+ ctx.logger.warn(
743
+ `[i18n] App "${appId}" has ${bundles.length} translation bundle(s) but no i18n service is registered. Translations will not be served via REST API. Register I18nServicePlugin from @objectstack/service-i18n, or use DevPlugin which auto-detects translations and registers the i18n service automatically.`
744
+ );
745
+ } else {
746
+ ctx.logger.debug("[i18n] No i18n service registered; skipping translation loading", { appId });
747
+ }
748
+ return;
749
+ }
750
+ const i18nConfig = this.bundle.i18n || (this.bundle.manifest || this.bundle)?.i18n;
751
+ if (i18nConfig?.defaultLocale && typeof i18nService.setDefaultLocale === "function") {
752
+ i18nService.setDefaultLocale(i18nConfig.defaultLocale);
753
+ ctx.logger.debug("[i18n] Set default locale", { appId, locale: i18nConfig.defaultLocale });
754
+ }
755
+ if (bundles.length === 0) {
756
+ return;
757
+ }
758
+ let loadedLocales = 0;
759
+ for (const bundle of bundles) {
760
+ for (const [locale, data] of Object.entries(bundle)) {
761
+ if (data && typeof data === "object") {
762
+ try {
763
+ i18nService.loadTranslations(locale, data);
764
+ loadedLocales++;
765
+ } catch (err) {
766
+ ctx.logger.warn("[i18n] Failed to load translations", { appId, locale, error: err.message });
767
+ }
768
+ }
769
+ }
770
+ }
771
+ const svcAny = i18nService;
772
+ if (svcAny._fallback || svcAny._dev) {
773
+ ctx.logger.info(
774
+ `[i18n] Loaded ${loadedLocales} locale(s) into in-memory i18n fallback for "${appId}". For production, consider registering I18nServicePlugin from @objectstack/service-i18n.`
775
+ );
776
+ } else {
777
+ ctx.logger.info("[i18n] Loaded translation bundles", { appId, bundles: bundles.length, locales: loadedLocales });
778
+ }
779
+ }
713
780
  };
714
781
 
715
782
  // src/http-dispatcher.ts
716
- import { getEnv } from "@objectstack/core";
783
+ import { getEnv, resolveLocale } from "@objectstack/core";
717
784
  import { CoreServiceName } from "@objectstack/spec/system";
718
785
  function randomUUID() {
719
786
  if (globalThis.crypto && typeof globalThis.crypto.randomUUID === "function") {
@@ -749,25 +816,61 @@ var HttpDispatcher = class {
749
816
  return this.kernel.broker;
750
817
  }
751
818
  /**
752
- * Generates the discovery JSON response for the API root
819
+ * Generates the discovery JSON response for the API root.
820
+ *
821
+ * Uses the same async `resolveService()` fallback chain that request
822
+ * handlers use, so the reported service status is always consistent
823
+ * with the actual runtime availability.
753
824
  */
754
- getDiscoveryInfo(prefix) {
755
- const services = this.getServicesMap();
756
- const hasAuth = !!services[CoreServiceName.enum.auth];
757
- const hasGraphQL = !!(services[CoreServiceName.enum.graphql] || this.kernel.graphql);
758
- const hasSearch = !!services[CoreServiceName.enum.search];
759
- const hasWebSockets = !!services[CoreServiceName.enum.realtime];
760
- const hasFiles = !!(services[CoreServiceName.enum["file-storage"]] || services["storage"]?.supportsFiles);
761
- const hasAnalytics = !!services[CoreServiceName.enum.analytics];
762
- const hasWorkflow = !!services[CoreServiceName.enum.workflow];
763
- const hasAi = !!services[CoreServiceName.enum.ai];
764
- const hasNotification = !!services[CoreServiceName.enum.notification];
765
- const hasI18n = !!services[CoreServiceName.enum.i18n];
766
- const hasUi = !!services[CoreServiceName.enum.ui];
767
- const hasAutomation = !!services[CoreServiceName.enum.automation];
768
- const hasCache = !!services[CoreServiceName.enum.cache];
769
- const hasQueue = !!services[CoreServiceName.enum.queue];
770
- const hasJob = !!services[CoreServiceName.enum.job];
825
+ async getDiscoveryInfo(prefix) {
826
+ const [
827
+ authSvc,
828
+ graphqlSvc,
829
+ searchSvc,
830
+ realtimeSvc,
831
+ filesSvc,
832
+ analyticsSvc,
833
+ workflowSvc,
834
+ aiSvc,
835
+ notificationSvc,
836
+ i18nSvc,
837
+ uiSvc,
838
+ automationSvc,
839
+ cacheSvc,
840
+ queueSvc,
841
+ jobSvc
842
+ ] = await Promise.all([
843
+ this.resolveService(CoreServiceName.enum.auth),
844
+ this.resolveService(CoreServiceName.enum.graphql),
845
+ this.resolveService(CoreServiceName.enum.search),
846
+ this.resolveService(CoreServiceName.enum.realtime),
847
+ this.resolveService(CoreServiceName.enum["file-storage"]),
848
+ this.resolveService(CoreServiceName.enum.analytics),
849
+ this.resolveService(CoreServiceName.enum.workflow),
850
+ this.resolveService(CoreServiceName.enum.ai),
851
+ this.resolveService(CoreServiceName.enum.notification),
852
+ this.resolveService(CoreServiceName.enum.i18n),
853
+ this.resolveService(CoreServiceName.enum.ui),
854
+ this.resolveService(CoreServiceName.enum.automation),
855
+ this.resolveService(CoreServiceName.enum.cache),
856
+ this.resolveService(CoreServiceName.enum.queue),
857
+ this.resolveService(CoreServiceName.enum.job)
858
+ ]);
859
+ const hasAuth = !!authSvc;
860
+ const hasGraphQL = !!(graphqlSvc || this.kernel.graphql);
861
+ const hasSearch = !!searchSvc;
862
+ const hasWebSockets = !!realtimeSvc;
863
+ const hasFiles = !!filesSvc;
864
+ const hasAnalytics = !!analyticsSvc;
865
+ const hasWorkflow = !!workflowSvc;
866
+ const hasAi = !!aiSvc;
867
+ const hasNotification = !!notificationSvc;
868
+ const hasI18n = !!i18nSvc;
869
+ const hasUi = !!uiSvc;
870
+ const hasAutomation = !!automationSvc;
871
+ const hasCache = !!cacheSvc;
872
+ const hasQueue = !!queueSvc;
873
+ const hasJob = !!jobSvc;
771
874
  const routes = {
772
875
  data: `${prefix}/data`,
773
876
  metadata: `${prefix}/meta`,
@@ -795,6 +898,16 @@ var HttpDispatcher = class {
795
898
  status: "unavailable",
796
899
  message: `Install a ${name} plugin to enable`
797
900
  });
901
+ let locale = { default: "en", supported: ["en"], timezone: "UTC" };
902
+ if (hasI18n && i18nSvc) {
903
+ const defaultLocale = typeof i18nSvc.getDefaultLocale === "function" ? i18nSvc.getDefaultLocale() : "en";
904
+ const locales = typeof i18nSvc.getLocales === "function" ? i18nSvc.getLocales() : [];
905
+ locale = {
906
+ default: defaultLocale,
907
+ supported: locales.length > 0 ? locales : [defaultLocale],
908
+ timezone: "UTC"
909
+ };
910
+ }
798
911
  return {
799
912
  name: "ObjectOS",
800
913
  version: "1.0.0",
@@ -834,11 +947,7 @@ var HttpDispatcher = class {
834
947
  "file-storage": hasFiles ? svcAvailable(routes.storage) : svcUnavailable("file-storage"),
835
948
  search: hasSearch ? svcAvailable() : svcUnavailable("search")
836
949
  },
837
- locale: {
838
- default: "en",
839
- supported: ["en", "zh-CN"],
840
- timezone: "UTC"
841
- }
950
+ locale
842
951
  };
843
952
  }
844
953
  /**
@@ -1152,13 +1261,24 @@ var HttpDispatcher = class {
1152
1261
  if (parts[0] === "translations") {
1153
1262
  const locale = parts[1] ? decodeURIComponent(parts[1]) : query?.locale;
1154
1263
  if (!locale) return { handled: true, response: this.error("Missing locale parameter", 400) };
1155
- const translations = i18nService.getTranslations(locale);
1264
+ let translations = i18nService.getTranslations(locale);
1265
+ if (Object.keys(translations).length === 0) {
1266
+ const availableLocales = typeof i18nService.getLocales === "function" ? i18nService.getLocales() : [];
1267
+ const resolved = resolveLocale(locale, availableLocales);
1268
+ if (resolved && resolved !== locale) {
1269
+ translations = i18nService.getTranslations(resolved);
1270
+ return { handled: true, response: this.success({ locale: resolved, requestedLocale: locale, translations }) };
1271
+ }
1272
+ }
1156
1273
  return { handled: true, response: this.success({ locale, translations }) };
1157
1274
  }
1158
1275
  if (parts[0] === "labels" && parts.length >= 2) {
1159
1276
  const objectName = decodeURIComponent(parts[1]);
1160
- const locale = parts[2] ? decodeURIComponent(parts[2]) : query?.locale;
1277
+ let locale = parts[2] ? decodeURIComponent(parts[2]) : query?.locale;
1161
1278
  if (!locale) return { handled: true, response: this.error("Missing locale parameter", 400) };
1279
+ const availableLocales = typeof i18nService.getLocales === "function" ? i18nService.getLocales() : [];
1280
+ const resolved = resolveLocale(locale, availableLocales);
1281
+ if (resolved) locale = resolved;
1162
1282
  if (typeof i18nService.getFieldLabels === "function") {
1163
1283
  const labels2 = i18nService.getFieldLabels(objectName, locale);
1164
1284
  return { handled: true, response: this.success({ object: objectName, locale, labels: labels2 }) };
@@ -1533,7 +1653,7 @@ var HttpDispatcher = class {
1533
1653
  async dispatch(method, path, body, query, context) {
1534
1654
  const cleanPath = path.replace(/\/$/, "");
1535
1655
  if (cleanPath === "" && method === "GET") {
1536
- const info = this.getDiscoveryInfo("");
1656
+ const info = await this.getDiscoveryInfo("");
1537
1657
  return {
1538
1658
  handled: true,
1539
1659
  response: this.success(info)
@@ -1681,10 +1801,10 @@ function createDispatcherPlugin(config = {}) {
1681
1801
  const dispatcher = new HttpDispatcher(kernel);
1682
1802
  const prefix = config.prefix || "/api/v1";
1683
1803
  server.get("/.well-known/objectstack", async (_req, res) => {
1684
- res.json({ data: dispatcher.getDiscoveryInfo(prefix) });
1804
+ res.json({ data: await dispatcher.getDiscoveryInfo(prefix) });
1685
1805
  });
1686
1806
  server.get(`${prefix}/discovery`, async (_req, res) => {
1687
- res.json({ data: dispatcher.getDiscoveryInfo(prefix) });
1807
+ res.json({ data: await dispatcher.getDiscoveryInfo(prefix) });
1688
1808
  });
1689
1809
  server.post(`${prefix}/auth/login`, async (req, res) => {
1690
1810
  try {
@@ -1806,6 +1926,30 @@ function createDispatcherPlugin(config = {}) {
1806
1926
  errorResponse(err, res);
1807
1927
  }
1808
1928
  });
1929
+ server.get(`${prefix}/i18n/locales`, async (req, res) => {
1930
+ try {
1931
+ const result = await dispatcher.handleI18n("/locales", "GET", req.query, { request: req });
1932
+ sendResult(result, res);
1933
+ } catch (err) {
1934
+ errorResponse(err, res);
1935
+ }
1936
+ });
1937
+ server.get(`${prefix}/i18n/translations/:locale`, async (req, res) => {
1938
+ try {
1939
+ const result = await dispatcher.handleI18n(`/translations/${req.params.locale}`, "GET", req.query, { request: req });
1940
+ sendResult(result, res);
1941
+ } catch (err) {
1942
+ errorResponse(err, res);
1943
+ }
1944
+ });
1945
+ server.get(`${prefix}/i18n/labels/:object/:locale`, async (req, res) => {
1946
+ try {
1947
+ const result = await dispatcher.handleI18n(`/labels/${req.params.object}/${req.params.locale}`, "GET", req.query, { request: req });
1948
+ sendResult(result, res);
1949
+ } catch (err) {
1950
+ errorResponse(err, res);
1951
+ }
1952
+ });
1809
1953
  server.get(`${prefix}/automation`, async (req, res) => {
1810
1954
  try {
1811
1955
  const result = await dispatcher.handleAutomation("", "GET", {}, { request: req });