@laioutr/app-shopware 0.15.4 → 0.17.0

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.
Files changed (38) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/module.mjs +7 -2
  3. package/dist/runtime/app/components/ShopwareEmbedFrame.d.vue.ts +13 -3
  4. package/dist/runtime/app/components/ShopwareEmbedFrame.vue +48 -5
  5. package/dist/runtime/app/components/ShopwareEmbedFrame.vue.d.ts +13 -3
  6. package/dist/runtime/app/composables/useShopwareEmbedBridge.d.ts +3 -1
  7. package/dist/runtime/app/composables/useShopwareEmbedBridge.js +7 -3
  8. package/dist/runtime/app/const/bridge.d.ts +20 -0
  9. package/dist/runtime/app/const/bridge.js +7 -0
  10. package/dist/runtime/app/lib/createBridgeHandler.d.ts +3 -0
  11. package/dist/runtime/app/lib/createBridgeHandler.js +7 -1
  12. package/dist/runtime/app/lib/createOrderHandoffRefresher.d.ts +24 -0
  13. package/dist/runtime/app/lib/createOrderHandoffRefresher.js +21 -0
  14. package/dist/runtime/app/lib/parseBridgeMessage.js +2 -1
  15. package/dist/runtime/app/sections/SectionShopwareCheckout.vue +42 -4
  16. package/dist/runtime/server/const/checkout.d.ts +1 -1
  17. package/dist/runtime/server/const/checkout.js +10 -2
  18. package/dist/runtime/server/orchestr/product/base.resolver.js +18 -2
  19. package/dist/runtime/server/orchestr/product/variants.link.js +6 -4
  20. package/dist/runtime/server/orchestr/product-variant/base.resolver.js +9 -4
  21. package/dist/runtime/server/orchestr-helper/requestedFields.d.ts +1 -3
  22. package/dist/runtime/server/orchestr-helper/requestedFields.js +54 -42
  23. package/dist/runtime/server/routes/checkout.js +12 -1
  24. package/dist/runtime/server/routes/order-handoff.post.d.ts +14 -0
  25. package/dist/runtime/server/routes/order-handoff.post.js +33 -0
  26. package/dist/runtime/server/shopware-helper/fetchAllProductVariants.d.ts +2 -1
  27. package/dist/runtime/server/shopware-helper/fetchAllProductVariants.js +39 -11
  28. package/dist/runtime/server/shopware-helper/optionGroupsMapper.d.ts +23 -0
  29. package/dist/runtime/server/shopware-helper/optionGroupsMapper.js +37 -0
  30. package/dist/runtime/server/shopware-helper/resolveCheckout.d.ts +2 -0
  31. package/dist/runtime/server/shopware-helper/resolveCheckout.js +4 -3
  32. package/dist/runtime/server/shopware-helper/sessionHandoff.d.ts +6 -0
  33. package/dist/runtime/server/shopware-helper/sessionHandoff.js +6 -1
  34. package/dist/runtime/server/shopware-helper/wellKnownOptionName.d.ts +2 -0
  35. package/dist/runtime/server/shopware-helper/wellKnownOptionName.js +278 -0
  36. package/dist/runtime/shared/const/checkout.d.ts +33 -0
  37. package/dist/runtime/shared/const/checkout.js +5 -0
  38. package/package.json +3 -3
@@ -1,47 +1,5 @@
1
1
  import { MediaIncludes } from "../const/includes.js";
2
2
  import { mergeIncludes } from "../shopware-helper/criteria.js";
3
- const addAssociation = (name, add, association = {}) => add ? { [name]: association } : {};
4
- export const resolveProductFields = async ({ loadVariants }, resolveCriteria) => {
5
- const variantFields = loadVariants ? await resolveProductVariantFields(resolveCriteria) : void 0;
6
- return resolveCriteria("product", {
7
- associations: {
8
- cover: { associations: { media: {} } },
9
- media: { associations: { media: {} } },
10
- ...addAssociation("children", loadVariants, variantFields ? { associations: variantFields.associations } : {})
11
- },
12
- includes: mergeIncludes(
13
- {
14
- product: [
15
- "id",
16
- "parentId",
17
- "name",
18
- "seoUrls",
19
- "productNumber",
20
- "ean",
21
- "translated",
22
- "manufacturer",
23
- "description",
24
- "cover",
25
- "metaTitle",
26
- "metaDescription",
27
- "cover",
28
- "media",
29
- "minPurchase",
30
- "purchaseSteps",
31
- "maxPurchase",
32
- "calculatedPrice",
33
- "calculatedPrices",
34
- "children",
35
- "ratingAverage",
36
- "productReviews"
37
- ],
38
- product_media: ["id", "mediaId", "media"],
39
- media: MediaIncludes
40
- },
41
- variantFields?.includes ?? {}
42
- )
43
- });
44
- };
45
3
  export const resolveProductVariantFields = async (resolveCriteria) => resolveCriteria("product-variant", {
46
4
  associations: {
47
5
  cover: { associations: { media: {} } },
@@ -86,3 +44,57 @@ export const resolveProductVariantFields = async (resolveCriteria) => resolveCri
86
44
  property_group: ["id", "name", "translated"]
87
45
  }
88
46
  });
47
+ export const resolveProductFields = async (resolveCriteria) => {
48
+ const variantFields = await resolveProductVariantFields(resolveCriteria);
49
+ return resolveCriteria("product", {
50
+ associations: {
51
+ cover: { associations: { media: {} } },
52
+ media: { associations: { media: {} } },
53
+ // Only the parent carries the configurator, which is what defines the option
54
+ // axes; `properties` on the same row are filterable facets and never do.
55
+ // `media` needs its own association: without it the option's swatch image is
56
+ // never returned, whatever the group's displayType claims.
57
+ configuratorSettings: { associations: { option: { associations: { group: {}, media: {} } } } },
58
+ // Only the variant projection's `includes` are merged below, never its
59
+ // associations, so the selected options need associating here too or the
60
+ // default variant reports none.
61
+ options: { associations: { group: {} } }
62
+ },
63
+ includes: mergeIncludes(
64
+ {
65
+ product: [
66
+ "id",
67
+ "parentId",
68
+ "name",
69
+ "seoUrls",
70
+ "productNumber",
71
+ "ean",
72
+ "translated",
73
+ "manufacturer",
74
+ "description",
75
+ "cover",
76
+ "metaTitle",
77
+ "metaDescription",
78
+ "cover",
79
+ "media",
80
+ "minPurchase",
81
+ "purchaseSteps",
82
+ "maxPurchase",
83
+ "calculatedPrice",
84
+ "calculatedPrices",
85
+ "ratingAverage",
86
+ "productReviews",
87
+ "configuratorSettings"
88
+ ],
89
+ product_media: ["id", "mediaId", "media"],
90
+ media: MediaIncludes,
91
+ product_configurator_setting: ["id", "optionId", "option", "position"],
92
+ // Merged with the variant projection's narrower list, which carries neither
93
+ // the swatch fields nor the ordering the configurator is authored in.
94
+ property_group_option: ["colorHexCode", "media", "position"],
95
+ property_group: ["displayType", "position"]
96
+ },
97
+ variantFields.includes
98
+ )
99
+ });
100
+ };
@@ -1,5 +1,14 @@
1
1
  import { consola } from "consola";
2
- import { createError, defineEventHandler, getCookie, getRequestURL, sendRedirect, useRuntimeConfig } from "#imports";
2
+ import {
3
+ createError,
4
+ defineEventHandler,
5
+ getCookie,
6
+ getQuery,
7
+ getRequestURL,
8
+ sendRedirect,
9
+ useRuntimeConfig
10
+ } from "#imports";
11
+ import { RETRY_ORDER_QUERY_KEY } from "../const/checkout.js";
3
12
  import { CONTEXT_TOKEN_COOKIE } from "../const/cookieKeys.js";
4
13
  import { bootstrapGuestContextToken } from "../shopware-helper/bootstrapContextToken.js";
5
14
  import { persistContextToken } from "../shopware-helper/persistContextToken.js";
@@ -17,10 +26,12 @@ export default defineEventHandler(async (event) => {
17
26
  log.error("Failed to bootstrap a guest context token", cause);
18
27
  }
19
28
  }
29
+ const retryOrderId = getQuery(event)[RETRY_ORDER_QUERY_KEY];
20
30
  const plan = await resolveCheckout({
21
31
  config,
22
32
  contextToken,
23
33
  origin: getRequestURL(event).origin,
34
+ retryOrderId: typeof retryOrderId === "string" ? retryOrderId : null,
24
35
  mint: mintSessionHandoffCode
25
36
  });
26
37
  if (plan.kind === "error") {
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Mint a single-use session-handoff code for the embedded checkout's top-level order submit.
3
+ *
4
+ * The confirm form targets `_top` so redirect-based payment providers are never framed, and
5
+ * the storefront route it posts to redeems this code to install a session in the top-level
6
+ * cookie jar before forwarding to Shopware's order endpoint. Codes expire in about a minute,
7
+ * so the section re-mints while the shopper sits on the confirm page rather than once on load.
8
+ *
9
+ * The shopper reaches confirm through the checkout handoff, which bootstraps a context token
10
+ * when none exists — so a missing cookie here means the session was lost, not that the
11
+ * shopper is new. Fail rather than mint against a fresh guest cart the order would not match.
12
+ */
13
+ declare const _default: any;
14
+ export default _default;
@@ -0,0 +1,33 @@
1
+ import { consola } from "consola";
2
+ import { createError, defineEventHandler, getCookie, getRequestURL, readBody, useRuntimeConfig } from "#imports";
3
+ import { CHECKOUT_REDIRECT_ROUTE } from "../const/checkout.js";
4
+ import { CONTEXT_TOKEN_COOKIE } from "../const/cookieKeys.js";
5
+ import { mintSessionHandoffCode } from "../shopware-helper/sessionHandoff.js";
6
+ const log = consola.withTag("shopware/order-handoff");
7
+ export default defineEventHandler(async (event) => {
8
+ const config = useRuntimeConfig()["@laioutr/app-shopware"];
9
+ const contextToken = getCookie(event, CONTEXT_TOKEN_COOKIE);
10
+ if (!contextToken) {
11
+ throw createError({ statusCode: 409, statusMessage: "No cart context to hand off" });
12
+ }
13
+ const origin = getRequestURL(event).origin;
14
+ const body = await readBody(event).catch(() => ({}));
15
+ const finishUrl = typeof body?.finishUrl === "string" ? body.finishUrl : void 0;
16
+ const checkoutUrl = typeof body?.checkoutUrl === "string" ? body.checkoutUrl : void 0;
17
+ try {
18
+ const code = await mintSessionHandoffCode({
19
+ endpoint: config.endpoint,
20
+ accessToken: config.accessToken,
21
+ contextToken,
22
+ loginSuccessCallback: config.checkoutLoginCallbackUrl ?? origin,
23
+ logoutSuccessCallback: config.checkoutLogoutCallbackUrl ?? origin,
24
+ redirectRoute: CHECKOUT_REDIRECT_ROUTE,
25
+ finishSuccessCallback: finishUrl,
26
+ checkoutCallback: checkoutUrl
27
+ });
28
+ return { code };
29
+ } catch (cause) {
30
+ log.error("Failed to mint an order handoff code", cause);
31
+ throw createError({ statusCode: 502, statusMessage: "Order handoff mint failed" });
32
+ }
33
+ });
@@ -1,9 +1,10 @@
1
1
  import type { ResolveCriteria } from '../types/criteria.js';
2
2
  import { StorefrontClient } from '../types/shopware.js';
3
- export declare const fetchAllProducts: (storefrontClient: StorefrontClient, { productIds, loadVariants, resolveCriteria }: {
3
+ export declare const fetchAllProducts: (storefrontClient: StorefrontClient, { productIds, loadVariants, resolveCriteria, maxLimit, }: {
4
4
  productIds: string[];
5
5
  loadVariants: boolean;
6
6
  resolveCriteria: ResolveCriteria;
7
+ maxLimit: number;
7
8
  }) => Promise<{
8
9
  active?: boolean;
9
10
  apiAlias: "product";
@@ -1,17 +1,45 @@
1
1
  import { toRequestCriteria } from "./criteria.js";
2
- import { resolveProductFields } from "../orchestr-helper/requestedFields.js";
3
- export const fetchAllProducts = async (storefrontClient, { productIds, loadVariants, resolveCriteria }) => {
2
+ import { resolveProductFields, resolveProductVariantFields } from "../orchestr-helper/requestedFields.js";
3
+ const readVariants = async (storefrontClient, { parentIds, resolveCriteria, maxLimit }) => {
4
+ const criteria = await resolveProductVariantFields(resolveCriteria);
5
+ const variants = [];
6
+ const seen = /* @__PURE__ */ new Set();
7
+ for (let page = 1; ; page++) {
8
+ const response = await storefrontClient.invoke("readProduct post /product", {
9
+ body: {
10
+ page,
11
+ limit: maxLimit,
12
+ filter: [{ type: "equalsAny", field: "parentId", value: parentIds }],
13
+ ...toRequestCriteria(criteria)
14
+ }
15
+ });
16
+ const elements = response.data.elements ?? [];
17
+ const fresh = elements.filter((element) => element.id && !seen.has(element.id));
18
+ for (const element of fresh) seen.add(element.id);
19
+ variants.push(...fresh);
20
+ if (elements.length < maxLimit || fresh.length === 0) break;
21
+ }
22
+ return variants;
23
+ };
24
+ export const fetchAllProducts = async (storefrontClient, {
25
+ productIds,
26
+ loadVariants,
27
+ resolveCriteria,
28
+ maxLimit
29
+ }) => {
4
30
  if (productIds.length === 0) {
5
31
  return [];
6
32
  }
7
- const criteria = await resolveProductFields({ loadVariants }, resolveCriteria);
8
- const response = await storefrontClient.invoke("readProduct post /product", {
9
- body: {
10
- ids: productIds,
11
- ...toRequestCriteria(criteria)
12
- }
13
- });
33
+ const criteria = await resolveProductFields(resolveCriteria);
34
+ const [response, variants] = await Promise.all([
35
+ storefrontClient.invoke("readProduct post /product", {
36
+ body: {
37
+ ids: productIds,
38
+ ...toRequestCriteria(criteria)
39
+ }
40
+ }),
41
+ loadVariants ? readVariants(storefrontClient, { parentIds: productIds, resolveCriteria, maxLimit }) : []
42
+ ]);
14
43
  const products = response.data.elements ?? [];
15
- const all = [...products.flatMap((product) => product.children ?? []), ...products];
16
- return all;
44
+ return [...variants, ...products];
17
45
  };
@@ -0,0 +1,23 @@
1
+ import type { ShopwareProduct } from '../types/shopware.js';
2
+ import type { Swatch } from '@laioutr-core/core-types/common';
3
+ /**
4
+ * The option axes a product offers, read from its configurator.
5
+ *
6
+ * The configurator is what defines a variant; `properties` on the same product are
7
+ * filterable facets and never do. Only the parent carries it, so this must be given
8
+ * the parent row rather than the variant the rest of the projection reads from.
9
+ *
10
+ * Per-value stock and a per-value variant id are deliberately absent: both would
11
+ * mean loading every child variant, which is the cost this component exists to
12
+ * avoid. A consumer that needs them falls back to the variants link.
13
+ */
14
+ export declare const mapProductOptionGroups: (product: Pick<ShopwareProduct, "configuratorSettings">) => {
15
+ groups: {
16
+ name: string;
17
+ wellKnownName: import("@laioutr-core/canonical-types/entity/product-variant").WellKnownOptionName | undefined;
18
+ values: {
19
+ value: string;
20
+ swatch: Swatch | undefined;
21
+ }[];
22
+ }[];
23
+ };
@@ -0,0 +1,37 @@
1
+ import { mapMedia } from "./mediaMapper.js";
2
+ import { swTranslated } from "./swTranslated.js";
3
+ import { guessWellKnownName } from "./wellKnownOptionName.js";
4
+ const byPosition = (a, b) => (a.position ?? 0) - (b.position ?? 0);
5
+ const bySettingThenOption = (a, b) => (a.position ?? 0) - (b.position ?? 0) || (a.option.position ?? 0) - (b.option.position ?? 0);
6
+ const mapSwatch = (option) => {
7
+ const hex = swTranslated(option, "colorHexCode") ?? option.colorHexCode;
8
+ if (hex) return ["color", hex];
9
+ const media = option.media ? mapMedia(option.media) : void 0;
10
+ return media?.type === "image" ? ["image", media] : void 0;
11
+ };
12
+ export const mapProductOptionGroups = (product) => {
13
+ const settings = (product.configuratorSettings ?? []).filter((setting) => !!setting.option);
14
+ const groups = /* @__PURE__ */ new Map();
15
+ for (const setting of settings) {
16
+ const group = setting.option.group;
17
+ const key = group?.id ?? swTranslated(group, "name") ?? "";
18
+ const existing = groups.get(key);
19
+ if (existing) {
20
+ existing.settings.push(setting);
21
+ continue;
22
+ }
23
+ groups.set(key, { name: swTranslated(group, "name") ?? "", position: group?.position ?? 0, settings: [setting] });
24
+ }
25
+ return {
26
+ groups: [...groups.values()].sort(byPosition).map((group) => ({
27
+ name: group.name,
28
+ wellKnownName: guessWellKnownName(group.name),
29
+ // The merchant's configurator order, which is what keeps a size run out of
30
+ // alphabetical order.
31
+ values: [...group.settings].sort(bySettingThenOption).map((setting) => ({
32
+ value: swTranslated(setting.option, "name") ?? "",
33
+ swatch: mapSwatch(setting.option)
34
+ }))
35
+ }))
36
+ };
37
+ };
@@ -21,6 +21,8 @@ export interface ResolveCheckoutDeps {
21
21
  contextToken: string | null | undefined;
22
22
  /** Request origin used for the login/logout success callbacks. */
23
23
  origin: string;
24
+ /** Order to retry payment for, from the storefront's `retry-order` bounce. */
25
+ retryOrderId?: string | null;
24
26
  /** Injected minter (real: {@link mintSessionHandoffCode}) — keeps this decision logic pure/testable. */
25
27
  mint: (params: MintSessionHandoffParams) => Promise<string>;
26
28
  }
@@ -1,7 +1,7 @@
1
1
  import { buildConnectSessionUrl } from "./checkoutUrl.js";
2
- import { CHECKOUT_REDIRECT_ROUTE } from "../const/checkout.js";
2
+ import { CHECKOUT_REDIRECT_ROUTE, CHECKOUT_RETRY_ROUTE, RETRY_FRAME_MARKER_KEY } from "../const/checkout.js";
3
3
  export const resolveCheckout = async (deps) => {
4
- const { config, contextToken, origin, mint } = deps;
4
+ const { config, contextToken, origin, retryOrderId, mint } = deps;
5
5
  if (!contextToken) {
6
6
  return { kind: "redirect", url: "/" };
7
7
  }
@@ -15,7 +15,8 @@ export const resolveCheckout = async (deps) => {
15
15
  contextToken,
16
16
  loginSuccessCallback: config.checkoutLoginCallbackUrl ?? origin,
17
17
  logoutSuccessCallback: config.checkoutLogoutCallbackUrl ?? origin,
18
- redirectRoute: CHECKOUT_REDIRECT_ROUTE
18
+ redirectRoute: retryOrderId ? CHECKOUT_RETRY_ROUTE : CHECKOUT_REDIRECT_ROUTE,
19
+ ...retryOrderId ? { redirectRouteParams: { orderId: retryOrderId, [RETRY_FRAME_MARKER_KEY]: "1" } } : {}
19
20
  });
20
21
  return { kind: "redirect", url: buildConnectSessionUrl({ storefrontUrl: config.storefrontUrl, code }) };
21
22
  } catch (cause) {
@@ -11,6 +11,12 @@ export interface MintSessionHandoffParams {
11
11
  logoutSuccessCallback: string;
12
12
  /** Internal Shopware route the handoff lands on after redeem. */
13
13
  redirectRoute: string;
14
+ /** Absolute URL of the laioutr page shown after a completed order. */
15
+ finishSuccessCallback?: string;
16
+ /** Absolute URL of the laioutr checkout page, used to re-frame a payment retry. */
17
+ checkoutCallback?: string;
18
+ /** Parameters for `redirectRoute`; the retry route is keyed by `orderId`. */
19
+ redirectRouteParams?: Record<string, string>;
14
20
  }
15
21
  /**
16
22
  * Mint a single-use session-handoff code against the `LaioutrConnector` plugin's
@@ -13,10 +13,15 @@ export const mintSessionHandoffCode = async (params) => {
13
13
  "sw-access-key": params.accessToken,
14
14
  "sw-context-token": params.contextToken
15
15
  },
16
+ // Undefined keys are omitted rather than sent as null, so a plugin build that predates
17
+ // the return trip still accepts the call.
16
18
  body: {
17
19
  "login-success-callback": params.loginSuccessCallback,
18
20
  "logout-success-callback": params.logoutSuccessCallback,
19
- "redirect-route": params.redirectRoute
21
+ "redirect-route": params.redirectRoute,
22
+ ...params.finishSuccessCallback ? { "finish-success-callback": params.finishSuccessCallback } : {},
23
+ ...params.checkoutCallback ? { "checkout-callback": params.checkoutCallback } : {},
24
+ ...params.redirectRouteParams ? { "redirect-route-params": params.redirectRouteParams } : {}
20
25
  }
21
26
  });
22
27
  } catch (err) {
@@ -0,0 +1,2 @@
1
+ import type { WellKnownOptionName } from '@laioutr-core/canonical-types/entity/product-variant';
2
+ export declare const guessWellKnownName: (name: string) => WellKnownOptionName | undefined;
@@ -0,0 +1,278 @@
1
+ const OPTION_NAME_ALIASES = {
2
+ color: [
3
+ "color",
4
+ // en (US), es
5
+ "colour",
6
+ // en (GB)
7
+ "farbe",
8
+ // de
9
+ "farbton",
10
+ // de — shade rather than colour, and a real axis name in the wild
11
+ "kleur",
12
+ // nl
13
+ "couleur",
14
+ // fr
15
+ "colore",
16
+ // it
17
+ "cor",
18
+ // pt
19
+ "kolor",
20
+ // pl
21
+ "barva",
22
+ // cs, sl
23
+ "farba",
24
+ // sk
25
+ "boja",
26
+ // hr
27
+ "sz\xEDn",
28
+ // hu
29
+ "culoare",
30
+ // ro
31
+ "\u0446\u0432\u044F\u0442",
32
+ // bg
33
+ "\u03C7\u03C1\u03CE\u03BC\u03B1",
34
+ // el
35
+ "f\xE4rg",
36
+ // sv
37
+ "farve",
38
+ // da
39
+ "farge",
40
+ // no
41
+ "v\xE4ri",
42
+ // fi
43
+ "litur",
44
+ // is
45
+ "v\xE4rv",
46
+ // et
47
+ "kr\u0101sa",
48
+ // lv
49
+ "spalva",
50
+ // lt
51
+ "renk",
52
+ // tr
53
+ "\u8272",
54
+ // ja
55
+ "\u30AB\u30E9\u30FC",
56
+ // ja
57
+ "\uC0C9\uC0C1",
58
+ // ko
59
+ "\uCEEC\uB7EC",
60
+ // ko
61
+ "\u989C\u8272",
62
+ // zh-Hans
63
+ "\u984F\u8272"
64
+ // zh-Hant
65
+ ],
66
+ size: [
67
+ "size",
68
+ // en
69
+ "gr\xF6\xDFe",
70
+ // de
71
+ "groesse",
72
+ // de, ASCII transliteration — "oe" survives diacritic folding
73
+ "maat",
74
+ // nl
75
+ "grootte",
76
+ // nl
77
+ "taille",
78
+ // fr
79
+ "pointure",
80
+ // fr, footwear
81
+ "taglia",
82
+ // it
83
+ "misura",
84
+ // it
85
+ "talla",
86
+ // es
87
+ "tama\xF1o",
88
+ // es
89
+ "tamanho",
90
+ // pt
91
+ "rozmiar",
92
+ // pl
93
+ "velikost",
94
+ // cs, sl
95
+ "ve\u013Ekos\u0165",
96
+ // sk
97
+ "veli\u010Dina",
98
+ // hr
99
+ "m\xE9ret",
100
+ // hu
101
+ "m\u0103rime",
102
+ // ro
103
+ "\u0440\u0430\u0437\u043C\u0435\u0440",
104
+ // bg
105
+ "\u03BC\u03AD\u03B3\u03B5\u03B8\u03BF\u03C2",
106
+ // el
107
+ "storlek",
108
+ // sv
109
+ "st\xF8rrelse",
110
+ // da, no
111
+ "koko",
112
+ // fi
113
+ "st\xE6r\xF0",
114
+ // is
115
+ "suurus",
116
+ // et
117
+ "izm\u0113rs",
118
+ // lv
119
+ "dydis",
120
+ // lt
121
+ "beden",
122
+ // tr
123
+ "\u30B5\u30A4\u30BA",
124
+ // ja
125
+ "\uC0AC\uC774\uC988",
126
+ // ko
127
+ "\uD06C\uAE30",
128
+ // ko
129
+ "\u5C3A\u5BF8",
130
+ // zh
131
+ "\u5C3A\u7801",
132
+ // zh-Hans
133
+ "\u5C3A\u78BC"
134
+ // zh-Hant
135
+ ],
136
+ material: [
137
+ "material",
138
+ // en, de, es, pt, ro, sv, sl, and cs/sk `materiál` once folded
139
+ "materiaal",
140
+ // nl
141
+ "mati\xE8re",
142
+ // fr
143
+ "mat\xE9riau",
144
+ // fr
145
+ "materiale",
146
+ // it, da, no
147
+ "materia\u0142",
148
+ // pl
149
+ "materijal",
150
+ // hr
151
+ "anyag",
152
+ // hu
153
+ "\u043C\u0430\u0442\u0435\u0440\u0438\u0430\u043B",
154
+ // bg
155
+ "\u03C5\u03BB\u03B9\u03BA\u03CC",
156
+ // el
157
+ "materiaali",
158
+ // fi
159
+ "efni",
160
+ // is
161
+ "materjal",
162
+ // et
163
+ "materi\u0101ls",
164
+ // lv
165
+ "med\u017Eiaga",
166
+ // lt
167
+ "malzeme",
168
+ // tr
169
+ "\u7D20\u6750",
170
+ // ja
171
+ "\u6750\u8CEA",
172
+ // ja, zh-Hant
173
+ "\uC18C\uC7AC",
174
+ // ko
175
+ "\uC7AC\uC9C8",
176
+ // ko
177
+ "\u6750\u6599",
178
+ // zh
179
+ "\u6750\u8D28"
180
+ // zh-Hans
181
+ ],
182
+ style: [
183
+ "style",
184
+ // en, fr
185
+ "stil",
186
+ // de, sv, da, no, hr, tr
187
+ "stijl",
188
+ // nl
189
+ "stile",
190
+ // it
191
+ "estilo",
192
+ // es, pt
193
+ "styl",
194
+ // pl, cs, and sk `štýl` once folded
195
+ "slog",
196
+ // sl
197
+ "st\xEDlus",
198
+ // hu
199
+ "\u0441\u0442\u0438\u043B",
200
+ // bg
201
+ "\u03C3\u03C4\u03C5\u03BB",
202
+ // el
203
+ "tyyli",
204
+ // fi
205
+ "st\xEDll",
206
+ // is
207
+ "stiil",
208
+ // et
209
+ "stils",
210
+ // lv
211
+ "stilius",
212
+ // lt
213
+ "\u30B9\u30BF\u30A4\u30EB",
214
+ // ja
215
+ "\uC2A4\uD0C0\uC77C",
216
+ // ko
217
+ "\u6B3E\u5F0F"
218
+ // zh
219
+ ],
220
+ type: [
221
+ "type",
222
+ // en, nl, fr, da, no
223
+ "typ",
224
+ // de, pl, cs, sk, sv
225
+ "tipo",
226
+ // it, es, pt
227
+ "tip",
228
+ // sl, hr, tr
229
+ "vrsta",
230
+ // sl, hr
231
+ "rodzaj",
232
+ // pl
233
+ "t\xEDpus",
234
+ // hu
235
+ "\u0442\u0438\u043F",
236
+ // bg
237
+ "\u03C4\u03CD\u03C0\u03BF\u03C2",
238
+ // el
239
+ "tyyppi",
240
+ // fi
241
+ "tegund",
242
+ // is
243
+ "t\xFC\xFCp",
244
+ // et
245
+ "veids",
246
+ // lv
247
+ "tipas",
248
+ // lt
249
+ "\u30BF\u30A4\u30D7",
250
+ // ja
251
+ "\u7A2E\u985E",
252
+ // ja
253
+ "\uC720\uD615",
254
+ // ko
255
+ "\uD0C0\uC785",
256
+ // ko
257
+ "\u7C7B\u578B",
258
+ // zh-Hans
259
+ "\u985E\u578B"
260
+ // zh-Hant
261
+ ]
262
+ };
263
+ const normalizeOptionName = (name) => name.trim().toLowerCase().replace(/ß/g, "ss").normalize("NFD").replace(/\p{M}/gu, "");
264
+ const OPTION_NAME_LOOKUP = new Map(
265
+ Object.entries(OPTION_NAME_ALIASES).flatMap(
266
+ ([wellKnownName, aliases]) => aliases.map((alias) => [normalizeOptionName(alias), wellKnownName])
267
+ )
268
+ );
269
+ export const guessWellKnownName = (name) => {
270
+ const normalized = normalizeOptionName(name);
271
+ const exact = OPTION_NAME_LOOKUP.get(normalized);
272
+ if (exact) return exact;
273
+ for (const token of normalized.split(/[^\p{L}\p{N}]+/u)) {
274
+ const match = OPTION_NAME_LOOKUP.get(token);
275
+ if (match) return match;
276
+ }
277
+ return void 0;
278
+ };