diff --git a/studio/src/editor-panel.js b/studio/src/editor-panel.js index 655e36fc6..48d610038 100644 --- a/studio/src/editor-panel.js +++ b/studio/src/editor-panel.js @@ -444,7 +444,7 @@ export default class EditorPanel extends LitElement { const fragmentLocale = extractLocaleFromPath(this.fragment?.path); if (fragmentLocale && fragmentLocale !== Store.filters.value.locale) { Store.filters.set((prev) => ({ ...prev, locale: fragmentLocale })); - await this.repository.loadPreviewPlaceholders(); + void this.repository.loadPreviewPlaceholders(); this.fragmentStore?.resolvePreviewFragment?.(); } } diff --git a/studio/src/mas-fragment-editor.js b/studio/src/mas-fragment-editor.js index 5a91a0c78..a80538c74 100644 --- a/studio/src/mas-fragment-editor.js +++ b/studio/src/mas-fragment-editor.js @@ -698,6 +698,9 @@ export default class MasFragmentEditor extends LitElement { // Initializes editor state when the fragment already exists in the list store cache. async #initializeFromCachedStore(fragmentId, existingStore) { + if (this.repository.search.value.path) { + void this.repository.loadPreviewPlaceholders(Store.localeOrRegion()); + } const fragmentPath = existingStore.get().path; const fragmentLocale = extractLocaleFromPath(fragmentPath); @@ -717,10 +720,15 @@ export default class MasFragmentEditor extends LitElement { const localeChanged = fragmentLocale && fragmentLocale !== Store.localeOrRegion(); if (localeChanged) { Store.search.set((prev) => ({ ...prev, region: fragmentLocale })); - await this.repository.loadPreviewPlaceholders(); + void this.repository.loadPreviewPlaceholders(); existingStore.resolvePreviewFragment(); } await this.#attachParentToCachedVariation(existingStore, fragmentPath); + if (fragmentLocale && fragmentLocale !== Store.localeOrRegion()) { + Store.search.set((prev) => ({ ...prev, region: fragmentLocale })); + void this.repository.loadPreviewPlaceholders(); + existingStore.resolvePreviewFragment(); + } } else { this.localeDefaultFragment = existingStore.parentFragment; } @@ -770,8 +778,9 @@ export default class MasFragmentEditor extends LitElement { // Initializes editor state for fragments that are not yet present in list store cache. async #initializeFromRepository(fragmentId) { try { - // Start loading placeholders early - const placeholdersPromise = this.repository.loadPreviewPlaceholders().catch(() => null); + if (this.repository.search.value.path) { + void this.repository.loadPreviewPlaceholders(Store.localeOrRegion()); + } const fragmentData = await this.repository.aem.sites.cf.fragments.getById(fragmentId); const fragment = new Fragment(fragmentData); @@ -782,8 +791,16 @@ export default class MasFragmentEditor extends LitElement { const parentFragment = await this.#resolveParentForFetchedVariation(fragmentId, fragment, isVariationAfterContext); const isVariationForStore = isVariationAfterContext || !!parentFragment; - // Wait for placeholders before creating stores (needed for preview resolution) - await placeholdersPromise; + if (isVariationForStore) { + const fragmentLocale = extractLocaleFromPath(fragment.path); + if (fragmentLocale && fragmentLocale !== Store.localeOrRegion()) { + Store.search.set((prev) => ({ ...prev, region: fragmentLocale })); + } + } + + if (this.repository.search.value.path) { + void this.repository.loadPreviewPlaceholders(); + } const fragmentStore = generateFragmentStore(fragment, parentFragment); // Only add to main list if not a variation (variations appear under parent's variations panel) @@ -801,7 +818,7 @@ export default class MasFragmentEditor extends LitElement { const localeChanged = fragmentLocale !== Store.localeOrRegion(); Store.search.set((prev) => ({ ...prev, region: fragmentLocale })); if (localeChanged) { - await this.repository.loadPreviewPlaceholders(); + void this.repository.loadPreviewPlaceholders(); fragmentStore.resolvePreviewFragment(); } } diff --git a/studio/src/mas-repository.js b/studio/src/mas-repository.js index 7e2d3030b..4bb6438d1 100644 --- a/studio/src/mas-repository.js +++ b/studio/src/mas-repository.js @@ -106,7 +106,7 @@ export class MasRepository extends LitElement { collections: null, }; this.dictionaryCache = new Map(); - this.inflightDictionaryRequest = null; + this.inflightDictionaryByKey = new Map(); this.saveFragment = this.saveFragment.bind(this); this.copyFragment = this.copyFragment.bind(this); this.publishFragment = this.publishFragment.bind(this); @@ -124,6 +124,8 @@ export class MasRepository extends LitElement { #abortControllers; #searchCursor = null; #addonPlaceholdersRequest = null; + #previewDictionaryAbortByKey = new Map(); + #previewDictionaryLoadingDepth = 0; /** * When personalization is off, exclude fragments that carry mas:pzn/… tags except mas:pzn/country/…. @@ -149,12 +151,14 @@ export class MasRepository extends LitElement { // Invalidate dictionary cache when filters or search path change Store.filters.subscribe(() => { this.dictionaryCache.clear(); + Store.placeholders.previewByLocale.set({}); if (this.page.value === PAGE_NAMES.CONTENT) { this.#searchCursor = null; } }); Store.search.subscribe(() => { this.dictionaryCache.clear(); + Store.placeholders.previewByLocale.set({}); this.#searchCursor = null; }); @@ -760,71 +764,86 @@ export class MasRepository extends LitElement { } } - async loadPreviewPlaceholders() { + /** + * Loads preview dictionary for `locale` (defaults to surface locale) into `Store.placeholders.previewByLocale`. + * Safe to call in parallel for different locales; duplicate cache keys share one in-flight request. + * @param {string} [locale] + */ + async loadPreviewPlaceholders(locale = Store.localeOrRegion()) { if (!this.search.value.path) return; - const cacheKey = `${this.filters.value.locale}_${this.search.value.path}`; + const path = this.search.value.path; + const cacheKey = `${locale}_${path}`; - // Return cached result if available if (this.dictionaryCache.has(cacheKey)) { - Store.placeholders.preview.set(this.dictionaryCache.get(cacheKey)); + const cached = this.dictionaryCache.get(cacheKey); + Store.placeholders.previewByLocale.set((prev) => ({ ...prev, [locale]: cached })); return; } - // Return existing promise if same cache key is already in-flight - if (this.inflightDictionaryRequest?.cacheKey === cacheKey) { - return this.inflightDictionaryRequest.promise; + if (this.inflightDictionaryByKey.has(cacheKey)) { + return this.inflightDictionaryByKey.get(cacheKey); } - // Abort previous placeholder fetch if still running with different cache key - if (this.#abortControllers.placeholders) { - this.#abortControllers.placeholders.abort(); - } - this.#abortControllers.placeholders = new AbortController(); + const previousAbort = this.#previewDictionaryAbortByKey.get(cacheKey); + previousAbort?.abort(); + const abortController = new AbortController(); + this.#previewDictionaryAbortByKey.set(cacheKey, abortController); - try { - const promise = this.fetchDictionary(this.#abortControllers.placeholders); - this.inflightDictionaryRequest = { promise, cacheKey }; - const result = await promise; - - // Verify cache key hasn't changed during fetch (prevents stale data) - const currentKey = `${this.filters.value.locale}_${this.search.value.path}`; - if (currentKey === cacheKey) { - // If result is empty and locale isn't en_US, try fallback - if ((!result || Object.keys(result).length === 0) && this.filters.value.locale !== 'en_US') { + const promise = (async () => { + this.#previewDictionaryLoadingDepth += 1; + if (this.#previewDictionaryLoadingDepth === 1) { + Store.placeholders.list.loading.set(true); + } + try { + const result = await this.fetchDictionary(abortController, locale); + + if (this.search.value.path !== path) return; + + const mergeDict = (dict) => { + this.dictionaryCache.set(cacheKey, dict); + Store.placeholders.previewByLocale.set((prev) => ({ ...prev, [locale]: dict })); + }; + + if ((!result || Object.keys(result).length === 0) && locale !== 'en_US') { const fallbackContext = { preview: { url: 'https://odinpreview.corp.adobe.com/adobe/sites/cf/fragments', }, locale: 'en_US', surface: this.search.value.path, - signal: this.#abortControllers.placeholders?.signal, + signal: abortController.signal, }; const fallbackResult = await getDictionary(fallbackContext); - this.dictionaryCache.set(cacheKey, fallbackResult); - Store.placeholders.preview.set(fallbackResult); + if (this.search.value.path !== path) return; + mergeDict(fallbackResult); } else { - this.dictionaryCache.set(cacheKey, result); - Store.placeholders.preview.set(result); + mergeDict(result); + } + } catch (error) { + if (error.name === 'AbortError') return; + this.processError(error, 'Could not load preview placeholders.'); + } finally { + this.inflightDictionaryByKey.delete(cacheKey); + this.#previewDictionaryAbortByKey.delete(cacheKey); + this.#previewDictionaryLoadingDepth -= 1; + if (this.#previewDictionaryLoadingDepth === 0) { + Store.placeholders.list.loading.set(false); } } - } catch (error) { - if (error.name === 'AbortError') return; // Silent abort during navigation - this.processError(error, 'Could not load preview placeholders.'); - } finally { - this.inflightDictionaryRequest = null; - this.#abortControllers.placeholders = null; - Store.placeholders.list.loading.set(false); - } + })(); + + this.inflightDictionaryByKey.set(cacheKey, promise); + return promise; } - async fetchDictionary(abortController) { + async fetchDictionary(abortController, locale = Store.localeOrRegion()) { const context = { preview: { url: 'https://odinpreview.corp.adobe.com/adobe/sites/cf/fragments', }, - locale: this.filters.value.locale, + locale, surface: this.search.value.path, networkConfig: { mainTimeout: 15000, @@ -2187,8 +2206,8 @@ export class MasRepository extends LitElement { Store.placeholders.addons.loading.set(true); try { await this.loadPreviewPlaceholders(); - const dictionary = Store.placeholders.preview.get(); - if (dictionary) { + const dictionary = Store.previewDictionary(); + if (Store.previewDictionaryReady()) { const addonFragments = Object.keys(dictionary) .filter((key) => /^addon-/.test(key)) .map((key) => ({ value: key, itemText: key })); diff --git a/studio/src/reactivity/preview-fragment-store.js b/studio/src/reactivity/preview-fragment-store.js index 112ad8cf2..1a45eab65 100644 --- a/studio/src/reactivity/preview-fragment-store.js +++ b/studio/src/reactivity/preview-fragment-store.js @@ -73,8 +73,8 @@ export class PreviewFragmentStore extends FragmentStore { super(fragmentInstance, validator); this.lazy = lazy; - this.placeholderUnsubscribe = Store.placeholders.preview.subscribe(() => { - if (!this.lazy && !this.resolved && Store.placeholders.preview.value) { + this.placeholderUnsubscribe = Store.placeholders.previewByLocale.subscribe(() => { + if (!this.lazy && !this.resolved && Store.previewDictionaryReady()) { this.resolveFragment(true); } }); @@ -171,7 +171,7 @@ export class PreviewFragmentStore extends FragmentStore { return; } - if (this.isCollection || !Store.placeholders.preview.value) { + if (this.isCollection || !Store.previewDictionaryReady()) { this.resolved = true; this.refreshAemFragment(true); this.notify(); @@ -215,7 +215,7 @@ export class PreviewFragmentStore extends FragmentStore { const context = { locale: Store.localeOrRegion(), surface: Store.surface(), - dictionary: Store.placeholders.preview.value, + dictionary: Store.previewDictionary(), }; const result = await previewStudioFragment(body, context); @@ -295,7 +295,7 @@ export class PreviewFragmentStore extends FragmentStore { */ dispose() { if (this.placeholderUnsubscribe) { - Store.placeholders.preview.unsubscribe(this.placeholderUnsubscribe); + Store.placeholders.previewByLocale.unsubscribe(this.placeholderUnsubscribe); this.placeholderUnsubscribe = null; } } diff --git a/studio/src/store.js b/studio/src/store.js index ea4690c61..82488711c 100644 --- a/studio/src/store.js +++ b/studio/src/store.js @@ -71,7 +71,7 @@ const Store = { loading: new ReactiveStore(false), data: new ReactiveStore([{ value: 'disabled', itemText: 'disabled' }]), }, - preview: new ReactiveStore(null), + previewByLocale: new ReactiveStore({}), }, settings: new SettingsStore(), profile: new ReactiveStore({}), @@ -102,6 +102,15 @@ const Store = { localeOrRegion: function () { return Store.search.value.region || Store.filters.value.locale || 'en_US'; }, + previewDictionary: function () { + const locale = Store.localeOrRegion(); + return Store.placeholders.previewByLocale.value[locale]; + }, + /** True when the active locale has a loaded dictionary with at least one entry (empty `{}` is not ready). */ + previewDictionaryReady: function () { + const d = Store.previewDictionary(); + return d != null && Object.keys(d).length > 0; + }, removeRegionOverride: function () { if (Store.search.value.region) { Store.search.set((prev) => ({ ...prev, region: null })); @@ -261,7 +270,7 @@ Store.page.subscribe((value) => { Store.sort.set({ sortBy: SORT_COLUMNS[value]?.[0], sortDirection: 'asc' }); }); -Store.placeholders.preview.subscribe(() => { +Store.placeholders.previewByLocale.subscribe(() => { if (Store.page.value === PAGE_NAMES.CONTENT) { for (const fragmentStore of Store.fragments.list.data.value) { fragmentStore.resolvePreviewFragment(); diff --git a/studio/test/editors/merch-card-collection-editor-variation.test.html b/studio/test/editors/merch-card-collection-editor-variation.test.html index e9a23a307..1b3ff4889 100644 --- a/studio/test/editors/merch-card-collection-editor-variation.test.html +++ b/studio/test/editors/merch-card-collection-editor-variation.test.html @@ -120,7 +120,7 @@ fragmentStore = generateFragmentStore(variationFragment, parentFragment); // Disable preview resolution — collection editor doesn't need it - Store.placeholders.preview.set(null); + Store.placeholders.previewByLocale.set({}); Store.fragments.list.data.set([fragmentStore]); const updateCalls = []; diff --git a/studio/test/editors/merch-card-editor.test.html b/studio/test/editors/merch-card-editor.test.html index 149524872..0057e51bd 100644 --- a/studio/test/editors/merch-card-editor.test.html +++ b/studio/test/editors/merch-card-editor.test.html @@ -219,7 +219,7 @@ Store.fragmentEditor.fragmentId.set(`test-variation-${testId}`); Store.fragments.inEdit.set(fragmentStore); Store.fragments.list.data.set([fragmentStore]); - Store.placeholders.preview.set({}); + Store.placeholders.previewByLocale.set({ [variationLocale]: {} }); Store.search.set((prev) => ({ ...prev, surface: 'web', path: 'nala', region: variationLocale })); Store.viewMode.set('editing'); Store.settings.rows.set(createDefaultSettingsRows()); @@ -810,7 +810,7 @@ Store.fragmentEditor.fragmentId.set(`test-settings-${testId}`); Store.fragments.inEdit.set(store); Store.fragments.list.data.set([store]); - Store.placeholders.preview.set({}); + Store.placeholders.previewByLocale.set({ en_AU: {} }); Store.search.set((prev) => ({ ...prev, surface: 'web', path: 'nala', region: 'en_AU' })); Store.viewMode.set('editing'); Store.settings.rows.set(settingsRows); diff --git a/studio/test/mas-fragment-editor.test.js b/studio/test/mas-fragment-editor.test.js index 27ae70421..88763fdbe 100644 --- a/studio/test/mas-fragment-editor.test.js +++ b/studio/test/mas-fragment-editor.test.js @@ -4,7 +4,7 @@ import '../src/mas-fragment-editor.js'; import MasFragmentEditor from '../src/mas-fragment-editor.js'; import Store from '../src/store.js'; import { Fragment } from '../src/aem/fragment.js'; -import generateFragmentStore, { SourceFragmentStore } from '../src/reactivity/source-fragment-store.js'; +import generateFragmentStore from '../src/reactivity/source-fragment-store.js'; import { PAGE_NAMES, CARD_MODEL_PATH, ODIN_PREVIEW_ORIGIN } from '../src/constants.js'; import router from '../src/router.js'; import Events from '../src/events.js'; @@ -215,6 +215,7 @@ describe('MasFragmentEditor', () => { mockRepo = { refreshFragment: sandbox.stub().resolves(), loadPreviewPlaceholders: sandbox.stub().resolves(), + search: { value: { path: 'sandbox' } }, aem: { sites: { cf: { @@ -240,6 +241,7 @@ describe('MasFragmentEditor', () => { search: structuredClone(Store.search.get()), filters: structuredClone(Store.filters.get()), translatedLocales: Store.fragmentEditor.translatedLocales.get(), + placeholdersPreview: Store.placeholders.previewByLocale.get(), }; Store.fragments.list.data.value = []; @@ -249,6 +251,7 @@ describe('MasFragmentEditor', () => { Store.search.value = {}; Store.filters.value = { locale: 'en_US' }; Store.fragmentEditor.translatedLocales.value = null; + Store.placeholders.previewByLocale.value = {}; }); afterEach(() => { @@ -262,6 +265,7 @@ describe('MasFragmentEditor', () => { Store.search.value = originalStoreState.search; Store.filters.value = originalStoreState.filters; Store.fragmentEditor.translatedLocales.value = originalStoreState.translatedLocales; + Store.placeholders.previewByLocale.value = originalStoreState.placeholdersPreview; }); it('returns early when no fragment ID exists', async () => { @@ -327,7 +331,7 @@ describe('MasFragmentEditor', () => { await el.initFragment(); - expect(mockRepo.loadPreviewPlaceholders.calledOnce).to.be.true; + expect(mockRepo.loadPreviewPlaceholders.callCount).to.equal(2); expect(el.editorContextStore.loadFragmentContext.calledOnceWith('new-id', fragmentData.path)).to.be.true; expect(Store.fragments.list.data.get()).to.have.lengthOf(1); expect(Store.fragments.list.data.get()[0].id).to.equal('new-id'); @@ -358,7 +362,6 @@ describe('MasFragmentEditor', () => { it('reloads locale placeholders for variations when active locale differs', async () => { const fragmentData = createFragmentData({ id: 'variation-id', locale: 'fr_FR', slug: 'variation' }); - const resolvePreviewSpy = sandbox.spy(SourceFragmentStore.prototype, 'resolvePreviewFragment'); Store.filters.value = { locale: 'tr_TR' }; mockRepo.aem.sites.cf.fragments.getById.resolves(fragmentData); @@ -366,11 +369,59 @@ describe('MasFragmentEditor', () => { sandbox.stub(el, 'resolveVariationParentFragment').resolves(null); Store.fragmentEditor.fragmentId.value = 'variation-id'; + mockRepo.loadPreviewPlaceholders.callsFake(async () => { + Store.placeholders.previewByLocale.set((prev) => ({ ...prev, fr_FR: { testDictionary: true } })); + }); + + await el.initFragment(); + + expect(mockRepo.loadPreviewPlaceholders.callCount).to.equal(2); + expect(Store.search.get().region).to.equal('fr_FR'); + expect(Store.previewDictionary()).to.deep.equal({ testDictionary: true }); + }); + + it('reloads locale placeholders for cached variation when locale differs from Store.localeOrRegion()', async () => { + const fragmentData = createFragmentData({ id: 'cached-var-id', locale: 'fr_FR', slug: 'variation' }); + const parentData = createFragmentData({ id: 'parent-id', locale: 'en_US', slug: 'default' }); + const sourceStore = generateFragmentStore(new Fragment(fragmentData), new Fragment(parentData)); + sandbox.spy(sourceStore, 'resolvePreviewFragment'); + + mockRepo.loadPreviewPlaceholders.callsFake(async () => { + Store.placeholders.previewByLocale.set((prev) => ({ ...prev, fr_FR: { fromCachedVariation: true } })); + }); + + el.editorContextStore.isVariation.returns(true); + sandbox.stub(el, 'resolveVariationParentFragment').resolves(new Fragment(parentData)); + + Store.filters.value = { locale: 'tr_TR' }; + Store.search.value = { path: 'sandbox' }; + Store.fragments.list.data.value = [sourceStore]; + Store.fragmentEditor.fragmentId.value = 'cached-var-id'; + await el.initFragment(); expect(mockRepo.loadPreviewPlaceholders.callCount).to.equal(2); - expect(resolvePreviewSpy.calledOnce).to.be.true; expect(Store.search.get().region).to.equal('fr_FR'); + expect(Store.previewDictionary()).to.deep.equal({ fromCachedVariation: true }); + expect(sourceStore.resolvePreviewFragment.calledOnce).to.be.true; + }); + + it('does not reload preview placeholders when cached variation locale matches Store.localeOrRegion()', async () => { + const fragmentData = createFragmentData({ id: 'cached-var-id', locale: 'fr_FR', slug: 'variation' }); + const parentData = createFragmentData({ id: 'parent-id', locale: 'en_US', slug: 'default' }); + const sourceStore = generateFragmentStore(new Fragment(fragmentData), new Fragment(parentData)); + + el.editorContextStore.isVariation.returns(true); + sandbox.stub(el, 'resolveVariationParentFragment').resolves(new Fragment(parentData)); + + Store.filters.value = { locale: 'fr_FR' }; + Store.search.value = { path: 'sandbox' }; + Store.fragments.list.data.value = [sourceStore]; + Store.fragmentEditor.fragmentId.value = 'cached-var-id'; + + await el.initFragment(); + + expect(mockRepo.loadPreviewPlaceholders.callCount).to.equal(1); }); it('uses pending parent from create variation event when context is not ready', async () => { diff --git a/studio/test/mas-repository.test.js b/studio/test/mas-repository.test.js index 013ddc1be..5420232ae 100644 --- a/studio/test/mas-repository.test.js +++ b/studio/test/mas-repository.test.js @@ -2871,4 +2871,42 @@ describe('MasRepository dictionary helpers', () => { expect(fragmentDeletedEmitStub.calledOnceWith(fragment)).to.be.true; }); }); + + describe('loadPreviewPlaceholders', () => { + let previousSearch; + let previousFilters; + let previousPreview; + + beforeEach(() => { + previousSearch = structuredClone(Store.search.get()); + previousFilters = structuredClone(Store.filters.get()); + previousPreview = Store.placeholders.previewByLocale.get(); + }); + + afterEach(() => { + Store.search.value = previousSearch; + Store.filters.value = previousFilters; + Store.placeholders.previewByLocale.value = previousPreview; + }); + + it('uses Store.localeOrRegion() for cache key and fetchDictionary locale', async () => { + const repository = createFullRepository(); + repository.dictionaryCache.clear(); + + Store.search.value = { path: 'sandbox' }; + Store.filters.value = { locale: 'en_US' }; + Store.search.set((prev) => ({ ...prev, region: 'fr_FR' })); + // Unconnected repo: StoreController does not receive Store updates unless we sync. + repository.search.value = Store.search.get(); + + const fetchStub = sandbox.stub(repository, 'fetchDictionary').resolves({ dictKey: 'dictVal' }); + + await repository.loadPreviewPlaceholders(); + + expect(fetchStub.calledOnce).to.be.true; + expect(fetchStub.firstCall.args[1]).to.equal('fr_FR'); + expect(repository.dictionaryCache.has('fr_FR_sandbox')).to.be.true; + expect(Store.placeholders.previewByLocale.get().fr_FR).to.deep.equal({ dictKey: 'dictVal' }); + }); + }); }); diff --git a/studio/test/reactivity/preview-fragment-store.test.js b/studio/test/reactivity/preview-fragment-store.test.js index 383bf5f03..7b778e3f3 100644 --- a/studio/test/reactivity/preview-fragment-store.test.js +++ b/studio/test/reactivity/preview-fragment-store.test.js @@ -102,7 +102,7 @@ describe('mergeResolvedPreviewFields', () => { describe('PreviewFragmentStore', () => { let sandbox; let placeholderSubscribers; - let originalPlaceholdersPreview; + let originalPlaceholdersPreviewByLocale; let originalSurface; let originalLocaleOrRegion; @@ -122,10 +122,10 @@ describe('PreviewFragmentStore', () => { sandbox = sinon.createSandbox(); placeholderSubscribers = []; - originalPlaceholdersPreview = Store.placeholders.preview; + originalPlaceholdersPreviewByLocale = Store.placeholders.previewByLocale; - Store.placeholders.preview = { - value: { key: 'value' }, + Store.placeholders.previewByLocale = { + value: { en_US: { key: 'value' } }, subscribe: (fn) => { placeholderSubscribers.push(fn); return fn; @@ -144,7 +144,7 @@ describe('PreviewFragmentStore', () => { afterEach(() => { sandbox.restore(); - Store.placeholders.preview = originalPlaceholdersPreview; + Store.placeholders.previewByLocale = originalPlaceholdersPreviewByLocale; Store.surface = originalSurface; Store.localeOrRegion = originalLocaleOrRegion; });