From aef72a104a2a8eb995e3723b888924cb1b8d5557 Mon Sep 17 00:00:00 2001 From: mkucmus Date: Thu, 11 Jun 2026 12:08:07 +0200 Subject: [PATCH 1/3] feat(composables): extract language info directly from context --- .changeset/current-locale-from-context.md | 5 +++++ .../useSessionContext/useSessionContext.test.ts | 16 ++++++++++++++++ .../src/useSessionContext/useSessionContext.ts | 10 ++++++++++ templates/vue-demo-store/app/app.vue | 8 ++++++-- templates/vue-starter-template/app/app.vue | 8 ++++++-- 5 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 .changeset/current-locale-from-context.md diff --git a/.changeset/current-locale-from-context.md b/.changeset/current-locale-from-context.md new file mode 100644 index 000000000..16b3e2364 --- /dev/null +++ b/.changeset/current-locale-from-context.md @@ -0,0 +1,5 @@ +--- +"@shopware/composables": minor +--- + +`useSessionContext`: expose `currentLocaleCode` — the active language's locale code (e.g. `"en-GB"`), read directly from the context's `languageInfo`. The current locale can now be derived from the session context alone, without loading the full language list via `useInternationalization().getAvailableLanguages()` just to map the current `languageId` to a locale. diff --git a/packages/composables/src/useSessionContext/useSessionContext.test.ts b/packages/composables/src/useSessionContext/useSessionContext.test.ts index 64e8a64ef..f0a5a9a03 100644 --- a/packages/composables/src/useSessionContext/useSessionContext.test.ts +++ b/packages/composables/src/useSessionContext/useSessionContext.test.ts @@ -67,6 +67,22 @@ describe("useSessionContext", () => { expect(consoleErrorSpy).toHaveBeenCalledTimes(1); }); + it("currentLocaleCode reads languageInfo.localeCode from the context", async () => { + const { vm, injections } = useSetup(() => useSessionContext()); + injections.apiClient.invoke.mockResolvedValue({ + data: { + languageInfo: { name: "English", localeCode: "en-GB" }, + } as Schemas["SalesChannelContext"], + }); + await vm.refreshSessionContext(); + expect(vm.currentLocaleCode).toBe("en-GB"); + }); + + it("currentLocaleCode is undefined when the context has no languageInfo", () => { + const { vm } = useSetup(() => useSessionContext()); + expect(vm.currentLocaleCode).toBeUndefined(); + }); + it("setShippingMethod", async () => { const { vm, injections } = useSetup(() => useSessionContext()); injections.apiClient.invoke.mockResolvedValue({ data: {} }); diff --git a/packages/composables/src/useSessionContext/useSessionContext.ts b/packages/composables/src/useSessionContext/useSessionContext.ts index 738d5a679..ff51ad5de 100644 --- a/packages/composables/src/useSessionContext/useSessionContext.ts +++ b/packages/composables/src/useSessionContext/useSessionContext.ts @@ -103,6 +103,12 @@ export type UseSessionContextReturn = { * current language code */ currentLanguageId: ComputedRef; + /** + * Locale code of the active language (e.g. `"en-GB"`), read from the + * context's `languageInfo`. Available from the session context alone — no + * separate language list request is required to detect the current locale. + */ + currentLocaleCode: ComputedRef; /** * current context's customer object */ @@ -262,6 +268,9 @@ export function useSessionContext( const languageIdChain = computed( () => sessionContext.value?.context?.languageIdChain?.[0] || "", ); + const currentLocaleCode = computed( + () => sessionContext.value?.languageInfo?.localeCode, + ); const taxState = computed(() => sessionContext.value?.context?.taxState); const userFromContext = computed(() => sessionContext.value?.customer); @@ -287,6 +296,7 @@ export function useSessionContext( languageIdChain, salesChannelLanguageId: languageId, currentLanguageId: languageIdChain, + currentLocaleCode, setCountry, setContext, }; diff --git a/templates/vue-demo-store/app/app.vue b/templates/vue-demo-store/app/app.vue index aa2d07458..35cb80b56 100644 --- a/templates/vue-demo-store/app/app.vue +++ b/templates/vue-demo-store/app/app.vue @@ -58,7 +58,8 @@ const { changeLanguage, languages: storeLanguages, } = useInternationalization(); -const { languageIdChain, refreshSessionContext } = useSessionContext(); +const { languageIdChain, currentLocaleCode, refreshSessionContext } = + useSessionContext(); const { data: languages } = await useAsyncData("languages", async () => { return await getAvailableLanguages(); @@ -86,7 +87,10 @@ if (languages.value?.elements.length && router.currentRoute.value.name) { languageToChangeId = localeProperties.value.localeId as string; } } else { - const sessionLanguage = getLanguageCodeFromId(languageIdChain.value); + // Prefer the active locale from the context's `languageInfo`; fall back to + // mapping the language id against the fetched list for older backends. + const sessionLanguage = + currentLocaleCode.value ?? getLanguageCodeFromId(languageIdChain.value); // If languages are not the same, set one from prefix if (sessionLanguage !== prefix) { diff --git a/templates/vue-starter-template/app/app.vue b/templates/vue-starter-template/app/app.vue index 015d2b1fa..7f256dac6 100644 --- a/templates/vue-starter-template/app/app.vue +++ b/templates/vue-starter-template/app/app.vue @@ -75,7 +75,8 @@ const { const router = useRouter(); const route = useRoute(); -const { languageIdChain, refreshSessionContext } = useSessionContext(); +const { languageIdChain, currentLocaleCode, refreshSessionContext } = + useSessionContext(); let languageToChangeId: string | null = null; @@ -100,7 +101,10 @@ if (languages && router.currentRoute.value.name) { languageToChangeId = localeProperties.value.localeId as string; } } else { - const sessionLanguage = getLanguageCodeFromId(languageIdChain.value); + // Prefer the active locale from the context's `languageInfo`; fall back to + // mapping the language id against the fetched list for older backends. + const sessionLanguage = + currentLocaleCode.value ?? getLanguageCodeFromId(languageIdChain.value); // If languages are not the same, set one from prefix if (sessionLanguage !== prefix) { From 92e7cc799c5302a4056221b765e934cc6511180b Mon Sep 17 00:00:00 2001 From: mkucmus Date: Fri, 12 Jun 2026 09:49:36 +0200 Subject: [PATCH 2/3] fix: example types consistency --- .../api-gen.config.json | 3 +- .../api-types/adyenSchema.overrides.json | 18 +++++ .../api-types/storeApiTypes.d.ts | 79 +++++++++---------- .../api-types/storeApiTypes.overrides.ts | 77 ------------------ .../useSessionContext.test.ts | 2 +- templates/vue-demo-store/app/app.vue | 2 - templates/vue-starter-template/app/app.vue | 2 - 7 files changed, 60 insertions(+), 123 deletions(-) create mode 100644 examples/adyen-dropin-component/api-types/adyenSchema.overrides.json diff --git a/examples/adyen-dropin-component/api-gen.config.json b/examples/adyen-dropin-component/api-gen.config.json index 9a7d6e7b7..ec3676e09 100644 --- a/examples/adyen-dropin-component/api-gen.config.json +++ b/examples/adyen-dropin-component/api-gen.config.json @@ -3,7 +3,8 @@ "store-api": { "patches": [ "storeApiSchema.overrides.json", - "storeApiSchemaCommercial.overrides.json" + "storeApiSchemaCommercial.overrides.json", + "adyenSchema.overrides.json" ] } } diff --git a/examples/adyen-dropin-component/api-types/adyenSchema.overrides.json b/examples/adyen-dropin-component/api-types/adyenSchema.overrides.json new file mode 100644 index 000000000..4921bd3ed --- /dev/null +++ b/examples/adyen-dropin-component/api-types/adyenSchema.overrides.json @@ -0,0 +1,18 @@ +{ + "components": { + "SalesChannelContext": [ + { + "properties": { + "extensions": { + "type": "object", + "properties": { + "adyenData": { + "type": "object" + } + } + } + } + } + ] + } +} diff --git a/examples/adyen-dropin-component/api-types/storeApiTypes.d.ts b/examples/adyen-dropin-component/api-types/storeApiTypes.d.ts index 51d38b851..da73ad1e9 100644 --- a/examples/adyen-dropin-component/api-types/storeApiTypes.d.ts +++ b/examples/adyen-dropin-component/api-types/storeApiTypes.d.ts @@ -7526,6 +7526,7 @@ export type Schemas = { readonly updatedAt?: string; }; SalesChannelContext: { + /** @enum {string} */ apiAlias: "sales_channel_context"; /** Core context with general configuration values and state */ context?: { @@ -7533,66 +7534,55 @@ export type Schemas = { currencyId?: string; /** Format: int32 */ currencyPrecision?: number; - languageIdChain?: string[]; // TODO: [OpenAPI][SalesChannelContext] languageIdChain field should be defined properly in context + languageIdChain?: string[]; scope?: string; - source?: string; + source?: { + salesChannelId: string; + /** @enum {string} */ + type: "sales-channel" | "shop-api"; + }; taxState?: string; useCache?: boolean; versionId?: string; }; - /** Currency associated with the current user */ - currency?: components["schemas"]["Currency"]; // TODO: [OpenAPI][SalesChannelContext] currency field should be defined reusing Currency schema + currency?: components["schemas"]["Currency"]; /** Customer group of the current user */ currentCustomerGroup?: { displayGross?: boolean; name?: string; }; - customer?: components["schemas"]["Customer"]; // TODO: [OpenAPI][SalesChannelContext] customer field should be defined reusing Customer schema - extensions: { - adyenData: object; + customer?: null | components["schemas"]["Customer"]; + extensions?: { + adyenData?: GenericRecord; }; /** Fallback group if the default customer group is not applicable */ fallbackCustomerGroup?: { displayGross?: boolean; name?: string; }; - /** Selected payment method */ - paymentMethod?: components["schemas"]["PaymentMethod"] & { - fundingSource: components["schemas"]["AdyenPaymentMethod"]["fundingSource"]; - }; // TODO: [OpenAPI][SalesChannelContext] paymentMethod field should be defined properly reusing PaymentMethod schema - /** Information about the current sales channel */ - salesChannel?: { - accessKey?: string; - active?: boolean; - analyticsId?: string; - countryId?: string; - currencyId?: string; - customerGroupId?: string; - footerCategoryId?: string; - hreflangActive?: boolean; - hreflangDefaultDomainId?: string; - languageId?: string; - mailHeaderFooterId?: string; - maintenance?: boolean; - maintenanceIpWhitelist?: string; - name?: string; + itemRounding: { + /** @enum {string} */ + apiAlias: "shopware_core_framework_data_abstraction_layer_pricing_cash_rounding_config"; /** Format: int32 */ - navigationCategoryDepth?: number; - navigationCategoryId?: string; - paymentMethodId?: string; - serviceCategoryId?: string; - shippingMethodId?: string; - shortName?: string; - typeId?: string; + decimals: number; + /** Format: float */ + interval: number; + roundForNet: boolean; + }; + languageInfo: { + localeCode: string; + name: string; }; + measurementSystem?: components["schemas"]["ContextMeasurementSystemInfo"]; + paymentMethod?: components["schemas"]["PaymentMethod"]; + salesChannel: components["schemas"]["SalesChannel"]; shippingLocation?: { - // TODO: [OpenAPI][SalesChannelContext] shippingLocation field should be defined properly - apiAlias: "cart_delivery_shipping_location"; - country: components["schemas"]["Country"]; - address: components["schemas"]["CustomerAddress"]; + address?: components["schemas"]["CustomerAddress"]; + /** @enum {string} */ + apiAlias?: "cart_delivery_shipping_location"; + country?: components["schemas"]["Country"]; }; - /** Selected shipping method */ - shippingMethod?: components["schemas"]["ShippingMethod"]; // TODO: [OpenAPI][SalesChannelContext] shippingMethod field should be defined properly reusing ShippingMethod schema + shippingMethod?: components["schemas"]["ShippingMethod"]; /** Currently active tax rules and/or rates */ taxRules?: { name?: string; @@ -7601,6 +7591,15 @@ export type Schemas = { }[]; /** Context the user session */ token?: string; + totalRounding: { + /** @enum {string} */ + apiAlias: "shopware_core_framework_data_abstraction_layer_pricing_cash_rounding_config"; + /** Format: int32 */ + decimals: number; + /** Format: float */ + interval: number; + roundForNet: boolean; + }; }; SalesChannelDomain: { /** Format: date-time */ diff --git a/examples/adyen-dropin-component/api-types/storeApiTypes.overrides.ts b/examples/adyen-dropin-component/api-types/storeApiTypes.overrides.ts index ba1114268..c1d27da91 100644 --- a/examples/adyen-dropin-component/api-types/storeApiTypes.overrides.ts +++ b/examples/adyen-dropin-component/api-types/storeApiTypes.overrides.ts @@ -64,83 +64,6 @@ export type Schemas = { storedPaymentMethodId?: string; id: string; }; - SalesChannelContext: { - apiAlias: "sales_channel_context"; - /** Core context with general configuration values and state */ - context?: { - currencyFactor?: number; - currencyId?: string; - /** Format: int32 */ - currencyPrecision?: number; - languageIdChain?: string[]; // TODO: [OpenAPI][SalesChannelContext] languageIdChain field should be defined properly in context - scope?: string; - source?: string; - taxState?: string; - useCache?: boolean; - versionId?: string; - }; - /** Currency associated with the current user */ - currency?: components["schemas"]["Currency"]; // TODO: [OpenAPI][SalesChannelContext] currency field should be defined reusing Currency schema - /** Customer group of the current user */ - currentCustomerGroup?: { - displayGross?: boolean; - name?: string; - }; - customer?: components["schemas"]["Customer"]; // TODO: [OpenAPI][SalesChannelContext] customer field should be defined reusing Customer schema - extensions: { - adyenData: object; - }; - /** Fallback group if the default customer group is not applicable */ - fallbackCustomerGroup?: { - displayGross?: boolean; - name?: string; - }; - /** Selected payment method */ - paymentMethod?: components["schemas"]["PaymentMethod"] & { - fundingSource: components["schemas"]["AdyenPaymentMethod"]["fundingSource"]; - }; // TODO: [OpenAPI][SalesChannelContext] paymentMethod field should be defined properly reusing PaymentMethod schema - /** Information about the current sales channel */ - salesChannel?: { - accessKey?: string; - active?: boolean; - analyticsId?: string; - countryId?: string; - currencyId?: string; - customerGroupId?: string; - footerCategoryId?: string; - hreflangActive?: boolean; - hreflangDefaultDomainId?: string; - languageId?: string; - mailHeaderFooterId?: string; - maintenance?: boolean; - maintenanceIpWhitelist?: string; - name?: string; - /** Format: int32 */ - navigationCategoryDepth?: number; - navigationCategoryId?: string; - paymentMethodId?: string; - serviceCategoryId?: string; - shippingMethodId?: string; - shortName?: string; - typeId?: string; - }; - shippingLocation?: { - // TODO: [OpenAPI][SalesChannelContext] shippingLocation field should be defined properly - apiAlias: "cart_delivery_shipping_location"; - country: components["schemas"]["Country"]; - address: components["schemas"]["CustomerAddress"]; - }; - /** Selected shipping method */ - shippingMethod?: components["schemas"]["ShippingMethod"]; // TODO: [OpenAPI][SalesChannelContext] shippingMethod field should be defined properly reusing ShippingMethod schema - /** Currently active tax rules and/or rates */ - taxRules?: { - name?: string; - /** Format: float */ - taxRate?: number; - }[]; - /** Context the user session */ - token?: string; - }; }; export type operations = { diff --git a/packages/composables/src/useSessionContext/useSessionContext.test.ts b/packages/composables/src/useSessionContext/useSessionContext.test.ts index f0a5a9a03..9e4cbe5ac 100644 --- a/packages/composables/src/useSessionContext/useSessionContext.test.ts +++ b/packages/composables/src/useSessionContext/useSessionContext.test.ts @@ -72,7 +72,7 @@ describe("useSessionContext", () => { injections.apiClient.invoke.mockResolvedValue({ data: { languageInfo: { name: "English", localeCode: "en-GB" }, - } as Schemas["SalesChannelContext"], + }, }); await vm.refreshSessionContext(); expect(vm.currentLocaleCode).toBe("en-GB"); diff --git a/templates/vue-demo-store/app/app.vue b/templates/vue-demo-store/app/app.vue index 35cb80b56..7f47ec590 100644 --- a/templates/vue-demo-store/app/app.vue +++ b/templates/vue-demo-store/app/app.vue @@ -87,8 +87,6 @@ if (languages.value?.elements.length && router.currentRoute.value.name) { languageToChangeId = localeProperties.value.localeId as string; } } else { - // Prefer the active locale from the context's `languageInfo`; fall back to - // mapping the language id against the fetched list for older backends. const sessionLanguage = currentLocaleCode.value ?? getLanguageCodeFromId(languageIdChain.value); diff --git a/templates/vue-starter-template/app/app.vue b/templates/vue-starter-template/app/app.vue index 7f256dac6..0a6460bb9 100644 --- a/templates/vue-starter-template/app/app.vue +++ b/templates/vue-starter-template/app/app.vue @@ -101,8 +101,6 @@ if (languages && router.currentRoute.value.name) { languageToChangeId = localeProperties.value.localeId as string; } } else { - // Prefer the active locale from the context's `languageInfo`; fall back to - // mapping the language id against the fetched list for older backends. const sessionLanguage = currentLocaleCode.value ?? getLanguageCodeFromId(languageIdChain.value); From 5bc17f44a2bc1524f63a09529599a84c05dfa9be Mon Sep 17 00:00:00 2001 From: mkucmus Date: Fri, 12 Jun 2026 10:14:03 +0200 Subject: [PATCH 3/3] fix: types --- .../api-types/adyenSchema.overrides.json | 3 ++- examples/adyen-dropin-component/api-types/storeApiTypes.d.ts | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/examples/adyen-dropin-component/api-types/adyenSchema.overrides.json b/examples/adyen-dropin-component/api-types/adyenSchema.overrides.json index 4921bd3ed..e668a5ec1 100644 --- a/examples/adyen-dropin-component/api-types/adyenSchema.overrides.json +++ b/examples/adyen-dropin-component/api-types/adyenSchema.overrides.json @@ -7,7 +7,8 @@ "type": "object", "properties": { "adyenData": { - "type": "object" + "type": "object", + "additionalProperties": true } } } diff --git a/examples/adyen-dropin-component/api-types/storeApiTypes.d.ts b/examples/adyen-dropin-component/api-types/storeApiTypes.d.ts index da73ad1e9..851f0f45c 100644 --- a/examples/adyen-dropin-component/api-types/storeApiTypes.d.ts +++ b/examples/adyen-dropin-component/api-types/storeApiTypes.d.ts @@ -7553,7 +7553,9 @@ export type Schemas = { }; customer?: null | components["schemas"]["Customer"]; extensions?: { - adyenData?: GenericRecord; + adyenData?: { + [key: string]: unknown; + }; }; /** Fallback group if the default customer group is not applicable */ fallbackCustomerGroup?: {