@shipeasy/sdk 2.1.5 → 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 {
@@ -792,19 +794,119 @@ function isEditLabelsMode() {
792
794
  if (typeof window !== "undefined") {
793
795
  return !!window.__SE_BOOTSTRAP?.editLabels || new URLSearchParams(location.search).has("se_edit_labels");
794
796
  }
795
- return globalThis[_EDIT_MODE_SSR_SYM]?.() ?? false;
797
+ const val = globalThis[_EDIT_MODE_SSR_SYM];
798
+ return typeof val === "boolean" ? val : typeof val === "function" ? val() : false;
796
799
  }
797
800
  function interpolate(raw, variables) {
798
801
  if (!variables) return raw;
799
- 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;
800
876
  }
801
877
  var i18n = {
802
- t(key, variables) {
803
- if (typeof window !== "undefined" && window.i18n) return window.i18n.t(key, variables);
804
- const store = getSSRI18nStore();
805
- 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);
806
890
  return key;
807
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
+ },
808
910
  /**
809
911
  * Translate a key and return a framework element (e.g. React <span>)
810
912
  * carrying `data-label` / `data-variables` attributes so the ShipEasy
@@ -817,18 +919,30 @@ var i18n = {
817
919
  * Falls back to a plain translated string if `createElement` was not
818
920
  * configured (e.g. server-side or in non-JSX contexts).
819
921
  */
820
- tEl(key, fallback, variables, desc) {
821
- const hasTranslation = typeof window !== "undefined" && Boolean(window.i18n) || Boolean(getSSRI18nStore()?.strings[key]);
822
- const translated = hasTranslation ? this.t(key, variables) : void 0;
823
- const text = translated && translated !== key ? translated : fallback;
824
- if (isEditLabelsMode()) return encodeLabelMarker(key, text);
825
- if (_createElement) return _createElement("span", labelAttrs(key, variables, desc), text);
826
- 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);
827
935
  },
828
- /** Wire up the element creator once at app startup (call before any tEl use). */
829
- // 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
+ */
830
941
  configure(opts) {
831
- _createElement = opts.createElement;
942
+ if (opts.components) {
943
+ _configuredComponents = { ..._configuredComponents, ...opts.components };
944
+ }
945
+ if (opts.createElement) _createElement = opts.createElement;
832
946
  },
833
947
  get locale() {
834
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 {
@@ -749,19 +751,119 @@ function isEditLabelsMode() {
749
751
  if (typeof window !== "undefined") {
750
752
  return !!window.__SE_BOOTSTRAP?.editLabels || new URLSearchParams(location.search).has("se_edit_labels");
751
753
  }
752
- return globalThis[_EDIT_MODE_SSR_SYM]?.() ?? false;
754
+ const val = globalThis[_EDIT_MODE_SSR_SYM];
755
+ return typeof val === "boolean" ? val : typeof val === "function" ? val() : false;
753
756
  }
754
757
  function interpolate(raw, variables) {
755
758
  if (!variables) return raw;
756
- 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;
757
833
  }
758
834
  var i18n = {
759
- t(key, variables) {
760
- if (typeof window !== "undefined" && window.i18n) return window.i18n.t(key, variables);
761
- const store = getSSRI18nStore();
762
- 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);
763
847
  return key;
764
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
+ },
765
867
  /**
766
868
  * Translate a key and return a framework element (e.g. React <span>)
767
869
  * carrying `data-label` / `data-variables` attributes so the ShipEasy
@@ -774,18 +876,30 @@ var i18n = {
774
876
  * Falls back to a plain translated string if `createElement` was not
775
877
  * configured (e.g. server-side or in non-JSX contexts).
776
878
  */
777
- tEl(key, fallback, variables, desc) {
778
- const hasTranslation = typeof window !== "undefined" && Boolean(window.i18n) || Boolean(getSSRI18nStore()?.strings[key]);
779
- const translated = hasTranslation ? this.t(key, variables) : void 0;
780
- const text = translated && translated !== key ? translated : fallback;
781
- if (isEditLabelsMode()) return encodeLabelMarker(key, text);
782
- if (_createElement) return _createElement("span", labelAttrs(key, variables, desc), text);
783
- 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);
784
892
  },
785
- /** Wire up the element creator once at app startup (call before any tEl use). */
786
- // 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
+ */
787
898
  configure(opts) {
788
- _createElement = opts.createElement;
899
+ if (opts.components) {
900
+ _configuredComponents = { ..._configuredComponents, ...opts.components };
901
+ }
902
+ if (opts.createElement) _createElement = opts.createElement;
789
903
  },
790
904
  get locale() {
791
905
  if (typeof window !== "undefined" && window.i18n) return window.i18n.locale;
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/server/index.ts
@@ -356,9 +366,31 @@ var FlagsClient = class {
356
366
  var _I18N_SSR_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-i18n");
357
367
  var _EDIT_MODE_SSR_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-edit-mode");
358
368
  var _i18nALS = new import_node_async_hooks.AsyncLocalStorage();
359
- var _editModeALS = new import_node_async_hooks.AsyncLocalStorage();
360
- globalThis[_I18N_SSR_SYM] = () => _i18nALS.getStore() ?? null;
361
- globalThis[_EDIT_MODE_SSR_SYM] = () => _editModeALS.getStore() ?? 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;
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
+ });
362
394
  var i18n = {
363
395
  /**
364
396
  * Fetch translation labels for the current request and store them in an
@@ -374,10 +406,18 @@ var i18n = {
374
406
  * @param cdnBaseUrl Optional override for the i18n CDN (default: cdn.i18n.shipeasy.ai)
375
407
  */
376
408
  async init(key, profile, cdnBaseUrl) {
377
- 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
+ }
378
416
  const labels = await fetchLabelsForSSR({ key, profile, cdnBaseUrl }).catch(() => null);
379
417
  const locale = profile.split(":")[0] || "en";
380
- _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);
381
421
  },
382
422
  /**
383
423
  * Return the translation strings loaded for the current request.
@@ -388,13 +428,14 @@ var i18n = {
388
428
  return _i18nALS.getStore() ?? { strings: {}, locale: "en" };
389
429
  }
390
430
  };
391
- var DEFAULT_I18N_CDN = "https://cdn.i18n.shipeasy.ai";
392
- async function fetchJson(url, timeoutMs = 2e3) {
431
+ var DEFAULT_I18N_CDN = "https://cdn.shipeasy.ai";
432
+ async function fetchJson(url, timeoutMs = 2e3, headers) {
393
433
  const controller = new AbortController();
394
434
  const timer = setTimeout(() => controller.abort(), timeoutMs);
395
435
  try {
396
436
  const res = await fetch(url, {
397
437
  signal: controller.signal,
438
+ headers,
398
439
  next: { revalidate: 60 }
399
440
  });
400
441
  if (!res.ok) throw new Error(`HTTP ${res.status} fetching ${url}`);
@@ -405,20 +446,24 @@ async function fetchJson(url, timeoutMs = 2e3) {
405
446
  }
406
447
  async function fetchLabelsForSSR(opts) {
407
448
  const cdn = opts.cdnBaseUrl ?? DEFAULT_I18N_CDN;
408
- const chunk = opts.chunk ?? "index";
409
449
  try {
410
- const manifest = await fetchJson(
411
- `${cdn}/labels/${opts.key}/${opts.profile}/manifest.json`,
412
- 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 }
413
454
  );
414
- const fileUrl = manifest[chunk];
415
- if (!fileUrl) return null;
416
- 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
+ };
417
461
  } catch {
418
462
  return null;
419
463
  }
420
464
  }
421
465
  var _server = null;
466
+ var _rememberedClientKey = null;
422
467
  function configureShipeasyServer(opts) {
423
468
  if (_server) return _server;
424
469
  _server = new FlagsClient(opts);
@@ -438,13 +483,24 @@ async function shipeasy(opts) {
438
483
  console.warn("[shipeasy] apiKey not set \u2014 falling back to clientKey for server requests.");
439
484
  }
440
485
  const apiKey = opts.apiKey ?? opts.clientKey ?? "";
441
- const clientKey = opts.clientKey ?? opts.apiKey ?? "";
486
+ const clientKey = opts.clientKey ?? _rememberedClientKey ?? opts.apiKey ?? "";
487
+ if (opts.clientKey && !_rememberedClientKey) _rememberedClientKey = opts.clientKey;
442
488
  const profile = opts.i18nDefaultProfile ?? "en:prod";
443
489
  flags.configure({ apiKey });
444
- const editLabels = opts.urlOverrides ? new URLSearchParams(opts.urlOverrides).has("se_edit_labels") : false;
445
- _editModeALS.enterWith(editLabels);
490
+ let resolvedUrlOverrides = opts.urlOverrides;
491
+ if (!resolvedUrlOverrides) {
492
+ try {
493
+ const { headers } = await import("next/headers");
494
+ const h = await Promise.resolve(headers());
495
+ const search = h.get("x-se-search") ?? "";
496
+ if (search) resolvedUrlOverrides = search;
497
+ } catch {
498
+ }
499
+ }
500
+ const editLabels = resolvedUrlOverrides ? new URLSearchParams(resolvedUrlOverrides).has("se_edit_labels") : false;
501
+ globalThis[_EDIT_MODE_SSR_SYM] = editLabels;
446
502
  await Promise.allSettled([flags.initOnce(), i18n.init(clientKey, profile)]);
447
- const bootstrap = flags.evaluate(opts.user ?? {}, opts.urlOverrides);
503
+ const bootstrap = flags.evaluate(opts.user ?? {}, resolvedUrlOverrides);
448
504
  const i18nData = i18n.getForRequest();
449
505
  return {
450
506
  flags: bootstrap.flags,
@@ -323,9 +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
- var _editModeALS = new AsyncLocalStorage();
327
- globalThis[_I18N_SSR_SYM] = () => _i18nALS.getStore() ?? null;
328
- globalThis[_EDIT_MODE_SSR_SYM] = () => _editModeALS.getStore() ?? 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;
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
+ });
329
351
  var i18n = {
330
352
  /**
331
353
  * Fetch translation labels for the current request and store them in an
@@ -341,10 +363,18 @@ var i18n = {
341
363
  * @param cdnBaseUrl Optional override for the i18n CDN (default: cdn.i18n.shipeasy.ai)
342
364
  */
343
365
  async init(key, profile, cdnBaseUrl) {
344
- 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
+ }
345
373
  const labels = await fetchLabelsForSSR({ key, profile, cdnBaseUrl }).catch(() => null);
346
374
  const locale = profile.split(":")[0] || "en";
347
- _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);
348
378
  },
349
379
  /**
350
380
  * Return the translation strings loaded for the current request.
@@ -355,13 +385,14 @@ var i18n = {
355
385
  return _i18nALS.getStore() ?? { strings: {}, locale: "en" };
356
386
  }
357
387
  };
358
- var DEFAULT_I18N_CDN = "https://cdn.i18n.shipeasy.ai";
359
- async function fetchJson(url, timeoutMs = 2e3) {
388
+ var DEFAULT_I18N_CDN = "https://cdn.shipeasy.ai";
389
+ async function fetchJson(url, timeoutMs = 2e3, headers) {
360
390
  const controller = new AbortController();
361
391
  const timer = setTimeout(() => controller.abort(), timeoutMs);
362
392
  try {
363
393
  const res = await fetch(url, {
364
394
  signal: controller.signal,
395
+ headers,
365
396
  next: { revalidate: 60 }
366
397
  });
367
398
  if (!res.ok) throw new Error(`HTTP ${res.status} fetching ${url}`);
@@ -372,20 +403,24 @@ async function fetchJson(url, timeoutMs = 2e3) {
372
403
  }
373
404
  async function fetchLabelsForSSR(opts) {
374
405
  const cdn = opts.cdnBaseUrl ?? DEFAULT_I18N_CDN;
375
- const chunk = opts.chunk ?? "index";
376
406
  try {
377
- const manifest = await fetchJson(
378
- `${cdn}/labels/${opts.key}/${opts.profile}/manifest.json`,
379
- 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 }
380
411
  );
381
- const fileUrl = manifest[chunk];
382
- if (!fileUrl) return null;
383
- 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
+ };
384
418
  } catch {
385
419
  return null;
386
420
  }
387
421
  }
388
422
  var _server = null;
423
+ var _rememberedClientKey = null;
389
424
  function configureShipeasyServer(opts) {
390
425
  if (_server) return _server;
391
426
  _server = new FlagsClient(opts);
@@ -405,13 +440,24 @@ async function shipeasy(opts) {
405
440
  console.warn("[shipeasy] apiKey not set \u2014 falling back to clientKey for server requests.");
406
441
  }
407
442
  const apiKey = opts.apiKey ?? opts.clientKey ?? "";
408
- const clientKey = opts.clientKey ?? opts.apiKey ?? "";
443
+ const clientKey = opts.clientKey ?? _rememberedClientKey ?? opts.apiKey ?? "";
444
+ if (opts.clientKey && !_rememberedClientKey) _rememberedClientKey = opts.clientKey;
409
445
  const profile = opts.i18nDefaultProfile ?? "en:prod";
410
446
  flags.configure({ apiKey });
411
- const editLabels = opts.urlOverrides ? new URLSearchParams(opts.urlOverrides).has("se_edit_labels") : false;
412
- _editModeALS.enterWith(editLabels);
447
+ let resolvedUrlOverrides = opts.urlOverrides;
448
+ if (!resolvedUrlOverrides) {
449
+ try {
450
+ const { headers } = await import("next/headers");
451
+ const h = await Promise.resolve(headers());
452
+ const search = h.get("x-se-search") ?? "";
453
+ if (search) resolvedUrlOverrides = search;
454
+ } catch {
455
+ }
456
+ }
457
+ const editLabels = resolvedUrlOverrides ? new URLSearchParams(resolvedUrlOverrides).has("se_edit_labels") : false;
458
+ globalThis[_EDIT_MODE_SSR_SYM] = editLabels;
413
459
  await Promise.allSettled([flags.initOnce(), i18n.init(clientKey, profile)]);
414
- const bootstrap = flags.evaluate(opts.user ?? {}, opts.urlOverrides);
460
+ const bootstrap = flags.evaluate(opts.user ?? {}, resolvedUrlOverrides);
415
461
  const i18nData = i18n.getForRequest();
416
462
  return {
417
463
  flags: bootstrap.flags,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipeasy/sdk",
3
- "version": "2.1.5",
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
+ }