@weaverse/next 0.1.0-alpha.7 → 0.1.0-alpha.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.
package/dist/index.js CHANGED
@@ -28,12 +28,47 @@ function generateDataFromSchema(schema) {
28
28
 
29
29
  // src/item.ts
30
30
  var WeaverseNextItem = class extends react.WeaverseItemStore {
31
+ /**
32
+ * Cached merged snapshot (base `_store` + translations). Reset whenever the
33
+ * `_store` ref or the item's translation entry ref changes, so
34
+ * `useSyncExternalStore` sees a stable object between unrelated re-renders.
35
+ */
36
+ _cachedSnapshot = null;
37
+ _snapshotStoreRef = null;
38
+ _snapshotTranslationRef = null;
31
39
  constructor(initialData, weaverse) {
32
40
  super(initialData, weaverse);
33
41
  let { data, ...rest } = initialData;
34
42
  let schemaData = generateDataFromSchema(this.Element?.schema);
35
43
  Object.assign(this._store, schemaData, data, rest);
36
44
  }
45
+ /**
46
+ * Overlay item-level translations onto the base store when the runtime is in
47
+ * translation mode. Mirrors Hydrogen's `WeaverseHydrogenItem.getSnapShot()`:
48
+ * memoized by `_store` ref + translation entry ref so the returned object is
49
+ * referentially stable, and a fast path that returns the base store untouched
50
+ * when the item has no translations.
51
+ */
52
+ getSnapShot = () => {
53
+ let base = this._store;
54
+ let translations = this.weaverse.translationMap[this._id];
55
+ if (!translations) {
56
+ return base;
57
+ }
58
+ if (this._cachedSnapshot && this._snapshotStoreRef === base && this._snapshotTranslationRef === translations) {
59
+ return this._cachedSnapshot;
60
+ }
61
+ let merged = { ...base };
62
+ for (let [key, entry] of Object.entries(translations)) {
63
+ if (entry.translatedValue !== void 0) {
64
+ merged[key] = entry.translatedValue;
65
+ }
66
+ }
67
+ this._cachedSnapshot = merged;
68
+ this._snapshotStoreRef = base;
69
+ this._snapshotTranslationRef = translations;
70
+ return merged;
71
+ };
37
72
  };
38
73
  function ensureNextItemConstructor() {
39
74
  if (!react.Weaverse.ItemConstructor || react.Weaverse.ItemConstructor === react.WeaverseItemStore || react.Weaverse.ItemConstructor === WeaverseNextItem) {
@@ -158,9 +193,136 @@ function createWeaverseNextThemeSettingsStore(options = {}) {
158
193
  };
159
194
  return store;
160
195
  }
196
+ function getNestedKey(obj, path, fallback) {
197
+ let keys = path.split(".");
198
+ let current = obj;
199
+ for (let key of keys) {
200
+ if (current == null || typeof current !== "object") {
201
+ return fallback;
202
+ }
203
+ current = current[key];
204
+ }
205
+ if (typeof current === "string") {
206
+ return current;
207
+ }
208
+ return fallback;
209
+ }
210
+ function interpolate(template, variables) {
211
+ if (!variables) {
212
+ return template;
213
+ }
214
+ return template.replace(/\{\{(\w+)\}\}/g, (match, key) => {
215
+ let value = variables[key];
216
+ return value === void 0 ? match : String(value);
217
+ });
218
+ }
219
+ var TranslationContext = react$1.createContext(null);
220
+ TranslationContext.displayName = "WeaverseNextTranslationContext";
221
+ function createTranslate(sources) {
222
+ let { staticContent, merchantOverrides, designOverrides, externalT } = sources;
223
+ return (key, variables) => {
224
+ if (externalT) {
225
+ let external = externalT(key, variables);
226
+ if (external && external !== key) {
227
+ return external;
228
+ }
229
+ }
230
+ let design = designOverrides && Object.hasOwn(designOverrides, key) ? designOverrides[key] : void 0;
231
+ let override = merchantOverrides ? getNestedKey(merchantOverrides, key) : void 0;
232
+ let staticValue = getNestedKey(staticContent, key);
233
+ let raw = design ?? override ?? staticValue ?? key;
234
+ return interpolate(raw, variables);
235
+ };
236
+ }
237
+ var EMPTY = {};
238
+ var NOOP_SUBSCRIBE = () => () => {
239
+ };
240
+ var NOOP_SNAPSHOT = () => EMPTY;
241
+ function TranslationProvider({
242
+ staticContent,
243
+ merchantOverrides,
244
+ t: externalT,
245
+ translationStore,
246
+ themeTextStore,
247
+ children
248
+ }) {
249
+ let store = translationStore ?? themeTextStore ?? null;
250
+ let designOverrides = react.useSafeExternalStore(
251
+ store?.subscribe ?? NOOP_SUBSCRIBE,
252
+ store?.getSnapshot ?? NOOP_SNAPSHOT,
253
+ store?.getServerSnapshot ?? NOOP_SNAPSHOT
254
+ );
255
+ let value = react$1.useMemo(
256
+ () => ({
257
+ t: createTranslate({
258
+ staticContent,
259
+ merchantOverrides,
260
+ designOverrides,
261
+ externalT
262
+ }),
263
+ translationStore: store,
264
+ // Deprecated alias — same instance, kept for backward compatibility.
265
+ themeTextStore: store,
266
+ merchantOverrides: merchantOverrides ?? null
267
+ }),
268
+ [staticContent, merchantOverrides, designOverrides, externalT, store]
269
+ );
270
+ return /* @__PURE__ */ jsxRuntime.jsx(TranslationContext.Provider, { value, children });
271
+ }
272
+ function useTranslation() {
273
+ let ctx = react$1.useContext(TranslationContext);
274
+ if (!ctx) {
275
+ throw new Error(
276
+ "useTranslation must be used within <TranslationProvider>. Wrap your app with <WeaverseNextRootProvider>."
277
+ );
278
+ }
279
+ return ctx;
280
+ }
281
+ var ThemeTextProvider = TranslationProvider;
282
+ var useThemeText = useTranslation;
283
+
284
+ // src/translation-store.ts
285
+ var TranslationStore = class {
286
+ overrides = {};
287
+ listeners = /* @__PURE__ */ new Set();
288
+ setOverrides = (overrides) => {
289
+ this.overrides = { ...overrides };
290
+ this.emit();
291
+ };
292
+ updateOverrides = (newOverrides) => {
293
+ this.overrides = { ...this.overrides, ...newOverrides };
294
+ this.emit();
295
+ };
296
+ getSnapshot = () => this.overrides;
297
+ getServerSnapshot = () => this.overrides;
298
+ subscribe = (callback) => {
299
+ this.listeners.add(callback);
300
+ return () => {
301
+ this.listeners.delete(callback);
302
+ };
303
+ };
304
+ emit = () => {
305
+ for (const listener of Array.from(this.listeners)) {
306
+ try {
307
+ listener();
308
+ } catch (error) {
309
+ console.error("TranslationStore: Listener error", error);
310
+ }
311
+ }
312
+ };
313
+ };
314
+ var ThemeTextStore = TranslationStore;
161
315
  var WeaverseNextRootContext = react$1.createContext(null);
162
316
  function WeaverseNextRootProvider(props) {
163
- let { children, initialThemeSettings, themeSchema, publicEnv } = props;
317
+ let {
318
+ children,
319
+ initialThemeSettings,
320
+ merchantOverrides,
321
+ staticContent,
322
+ t,
323
+ themeSchema,
324
+ publicEnv
325
+ } = props;
164
326
  let storeRef = react$1.useRef(null);
165
327
  if (!storeRef.current) {
166
328
  storeRef.current = createWeaverseNextThemeSettingsStore({
@@ -170,6 +332,11 @@ function WeaverseNextRootProvider(props) {
170
332
  });
171
333
  }
172
334
  let themeSettingsStore = storeRef.current;
335
+ let translationStoreRef = react$1.useRef(null);
336
+ if (!translationStoreRef.current) {
337
+ translationStoreRef.current = new TranslationStore();
338
+ }
339
+ let translationStore = translationStoreRef.current;
173
340
  let appliedThemeSettingsRef = react$1.useRef(initialThemeSettings);
174
341
  react$1.useEffect(() => {
175
342
  if (initialThemeSettings && appliedThemeSettingsRef.current !== initialThemeSettings) {
@@ -177,25 +344,59 @@ function WeaverseNextRootProvider(props) {
177
344
  appliedThemeSettingsRef.current = initialThemeSettings;
178
345
  }
179
346
  }, [initialThemeSettings, themeSettingsStore]);
347
+ let resolvedStaticContent = staticContent ?? getSchemaStaticContent(themeSchema);
180
348
  let rootContextValue = react$1.useMemo(
181
- () => ({ themeSettingsStore }),
182
- [themeSettingsStore]
349
+ () => ({
350
+ merchantOverrides,
351
+ staticContent: resolvedStaticContent,
352
+ themeSettingsStore,
353
+ translationStore
354
+ }),
355
+ [
356
+ merchantOverrides,
357
+ resolvedStaticContent,
358
+ themeSettingsStore,
359
+ translationStore
360
+ ]
183
361
  );
184
362
  let contextValue = react$1.useMemo(
185
363
  () => ({
364
+ merchantOverrides,
186
365
  themeSettings: themeSettingsStore.getSnapshot(),
187
- themeSettingsStore
366
+ themeSettingsStore,
367
+ translationStore
188
368
  }),
189
- [themeSettingsStore]
369
+ [merchantOverrides, themeSettingsStore, translationStore]
190
370
  );
191
- return /* @__PURE__ */ jsxRuntime.jsx(WeaverseNextRootContext.Provider, { value: rootContextValue, children: /* @__PURE__ */ jsxRuntime.jsx(WeaverseNextContext.Provider, { value: contextValue, children }) });
371
+ return /* @__PURE__ */ jsxRuntime.jsx(WeaverseNextRootContext.Provider, { value: rootContextValue, children: /* @__PURE__ */ jsxRuntime.jsx(WeaverseNextContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsxRuntime.jsx(
372
+ TranslationProvider,
373
+ {
374
+ merchantOverrides,
375
+ staticContent: resolvedStaticContent,
376
+ t,
377
+ translationStore,
378
+ children
379
+ }
380
+ ) }) });
192
381
  }
193
382
  var EMPTY_THEME_SETTINGS = {};
383
+ var EMPTY_STATIC_CONTENT = {};
384
+ function getSchemaStaticContent(schema) {
385
+ let i18n = schema?.i18n;
386
+ return i18n?.staticContent ?? EMPTY_STATIC_CONTENT;
387
+ }
194
388
  var WeaverseNextContext = react$1.createContext(null);
195
389
  function WeaverseNextProvider(props) {
196
390
  let { children, client, rootData, pageData, commerce, themeSettings } = props;
197
391
  let rootContext = react$1.useContext(WeaverseNextRootContext);
198
392
  let themeSettingsValue = themeSettings ?? client?.themeSettings ?? EMPTY_THEME_SETTINGS;
393
+ let ownTranslationStoreRef = react$1.useRef(null);
394
+ if (!(rootContext || ownTranslationStoreRef.current)) {
395
+ ownTranslationStoreRef.current = new TranslationStore();
396
+ }
397
+ let translationStore = rootContext?.translationStore ?? ownTranslationStoreRef.current;
398
+ let staticContent = props.staticContent ?? rootContext?.staticContent ?? getSchemaStaticContent(client?.themeSchema);
399
+ let merchantOverrides = props.merchantOverrides ?? rootContext?.merchantOverrides;
199
400
  let ownThemeSettingsStore = react$1.useMemo(() => {
200
401
  if (rootContext) {
201
402
  return null;
@@ -219,12 +420,31 @@ function WeaverseNextProvider(props) {
219
420
  rootData,
220
421
  pageData,
221
422
  commerce: commerce ?? client?.commerce,
423
+ merchantOverrides,
222
424
  themeSettings: themeSettingsStore.getSnapshot(),
223
- themeSettingsStore
425
+ themeSettingsStore,
426
+ translationStore
224
427
  }),
225
- [client, rootData, pageData, commerce, themeSettingsStore]
428
+ [
429
+ client,
430
+ rootData,
431
+ pageData,
432
+ commerce,
433
+ merchantOverrides,
434
+ themeSettingsStore,
435
+ translationStore
436
+ ]
226
437
  );
227
- return /* @__PURE__ */ jsxRuntime.jsx(WeaverseNextContext.Provider, { value, children });
438
+ return /* @__PURE__ */ jsxRuntime.jsx(WeaverseNextContext.Provider, { value, children: /* @__PURE__ */ jsxRuntime.jsx(
439
+ TranslationProvider,
440
+ {
441
+ merchantOverrides,
442
+ staticContent,
443
+ t: props.t,
444
+ translationStore,
445
+ children
446
+ }
447
+ ) });
228
448
  }
229
449
 
230
450
  // src/hooks.ts
@@ -452,9 +672,15 @@ var WeaverseNextRuntime = class extends react.Weaverse {
452
672
  isPreviewMode;
453
673
  isRevisionPreview;
454
674
  sectionType;
675
+ // ─── Item-level translation sidecar (design mode only) ─────────────
676
+ /**
677
+ * Translation map received from Builder in design mode, keyed by item ID.
678
+ * `WeaverseNextItem.getSnapShot()` overlays these onto the data handed to
679
+ * components, so translations render without mutating the base `_store`.
680
+ */
455
681
  translationMap = {};
456
- translationLocale = "";
457
- translationLanguageId = "";
682
+ translationLocale = String();
683
+ translationLanguageId = String();
458
684
  constructor(config) {
459
685
  let { client, data } = config;
460
686
  let page = getRenderablePage(data);
@@ -476,7 +702,9 @@ var WeaverseNextRuntime = class extends react.Weaverse {
476
702
  this.isPreviewMode = requestContext?.isPreviewMode ?? false;
477
703
  this.isRevisionPreview = requestContext?.isRevisionPreview ?? false;
478
704
  this.sectionType = requestContext?.sectionType;
705
+ let translationStore = config.translationStore ?? new TranslationStore();
479
706
  this.internal = {
707
+ merchantOverrides: config.merchantOverrides,
480
708
  navigate: config.navigate,
481
709
  pageAssignment: data.pageAssignment,
482
710
  project: data.project,
@@ -484,17 +712,98 @@ var WeaverseNextRuntime = class extends react.Weaverse {
484
712
  themeSettingsStore: config.themeSettingsStore ?? createWeaverseNextThemeSettingsStore({
485
713
  schema: client?.themeSchema,
486
714
  settings: client?.themeSettings
487
- })
715
+ }),
716
+ translationStore,
717
+ themeTextStore: translationStore
488
718
  };
719
+ this.extractTranslationSidecar();
489
720
  }
490
- extractTranslationSidecar = () => void 0;
721
+ /**
722
+ * Pull `translationMap` / `translationLocale` / `translationLanguageId` off
723
+ * the current page data (Builder attaches them in design mode). Unlike
724
+ * Hydrogen, this also clears the sidecar to defaults when the new page data
725
+ * carries none, so a reused runtime / project-data swap can't leave a stale
726
+ * map overlaying the fresh tree.
727
+ */
728
+ extractTranslationSidecar = () => {
729
+ let pageData = this.data;
730
+ let translationMap = pageData?.translationMap;
731
+ if (translationMap) {
732
+ this.translationMap = translationMap;
733
+ this.translationLocale = pageData?.translationLocale || "";
734
+ this.translationLanguageId = pageData?.translationLanguageId || "";
735
+ } else {
736
+ this.translationMap = {};
737
+ this.translationLocale = "";
738
+ this.translationLanguageId = "";
739
+ }
740
+ };
741
+ /**
742
+ * Replace the translation sidecar (Builder pushes this when translation data
743
+ * arrives or the active language changes) and refresh every item so their
744
+ * merged snapshots recompute.
745
+ */
491
746
  setTranslationSidecar = (map = {}, locale = "", languageId = "") => {
492
747
  this.translationMap = map;
493
748
  this.translationLocale = locale;
494
749
  this.translationLanguageId = languageId;
750
+ this.refreshAllItems();
751
+ };
752
+ /**
753
+ * Update a single translatable field for one item. Assigns a new per-item
754
+ * entry object so `WeaverseNextItem.getSnapShot()`'s translation-ref cache
755
+ * invalidates, then triggers only that item's subscribers.
756
+ */
757
+ updateTranslation = (itemId, key, originalValue, translatedValue) => {
758
+ if (!this.translationMap[itemId]) {
759
+ this.translationMap[itemId] = {};
760
+ }
761
+ this.translationMap[itemId] = {
762
+ ...this.translationMap[itemId],
763
+ [key]: { originalValue, translatedValue }
764
+ };
765
+ let instance = this.itemInstances.get(itemId);
766
+ if (instance) {
767
+ instance.triggerUpdate();
768
+ }
769
+ };
770
+ /**
771
+ * Collect translation changes for the save flow. Returns `undefined` unless
772
+ * both the locale and language ID are set and at least one entry exists;
773
+ * each entry key is namespaced as `data.<field>` to match Builder.
774
+ */
775
+ getTranslationChanges = () => {
776
+ let languageId = this.translationLanguageId;
777
+ if (!(this.translationLocale && languageId)) {
778
+ return;
779
+ }
780
+ let entries = [];
781
+ for (let [itemId, fields] of Object.entries(this.translationMap)) {
782
+ for (let [key, entry] of Object.entries(fields)) {
783
+ entries.push({
784
+ itemId,
785
+ key: `data.${key}`,
786
+ originalValue: entry.originalValue,
787
+ translatedValue: entry.translatedValue
788
+ });
789
+ }
790
+ }
791
+ if (entries.length === 0) {
792
+ return;
793
+ }
794
+ return { languageId, entries };
795
+ };
796
+ /**
797
+ * Re-extract the translation sidecar whenever project data is replaced (route
798
+ * change / non-design reuse), before rebuilding item stores. Takes the
799
+ * renderable page shape (`rootId` resolved) that `getRenderablePage()` and
800
+ * the reuse branch pass in.
801
+ */
802
+ setProjectData = (data) => {
803
+ this.data = data;
804
+ this.extractTranslationSidecar();
805
+ this.initProject();
495
806
  };
496
- updateTranslation = () => void 0;
497
- getTranslationChanges = () => void 0;
498
807
  };
499
808
  function createWeaverseNextRuntime(config) {
500
809
  let requestInfo = buildWeaverseNextRequestInfo(config.client?.requestContext);
@@ -528,6 +837,11 @@ function createWeaverseNextRuntime(config) {
528
837
  if (config.themeSettingsStore) {
529
838
  existing.internal.themeSettingsStore = config.themeSettingsStore;
530
839
  }
840
+ if (config.translationStore) {
841
+ existing.internal.translationStore = config.translationStore;
842
+ existing.internal.themeTextStore = config.translationStore;
843
+ }
844
+ existing.internal.merchantOverrides = config.merchantOverrides;
531
845
  registerRuntime(existing);
532
846
  return existing;
533
847
  }
@@ -706,9 +1020,19 @@ var WeaverseNextRenderer = react$1.memo(function WeaverseNextRendererComponent(p
706
1020
  client,
707
1021
  data: { ...data, page },
708
1022
  dataContext,
709
- themeSettingsStore: context?.themeSettingsStore
1023
+ merchantOverrides: context?.merchantOverrides,
1024
+ themeSettingsStore: context?.themeSettingsStore,
1025
+ translationStore: context?.translationStore
710
1026
  });
711
- }, [client, page, data, dataContext, context?.themeSettingsStore]);
1027
+ }, [
1028
+ client,
1029
+ page,
1030
+ data,
1031
+ dataContext,
1032
+ context?.merchantOverrides,
1033
+ context?.themeSettingsStore,
1034
+ context?.translationStore
1035
+ ]);
712
1036
  if (!weaverse) {
713
1037
  return null;
714
1038
  }
@@ -916,6 +1240,10 @@ Object.defineProperty(exports, "createSchema", {
916
1240
  get: function () { return schema.createSchema; }
917
1241
  });
918
1242
  exports.DEFAULT_REVALIDATE_ENDPOINT = DEFAULT_REVALIDATE_ENDPOINT;
1243
+ exports.ThemeTextProvider = ThemeTextProvider;
1244
+ exports.ThemeTextStore = ThemeTextStore;
1245
+ exports.TranslationProvider = TranslationProvider;
1246
+ exports.TranslationStore = TranslationStore;
919
1247
  exports.WeaverseNextProvider = WeaverseNextProvider;
920
1248
  exports.WeaverseNextRenderer = WeaverseNextRenderer;
921
1249
  exports.WeaverseNextRootProvider = WeaverseNextRootProvider;
@@ -925,17 +1253,22 @@ exports.WeaverseNextStudioBridge = WeaverseNextStudioBridge;
925
1253
  exports.WeaverseNextStudioConnect = WeaverseNextStudioConnect;
926
1254
  exports.bindWeaverseNextStudioRuntime = bindWeaverseNextStudioRuntime;
927
1255
  exports.buildWeaverseNextRequestInfo = buildWeaverseNextRequestInfo;
1256
+ exports.createTranslate = createTranslate;
928
1257
  exports.createWeaverseNextClient = createWeaverseNextClient;
929
1258
  exports.createWeaverseNextRuntime = createWeaverseNextRuntime;
930
1259
  exports.createWeaverseNextStudioInternals = createWeaverseNextStudioInternals;
931
1260
  exports.createWeaverseNextThemeSettingsStore = createWeaverseNextThemeSettingsStore;
932
1261
  exports.formatWeaverseNextSeoMetadata = formatWeaverseNextSeoMetadata;
1262
+ exports.getNestedKey = getNestedKey;
933
1263
  exports.getWeaverseNextSeoMetadata = getWeaverseNextSeoMetadata;
1264
+ exports.interpolate = interpolate;
934
1265
  exports.loadWeaverseNextStudioScript = loadWeaverseNextStudioScript;
935
1266
  exports.resolveWeaverseNextStudioScriptSrc = resolveWeaverseNextStudioScriptSrc;
936
1267
  exports.revalidateWeaverseNextItem = revalidateWeaverseNextItem;
937
1268
  exports.runWeaverseComponentLoaders = runWeaverseComponentLoaders;
938
1269
  exports.useThemeSettings = useThemeSettings;
1270
+ exports.useThemeText = useThemeText;
1271
+ exports.useTranslation = useTranslation;
939
1272
  exports.useWeaverseCommerce = useWeaverseCommerce;
940
1273
  exports.useWeaverseNextStudioInternals = useWeaverseNextStudioInternals;
941
1274
  exports.useWeaversePageData = useWeaversePageData;