@shipeasy/sdk 2.1.7 → 2.1.8

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.
@@ -192,37 +192,31 @@ interface LabelAttrs {
192
192
  "data-label-desc"?: string;
193
193
  }
194
194
  declare function labelAttrs(key: string, variables?: Record<string, string | number>, desc?: string): LabelAttrs;
195
- /**
196
- * Universal i18n facade. Backed by the `window.i18n` global the loader
197
- * script installs. Returns the key itself when the loader hasn't run
198
- * (SSR, missing script tag, before profile fetch completes), so call
199
- * sites never need to null-check.
200
- */
201
- declare const i18n: {
202
- t(key: string, variables?: Record<string, string | number>): string;
203
- /**
204
- * Translate a key and return a framework element (e.g. React <span>)
205
- * carrying `data-label` / `data-variables` attributes so the ShipEasy
206
- * devtools "Edit labels" overlay can highlight and edit it in place.
207
- *
208
- * Requires a one-time setup call: `i18n.configure({ createElement })`.
209
- * The returned value is whatever `createElement` returns — pass React's
210
- * `createElement`, Vue's `h`, Solid's `createSignal`-based factory, etc.
211
- *
212
- * Falls back to a plain translated string if `createElement` was not
213
- * configured (e.g. server-side or in non-JSX contexts).
214
- */
215
- tEl(key: string, fallback: string, variables?: Record<string, string | number>, desc?: string): any;
216
- /** Wire up the element creator once at app startup (call before any tEl use). */
195
+ type I18nVariables = Record<string, string | number | null | undefined>;
196
+ type I18nTagRenderer = (content: string) => unknown;
197
+ type I18nRichComponents = Record<string, I18nTagRenderer>;
198
+ declare const __i18nKeyBrand: unique symbol;
199
+ declare const __i18nStringBrand: unique symbol;
200
+ type I18nKey = string & {
201
+ readonly [__i18nKeyBrand]?: never;
202
+ };
203
+ type I18nString = string & {
204
+ readonly [__i18nStringBrand]?: never;
205
+ };
206
+ interface I18nFacade {
207
+ t<F extends string>(key: I18nKey, fallback: F, variables?: I18nVariables): F & I18nString;
208
+ t(key: I18nKey, variables?: I18nVariables): I18nString;
209
+ rich(key: I18nKey, fallback: string, components?: I18nRichComponents, variables?: I18nVariables): unknown;
210
+ tEl<F extends string>(key: I18nKey, fallback: F, variables?: I18nVariables, desc?: string): F & I18nString;
217
211
  configure(opts: {
218
- createElement: (tag: string, props: object, children: string) => any;
212
+ components?: I18nRichComponents;
213
+ createElement?: (tag: string, props: object, children: string) => unknown;
219
214
  }): void;
220
215
  readonly locale: string | null;
221
216
  readonly ready: boolean;
222
- /** Resolves when the loader has installed window.i18n and fetched a profile. */
223
217
  whenReady(): Promise<void>;
224
- /** Subscribe to locale/profile updates. Returns an unsubscribe fn. */
225
218
  onUpdate(cb: () => void): () => void;
226
- };
219
+ }
220
+ declare const i18n: I18nFacade;
227
221
 
228
- export { type BootstrapPayload, type ExperimentResult, FlagsClientBrowser, type FlagsClientBrowserEnv, type FlagsClientBrowserOptions, LABEL_MARKER_END, LABEL_MARKER_RE, LABEL_MARKER_SEP, LABEL_MARKER_START, type LabelAttrs, type ShipeasyClientConfig, type ShipeasySdkBridge, type User, _resetShipeasyForTests, attachDevtools, configureShipeasy, encodeLabelMarker, flags, getShipeasyClient, i18n, isDevtoolsRequested, labelAttrs, loadDevtools, readConfigOverride, readExpOverride, readGateOverride, shipeasy, version };
222
+ export { type BootstrapPayload, type ExperimentResult, FlagsClientBrowser, type FlagsClientBrowserEnv, type FlagsClientBrowserOptions, type I18nFacade, type I18nKey, type I18nRichComponents, type I18nString, type I18nTagRenderer, type I18nVariables, LABEL_MARKER_END, LABEL_MARKER_RE, LABEL_MARKER_SEP, LABEL_MARKER_START, type LabelAttrs, type ShipeasyClientConfig, type ShipeasySdkBridge, type User, _resetShipeasyForTests, attachDevtools, configureShipeasy, encodeLabelMarker, flags, getShipeasyClient, i18n, isDevtoolsRequested, labelAttrs, loadDevtools, readConfigOverride, readExpOverride, readGateOverride, shipeasy, version };
@@ -192,37 +192,31 @@ interface LabelAttrs {
192
192
  "data-label-desc"?: string;
193
193
  }
194
194
  declare function labelAttrs(key: string, variables?: Record<string, string | number>, desc?: string): LabelAttrs;
195
- /**
196
- * Universal i18n facade. Backed by the `window.i18n` global the loader
197
- * script installs. Returns the key itself when the loader hasn't run
198
- * (SSR, missing script tag, before profile fetch completes), so call
199
- * sites never need to null-check.
200
- */
201
- declare const i18n: {
202
- t(key: string, variables?: Record<string, string | number>): string;
203
- /**
204
- * Translate a key and return a framework element (e.g. React <span>)
205
- * carrying `data-label` / `data-variables` attributes so the ShipEasy
206
- * devtools "Edit labels" overlay can highlight and edit it in place.
207
- *
208
- * Requires a one-time setup call: `i18n.configure({ createElement })`.
209
- * The returned value is whatever `createElement` returns — pass React's
210
- * `createElement`, Vue's `h`, Solid's `createSignal`-based factory, etc.
211
- *
212
- * Falls back to a plain translated string if `createElement` was not
213
- * configured (e.g. server-side or in non-JSX contexts).
214
- */
215
- tEl(key: string, fallback: string, variables?: Record<string, string | number>, desc?: string): any;
216
- /** Wire up the element creator once at app startup (call before any tEl use). */
195
+ type I18nVariables = Record<string, string | number | null | undefined>;
196
+ type I18nTagRenderer = (content: string) => unknown;
197
+ type I18nRichComponents = Record<string, I18nTagRenderer>;
198
+ declare const __i18nKeyBrand: unique symbol;
199
+ declare const __i18nStringBrand: unique symbol;
200
+ type I18nKey = string & {
201
+ readonly [__i18nKeyBrand]?: never;
202
+ };
203
+ type I18nString = string & {
204
+ readonly [__i18nStringBrand]?: never;
205
+ };
206
+ interface I18nFacade {
207
+ t<F extends string>(key: I18nKey, fallback: F, variables?: I18nVariables): F & I18nString;
208
+ t(key: I18nKey, variables?: I18nVariables): I18nString;
209
+ rich(key: I18nKey, fallback: string, components?: I18nRichComponents, variables?: I18nVariables): unknown;
210
+ tEl<F extends string>(key: I18nKey, fallback: F, variables?: I18nVariables, desc?: string): F & I18nString;
217
211
  configure(opts: {
218
- createElement: (tag: string, props: object, children: string) => any;
212
+ components?: I18nRichComponents;
213
+ createElement?: (tag: string, props: object, children: string) => unknown;
219
214
  }): void;
220
215
  readonly locale: string | null;
221
216
  readonly ready: boolean;
222
- /** Resolves when the loader has installed window.i18n and fetched a profile. */
223
217
  whenReady(): Promise<void>;
224
- /** Subscribe to locale/profile updates. Returns an unsubscribe fn. */
225
218
  onUpdate(cb: () => void): () => void;
226
- };
219
+ }
220
+ declare const i18n: I18nFacade;
227
221
 
228
- export { type BootstrapPayload, type ExperimentResult, FlagsClientBrowser, type FlagsClientBrowserEnv, type FlagsClientBrowserOptions, LABEL_MARKER_END, LABEL_MARKER_RE, LABEL_MARKER_SEP, LABEL_MARKER_START, type LabelAttrs, type ShipeasyClientConfig, type ShipeasySdkBridge, type User, _resetShipeasyForTests, attachDevtools, configureShipeasy, encodeLabelMarker, flags, getShipeasyClient, i18n, isDevtoolsRequested, labelAttrs, loadDevtools, readConfigOverride, readExpOverride, readGateOverride, shipeasy, version };
222
+ export { type BootstrapPayload, type ExperimentResult, FlagsClientBrowser, type FlagsClientBrowserEnv, type FlagsClientBrowserOptions, type I18nFacade, type I18nKey, type I18nRichComponents, type I18nString, type I18nTagRenderer, type I18nVariables, LABEL_MARKER_END, LABEL_MARKER_RE, LABEL_MARKER_SEP, LABEL_MARKER_START, type LabelAttrs, type ShipeasyClientConfig, type ShipeasySdkBridge, type User, _resetShipeasyForTests, attachDevtools, configureShipeasy, encodeLabelMarker, flags, getShipeasyClient, i18n, isDevtoolsRequested, labelAttrs, loadDevtools, readConfigOverride, readExpOverride, readGateOverride, shipeasy, version };
@@ -455,13 +455,15 @@ var FlagsClientBrowser = class {
455
455
  this.evalResult = data;
456
456
  }
457
457
  getFlag(name) {
458
+ if (this.evalResult === null) return false;
458
459
  const ov = readGateOverride(name);
459
460
  if (ov !== null) return ov;
460
- return this.evalResult?.flags[name] ?? false;
461
+ return this.evalResult.flags[name] ?? false;
461
462
  }
462
463
  getConfig(name, decode) {
464
+ if (this.evalResult === null) return void 0;
463
465
  const ov = readConfigOverride(name);
464
- const raw = ov !== void 0 ? ov : this.evalResult?.configs?.[name];
466
+ const raw = ov !== void 0 ? ov : this.evalResult.configs?.[name];
465
467
  if (raw === void 0) return void 0;
466
468
  if (!decode) return raw;
467
469
  try {
@@ -797,15 +799,114 @@ function isEditLabelsMode() {
797
799
  }
798
800
  function interpolate(raw, variables) {
799
801
  if (!variables) return raw;
800
- return raw.replace(/\{\{(\w+)\}\}/g, (_, k) => String(variables[k] ?? `{{${k}}}`));
802
+ return raw.replace(/\{\{(\w+)\}\}/g, (placeholder, k) => {
803
+ const v = variables[k];
804
+ return v != null ? String(v) : placeholder;
805
+ });
806
+ }
807
+ var _IS_BROWSER = typeof document !== "undefined";
808
+ var _RICH_HTML_TAGS = [
809
+ "b",
810
+ "i",
811
+ "u",
812
+ "s",
813
+ "em",
814
+ "strong",
815
+ "del",
816
+ "ins",
817
+ "mark",
818
+ "small",
819
+ "code",
820
+ "pre",
821
+ "kbd",
822
+ "sub",
823
+ "sup",
824
+ "span",
825
+ "a",
826
+ "p",
827
+ "br",
828
+ "hr"
829
+ ];
830
+ function _makeBuiltinTags() {
831
+ const tags = {};
832
+ for (const tag of _RICH_HTML_TAGS) {
833
+ tags[tag] = _IS_BROWSER ? (text) => {
834
+ const el = document.createElement(tag);
835
+ if (tag !== "br" && tag !== "hr") el.textContent = text;
836
+ return el;
837
+ } : (text) => tag === "br" || tag === "hr" ? `<${tag}>` : `<${tag}>${text}</${tag}>`;
838
+ }
839
+ return tags;
840
+ }
841
+ var _builtinTags = _makeBuiltinTags();
842
+ var _configuredComponents = {};
843
+ var _RICH_TAG_RE = /<(\w+)(?:\s*\/>|>([\s\S]*?)<\/\1>)/g;
844
+ function _parseRichText(text, components) {
845
+ const parts = [];
846
+ let lastIndex = 0;
847
+ let match;
848
+ let allStrings = true;
849
+ _RICH_TAG_RE.lastIndex = 0;
850
+ while ((match = _RICH_TAG_RE.exec(text)) !== null) {
851
+ if (match.index > lastIndex) parts.push(text.slice(lastIndex, match.index));
852
+ const tag = match[1];
853
+ const content = match[2] ?? "";
854
+ const renderer = components[tag] ?? _configuredComponents[tag] ?? _builtinTags[tag];
855
+ if (renderer) {
856
+ const rendered = renderer(content);
857
+ if (typeof rendered !== "string") allStrings = false;
858
+ parts.push(rendered);
859
+ } else {
860
+ parts.push(content);
861
+ }
862
+ lastIndex = _RICH_TAG_RE.lastIndex;
863
+ }
864
+ if (lastIndex < text.length) parts.push(text.slice(lastIndex));
865
+ if (allStrings) return parts.join("");
866
+ return parts;
867
+ }
868
+ function _resolveTranslation(key, variables) {
869
+ if (typeof window !== "undefined" && window.i18n) {
870
+ const v = window.i18n.t(key, variables);
871
+ return v === key ? void 0 : v;
872
+ }
873
+ const store = getSSRI18nStore();
874
+ if (store?.strings[key]) return interpolate(store.strings[key], variables);
875
+ return void 0;
801
876
  }
802
877
  var i18n = {
803
- t(key, variables) {
804
- if (typeof window !== "undefined" && window.i18n) return window.i18n.t(key, variables);
805
- const store = getSSRI18nStore();
806
- if (store?.strings[key]) return interpolate(store.strings[key], variables);
878
+ t(key, fallbackOrVars, maybeVars) {
879
+ let fallback;
880
+ let variables;
881
+ if (typeof fallbackOrVars === "string") {
882
+ fallback = fallbackOrVars;
883
+ variables = maybeVars;
884
+ } else {
885
+ variables = fallbackOrVars;
886
+ }
887
+ const resolved = _resolveTranslation(key, variables);
888
+ if (resolved !== void 0) return resolved;
889
+ if (fallback !== void 0) return interpolate(fallback, variables);
807
890
  return key;
808
891
  },
892
+ /**
893
+ * Translate a key whose value contains `<tag>content</tag>` segments and
894
+ * render the tagged segments via per-call `components`, `configure()`-supplied
895
+ * components, or the built-in HTML tag renderers.
896
+ *
897
+ * Return shape:
898
+ * - all renderers return strings → returns a concatenated `string`
899
+ * - any renderer returns a non-string (e.g. JSX, DOM node) → returns
900
+ * `Array<string | T>` and the caller is responsible for rendering
901
+ *
902
+ * Framework-agnostic: this method does pure string parsing + callback
903
+ * execution. No React / DOM dependency in the SDK itself.
904
+ */
905
+ rich(key, fallback, components, variables) {
906
+ const resolved = _resolveTranslation(key, variables);
907
+ const raw = resolved ?? interpolate(fallback, variables);
908
+ return _parseRichText(raw, components ?? {});
909
+ },
809
910
  /**
810
911
  * Translate a key and return a framework element (e.g. React <span>)
811
912
  * carrying `data-label` / `data-variables` attributes so the ShipEasy
@@ -818,18 +919,30 @@ var i18n = {
818
919
  * Falls back to a plain translated string if `createElement` was not
819
920
  * configured (e.g. server-side or in non-JSX contexts).
820
921
  */
821
- tEl(key, fallback, variables, desc) {
822
- const hasTranslation = typeof window !== "undefined" && Boolean(window.i18n) || Boolean(getSSRI18nStore()?.strings[key]);
823
- const translated = hasTranslation ? this.t(key, variables) : void 0;
824
- const text = translated && translated !== key ? translated : fallback;
825
- if (isEditLabelsMode()) return encodeLabelMarker(key, text);
826
- if (_createElement) return _createElement("span", labelAttrs(key, variables, desc), text);
827
- return text;
922
+ /**
923
+ * @deprecated Use `t(key, fallback, variables)` instead. tEl() now delegates
924
+ * to t() and returns the translated string. Prior behaviour (createElement
925
+ * wrapping + edit-mode markers) was a devtools feature that conflicted with
926
+ * type-safe usage and has been removed.
927
+ */
928
+ tEl(key, fallback, variables, _desc) {
929
+ if (isEditLabelsMode()) {
930
+ const resolved = _resolveTranslation(key, variables);
931
+ const text = resolved ?? interpolate(fallback, variables);
932
+ return encodeLabelMarker(key, text);
933
+ }
934
+ return this.t(key, fallback, variables);
828
935
  },
829
- /** Wire up the element creator once at app startup (call before any tEl use). */
830
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
936
+ /**
937
+ * Configure global rich-text component overrides and (legacy) the createElement
938
+ * factory. `components` registers default renderers used by `rich()` when no
939
+ * per-call override is supplied (e.g. swap `<a>` for a framework Link).
940
+ */
831
941
  configure(opts) {
832
- _createElement = opts.createElement;
942
+ if (opts.components) {
943
+ _configuredComponents = { ..._configuredComponents, ...opts.components };
944
+ }
945
+ if (opts.createElement) _createElement = opts.createElement;
833
946
  },
834
947
  get locale() {
835
948
  if (typeof window !== "undefined" && window.i18n) return window.i18n.locale;
@@ -412,13 +412,15 @@ var FlagsClientBrowser = class {
412
412
  this.evalResult = data;
413
413
  }
414
414
  getFlag(name) {
415
+ if (this.evalResult === null) return false;
415
416
  const ov = readGateOverride(name);
416
417
  if (ov !== null) return ov;
417
- return this.evalResult?.flags[name] ?? false;
418
+ return this.evalResult.flags[name] ?? false;
418
419
  }
419
420
  getConfig(name, decode) {
421
+ if (this.evalResult === null) return void 0;
420
422
  const ov = readConfigOverride(name);
421
- const raw = ov !== void 0 ? ov : this.evalResult?.configs?.[name];
423
+ const raw = ov !== void 0 ? ov : this.evalResult.configs?.[name];
422
424
  if (raw === void 0) return void 0;
423
425
  if (!decode) return raw;
424
426
  try {
@@ -754,15 +756,114 @@ function isEditLabelsMode() {
754
756
  }
755
757
  function interpolate(raw, variables) {
756
758
  if (!variables) return raw;
757
- return raw.replace(/\{\{(\w+)\}\}/g, (_, k) => String(variables[k] ?? `{{${k}}}`));
759
+ return raw.replace(/\{\{(\w+)\}\}/g, (placeholder, k) => {
760
+ const v = variables[k];
761
+ return v != null ? String(v) : placeholder;
762
+ });
763
+ }
764
+ var _IS_BROWSER = typeof document !== "undefined";
765
+ var _RICH_HTML_TAGS = [
766
+ "b",
767
+ "i",
768
+ "u",
769
+ "s",
770
+ "em",
771
+ "strong",
772
+ "del",
773
+ "ins",
774
+ "mark",
775
+ "small",
776
+ "code",
777
+ "pre",
778
+ "kbd",
779
+ "sub",
780
+ "sup",
781
+ "span",
782
+ "a",
783
+ "p",
784
+ "br",
785
+ "hr"
786
+ ];
787
+ function _makeBuiltinTags() {
788
+ const tags = {};
789
+ for (const tag of _RICH_HTML_TAGS) {
790
+ tags[tag] = _IS_BROWSER ? (text) => {
791
+ const el = document.createElement(tag);
792
+ if (tag !== "br" && tag !== "hr") el.textContent = text;
793
+ return el;
794
+ } : (text) => tag === "br" || tag === "hr" ? `<${tag}>` : `<${tag}>${text}</${tag}>`;
795
+ }
796
+ return tags;
797
+ }
798
+ var _builtinTags = _makeBuiltinTags();
799
+ var _configuredComponents = {};
800
+ var _RICH_TAG_RE = /<(\w+)(?:\s*\/>|>([\s\S]*?)<\/\1>)/g;
801
+ function _parseRichText(text, components) {
802
+ const parts = [];
803
+ let lastIndex = 0;
804
+ let match;
805
+ let allStrings = true;
806
+ _RICH_TAG_RE.lastIndex = 0;
807
+ while ((match = _RICH_TAG_RE.exec(text)) !== null) {
808
+ if (match.index > lastIndex) parts.push(text.slice(lastIndex, match.index));
809
+ const tag = match[1];
810
+ const content = match[2] ?? "";
811
+ const renderer = components[tag] ?? _configuredComponents[tag] ?? _builtinTags[tag];
812
+ if (renderer) {
813
+ const rendered = renderer(content);
814
+ if (typeof rendered !== "string") allStrings = false;
815
+ parts.push(rendered);
816
+ } else {
817
+ parts.push(content);
818
+ }
819
+ lastIndex = _RICH_TAG_RE.lastIndex;
820
+ }
821
+ if (lastIndex < text.length) parts.push(text.slice(lastIndex));
822
+ if (allStrings) return parts.join("");
823
+ return parts;
824
+ }
825
+ function _resolveTranslation(key, variables) {
826
+ if (typeof window !== "undefined" && window.i18n) {
827
+ const v = window.i18n.t(key, variables);
828
+ return v === key ? void 0 : v;
829
+ }
830
+ const store = getSSRI18nStore();
831
+ if (store?.strings[key]) return interpolate(store.strings[key], variables);
832
+ return void 0;
758
833
  }
759
834
  var i18n = {
760
- t(key, variables) {
761
- if (typeof window !== "undefined" && window.i18n) return window.i18n.t(key, variables);
762
- const store = getSSRI18nStore();
763
- if (store?.strings[key]) return interpolate(store.strings[key], variables);
835
+ t(key, fallbackOrVars, maybeVars) {
836
+ let fallback;
837
+ let variables;
838
+ if (typeof fallbackOrVars === "string") {
839
+ fallback = fallbackOrVars;
840
+ variables = maybeVars;
841
+ } else {
842
+ variables = fallbackOrVars;
843
+ }
844
+ const resolved = _resolveTranslation(key, variables);
845
+ if (resolved !== void 0) return resolved;
846
+ if (fallback !== void 0) return interpolate(fallback, variables);
764
847
  return key;
765
848
  },
849
+ /**
850
+ * Translate a key whose value contains `<tag>content</tag>` segments and
851
+ * render the tagged segments via per-call `components`, `configure()`-supplied
852
+ * components, or the built-in HTML tag renderers.
853
+ *
854
+ * Return shape:
855
+ * - all renderers return strings → returns a concatenated `string`
856
+ * - any renderer returns a non-string (e.g. JSX, DOM node) → returns
857
+ * `Array<string | T>` and the caller is responsible for rendering
858
+ *
859
+ * Framework-agnostic: this method does pure string parsing + callback
860
+ * execution. No React / DOM dependency in the SDK itself.
861
+ */
862
+ rich(key, fallback, components, variables) {
863
+ const resolved = _resolveTranslation(key, variables);
864
+ const raw = resolved ?? interpolate(fallback, variables);
865
+ return _parseRichText(raw, components ?? {});
866
+ },
766
867
  /**
767
868
  * Translate a key and return a framework element (e.g. React <span>)
768
869
  * carrying `data-label` / `data-variables` attributes so the ShipEasy
@@ -775,18 +876,30 @@ var i18n = {
775
876
  * Falls back to a plain translated string if `createElement` was not
776
877
  * configured (e.g. server-side or in non-JSX contexts).
777
878
  */
778
- tEl(key, fallback, variables, desc) {
779
- const hasTranslation = typeof window !== "undefined" && Boolean(window.i18n) || Boolean(getSSRI18nStore()?.strings[key]);
780
- const translated = hasTranslation ? this.t(key, variables) : void 0;
781
- const text = translated && translated !== key ? translated : fallback;
782
- if (isEditLabelsMode()) return encodeLabelMarker(key, text);
783
- if (_createElement) return _createElement("span", labelAttrs(key, variables, desc), text);
784
- return text;
879
+ /**
880
+ * @deprecated Use `t(key, fallback, variables)` instead. tEl() now delegates
881
+ * to t() and returns the translated string. Prior behaviour (createElement
882
+ * wrapping + edit-mode markers) was a devtools feature that conflicted with
883
+ * type-safe usage and has been removed.
884
+ */
885
+ tEl(key, fallback, variables, _desc) {
886
+ if (isEditLabelsMode()) {
887
+ const resolved = _resolveTranslation(key, variables);
888
+ const text = resolved ?? interpolate(fallback, variables);
889
+ return encodeLabelMarker(key, text);
890
+ }
891
+ return this.t(key, fallback, variables);
785
892
  },
786
- /** Wire up the element creator once at app startup (call before any tEl use). */
787
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
893
+ /**
894
+ * Configure global rich-text component overrides and (legacy) the createElement
895
+ * factory. `components` registers default renderers used by `rich()` when no
896
+ * per-call override is supplied (e.g. swap `<a>` for a framework Link).
897
+ */
788
898
  configure(opts) {
789
- _createElement = opts.createElement;
899
+ if (opts.components) {
900
+ _configuredComponents = { ..._configuredComponents, ...opts.components };
901
+ }
902
+ if (opts.createElement) _createElement = opts.createElement;
790
903
  },
791
904
  get locale() {
792
905
  if (typeof window !== "undefined" && window.i18n) return window.i18n.locale;
@@ -366,10 +366,31 @@ var FlagsClient = class {
366
366
  var _I18N_SSR_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-i18n");
367
367
  var _EDIT_MODE_SSR_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-edit-mode");
368
368
  var _i18nALS = new import_node_async_hooks.AsyncLocalStorage();
369
- globalThis[_I18N_SSR_SYM] = () => _i18nALS.getStore() ?? null;
370
- if (globalThis[_EDIT_MODE_SSR_SYM] === void 0) {
371
- globalThis[_EDIT_MODE_SSR_SYM] = false;
369
+ var _I18N_CACHE_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-i18n-cache");
370
+ var _i18nCache = globalThis[_I18N_CACHE_SYM] ?? (globalThis[_I18N_CACHE_SYM] = /* @__PURE__ */ new Map());
371
+ globalThis[_I18N_SSR_SYM] = () => {
372
+ const fromALS = _i18nALS.getStore();
373
+ if (fromALS && Object.keys(fromALS.strings).length > 0) return fromALS;
374
+ for (const v of _i18nCache.values()) {
375
+ if (Object.keys(v.strings).length > 0) return v;
376
+ }
377
+ return fromALS ?? null;
378
+ };
379
+ var _EDIT_MODE_ALS_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-edit-mode-als");
380
+ var _editModeALS = globalThis[_EDIT_MODE_ALS_SYM] ?? (globalThis[_EDIT_MODE_ALS_SYM] = new import_node_async_hooks.AsyncLocalStorage());
381
+ var _EDIT_MODE_FALLBACK_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-edit-mode-fallback");
382
+ if (globalThis[_EDIT_MODE_FALLBACK_SYM] === void 0) {
383
+ globalThis[_EDIT_MODE_FALLBACK_SYM] = false;
372
384
  }
385
+ Object.defineProperty(globalThis, _EDIT_MODE_SSR_SYM, {
386
+ get: () => _editModeALS.getStore() ?? globalThis[_EDIT_MODE_FALLBACK_SYM] ?? false,
387
+ set: (v) => {
388
+ const b = Boolean(v);
389
+ _editModeALS.enterWith(b);
390
+ globalThis[_EDIT_MODE_FALLBACK_SYM] = b;
391
+ },
392
+ configurable: true
393
+ });
373
394
  var i18n = {
374
395
  /**
375
396
  * Fetch translation labels for the current request and store them in an
@@ -385,10 +406,18 @@ var i18n = {
385
406
  * @param cdnBaseUrl Optional override for the i18n CDN (default: cdn.i18n.shipeasy.ai)
386
407
  */
387
408
  async init(key, profile, cdnBaseUrl) {
388
- if (_i18nALS.getStore() !== void 0) return;
409
+ const existingALS = _i18nALS.getStore();
410
+ if (existingALS && Object.keys(existingALS.strings).length > 0) return;
411
+ const cached = _i18nCache.get(profile);
412
+ if (cached && Object.keys(cached.strings).length > 0) {
413
+ _i18nALS.enterWith(cached);
414
+ return;
415
+ }
389
416
  const labels = await fetchLabelsForSSR({ key, profile, cdnBaseUrl }).catch(() => null);
390
417
  const locale = profile.split(":")[0] || "en";
391
- _i18nALS.enterWith({ strings: labels?.strings ?? {}, locale });
418
+ const store = { strings: labels?.strings ?? {}, locale };
419
+ if (Object.keys(store.strings).length > 0) _i18nCache.set(profile, store);
420
+ _i18nALS.enterWith(store);
392
421
  },
393
422
  /**
394
423
  * Return the translation strings loaded for the current request.
@@ -399,13 +428,14 @@ var i18n = {
399
428
  return _i18nALS.getStore() ?? { strings: {}, locale: "en" };
400
429
  }
401
430
  };
402
- var DEFAULT_I18N_CDN = "https://cdn.i18n.shipeasy.ai";
403
- async function fetchJson(url, timeoutMs = 2e3) {
431
+ var DEFAULT_I18N_CDN = "https://cdn.shipeasy.ai";
432
+ async function fetchJson(url, timeoutMs = 2e3, headers) {
404
433
  const controller = new AbortController();
405
434
  const timer = setTimeout(() => controller.abort(), timeoutMs);
406
435
  try {
407
436
  const res = await fetch(url, {
408
437
  signal: controller.signal,
438
+ headers,
409
439
  next: { revalidate: 60 }
410
440
  });
411
441
  if (!res.ok) throw new Error(`HTTP ${res.status} fetching ${url}`);
@@ -416,20 +446,24 @@ async function fetchJson(url, timeoutMs = 2e3) {
416
446
  }
417
447
  async function fetchLabelsForSSR(opts) {
418
448
  const cdn = opts.cdnBaseUrl ?? DEFAULT_I18N_CDN;
419
- const chunk = opts.chunk ?? "index";
420
449
  try {
421
- const manifest = await fetchJson(
422
- `${cdn}/labels/${opts.key}/${opts.profile}/manifest.json`,
423
- opts.timeoutMs
450
+ const body = await fetchJson(
451
+ `${cdn}/sdk/i18n/strings?profile=${encodeURIComponent(opts.profile)}`,
452
+ opts.timeoutMs,
453
+ { "X-SDK-Key": opts.key }
424
454
  );
425
- const fileUrl = manifest[chunk];
426
- if (!fileUrl) return null;
427
- return await fetchJson(fileUrl, opts.timeoutMs);
455
+ return {
456
+ v: 1,
457
+ profile: opts.profile,
458
+ chunk: opts.chunk ?? "default",
459
+ strings: body.strings ?? {}
460
+ };
428
461
  } catch {
429
462
  return null;
430
463
  }
431
464
  }
432
465
  var _server = null;
466
+ var _rememberedClientKey = null;
433
467
  function configureShipeasyServer(opts) {
434
468
  if (_server) return _server;
435
469
  _server = new FlagsClient(opts);
@@ -449,7 +483,8 @@ async function shipeasy(opts) {
449
483
  console.warn("[shipeasy] apiKey not set \u2014 falling back to clientKey for server requests.");
450
484
  }
451
485
  const apiKey = opts.apiKey ?? opts.clientKey ?? "";
452
- const clientKey = opts.clientKey ?? opts.apiKey ?? "";
486
+ const clientKey = opts.clientKey ?? _rememberedClientKey ?? opts.apiKey ?? "";
487
+ if (opts.clientKey && !_rememberedClientKey) _rememberedClientKey = opts.clientKey;
453
488
  const profile = opts.i18nDefaultProfile ?? "en:prod";
454
489
  flags.configure({ apiKey });
455
490
  let resolvedUrlOverrides = opts.urlOverrides;
@@ -323,10 +323,31 @@ var FlagsClient = class {
323
323
  var _I18N_SSR_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-i18n");
324
324
  var _EDIT_MODE_SSR_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-edit-mode");
325
325
  var _i18nALS = new AsyncLocalStorage();
326
- globalThis[_I18N_SSR_SYM] = () => _i18nALS.getStore() ?? null;
327
- if (globalThis[_EDIT_MODE_SSR_SYM] === void 0) {
328
- globalThis[_EDIT_MODE_SSR_SYM] = false;
326
+ var _I18N_CACHE_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-i18n-cache");
327
+ var _i18nCache = globalThis[_I18N_CACHE_SYM] ?? (globalThis[_I18N_CACHE_SYM] = /* @__PURE__ */ new Map());
328
+ globalThis[_I18N_SSR_SYM] = () => {
329
+ const fromALS = _i18nALS.getStore();
330
+ if (fromALS && Object.keys(fromALS.strings).length > 0) return fromALS;
331
+ for (const v of _i18nCache.values()) {
332
+ if (Object.keys(v.strings).length > 0) return v;
333
+ }
334
+ return fromALS ?? null;
335
+ };
336
+ var _EDIT_MODE_ALS_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-edit-mode-als");
337
+ var _editModeALS = globalThis[_EDIT_MODE_ALS_SYM] ?? (globalThis[_EDIT_MODE_ALS_SYM] = new AsyncLocalStorage());
338
+ var _EDIT_MODE_FALLBACK_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-edit-mode-fallback");
339
+ if (globalThis[_EDIT_MODE_FALLBACK_SYM] === void 0) {
340
+ globalThis[_EDIT_MODE_FALLBACK_SYM] = false;
329
341
  }
342
+ Object.defineProperty(globalThis, _EDIT_MODE_SSR_SYM, {
343
+ get: () => _editModeALS.getStore() ?? globalThis[_EDIT_MODE_FALLBACK_SYM] ?? false,
344
+ set: (v) => {
345
+ const b = Boolean(v);
346
+ _editModeALS.enterWith(b);
347
+ globalThis[_EDIT_MODE_FALLBACK_SYM] = b;
348
+ },
349
+ configurable: true
350
+ });
330
351
  var i18n = {
331
352
  /**
332
353
  * Fetch translation labels for the current request and store them in an
@@ -342,10 +363,18 @@ var i18n = {
342
363
  * @param cdnBaseUrl Optional override for the i18n CDN (default: cdn.i18n.shipeasy.ai)
343
364
  */
344
365
  async init(key, profile, cdnBaseUrl) {
345
- if (_i18nALS.getStore() !== void 0) return;
366
+ const existingALS = _i18nALS.getStore();
367
+ if (existingALS && Object.keys(existingALS.strings).length > 0) return;
368
+ const cached = _i18nCache.get(profile);
369
+ if (cached && Object.keys(cached.strings).length > 0) {
370
+ _i18nALS.enterWith(cached);
371
+ return;
372
+ }
346
373
  const labels = await fetchLabelsForSSR({ key, profile, cdnBaseUrl }).catch(() => null);
347
374
  const locale = profile.split(":")[0] || "en";
348
- _i18nALS.enterWith({ strings: labels?.strings ?? {}, locale });
375
+ const store = { strings: labels?.strings ?? {}, locale };
376
+ if (Object.keys(store.strings).length > 0) _i18nCache.set(profile, store);
377
+ _i18nALS.enterWith(store);
349
378
  },
350
379
  /**
351
380
  * Return the translation strings loaded for the current request.
@@ -356,13 +385,14 @@ var i18n = {
356
385
  return _i18nALS.getStore() ?? { strings: {}, locale: "en" };
357
386
  }
358
387
  };
359
- var DEFAULT_I18N_CDN = "https://cdn.i18n.shipeasy.ai";
360
- async function fetchJson(url, timeoutMs = 2e3) {
388
+ var DEFAULT_I18N_CDN = "https://cdn.shipeasy.ai";
389
+ async function fetchJson(url, timeoutMs = 2e3, headers) {
361
390
  const controller = new AbortController();
362
391
  const timer = setTimeout(() => controller.abort(), timeoutMs);
363
392
  try {
364
393
  const res = await fetch(url, {
365
394
  signal: controller.signal,
395
+ headers,
366
396
  next: { revalidate: 60 }
367
397
  });
368
398
  if (!res.ok) throw new Error(`HTTP ${res.status} fetching ${url}`);
@@ -373,20 +403,24 @@ async function fetchJson(url, timeoutMs = 2e3) {
373
403
  }
374
404
  async function fetchLabelsForSSR(opts) {
375
405
  const cdn = opts.cdnBaseUrl ?? DEFAULT_I18N_CDN;
376
- const chunk = opts.chunk ?? "index";
377
406
  try {
378
- const manifest = await fetchJson(
379
- `${cdn}/labels/${opts.key}/${opts.profile}/manifest.json`,
380
- opts.timeoutMs
407
+ const body = await fetchJson(
408
+ `${cdn}/sdk/i18n/strings?profile=${encodeURIComponent(opts.profile)}`,
409
+ opts.timeoutMs,
410
+ { "X-SDK-Key": opts.key }
381
411
  );
382
- const fileUrl = manifest[chunk];
383
- if (!fileUrl) return null;
384
- return await fetchJson(fileUrl, opts.timeoutMs);
412
+ return {
413
+ v: 1,
414
+ profile: opts.profile,
415
+ chunk: opts.chunk ?? "default",
416
+ strings: body.strings ?? {}
417
+ };
385
418
  } catch {
386
419
  return null;
387
420
  }
388
421
  }
389
422
  var _server = null;
423
+ var _rememberedClientKey = null;
390
424
  function configureShipeasyServer(opts) {
391
425
  if (_server) return _server;
392
426
  _server = new FlagsClient(opts);
@@ -406,7 +440,8 @@ async function shipeasy(opts) {
406
440
  console.warn("[shipeasy] apiKey not set \u2014 falling back to clientKey for server requests.");
407
441
  }
408
442
  const apiKey = opts.apiKey ?? opts.clientKey ?? "";
409
- const clientKey = opts.clientKey ?? opts.apiKey ?? "";
443
+ const clientKey = opts.clientKey ?? _rememberedClientKey ?? opts.apiKey ?? "";
444
+ if (opts.clientKey && !_rememberedClientKey) _rememberedClientKey = opts.clientKey;
410
445
  const profile = opts.i18nDefaultProfile ?? "en:prod";
411
446
  flags.configure({ apiKey });
412
447
  let resolvedUrlOverrides = opts.urlOverrides;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipeasy/sdk",
3
- "version": "2.1.7",
3
+ "version": "2.1.8",
4
4
  "description": "Shipeasy SDK — feature gates, runtime configs, experiments, and metrics for the Shipeasy hosted service.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "homepage": "https://shipeasy.ai",
@@ -48,35 +48,28 @@
48
48
  "LICENSE",
49
49
  "README.md"
50
50
  ],
51
+ "scripts": {
52
+ "build": "tsup",
53
+ "type-check": "tsc --noEmit",
54
+ "test": "vitest run",
55
+ "test:watch": "vitest",
56
+ "publish-loader": "wrangler r2 object put shipeasy-sdk/loader-v$npm_package_version.js --file=dist/loader/loader.global.js --content-type 'application/javascript; charset=utf-8' --cache-control 'public, max-age=31536000, immutable' --remote && wrangler r2 object put shipeasy-sdk/loader.js --file=dist/loader/loader.global.js --content-type 'application/javascript; charset=utf-8' --cache-control 'public, max-age=300' --remote"
57
+ },
51
58
  "dependencies": {
52
59
  "murmurhash-js": "^1.0.0"
53
60
  },
54
61
  "devDependencies": {
55
62
  "@types/murmurhash-js": "^1.0.6",
63
+ "@types/node": "^20.0.0",
56
64
  "tsup": "^8.3.0",
57
65
  "typescript": "^5.7.4",
58
66
  "vitest": "^2.1.0",
59
67
  "wrangler": "^4.83.0"
60
68
  },
61
- "peerDependencies": {
62
- "zod": "^4.0.0"
63
- },
64
- "peerDependenciesMeta": {
65
- "zod": {
66
- "optional": true
67
- }
68
- },
69
69
  "publishConfig": {
70
70
  "access": "public"
71
71
  },
72
72
  "engines": {
73
73
  "node": ">=20"
74
- },
75
- "scripts": {
76
- "build": "tsup",
77
- "type-check": "tsc --noEmit",
78
- "test": "vitest run",
79
- "test:watch": "vitest",
80
- "publish-loader": "wrangler r2 object put shipeasy-sdk/loader-v$npm_package_version.js --file=dist/loader/loader.global.js --content-type 'application/javascript; charset=utf-8' --cache-control 'public, max-age=31536000, immutable' --remote && wrangler r2 object put shipeasy-sdk/loader.js --file=dist/loader/loader.global.js --content-type 'application/javascript; charset=utf-8' --cache-control 'public, max-age=300' --remote"
81
74
  }
82
- }
75
+ }