@intlayer/core 9.0.0-canary.11 → 9.0.0-canary.13
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/cjs/dictionaryManipulator/index.cjs +3 -3
- package/dist/cjs/dictionaryManipulator/mergeQualifiedDictionaries.cjs +8 -6
- package/dist/cjs/dictionaryManipulator/mergeQualifiedDictionaries.cjs.map +1 -1
- package/dist/cjs/dictionaryManipulator/qualifiedDictionary.cjs +81 -40
- package/dist/cjs/dictionaryManipulator/qualifiedDictionary.cjs.map +1 -1
- package/dist/cjs/index.cjs +10 -3
- package/dist/cjs/localization/domainUtils.cjs +82 -0
- package/dist/cjs/localization/domainUtils.cjs.map +1 -0
- package/dist/cjs/localization/getLocalizedUrl.cjs +4 -11
- package/dist/cjs/localization/getLocalizedUrl.cjs.map +1 -1
- package/dist/cjs/localization/getPrefix.cjs +5 -9
- package/dist/cjs/localization/getPrefix.cjs.map +1 -1
- package/dist/cjs/localization/index.cjs +5 -0
- package/dist/cjs/messageFormat/index.cjs +2 -0
- package/dist/cjs/messageFormat/navigatePath.cjs +38 -0
- package/dist/cjs/messageFormat/navigatePath.cjs.map +1 -0
- package/dist/esm/dictionaryManipulator/index.mjs +2 -2
- package/dist/esm/dictionaryManipulator/mergeQualifiedDictionaries.mjs +9 -7
- package/dist/esm/dictionaryManipulator/mergeQualifiedDictionaries.mjs.map +1 -1
- package/dist/esm/dictionaryManipulator/qualifiedDictionary.mjs +79 -38
- package/dist/esm/dictionaryManipulator/qualifiedDictionary.mjs.map +1 -1
- package/dist/esm/index.mjs +4 -2
- package/dist/esm/localization/domainUtils.mjs +77 -0
- package/dist/esm/localization/domainUtils.mjs.map +1 -0
- package/dist/esm/localization/getLocalizedUrl.mjs +4 -11
- package/dist/esm/localization/getLocalizedUrl.mjs.map +1 -1
- package/dist/esm/localization/getPrefix.mjs +5 -9
- package/dist/esm/localization/getPrefix.mjs.map +1 -1
- package/dist/esm/localization/index.mjs +2 -1
- package/dist/esm/messageFormat/index.mjs +2 -1
- package/dist/esm/messageFormat/navigatePath.mjs +36 -0
- package/dist/esm/messageFormat/navigatePath.mjs.map +1 -0
- package/dist/types/deepTransformPlugins/getFilterMissingTranslationsContent.d.ts +1 -1
- package/dist/types/deepTransformPlugins/getFilterTranslationsOnlyContent.d.ts +1 -1
- package/dist/types/deepTransformPlugins/getFilteredLocalesContent.d.ts +1 -1
- package/dist/types/dictionaryManipulator/index.d.ts +2 -2
- package/dist/types/dictionaryManipulator/qualifiedDictionary.d.ts +33 -22
- package/dist/types/dictionaryManipulator/qualifiedDictionary.d.ts.map +1 -1
- package/dist/types/index.d.ts +4 -2
- package/dist/types/localization/domainUtils.d.ts +70 -0
- package/dist/types/localization/domainUtils.d.ts.map +1 -0
- package/dist/types/localization/getLocalizedUrl.d.ts.map +1 -1
- package/dist/types/localization/getPrefix.d.ts.map +1 -1
- package/dist/types/localization/index.d.ts +2 -1
- package/dist/types/messageFormat/index.d.ts +2 -1
- package/dist/types/messageFormat/navigatePath.d.ts +22 -0
- package/dist/types/messageFormat/navigatePath.d.ts.map +1 -0
- package/package.json +6 -6
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"getLocalizedUrl.cjs","names":["internationalization","resolveRoutingConfig","getPathWithoutLocale","getRewriteRules","getLocalizedPath","getCanonicalPath","checkIsURLAbsolute","getPrefix"],"sources":["../../../src/localization/getLocalizedUrl.ts"],"sourcesContent":["import { internationalization } from '@intlayer/config/built';\n\n// ── Tree-shake constants ──────────────────────────────────────────────────────\n// When these env vars are injected at build time, bundlers eliminate the\n// branches guarded by these constants.\n\nimport type { Locale } from '@intlayer/types/allLocales';\nimport type {\n LocalesValues,\n LocalizedUrl,\n ResolvedDefaultLocale,\n} from '@intlayer/types/module_augmentation';\nimport { checkIsURLAbsolute } from '../utils/checkIsURLAbsolute';\nimport { getPathWithoutLocale } from './getPathWithoutLocale';\nimport {\n getPrefix,\n type RoutingOptions,\n resolveRoutingConfig,\n} from './getPrefix';\nimport {\n getCanonicalPath,\n getLocalizedPath,\n getRewriteRules,\n} from './rewriteUtils';\n\nexport type { RoutingOptions };\n\n
|
|
1
|
+
{"version":3,"file":"getLocalizedUrl.cjs","names":["internationalization","resolveRoutingConfig","getPathWithoutLocale","getRewriteRules","getLocalizedPath","getCanonicalPath","checkIsURLAbsolute","getDomainHostname","getDomainOrigin","getPrefix"],"sources":["../../../src/localization/getLocalizedUrl.ts"],"sourcesContent":["import { internationalization } from '@intlayer/config/built';\n\n// ── Tree-shake constants ──────────────────────────────────────────────────────\n// When these env vars are injected at build time, bundlers eliminate the\n// branches guarded by these constants.\n\nimport type { Locale } from '@intlayer/types/allLocales';\nimport type {\n LocalesValues,\n LocalizedUrl,\n ResolvedDefaultLocale,\n} from '@intlayer/types/module_augmentation';\nimport { checkIsURLAbsolute } from '../utils/checkIsURLAbsolute';\nimport { getDomainHostname, getDomainOrigin } from './domainUtils';\nimport { getPathWithoutLocale } from './getPathWithoutLocale';\nimport {\n getPrefix,\n type RoutingOptions,\n resolveRoutingConfig,\n} from './getPrefix';\nimport {\n getCanonicalPath,\n getLocalizedPath,\n getRewriteRules,\n} from './rewriteUtils';\n\nexport type { RoutingOptions };\n\n/**\n * Generate URL by prefixing the given URL with the referenced locale or adding search parameters\n * based on the routing mode. Handles both absolute and relative URLs appropriately.\n *\n * This function gets the locales, default locale, and routing mode from the configuration if not provided.\n *\n * Example:\n *\n * ```ts\n * // prefix-no-default mode\n * getLocalizedUrl('/about', 'fr', { locales: ['en', 'fr'], defaultLocale: 'en', mode: 'prefix-no-default' });\n * // Returns '/fr/about' for the French locale\n * // Returns '/about' for the English locale (default)\n *\n * // prefix-all mode\n * getLocalizedUrl('/about', 'en', { locales: ['en', 'fr'], defaultLocale: 'en', mode: 'prefix-all' });\n * // Returns '/en/about' for the English locale\n * // Returns '/fr/about' for the French locale\n *\n * // search-params mode\n * getLocalizedUrl('/about', 'fr', { locales: ['en', 'fr'], defaultLocale: 'en', mode: 'search-params' });\n * // Returns '/about?locale=fr' for the French locale\n *\n * // no-prefix mode\n * getLocalizedUrl('/about', 'fr', { locales: ['en', 'fr'], defaultLocale: 'en', mode: 'no-prefix' });\n * // Returns '/about' for any locale\n * ```\n *\n * @param url - The original URL string to be processed.\n * @param currentLocale - The current locale.\n * @param options - Configuration options\n * @param options.locales - Optional array of supported locales. Defaults to configured locales.\n * @param options.defaultLocale - The default locale. Defaults to configured default locale.\n * @param options.mode - URL routing mode for locale handling. Defaults to configured mode.\n * @param options.currentDomain - Hostname of the page being rendered. Used to decide\n * whether to emit a relative URL (same domain) or an absolute URL (cross-domain).\n * Auto-detected from the input URL or `window.location` when omitted.\n * @returns The localized URL for the current locale.\n */\n/**\n * The return type is narrowed to a template-literal type when both `url` and\n * `currentLocale` are string literals and the routing mode / defaultLocale are\n * not overridden via `options`.\n */\nexport const getLocalizedUrl = <\n const T extends string,\n const L extends LocalesValues = ResolvedDefaultLocale,\n>(\n url: T,\n currentLocale: L = internationalization?.defaultLocale as L,\n options: RoutingOptions = {}\n): LocalizedUrl<T, L> => {\n const { defaultLocale, mode, locales, rewrite, domains, currentDomain } =\n resolveRoutingConfig(options);\n\n const urlWithoutLocale = getPathWithoutLocale(url, locales);\n const rewriteRules = getRewriteRules(rewrite, 'url');\n\n if (\n !(\n process.env.INTLAYER_ROUTING_MODE &&\n process.env.INTLAYER_ROUTING_MODE !== 'no-prefix'\n ) &&\n mode === 'no-prefix'\n ) {\n return getLocalizedPath(\n getCanonicalPath(urlWithoutLocale, undefined, rewriteRules),\n currentLocale as Locale,\n rewriteRules\n ).path as LocalizedUrl<T, L>;\n }\n\n const isAbsoluteUrl = checkIsURLAbsolute(urlWithoutLocale);\n const parsedUrl = isAbsoluteUrl\n ? new URL(urlWithoutLocale)\n : new URL(urlWithoutLocale, 'http://example.com');\n\n const translatedPathname = getLocalizedPath(\n getCanonicalPath(parsedUrl.pathname, undefined, rewriteRules),\n currentLocale as Locale,\n rewriteRules\n ).path;\n\n // ── Domain routing ────────────────────────────────────────────────────────\n // Resolve the \"current\" hostname so we can choose between a relative URL\n // (same domain) and an absolute URL (cross-domain).\n //\n // Detection priority:\n // 1. Explicit `currentDomain` option passed by the caller.\n // 2. Hostname extracted from an absolute input URL.\n // 3. `window.location.hostname` in browser environments.\n // When none of these is available we fall back to generating an absolute\n // URL for domain-mapped locales (safe for SSR/static generation): the\n // locale prefix may already be stripped for domain-exclusive locales, so a\n // relative URL would silently point to the wrong locale on the current\n // domain.\n const detectedCurrentHostname =\n process.env.INTLAYER_ROUTING_DOMAINS !== 'false'\n ? getDomainHostname(\n currentDomain ??\n (isAbsoluteUrl ? parsedUrl.hostname : undefined) ??\n (typeof window !== 'undefined'\n ? window?.location?.hostname\n : undefined) ??\n ''\n ) || null\n : null;\n\n const localeDomain =\n process.env.INTLAYER_ROUTING_DOMAINS !== 'false'\n ? domains?.[currentLocale as LocalesValues]\n : undefined;\n\n const localeDomainHostname = localeDomain\n ? getDomainHostname(localeDomain)\n : null;\n\n // Prepend the locale's domain when it differs from the current hostname,\n // or when the current hostname is unknown (SSR without `currentDomain`).\n const isCrossDomain =\n localeDomainHostname !== null &&\n (detectedCurrentHostname === null ||\n localeDomainHostname !== detectedCurrentHostname);\n\n const normalizedDomain =\n isCrossDomain && localeDomain ? getDomainOrigin(localeDomain) : null;\n\n const baseUrl = normalizedDomain\n ? normalizedDomain\n : isAbsoluteUrl\n ? `${parsedUrl.protocol}//${parsedUrl.host}`\n : '';\n // ─────────────────────────────────────────────────────────────────────────\n\n if (\n !(\n process.env.INTLAYER_ROUTING_MODE &&\n process.env.INTLAYER_ROUTING_MODE !== 'search-params'\n ) &&\n mode === 'search-params'\n ) {\n const searchParams = new URLSearchParams(parsedUrl.search);\n\n searchParams.set('locale', currentLocale.toString());\n\n const queryParams = searchParams.toString();\n const path = queryParams\n ? `${translatedPathname}?${queryParams}`\n : translatedPathname;\n\n return `${baseUrl}${path}${parsedUrl.hash}` as LocalizedUrl<T, L>;\n }\n\n const { prefix } = getPrefix(currentLocale, {\n defaultLocale,\n mode,\n locales,\n domains,\n });\n\n let localizedPath = `/${prefix}${translatedPathname}`.replace(/\\/+/g, '/');\n\n if (localizedPath.length > 1 && localizedPath.endsWith('/')) {\n localizedPath = localizedPath.slice(0, -1);\n }\n\n return `${baseUrl}${localizedPath}${parsedUrl.search}${parsedUrl.hash}` as LocalizedUrl<\n T,\n L\n >;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwEA,MAAa,mBAIX,KACA,gBAAmBA,6CAAsB,eACzC,UAA0B,EAAE,KACL;CACvB,MAAM,EAAE,eAAe,MAAM,SAAS,SAAS,SAAS,kBACtDC,oDAAqB,QAAQ;CAE/B,MAAM,mBAAmBC,+DAAqB,KAAK,QAAQ;CAC3D,MAAM,eAAeC,kDAAgB,SAAS,MAAM;AAEpD,KACE,EACE,QAAQ,IAAI,yBACZ,QAAQ,IAAI,0BAA0B,gBAExC,SAAS,YAET,QAAOC,mDACLC,mDAAiB,kBAAkB,QAAW,aAAa,EAC3D,eACA,aACD,CAAC;CAGJ,MAAM,gBAAgBC,oDAAmB,iBAAiB;CAC1D,MAAM,YAAY,gBACd,IAAI,IAAI,iBAAiB,GACzB,IAAI,IAAI,kBAAkB,qBAAqB;CAEnD,MAAM,qBAAqBF,mDACzBC,mDAAiB,UAAU,UAAU,QAAW,aAAa,EAC7D,eACA,aACD,CAAC;CAeF,MAAM,0BACJ,QAAQ,IAAI,6BAA6B,UACrCE,mDACE,kBACG,gBAAgB,UAAU,WAAW,YACrC,OAAO,WAAW,cACf,QAAQ,UAAU,WAClB,WACJ,GACH,IAAI,OACL;CAEN,MAAM,eACJ,QAAQ,IAAI,6BAA6B,UACrC,UAAU,iBACV;CAEN,MAAM,uBAAuB,eACzBA,mDAAkB,aAAa,GAC/B;CASJ,MAAM,mBAJJ,yBAAyB,SACxB,4BAA4B,QAC3B,yBAAyB,4BAGV,eAAeC,iDAAgB,aAAa,GAAG;CAElE,MAAM,UAAU,mBACZ,mBACA,gBACE,GAAG,UAAU,SAAS,IAAI,UAAU,SACpC;AAGN,KACE,EACE,QAAQ,IAAI,yBACZ,QAAQ,IAAI,0BAA0B,oBAExC,SAAS,iBACT;EACA,MAAM,eAAe,IAAI,gBAAgB,UAAU,OAAO;AAE1D,eAAa,IAAI,UAAU,cAAc,UAAU,CAAC;EAEpD,MAAM,cAAc,aAAa,UAAU;AAK3C,SAAO,GAAG,UAJG,cACT,GAAG,mBAAmB,GAAG,gBACzB,qBAEuB,UAAU;;CAGvC,MAAM,EAAE,WAAWC,yCAAU,eAAe;EAC1C;EACA;EACA;EACA;EACD,CAAC;CAEF,IAAI,gBAAgB,IAAI,SAAS,qBAAqB,QAAQ,QAAQ,IAAI;AAE1E,KAAI,cAAc,SAAS,KAAK,cAAc,SAAS,IAAI,CACzD,iBAAgB,cAAc,MAAM,GAAG,GAAG;AAG5C,QAAO,GAAG,UAAU,gBAAgB,UAAU,SAAS,UAAU"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
2
|
const require_runtime = require('../_virtual/_rolldown/runtime.cjs');
|
|
3
|
+
const require_localization_domainUtils = require('./domainUtils.cjs');
|
|
3
4
|
let _intlayer_config_built = require("@intlayer/config/built");
|
|
4
5
|
let _intlayer_config_defaultValues = require("@intlayer/config/defaultValues");
|
|
5
6
|
|
|
@@ -55,15 +56,10 @@ const getPrefix = (locale, options = {}) => {
|
|
|
55
56
|
prefix: "",
|
|
56
57
|
localePrefix: void 0
|
|
57
58
|
};
|
|
58
|
-
if (process.env.INTLAYER_ROUTING_DOMAINS !== "false" && domains) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
prefix: "",
|
|
63
|
-
localePrefix: void 0
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
}
|
|
59
|
+
if (process.env.INTLAYER_ROUTING_DOMAINS !== "false" && require_localization_domainUtils.isLocaleExclusiveOnDomain(locale, domains)) return {
|
|
60
|
+
prefix: "",
|
|
61
|
+
localePrefix: void 0
|
|
62
|
+
};
|
|
67
63
|
if (mode === "prefix-all" || mode === "prefix-no-default" && defaultLocale !== locale) return {
|
|
68
64
|
prefix: `${locale}/`,
|
|
69
65
|
localePrefix: locale
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"getPrefix.cjs","names":["internationalization","DEFAULT_LOCALE","routing","ROUTING_MODE","LOCALES"],"sources":["../../../src/localization/getPrefix.ts"],"sourcesContent":["import { internationalization, routing } from '@intlayer/config/built';\nimport {\n DEFAULT_LOCALE,\n LOCALES,\n ROUTING_MODE,\n} from '@intlayer/config/defaultValues';\n\n// ── Tree-shake constants ──────────────────────────────────────────────────────\n// When these env vars are injected at build time, bundlers eliminate the\n// branches guarded by these constants.\n\nimport type { Locale } from '@intlayer/types/allLocales';\nimport type { RoutingConfig } from '@intlayer/types/config';\nimport type {\n DeclaredLocales,\n LocalesValues,\n ResolvedDefaultLocale,\n ResolvedRoutingMode,\n} from '@intlayer/types/module_augmentation';\n\n/**\n * Shared routing options used across all URL localization functions.\n */\nexport type RoutingOptions = {\n locales?: LocalesValues[];\n defaultLocale?: LocalesValues;\n mode?: RoutingConfig['mode'];\n rewrite?: RoutingConfig['rewrite'];\n domains?: RoutingConfig['domains'];\n /**\n * The hostname of the page currently being rendered (e.g. `'intlayer.org'`).\n * When provided, `getLocalizedUrl` returns a relative URL for locales whose\n * configured domain matches `currentDomain`, and an absolute URL only when\n * the target locale lives on a different domain.\n *\n * When omitted the function tries to infer it from:\n * 1. The domain of an absolute input URL.\n * 2. `window.location.hostname` in browser environments.\n * Falls back to always generating absolute URLs when neither is available.\n */\n currentDomain?: string;\n};\n\n/**\n * Resolves routing configuration by merging provided options with configuration defaults.\n * Single source of truth for default routing config resolution across all localization functions.\n */\nexport const resolveRoutingConfig = (\n options: RoutingOptions = {}\n): Omit<RoutingOptions, 'defaultLocale' | 'mode' | 'locales'> & {\n defaultLocale: LocalesValues;\n mode: RoutingConfig['mode'];\n locales: LocalesValues[];\n} => ({\n defaultLocale: internationalization?.defaultLocale ?? DEFAULT_LOCALE,\n mode: routing?.mode ?? ROUTING_MODE,\n locales: internationalization?.locales ?? LOCALES,\n rewrite: routing?.rewrite,\n domains: routing?.domains,\n ...options,\n});\n\nexport type GetPrefixOptions = {\n defaultLocale?: LocalesValues;\n mode?: RoutingConfig['mode'];\n};\n\nexport type GetPrefixResult = {\n /**\n * The locale path segment appended to `/`, with a trailing slash (e.g. `'fr/'`).\n * Empty string when no prefix is needed.\n */\n prefix: string;\n /**\n * The bare locale identifier (e.g. `'fr'`), or `undefined` when no prefix is applied.\n */\n localePrefix: Locale | undefined;\n};\n\n/**\n * Narrowed return type for {@link getPrefix} that carries the locale literal through.\n *\n * Distributes over union locales — calling `getPrefix('fr')` in `prefix-no-default`\n * mode with `defaultLocale = 'en'` resolves to `{ prefix: 'fr/'; localePrefix: 'fr' }`.\n *\n * Note: domain-based routing and \"locale not in locales\" edge cases may return an\n * empty result at runtime regardless of what this type reports.\n */\nexport type GetPrefixResultNarrowed<\n L extends LocalesValues | undefined,\n Mode extends string = ResolvedRoutingMode,\n Default extends LocalesValues = ResolvedDefaultLocale,\n> = L extends string\n ? [string] extends [L] // L is wide (string / LocalesValues) → distribute over declared locales\n ? GetPrefixResultNarrowed<DeclaredLocales, Mode, Default>\n : [string] extends [Mode]\n ? GetPrefixResult // mode is wide → fall back to generic result\n : Mode extends 'prefix-all'\n ? { prefix: `${L}/`; localePrefix: L }\n : Mode extends 'prefix-no-default'\n ? L extends Default\n ? { prefix: ''; localePrefix: undefined }\n : { prefix: `${L}/`; localePrefix: L }\n : { prefix: ''; localePrefix: undefined } // no-prefix / search-params\n : { prefix: ''; localePrefix: undefined }; // locale is undefined\n\n/**\n * Determines the URL prefix for a given locale based on the routing mode configuration.\n *\n * Example:\n *\n * ```ts\n * // prefix-no-default mode with default locale\n * getPrefix('en', { defaultLocale: 'en', mode: 'prefix-no-default' })\n * // Returns { prefix: '', localePrefix: undefined }\n *\n * // prefix-no-default mode with non-default locale\n * getPrefix('fr', { defaultLocale: 'en', mode: 'prefix-no-default' })\n * // Returns { prefix: '/fr', localePrefix: 'fr' }\n *\n * // prefix-all mode\n * getPrefix('en', { defaultLocale: 'en', mode: 'prefix-all' })\n * // Returns { prefix: '/en', localePrefix: locale }\n *\n * // search-params mode\n * getPrefix('en', { defaultLocale: 'en', mode: 'search-params' })\n * // Returns { prefix: '', localePrefix: undefined }\n *\n * // no-prefix mode\n * getPrefix('en', { defaultLocale: 'en', mode: 'no-prefix' })\n * // Returns { prefix: '', localePrefix: undefined }\n * ```\n *\n * @param locale - The locale to check for prefix. If not provided, uses configured default locale.\n * @param options - Configuration options\n * @param options.defaultLocale - The default locale. Defaults to configured default locale.\n * @param options.mode - URL routing mode for locale handling. Defaults to configured mode.\n * @returns An object containing pathPrefix, prefix, and localePrefix for the given locale.\n */\nexport const getPrefix = <const L extends LocalesValues | undefined>(\n locale: L,\n options: RoutingOptions = {}\n): GetPrefixResultNarrowed<L> => {\n const { defaultLocale, mode, locales, domains } =\n resolveRoutingConfig(options);\n\n if (\n (process.env.INTLAYER_ROUTING_MODE &&\n process.env.INTLAYER_ROUTING_MODE !== 'prefix-all' &&\n process.env.INTLAYER_ROUTING_MODE !== 'prefix-no-default') ||\n !locale ||\n !locales.includes(locale)\n ) {\n return {\n prefix: '',\n localePrefix: undefined,\n } as GetPrefixResultNarrowed<L>;\n }\n\n // If this locale is the only one assigned to its domain, no URL prefix is needed\n // (the domain itself identifies the locale). Shared domains use normal prefix logic.\n if (process.env.INTLAYER_ROUTING_DOMAINS !== 'false'
|
|
1
|
+
{"version":3,"file":"getPrefix.cjs","names":["internationalization","DEFAULT_LOCALE","routing","ROUTING_MODE","LOCALES","isLocaleExclusiveOnDomain"],"sources":["../../../src/localization/getPrefix.ts"],"sourcesContent":["import { internationalization, routing } from '@intlayer/config/built';\nimport {\n DEFAULT_LOCALE,\n LOCALES,\n ROUTING_MODE,\n} from '@intlayer/config/defaultValues';\n\n// ── Tree-shake constants ──────────────────────────────────────────────────────\n// When these env vars are injected at build time, bundlers eliminate the\n// branches guarded by these constants.\n\nimport type { Locale } from '@intlayer/types/allLocales';\nimport type { RoutingConfig } from '@intlayer/types/config';\nimport type {\n DeclaredLocales,\n LocalesValues,\n ResolvedDefaultLocale,\n ResolvedRoutingMode,\n} from '@intlayer/types/module_augmentation';\nimport { isLocaleExclusiveOnDomain } from './domainUtils';\n\n/**\n * Shared routing options used across all URL localization functions.\n */\nexport type RoutingOptions = {\n locales?: LocalesValues[];\n defaultLocale?: LocalesValues;\n mode?: RoutingConfig['mode'];\n rewrite?: RoutingConfig['rewrite'];\n domains?: RoutingConfig['domains'];\n /**\n * The hostname of the page currently being rendered (e.g. `'intlayer.org'`).\n * When provided, `getLocalizedUrl` returns a relative URL for locales whose\n * configured domain matches `currentDomain`, and an absolute URL only when\n * the target locale lives on a different domain.\n *\n * When omitted the function tries to infer it from:\n * 1. The domain of an absolute input URL.\n * 2. `window.location.hostname` in browser environments.\n * Falls back to always generating absolute URLs when neither is available.\n */\n currentDomain?: string;\n};\n\n/**\n * Resolves routing configuration by merging provided options with configuration defaults.\n * Single source of truth for default routing config resolution across all localization functions.\n */\nexport const resolveRoutingConfig = (\n options: RoutingOptions = {}\n): Omit<RoutingOptions, 'defaultLocale' | 'mode' | 'locales'> & {\n defaultLocale: LocalesValues;\n mode: RoutingConfig['mode'];\n locales: LocalesValues[];\n} => ({\n defaultLocale: internationalization?.defaultLocale ?? DEFAULT_LOCALE,\n mode: routing?.mode ?? ROUTING_MODE,\n locales: internationalization?.locales ?? LOCALES,\n rewrite: routing?.rewrite,\n domains: routing?.domains,\n ...options,\n});\n\nexport type GetPrefixOptions = {\n defaultLocale?: LocalesValues;\n mode?: RoutingConfig['mode'];\n};\n\nexport type GetPrefixResult = {\n /**\n * The locale path segment appended to `/`, with a trailing slash (e.g. `'fr/'`).\n * Empty string when no prefix is needed.\n */\n prefix: string;\n /**\n * The bare locale identifier (e.g. `'fr'`), or `undefined` when no prefix is applied.\n */\n localePrefix: Locale | undefined;\n};\n\n/**\n * Narrowed return type for {@link getPrefix} that carries the locale literal through.\n *\n * Distributes over union locales — calling `getPrefix('fr')` in `prefix-no-default`\n * mode with `defaultLocale = 'en'` resolves to `{ prefix: 'fr/'; localePrefix: 'fr' }`.\n *\n * Note: domain-based routing and \"locale not in locales\" edge cases may return an\n * empty result at runtime regardless of what this type reports.\n */\nexport type GetPrefixResultNarrowed<\n L extends LocalesValues | undefined,\n Mode extends string = ResolvedRoutingMode,\n Default extends LocalesValues = ResolvedDefaultLocale,\n> = L extends string\n ? [string] extends [L] // L is wide (string / LocalesValues) → distribute over declared locales\n ? GetPrefixResultNarrowed<DeclaredLocales, Mode, Default>\n : [string] extends [Mode]\n ? GetPrefixResult // mode is wide → fall back to generic result\n : Mode extends 'prefix-all'\n ? { prefix: `${L}/`; localePrefix: L }\n : Mode extends 'prefix-no-default'\n ? L extends Default\n ? { prefix: ''; localePrefix: undefined }\n : { prefix: `${L}/`; localePrefix: L }\n : { prefix: ''; localePrefix: undefined } // no-prefix / search-params\n : { prefix: ''; localePrefix: undefined }; // locale is undefined\n\n/**\n * Determines the URL prefix for a given locale based on the routing mode configuration.\n *\n * Example:\n *\n * ```ts\n * // prefix-no-default mode with default locale\n * getPrefix('en', { defaultLocale: 'en', mode: 'prefix-no-default' })\n * // Returns { prefix: '', localePrefix: undefined }\n *\n * // prefix-no-default mode with non-default locale\n * getPrefix('fr', { defaultLocale: 'en', mode: 'prefix-no-default' })\n * // Returns { prefix: '/fr', localePrefix: 'fr' }\n *\n * // prefix-all mode\n * getPrefix('en', { defaultLocale: 'en', mode: 'prefix-all' })\n * // Returns { prefix: '/en', localePrefix: locale }\n *\n * // search-params mode\n * getPrefix('en', { defaultLocale: 'en', mode: 'search-params' })\n * // Returns { prefix: '', localePrefix: undefined }\n *\n * // no-prefix mode\n * getPrefix('en', { defaultLocale: 'en', mode: 'no-prefix' })\n * // Returns { prefix: '', localePrefix: undefined }\n * ```\n *\n * @param locale - The locale to check for prefix. If not provided, uses configured default locale.\n * @param options - Configuration options\n * @param options.defaultLocale - The default locale. Defaults to configured default locale.\n * @param options.mode - URL routing mode for locale handling. Defaults to configured mode.\n * @returns An object containing pathPrefix, prefix, and localePrefix for the given locale.\n */\nexport const getPrefix = <const L extends LocalesValues | undefined>(\n locale: L,\n options: RoutingOptions = {}\n): GetPrefixResultNarrowed<L> => {\n const { defaultLocale, mode, locales, domains } =\n resolveRoutingConfig(options);\n\n if (\n (process.env.INTLAYER_ROUTING_MODE &&\n process.env.INTLAYER_ROUTING_MODE !== 'prefix-all' &&\n process.env.INTLAYER_ROUTING_MODE !== 'prefix-no-default') ||\n !locale ||\n !locales.includes(locale)\n ) {\n return {\n prefix: '',\n localePrefix: undefined,\n } as GetPrefixResultNarrowed<L>;\n }\n\n // If this locale is the only one assigned to its domain, no URL prefix is needed\n // (the domain itself identifies the locale). Shared domains use normal prefix logic.\n if (\n process.env.INTLAYER_ROUTING_DOMAINS !== 'false' &&\n isLocaleExclusiveOnDomain(locale as LocalesValues, domains)\n ) {\n return {\n prefix: '',\n localePrefix: undefined,\n } as GetPrefixResultNarrowed<L>;\n }\n\n // Handle prefix-based modes (prefix-all or prefix-no-default)\n const shouldPrefix =\n mode === 'prefix-all' ||\n (mode === 'prefix-no-default' && defaultLocale !== locale);\n\n if (shouldPrefix) {\n return {\n prefix: `${locale}/`,\n localePrefix: locale as Locale,\n } as GetPrefixResultNarrowed<L>;\n }\n\n return {\n prefix: '',\n localePrefix: undefined,\n } as GetPrefixResultNarrowed<L>;\n};\n"],"mappings":";;;;;;;;;;;AAgDA,MAAa,wBACX,UAA0B,EAAE,MAKxB;CACJ,eAAeA,6CAAsB,iBAAiBC;CACtD,MAAMC,gCAAS,QAAQC;CACvB,SAASH,6CAAsB,WAAWI;CAC1C,SAASF,gCAAS;CAClB,SAASA,gCAAS;CAClB,GAAG;CACJ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+ED,MAAa,aACX,QACA,UAA0B,EAAE,KACG;CAC/B,MAAM,EAAE,eAAe,MAAM,SAAS,YACpC,qBAAqB,QAAQ;AAE/B,KACG,QAAQ,IAAI,yBACX,QAAQ,IAAI,0BAA0B,gBACtC,QAAQ,IAAI,0BAA0B,uBACxC,CAAC,UACD,CAAC,QAAQ,SAAS,OAAO,CAEzB,QAAO;EACL,QAAQ;EACR,cAAc;EACf;AAKH,KACE,QAAQ,IAAI,6BAA6B,WACzCG,2DAA0B,QAAyB,QAAQ,CAE3D,QAAO;EACL,QAAQ;EACR,cAAc;EACf;AAQH,KAHE,SAAS,gBACR,SAAS,uBAAuB,kBAAkB,OAGnD,QAAO;EACL,QAAQ,GAAG,OAAO;EAClB,cAAc;EACf;AAGH,QAAO;EACL,QAAQ;EACR,cAAc;EACf"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
2
|
const require_localization_getPathWithoutLocale = require('./getPathWithoutLocale.cjs');
|
|
3
3
|
const require_localization_comparePaths = require('./comparePaths.cjs');
|
|
4
|
+
const require_localization_domainUtils = require('./domainUtils.cjs');
|
|
4
5
|
const require_localization_getPrefix = require('./getPrefix.cjs');
|
|
5
6
|
const require_localization_rewriteUtils = require('./rewriteUtils.cjs');
|
|
6
7
|
const require_localization_getLocalizedUrl = require('./getLocalizedUrl.cjs');
|
|
@@ -22,9 +23,12 @@ exports.generateSitemap = require_localization_generateSitemap.generateSitemap;
|
|
|
22
23
|
exports.generateSitemapUrl = require_localization_generateSitemap.generateSitemapUrl;
|
|
23
24
|
exports.getBrowserLocale = require_localization_getBrowserLocale.getBrowserLocale;
|
|
24
25
|
exports.getCanonicalPath = require_localization_rewriteUtils.getCanonicalPath;
|
|
26
|
+
exports.getDomainHostname = require_localization_domainUtils.getDomainHostname;
|
|
27
|
+
exports.getDomainOrigin = require_localization_domainUtils.getDomainOrigin;
|
|
25
28
|
exports.getHTMLTextDir = require_localization_getHTMLTextDir.getHTMLTextDir;
|
|
26
29
|
exports.getInternalPath = require_localization_rewriteUtils.getInternalPath;
|
|
27
30
|
exports.getLocale = require_localization_getLocale.getLocale;
|
|
31
|
+
exports.getLocaleFromDomain = require_localization_domainUtils.getLocaleFromDomain;
|
|
28
32
|
exports.getLocaleFromPath = require_localization_getLocaleFromPath.getLocaleFromPath;
|
|
29
33
|
exports.getLocaleLang = require_localization_getLocaleLang.getLocaleLang;
|
|
30
34
|
exports.getLocaleName = require_localization_getLocaleName.getLocaleName;
|
|
@@ -35,6 +39,7 @@ exports.getPathWithoutLocale = require_localization_getPathWithoutLocale.getPath
|
|
|
35
39
|
exports.getPrefix = require_localization_getPrefix.getPrefix;
|
|
36
40
|
exports.getRewritePath = require_localization_rewriteUtils.getRewritePath;
|
|
37
41
|
exports.getRewriteRules = require_localization_rewriteUtils.getRewriteRules;
|
|
42
|
+
exports.isLocaleExclusiveOnDomain = require_localization_domainUtils.isLocaleExclusiveOnDomain;
|
|
38
43
|
exports.localeDetector = require_localization_localeDetector.localeDetector;
|
|
39
44
|
exports.localeFlatMap = require_localization_localeMapper.localeFlatMap;
|
|
40
45
|
exports.localeMap = require_localization_localeMapper.localeMap;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
2
|
const require_messageFormat_ICU = require('./ICU.cjs');
|
|
3
3
|
const require_messageFormat_i18next = require('./i18next.cjs');
|
|
4
|
+
const require_messageFormat_navigatePath = require('./navigatePath.cjs');
|
|
4
5
|
const require_messageFormat_po = require('./po.cjs');
|
|
5
6
|
const require_messageFormat_vue_i18n = require('./vue-i18n.cjs');
|
|
6
7
|
const require_messageFormat_resolveMessage = require('./resolveMessage.cjs');
|
|
@@ -12,6 +13,7 @@ exports.intlayerToI18nextFormatter = require_messageFormat_i18next.intlayerToI18
|
|
|
12
13
|
exports.intlayerToICUFormatter = require_messageFormat_ICU.intlayerToICUFormatter;
|
|
13
14
|
exports.intlayerToPortableObjectFormatter = require_messageFormat_po.intlayerToPortableObjectFormatter;
|
|
14
15
|
exports.intlayerToVueI18nFormatter = require_messageFormat_vue_i18n.intlayerToVueI18nFormatter;
|
|
16
|
+
exports.navigatePath = require_messageFormat_navigatePath.navigatePath;
|
|
15
17
|
exports.parseTaggedMessage = require_messageFormat_resolveMessage.parseTaggedMessage;
|
|
16
18
|
exports.portableObjectToIntlayerFormatter = require_messageFormat_po.portableObjectToIntlayerFormatter;
|
|
17
19
|
exports.resolveMessage = require_messageFormat_resolveMessage.resolveMessage;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
|
|
3
|
+
//#region src/messageFormat/navigatePath.ts
|
|
4
|
+
/**
|
|
5
|
+
* Reads a dotted `path` (e.g. `counter.label`) out of a nested content value.
|
|
6
|
+
*
|
|
7
|
+
* The full path is first tried as a *flat* property, supporting catalogs that
|
|
8
|
+
* use dotted keys as literal field names (i18next flat JSON files, lingui
|
|
9
|
+
* catalogs: `{ "section.title": "value" }`). When that misses, the path is
|
|
10
|
+
* split on `keySeparator` and walked segment by segment.
|
|
11
|
+
*
|
|
12
|
+
* Shared by every compat adapter's translation lookup — do not re-implement
|
|
13
|
+
* it locally.
|
|
14
|
+
*
|
|
15
|
+
* @param contentValue - The object (dictionary content) to read from.
|
|
16
|
+
* @param path - Separator-delimited path. An empty path returns `contentValue`.
|
|
17
|
+
* @param keySeparator - Segment separator; `false` disables nested traversal
|
|
18
|
+
* (flat lookup only), mirroring i18next's `keySeparator: false`.
|
|
19
|
+
* @returns The value found at `path`, or `undefined` when any segment is absent.
|
|
20
|
+
*/
|
|
21
|
+
const navigatePath = (contentValue, path, keySeparator = ".") => {
|
|
22
|
+
if (!path) return contentValue;
|
|
23
|
+
if (contentValue !== null && contentValue !== void 0 && typeof contentValue === "object") {
|
|
24
|
+
const flatValue = contentValue[path];
|
|
25
|
+
if (flatValue !== void 0) return flatValue;
|
|
26
|
+
}
|
|
27
|
+
if (keySeparator === false || !path.includes(keySeparator)) return;
|
|
28
|
+
let current = contentValue;
|
|
29
|
+
for (const part of path.split(keySeparator)) {
|
|
30
|
+
if (current === null || current === void 0 || typeof current !== "object") return;
|
|
31
|
+
current = current[part];
|
|
32
|
+
}
|
|
33
|
+
return current;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
//#endregion
|
|
37
|
+
exports.navigatePath = navigatePath;
|
|
38
|
+
//# sourceMappingURL=navigatePath.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"navigatePath.cjs","names":[],"sources":["../../../src/messageFormat/navigatePath.ts"],"sourcesContent":["/**\n * Reads a dotted `path` (e.g. `counter.label`) out of a nested content value.\n *\n * The full path is first tried as a *flat* property, supporting catalogs that\n * use dotted keys as literal field names (i18next flat JSON files, lingui\n * catalogs: `{ \"section.title\": \"value\" }`). When that misses, the path is\n * split on `keySeparator` and walked segment by segment.\n *\n * Shared by every compat adapter's translation lookup — do not re-implement\n * it locally.\n *\n * @param contentValue - The object (dictionary content) to read from.\n * @param path - Separator-delimited path. An empty path returns `contentValue`.\n * @param keySeparator - Segment separator; `false` disables nested traversal\n * (flat lookup only), mirroring i18next's `keySeparator: false`.\n * @returns The value found at `path`, or `undefined` when any segment is absent.\n */\nexport const navigatePath = (\n contentValue: unknown,\n path: string,\n keySeparator: string | false = '.'\n): unknown => {\n if (!path) return contentValue;\n\n if (\n contentValue !== null &&\n contentValue !== undefined &&\n typeof contentValue === 'object'\n ) {\n const flatValue = (contentValue as Record<string, unknown>)[path];\n if (flatValue !== undefined) return flatValue;\n }\n\n if (keySeparator === false || !path.includes(keySeparator)) {\n return undefined;\n }\n\n let current: unknown = contentValue;\n for (const part of path.split(keySeparator)) {\n if (\n current === null ||\n current === undefined ||\n typeof current !== 'object'\n ) {\n return undefined;\n }\n current = (current as Record<string, unknown>)[part];\n }\n return current;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAiBA,MAAa,gBACX,cACA,MACA,eAA+B,QACnB;AACZ,KAAI,CAAC,KAAM,QAAO;AAElB,KACE,iBAAiB,QACjB,iBAAiB,UACjB,OAAO,iBAAiB,UACxB;EACA,MAAM,YAAa,aAAyC;AAC5D,MAAI,cAAc,OAAW,QAAO;;AAGtC,KAAI,iBAAiB,SAAS,CAAC,KAAK,SAAS,aAAa,CACxD;CAGF,IAAI,UAAmB;AACvB,MAAK,MAAM,QAAQ,KAAK,MAAM,aAAa,EAAE;AAC3C,MACE,YAAY,QACZ,YAAY,UACZ,OAAO,YAAY,SAEnB;AAEF,YAAW,QAAoC;;AAEjD,QAAO"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { COMPOSITE_ID_SEPARATOR, QUALIFIER_DYNAMIC_TYPES_KEY, QUALIFIER_ORDER,
|
|
1
|
+
import { COMPOSITE_ID_SEPARATOR, QUALIFIER_DYNAMIC_TYPES_KEY, QUALIFIER_ORDER, getDictionaryCompositeIds, getDictionaryQualifierIds, getDictionaryQualifierTypes, getDictionarySelectorCacheKey, getVariantIds, isQualifiedDictionaryGroup, isQualifiedDynamicLoaderMap, parseDictionarySelector, reconstructQualifiedEntry, resolveQualifiedDictionary, resolveQualifiedDynamicContent, resolveQualifiedDynamicContentAsync, serializeVariant } from "./qualifiedDictionary.mjs";
|
|
2
2
|
import { editDictionaryByKeyPath } from "./editDictionaryByKeyPath.mjs";
|
|
3
3
|
import { getContentNodeByKeyPath } from "./getContentNodeByKeyPath.mjs";
|
|
4
4
|
import { getDefaultNode } from "./getDefaultNode.mjs";
|
|
@@ -13,4 +13,4 @@ import { removeContentNodeByKeyPath } from "./removeContentNodeByKeyPath.mjs";
|
|
|
13
13
|
import { renameContentNodeByKeyPath } from "./renameContentNodeByKeyPath.mjs";
|
|
14
14
|
import { updateNodeChildren } from "./updateNodeChildren.mjs";
|
|
15
15
|
|
|
16
|
-
export { COMPOSITE_ID_SEPARATOR, QUALIFIER_DYNAMIC_TYPES_KEY, QUALIFIER_ORDER, editDictionaryByKeyPath, getContentNodeByKeyPath, getDefaultNode,
|
|
16
|
+
export { COMPOSITE_ID_SEPARATOR, QUALIFIER_DYNAMIC_TYPES_KEY, QUALIFIER_ORDER, editDictionaryByKeyPath, getContentNodeByKeyPath, getDefaultNode, getDictionaryCompositeIds, getDictionaryQualifierIds, getDictionaryQualifierTypes, getDictionarySelectorCacheKey, getEmptyNode, getNodeChildren, getNodeType, getVariantIds, isQualifiedDictionaryGroup, isQualifiedDynamicLoaderMap, mergeDictionaries, mergeQualifiedDictionaries, normalizeDictionaries, normalizeDictionary, orderDictionaries, parseDictionarySelector, reconstructQualifiedEntry, removeContentNodeByKeyPath, renameContentNodeByKeyPath, resolveQualifiedDictionary, resolveQualifiedDynamicContent, resolveQualifiedDynamicContentAsync, serializeVariant, updateNodeChildren };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { QUALIFIER_ORDER,
|
|
1
|
+
import { QUALIFIER_ORDER, getDictionaryCompositeIds, getDictionaryQualifierTypes } from "./qualifiedDictionary.mjs";
|
|
2
2
|
import { mergeDictionaries } from "./mergeDictionaries.mjs";
|
|
3
3
|
import { log } from "@intlayer/config/built";
|
|
4
4
|
import { colorizeKey, getAppLogger } from "@intlayer/config/logger";
|
|
@@ -29,18 +29,20 @@ const mergeQualifiedDictionaries = (dictionaries) => {
|
|
|
29
29
|
const baseDictionaries = [];
|
|
30
30
|
const entriesDictionaries = /* @__PURE__ */ new Map();
|
|
31
31
|
dictionaries.forEach((dictionary, index) => {
|
|
32
|
-
if (perDictionaryTypes[index]
|
|
32
|
+
if (perDictionaryTypes[index]?.length === 0) {
|
|
33
33
|
baseDictionaries.push(dictionary);
|
|
34
34
|
return;
|
|
35
35
|
}
|
|
36
|
-
const
|
|
37
|
-
if (
|
|
36
|
+
const compositeIds = getDictionaryCompositeIds(dictionary, groupQualifierTypes);
|
|
37
|
+
if (compositeIds === void 0) {
|
|
38
38
|
appLogger(`Dictionary ${colorizeKey(dictionary.key)} declares (${perDictionaryTypes[index].join(", ")}) but the key's dimensions are (${groupQualifierTypes.join(", ")}); every entry must declare all of them. Entry ignored${dictionary.filePath ? ` - ${dictionary.filePath}` : ""}.`, { level: "error" });
|
|
39
39
|
return;
|
|
40
40
|
}
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
for (const compositeId of compositeIds) {
|
|
42
|
+
const existingEntries = entriesDictionaries.get(compositeId) ?? [];
|
|
43
|
+
existingEntries.push(dictionary);
|
|
44
|
+
entriesDictionaries.set(compositeId, existingEntries);
|
|
45
|
+
}
|
|
44
46
|
});
|
|
45
47
|
const content = {};
|
|
46
48
|
let importMode;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mergeQualifiedDictionaries.mjs","names":[],"sources":["../../../src/dictionaryManipulator/mergeQualifiedDictionaries.ts"],"sourcesContent":["import { log } from '@intlayer/config/built';\nimport { colorizeKey, getAppLogger } from '@intlayer/config/logger';\nimport type {\n Dictionary,\n DictionaryQualifierType,\n QualifiedDictionaryGroup,\n} from '@intlayer/types/dictionary';\nimport { mergeDictionaries } from './mergeDictionaries';\nimport {\n
|
|
1
|
+
{"version":3,"file":"mergeQualifiedDictionaries.mjs","names":[],"sources":["../../../src/dictionaryManipulator/mergeQualifiedDictionaries.ts"],"sourcesContent":["import { log } from '@intlayer/config/built';\nimport { colorizeKey, getAppLogger } from '@intlayer/config/logger';\nimport type {\n Dictionary,\n DictionaryQualifierType,\n QualifiedDictionaryGroup,\n} from '@intlayer/types/dictionary';\nimport { mergeDictionaries } from './mergeDictionaries';\nimport {\n getDictionaryCompositeIds,\n getDictionaryQualifierTypes,\n QUALIFIER_ORDER,\n} from './qualifiedDictionary';\n\n/**\n * Merges sibling dictionaries sharing the same key, honouring qualifiers.\n *\n * - No dictionary declares a qualifier → behaves exactly like\n * `mergeDictionaries` (single merged dictionary).\n * - At least one dictionary declares a qualifier → the group's dimension set is\n * the union of every declared dimension (in canonical order\n * `variant → item`). Dictionaries are grouped by their composite id\n * (one segment per dimension), merged within each group (locale completion /\n * priority overrides preserved), and a `QualifiedDictionaryGroup` is returned.\n * Unqualified siblings act as shared base content merged into every entry.\n *\n * Every qualified entry must declare ALL dimensions of the group; an entry that\n * declares only a subset is ambiguous and is rejected with an error log.\n */\nexport const mergeQualifiedDictionaries = (\n dictionaries: Dictionary[]\n): Dictionary | QualifiedDictionaryGroup => {\n const perDictionaryTypes = dictionaries.map(getDictionaryQualifierTypes);\n\n const declaredDimensions = new Set<DictionaryQualifierType>();\n for (const types of perDictionaryTypes) {\n for (const type of types) declaredDimensions.add(type);\n }\n\n // Canonical order, restricted to the dimensions actually declared.\n const groupQualifierTypes = QUALIFIER_ORDER.filter((qualifierType) =>\n declaredDimensions.has(qualifierType)\n );\n\n if (groupQualifierTypes.length === 0) {\n return mergeDictionaries(dictionaries);\n }\n\n const appLogger = getAppLogger({ log });\n\n const baseDictionaries: Dictionary[] = [];\n const entriesDictionaries = new Map<string, Dictionary[]>();\n\n dictionaries.forEach((dictionary, index) => {\n if (perDictionaryTypes[index]?.length === 0) {\n baseDictionaries.push(dictionary);\n return;\n }\n\n // A dictionary may map to several composite ids (array variant fan-out):\n // its content is registered under every id it lists.\n const compositeIds = getDictionaryCompositeIds(\n dictionary,\n groupQualifierTypes\n );\n\n if (compositeIds === undefined) {\n appLogger(\n `Dictionary ${colorizeKey(dictionary.key)} declares (${perDictionaryTypes[index].join(', ')}) but the key's dimensions are (${groupQualifierTypes.join(', ')}); every entry must declare all of them. Entry ignored${dictionary.filePath ? ` - ${dictionary.filePath}` : ''}.`,\n { level: 'error' }\n );\n return;\n }\n\n for (const compositeId of compositeIds) {\n const existingEntries = entriesDictionaries.get(compositeId) ?? [];\n existingEntries.push(dictionary);\n entriesDictionaries.set(compositeId, existingEntries);\n }\n });\n\n // `content` maps each composite id to its resolved content node directly; the\n // qualifier coordinates live in the key, not in a per-entry wrapper. For an\n // object variant the variant segment is the canonical serialization of the\n // object, so it fully identifies the entry — no side-map is needed.\n const content: Record<string, unknown> = {};\n\n let importMode: Dictionary['importMode'];\n\n for (const [compositeId, qualifiedDictionaries] of entriesDictionaries) {\n // Unqualified siblings act as shared base content: appended last so the\n // qualified entries take precedence (mergeDictionaries prefers first).\n const mergedEntry = mergeDictionaries([\n ...qualifiedDictionaries,\n ...baseDictionaries,\n ]);\n\n content[compositeId] = mergedEntry.content;\n\n const [firstQualified] = qualifiedDictionaries;\n\n importMode ??= firstQualified?.importMode;\n }\n\n const localIds = Array.from(\n new Set(\n dictionaries\n .filter((dictionary) => dictionary.localId)\n .map((dictionary) => dictionary.localId!)\n )\n );\n\n return {\n key: dictionaries[0]!.key,\n qualifierTypes: groupQualifierTypes,\n content,\n ...(importMode !== undefined && { importMode }),\n localIds,\n };\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AA6BA,MAAa,8BACX,iBAC0C;CAC1C,MAAM,qBAAqB,aAAa,IAAI,4BAA4B;CAExE,MAAM,qCAAqB,IAAI,KAA8B;AAC7D,MAAK,MAAM,SAAS,mBAClB,MAAK,MAAM,QAAQ,MAAO,oBAAmB,IAAI,KAAK;CAIxD,MAAM,sBAAsB,gBAAgB,QAAQ,kBAClD,mBAAmB,IAAI,cAAc,CACtC;AAED,KAAI,oBAAoB,WAAW,EACjC,QAAO,kBAAkB,aAAa;CAGxC,MAAM,YAAY,aAAa,EAAE,KAAK,CAAC;CAEvC,MAAM,mBAAiC,EAAE;CACzC,MAAM,sCAAsB,IAAI,KAA2B;AAE3D,cAAa,SAAS,YAAY,UAAU;AAC1C,MAAI,mBAAmB,QAAQ,WAAW,GAAG;AAC3C,oBAAiB,KAAK,WAAW;AACjC;;EAKF,MAAM,eAAe,0BACnB,YACA,oBACD;AAED,MAAI,iBAAiB,QAAW;AAC9B,aACE,cAAc,YAAY,WAAW,IAAI,CAAC,aAAa,mBAAmB,OAAO,KAAK,KAAK,CAAC,kCAAkC,oBAAoB,KAAK,KAAK,CAAC,wDAAwD,WAAW,WAAW,MAAM,WAAW,aAAa,GAAG,IAC5Q,EAAE,OAAO,SAAS,CACnB;AACD;;AAGF,OAAK,MAAM,eAAe,cAAc;GACtC,MAAM,kBAAkB,oBAAoB,IAAI,YAAY,IAAI,EAAE;AAClE,mBAAgB,KAAK,WAAW;AAChC,uBAAoB,IAAI,aAAa,gBAAgB;;GAEvD;CAMF,MAAM,UAAmC,EAAE;CAE3C,IAAI;AAEJ,MAAK,MAAM,CAAC,aAAa,0BAA0B,qBAAqB;AAQtE,UAAQ,eALY,kBAAkB,CACpC,GAAG,uBACH,GAAG,iBACJ,CAEiC,CAAC;EAEnC,MAAM,CAAC,kBAAkB;AAEzB,iBAAe,gBAAgB;;CAGjC,MAAM,WAAW,MAAM,KACrB,IAAI,IACF,aACG,QAAQ,eAAe,WAAW,QAAQ,CAC1C,KAAK,eAAe,WAAW,QAAS,CAC5C,CACF;AAED,QAAO;EACL,KAAK,aAAa,GAAI;EACtB,gBAAgB;EAChB;EACA,GAAI,eAAe,UAAa,EAAE,YAAY;EAC9C;EACD"}
|
|
@@ -11,21 +11,60 @@ const QUALIFIER_ORDER = ["variant", "item"];
|
|
|
11
11
|
*/
|
|
12
12
|
const COMPOSITE_ID_SEPARATOR = "/";
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
* Characters kept verbatim in an encoded qualifier segment. Everything else is
|
|
15
|
+
* percent-encoded so a segment can never contain the composite-id separator
|
|
16
|
+
* (`/`), path-hostile characters (`\` `:` `*` `?` `"` `<` `>` `|`, control
|
|
17
|
+
* chars), or characters that would break the generated loader modules (`'`).
|
|
18
|
+
*/
|
|
19
|
+
const SEGMENT_UNSAFE_CHARS = /[^A-Za-z0-9._&=-]/g;
|
|
20
|
+
/**
|
|
21
|
+
* Stricter set for the components of an object variant: also encodes `&` and
|
|
22
|
+
* `=` so the `field=value&field=value` serialization stays unambiguous.
|
|
23
|
+
*/
|
|
24
|
+
const COMPONENT_UNSAFE_CHARS = /[^A-Za-z0-9._-]/g;
|
|
25
|
+
/** Percent-encodes one UTF-16 code unit as a fixed-width `%XXXX` run. */
|
|
26
|
+
const percentEncodeChar = (char) => `%${char.charCodeAt(0).toString(16).toUpperCase().padStart(4, "0")}`;
|
|
27
|
+
const encodeSegmentText = (raw, unsafeChars) => {
|
|
28
|
+
if (raw === "") return "%";
|
|
29
|
+
const encoded = raw.replace(unsafeChars, percentEncodeChar);
|
|
30
|
+
if (encoded === "." || encoded === "..") return encoded.replace(/\./g, "%002E");
|
|
31
|
+
return encoded;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Canonical serialization of a single variant value into its identity string —
|
|
35
|
+
* the variant segment of a composite id, the chunk directory name in dynamic
|
|
36
|
+
* mode, and the runtime matching key.
|
|
16
37
|
*
|
|
17
38
|
* - `undefined` → `'default'` (the implicit fallback variant)
|
|
18
39
|
* - a string → the string itself (a named variant)
|
|
19
40
|
* - an object → its sorted `key=value` pairs joined by `&`
|
|
20
41
|
* (e.g. `{ userId: '123', id: 'abc' }` → `'id=abc&userId=123'`)
|
|
21
42
|
*
|
|
43
|
+
* Characters that are unsafe in file paths or generated code are
|
|
44
|
+
* percent-encoded (fixed-width `%XXXX` runs, injective). Common names —
|
|
45
|
+
* letters, digits, `-` `_` `.` — are left untouched. Both the declaration and
|
|
46
|
+
* the selector go through this function, so encoding never affects matching.
|
|
47
|
+
*
|
|
22
48
|
* Two variants resolve to the same entry iff their serializations are equal, so
|
|
23
49
|
* an object variant in a selector must equal the one declared on the dictionary.
|
|
24
50
|
*/
|
|
25
51
|
const serializeVariant = (variant) => {
|
|
26
52
|
if (variant === void 0) return "default";
|
|
27
|
-
if (typeof variant === "string") return variant;
|
|
28
|
-
return Object.keys(variant).sort().map((field) => `${field}=${variant[field]}`).join("&");
|
|
53
|
+
if (typeof variant === "string") return encodeSegmentText(variant, SEGMENT_UNSAFE_CHARS);
|
|
54
|
+
return Object.keys(variant).sort().map((field) => `${encodeSegmentText(field, COMPONENT_UNSAFE_CHARS)}=${encodeSegmentText(String(variant[field]), COMPONENT_UNSAFE_CHARS)}`).join("&");
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Normalizes the `variant` field of a dictionary into the list of variant ids
|
|
58
|
+
* the declaration registers under. A single value yields one id; an **array**
|
|
59
|
+
* fans out into one id per element (duplicates collapsed). Returns `undefined`
|
|
60
|
+
* when the dictionary does not declare the variant dimension (no `variant`
|
|
61
|
+
* field, or an empty array).
|
|
62
|
+
*/
|
|
63
|
+
const getVariantIds = (variant) => {
|
|
64
|
+
if (variant === void 0) return void 0;
|
|
65
|
+
const values = Array.isArray(variant) ? variant : [variant];
|
|
66
|
+
if (values.length === 0) return void 0;
|
|
67
|
+
return [...new Set(values.map(serializeVariant))];
|
|
29
68
|
};
|
|
30
69
|
/**
|
|
31
70
|
* Returns the qualifier dimensions declared on a dictionary, in canonical
|
|
@@ -34,49 +73,51 @@ const serializeVariant = (variant) => {
|
|
|
34
73
|
*/
|
|
35
74
|
const getDictionaryQualifierTypes = (dictionary) => {
|
|
36
75
|
const declaredQualifiers = [];
|
|
37
|
-
if (dictionary.variant !== void 0) declaredQualifiers.push("variant");
|
|
76
|
+
if (getVariantIds(dictionary.variant) !== void 0) declaredQualifiers.push("variant");
|
|
38
77
|
if (typeof dictionary.item === "number") declaredQualifiers.push("item");
|
|
39
78
|
return declaredQualifiers;
|
|
40
79
|
};
|
|
41
80
|
/**
|
|
42
|
-
* Returns the qualifier
|
|
43
|
-
* dimension —
|
|
81
|
+
* Returns the qualifier identifiers of a dictionary for the given qualifier
|
|
82
|
+
* dimension — the candidate segments of the composite entry ids.
|
|
44
83
|
*
|
|
45
|
-
* - 'variant' → the serialized variant (
|
|
46
|
-
* -
|
|
84
|
+
* - 'variant' → the serialized variant id(s); an array variant yields one id
|
|
85
|
+
* per element (declaration-side fan-out)
|
|
86
|
+
* - 'item' → the item index as a single-element list
|
|
47
87
|
*/
|
|
48
|
-
const
|
|
49
|
-
if (qualifierType === "variant") return
|
|
50
|
-
return dictionary.item === void 0 ? void 0 : String(dictionary.item);
|
|
88
|
+
const getDictionaryQualifierIds = (dictionary, qualifierType) => {
|
|
89
|
+
if (qualifierType === "variant") return getVariantIds(dictionary.variant);
|
|
90
|
+
return dictionary.item === void 0 ? void 0 : [String(dictionary.item)];
|
|
51
91
|
};
|
|
52
92
|
/**
|
|
53
|
-
*
|
|
54
|
-
* dimension
|
|
55
|
-
*
|
|
93
|
+
* Builds every composite entry id of a dictionary — the cartesian product of
|
|
94
|
+
* its per-dimension id lists, joined in canonical order. A dictionary with a
|
|
95
|
+
* plain (non-array) variant yields exactly one id; an array variant fans out
|
|
96
|
+
* into one id per element. `undefined` when a dimension of the set is missing.
|
|
56
97
|
*/
|
|
57
|
-
const
|
|
58
|
-
|
|
98
|
+
const getDictionaryCompositeIds = (dictionary, qualifierTypes) => {
|
|
99
|
+
let compositeIds = [""];
|
|
59
100
|
for (const qualifierType of qualifierTypes) {
|
|
60
|
-
const
|
|
61
|
-
if (
|
|
62
|
-
|
|
101
|
+
const ids = getDictionaryQualifierIds(dictionary, qualifierType);
|
|
102
|
+
if (ids === void 0) return void 0;
|
|
103
|
+
compositeIds = compositeIds.flatMap((prefix) => ids.map((id) => prefix === "" ? id : `${prefix}${"/"}${id}`));
|
|
63
104
|
}
|
|
64
|
-
return
|
|
105
|
+
return compositeIds;
|
|
65
106
|
};
|
|
66
107
|
/**
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
* Tests whether a group entry matches a selector across every declared
|
|
73
|
-
* dimension. The `item` dimension matches any value when the selector does not
|
|
74
|
-
* provide one (open collection axis).
|
|
108
|
+
* Tests whether a composite entry id matches a selector across every declared
|
|
109
|
+
* dimension. Segments are compared in their encoded form (both the stored id
|
|
110
|
+
* and the selector go through {@link serializeVariant}). The `item` dimension
|
|
111
|
+
* matches any value when the selector does not provide one (open collection
|
|
112
|
+
* axis).
|
|
75
113
|
*/
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
return
|
|
79
|
-
|
|
114
|
+
const compositeIdMatchesSelector = (compositeId, qualifierTypes, selector) => {
|
|
115
|
+
const segments = compositeId.split("/");
|
|
116
|
+
return qualifierTypes.every((qualifierType, index) => {
|
|
117
|
+
if (qualifierType === "variant") return segments[index] === serializeVariant(selector?.variant);
|
|
118
|
+
return selector?.item === void 0 || segments[index] === String(selector.item);
|
|
119
|
+
});
|
|
120
|
+
};
|
|
80
121
|
/**
|
|
81
122
|
* Type guard discriminating a `QualifiedDictionaryGroup` (merge output of a
|
|
82
123
|
* qualified key) from a plain `Dictionary`. Both carry a `content` field; only
|
|
@@ -88,11 +129,11 @@ const isQualifiedDictionaryGroup = (value) => typeof value === "object" && value
|
|
|
88
129
|
* qualified group: the content node stored under its composite id, plus the
|
|
89
130
|
* qualifier coordinates decoded from that id (`variant`, `item`).
|
|
90
131
|
*
|
|
91
|
-
* This keeps the resolver's
|
|
132
|
+
* This keeps the resolver's transform code unchanged: it still sees a
|
|
92
133
|
* `{ key, content, variant?, item? }` shape, even though the stored format no
|
|
93
134
|
* longer duplicates those fields per entry. The `variant` coordinate stays in
|
|
94
|
-
* its serialized form
|
|
95
|
-
*
|
|
135
|
+
* its serialized (encoded) form, e.g. `'id=abc&userId=123'` — matching happens
|
|
136
|
+
* on the composite id segments, never on this reconstructed field.
|
|
96
137
|
*/
|
|
97
138
|
const reconstructQualifiedEntry = (group, compositeId) => {
|
|
98
139
|
const segments = compositeId.split("/");
|
|
@@ -123,7 +164,7 @@ const resolveQualifiedDictionary = (dictionaryOrGroup, selector) => {
|
|
|
123
164
|
if (!isQualifiedDictionaryGroup(dictionaryOrGroup)) return dictionaryOrGroup;
|
|
124
165
|
const { qualifierTypes, content } = dictionaryOrGroup;
|
|
125
166
|
const itemAxisOpen = qualifierTypes.includes("item") && selector?.item === void 0;
|
|
126
|
-
const matchedEntries = Object.keys(content).
|
|
167
|
+
const matchedEntries = Object.keys(content).filter((compositeId) => compositeIdMatchesSelector(compositeId, qualifierTypes, selector)).map((compositeId) => reconstructQualifiedEntry(dictionaryOrGroup, compositeId));
|
|
127
168
|
if (itemAxisOpen) return matchedEntries.sort((left, right) => (left.item ?? 0) - (right.item ?? 0));
|
|
128
169
|
return matchedEntries[0] ?? null;
|
|
129
170
|
};
|
|
@@ -255,5 +296,5 @@ const resolveQualifiedDynamicContentAsync = async (params) => {
|
|
|
255
296
|
};
|
|
256
297
|
|
|
257
298
|
//#endregion
|
|
258
|
-
export { COMPOSITE_ID_SEPARATOR, QUALIFIER_DYNAMIC_TYPES_KEY, QUALIFIER_ORDER,
|
|
299
|
+
export { COMPOSITE_ID_SEPARATOR, QUALIFIER_DYNAMIC_TYPES_KEY, QUALIFIER_ORDER, getDictionaryCompositeIds, getDictionaryQualifierIds, getDictionaryQualifierTypes, getDictionarySelectorCacheKey, getVariantIds, isQualifiedDictionaryGroup, isQualifiedDynamicLoaderMap, parseDictionarySelector, reconstructQualifiedEntry, resolveQualifiedDictionary, resolveQualifiedDynamicContent, resolveQualifiedDynamicContentAsync, serializeVariant };
|
|
259
300
|
//# sourceMappingURL=qualifiedDictionary.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"qualifiedDictionary.mjs","names":[],"sources":["../../../src/dictionaryManipulator/qualifiedDictionary.ts"],"sourcesContent":["import type {\n Dictionary,\n DictionaryQualifierType,\n DictionarySelector,\n QualifiedDictionaryGroup,\n} from '@intlayer/types/dictionary';\nimport type { LocalesValues } from '@intlayer/types/module_augmentation';\n\n/**\n * Canonical order of qualifier dimensions. A key that declares both dimensions\n * always nests them in this order, with `item` innermost so it can act as the\n * collection (array) axis.\n */\nexport const QUALIFIER_ORDER = [\n 'variant',\n 'item',\n] as const satisfies readonly DictionaryQualifierType[];\n\n/**\n * Separator joining per-dimension ids into a composite entry id. Also used as\n * the chunk path separator in dynamic mode.\n */\nexport const COMPOSITE_ID_SEPARATOR = '/';\n\n/**\n * Canonical serialization of a variant value into its identity string — the\n * variant segment of a composite id and the runtime matching key.\n *\n * - `undefined` → `'default'` (the implicit fallback variant)\n * - a string → the string itself (a named variant)\n * - an object → its sorted `key=value` pairs joined by `&`\n * (e.g. `{ userId: '123', id: 'abc' }` → `'id=abc&userId=123'`)\n *\n * Two variants resolve to the same entry iff their serializations are equal, so\n * an object variant in a selector must equal the one declared on the dictionary.\n */\nexport const serializeVariant = (\n variant: string | Record<string, string | number> | undefined\n): string => {\n if (variant === undefined) return 'default';\n if (typeof variant === 'string') return variant;\n\n return Object.keys(variant)\n .sort()\n .map((field) => `${field}=${variant[field]}`)\n .join('&');\n};\n\n/**\n * Returns the qualifier dimensions declared on a dictionary, in canonical\n * order (`variant → item`). Empty when the dictionary is unqualified\n * (plain dictionary or shared base content of a qualified group).\n */\nexport const getDictionaryQualifierTypes = (\n dictionary: Dictionary\n): DictionaryQualifierType[] => {\n const declaredQualifiers: DictionaryQualifierType[] = [];\n\n if (dictionary.variant !== undefined) declaredQualifiers.push('variant');\n if (typeof dictionary.item === 'number') declaredQualifiers.push('item');\n\n return declaredQualifiers;\n};\n\n/**\n * Returns the qualifier identifier of a dictionary for the given qualifier\n * dimension — one segment of the composite entry id.\n *\n * - 'variant' → the serialized variant (named string or object identity)\n * - 'item' → the item index as string\n */\nexport const getDictionaryQualifierId = (\n dictionary: Dictionary,\n qualifierType: DictionaryQualifierType\n): string | undefined => {\n if (qualifierType === 'variant') {\n return dictionary.variant === undefined\n ? undefined\n : serializeVariant(dictionary.variant);\n }\n return dictionary.item === undefined ? undefined : String(dictionary.item);\n};\n\n/**\n * Returns the per-dimension id segments of a dictionary for the given ordered\n * dimension set, or `undefined` when the dictionary does not declare every\n * dimension of the set.\n */\nexport const getDictionaryQualifierSegments = (\n dictionary: Dictionary,\n qualifierTypes: DictionaryQualifierType[]\n): string[] | undefined => {\n const segments: string[] = [];\n\n for (const qualifierType of qualifierTypes) {\n const id = getDictionaryQualifierId(dictionary, qualifierType);\n if (id === undefined) return undefined;\n segments.push(id);\n }\n\n return segments;\n};\n\n/**\n * Builds the composite entry id of a dictionary — its per-dimension id segments\n * joined in canonical order. `undefined` when a dimension is missing.\n */\nexport const getDictionaryCompositeId = (\n dictionary: Dictionary,\n qualifierTypes: DictionaryQualifierType[]\n): string | undefined =>\n getDictionaryQualifierSegments(dictionary, qualifierTypes)?.join(\n COMPOSITE_ID_SEPARATOR\n );\n\n/**\n * Tests whether a group entry matches a selector across every declared\n * dimension. The `item` dimension matches any value when the selector does not\n * provide one (open collection axis).\n */\nconst entryMatchesSelector = (\n entry: Dictionary,\n qualifierTypes: DictionaryQualifierType[],\n selector: DictionarySelector | undefined\n): boolean =>\n qualifierTypes.every((qualifierType) => {\n if (qualifierType === 'variant') {\n return (\n serializeVariant(entry.variant) === serializeVariant(selector?.variant)\n );\n }\n\n // qualifierType === 'item'\n return (\n selector?.item === undefined ||\n String(entry.item) === String(selector.item)\n );\n });\n\n/**\n * Type guard discriminating a `QualifiedDictionaryGroup` (merge output of a\n * qualified key) from a plain `Dictionary`. Both carry a `content` field; only\n * the group declares `qualifierTypes`, which is therefore the discriminator.\n */\nexport const isQualifiedDictionaryGroup = (\n value: unknown\n): value is QualifiedDictionaryGroup =>\n typeof value === 'object' &&\n value !== null &&\n 'qualifierTypes' in value &&\n Array.isArray((value as { qualifierTypes: unknown }).qualifierTypes) &&\n 'content' in value;\n\n/**\n * Reconstructs a resolvable {@link Dictionary} from a single entry of a\n * qualified group: the content node stored under its composite id, plus the\n * qualifier coordinates decoded from that id (`variant`, `item`).\n *\n * This keeps the resolver's matching/transform code unchanged: it still sees a\n * `{ key, content, variant?, item? }` shape, even though the stored format no\n * longer duplicates those fields per entry. The `variant` coordinate stays in\n * its serialized form (e.g. `'id=abc&userId=123'`), which round-trips through\n * {@link serializeVariant} during matching.\n */\nexport const reconstructQualifiedEntry = (\n group: QualifiedDictionaryGroup,\n compositeId: string\n): Dictionary => {\n const segments = compositeId.split(COMPOSITE_ID_SEPARATOR);\n\n const entry = {\n key: group.key,\n content: group.content[compositeId],\n } as Dictionary;\n\n group.qualifierTypes.forEach((qualifierType, index) => {\n if (qualifierType === 'variant') {\n entry.variant = segments[index];\n } else if (qualifierType === 'item') {\n entry.item = Number(segments[index]);\n }\n });\n\n return entry;\n};\n\n/**\n * Resolves a dictionary (or qualified dictionary group) against a selector,\n * across every declared dimension.\n *\n * - Plain dictionary → returned as-is (selector ignored)\n * - `item` declared but not selected → every matching entry ordered by index\n * - `item` selected → the matching entry or null\n * - `variant` defaults to the `default` entry when not selected; an object\n * variant resolves only when the selector provides an equal object\n *\n * Dimensions compose: e.g. a variant × item key with `{ variant: 'promo' }`\n * returns every promo item as an array; adding `{ item: 2 }` narrows to one.\n */\nexport const resolveQualifiedDictionary = (\n dictionaryOrGroup: Dictionary | QualifiedDictionaryGroup,\n selector?: DictionarySelector\n): Dictionary | Dictionary[] | null => {\n if (!isQualifiedDictionaryGroup(dictionaryOrGroup)) {\n return dictionaryOrGroup;\n }\n\n const { qualifierTypes, content } = dictionaryOrGroup;\n\n const itemAxisOpen =\n qualifierTypes.includes('item') && selector?.item === undefined;\n\n const matchedEntries = Object.keys(content)\n .map((compositeId) =>\n reconstructQualifiedEntry(dictionaryOrGroup, compositeId)\n )\n .filter((entry) => entryMatchesSelector(entry, qualifierTypes, selector));\n\n if (itemAxisOpen) {\n return matchedEntries.sort(\n (left, right) => (left.item ?? 0) - (right.item ?? 0)\n );\n }\n\n return matchedEntries[0] ?? null;\n};\n\n/**\n * Splits the second argument of `getIntlayer` / `getDictionary` into the\n * effective locale and the selector object (if any).\n */\nexport const parseDictionarySelector = <L extends LocalesValues>(\n localeOrSelector?: L | DictionarySelector\n): { locale?: L; selector?: DictionarySelector } => {\n if (typeof localeOrSelector === 'object' && localeOrSelector !== null) {\n return {\n locale: localeOrSelector.locale as L | undefined,\n selector: localeOrSelector,\n };\n }\n\n return { locale: localeOrSelector };\n};\n\n/**\n * Builds a stable string identity of a selector (excluding `locale`), suitable\n * for cache keys and memoization dependencies.\n */\nexport const getDictionarySelectorCacheKey = (\n selector?: DictionarySelector\n): string => {\n if (!selector) return '';\n\n return Object.keys(selector)\n .filter((selectorKey) => selectorKey !== 'locale')\n .sort()\n .map((selectorKey) => {\n const value = selector[selectorKey as keyof DictionarySelector];\n const serialized =\n selectorKey === 'variant'\n ? serializeVariant(value as Parameters<typeof serializeVariant>[0])\n : String(value);\n return `${selectorKey}:${serialized}`;\n })\n .join('|');\n};\n\n/**\n * Marker property carrying the ordered qualifier dimensions on a dynamic loader\n * map. Its presence distinguishes a qualified group loader map (a nested tree\n * of chunks) from a plain dynamic loader map (one chunk per `locale`). Prefixed\n * and unlikely to collide with a real locale code.\n */\nexport const QUALIFIER_DYNAMIC_TYPES_KEY = '__intlayerQualifierTypes';\n\n/**\n * A lazily-imported per-locale dictionary chunk loader.\n */\nexport type DynamicDictionaryLoader = () => Promise<Dictionary>;\n\n/**\n * Nested tree of chunk loaders: one nesting level per declared dimension (in\n * canonical order), leaves are loaders.\n */\nexport type QualifiedDynamicLoaderTree = {\n [segment: string]: QualifiedDynamicLoaderTree | DynamicDictionaryLoader;\n};\n\n/**\n * Default export shape of a generated dynamic entry point for a qualified key.\n * One nesting level per dimension under each locale, plus the dimension marker.\n *\n * ```ts\n * {\n * __intlayerQualifierTypes: ['variant', 'item'],\n * en: { promo: { '1': () => import('./json/x/promo/1/en.json'), … }, … },\n * fr: { … },\n * }\n * ```\n */\nexport type QualifiedDynamicLoaderMap = {\n [QUALIFIER_DYNAMIC_TYPES_KEY]: DictionaryQualifierType[];\n [locale: string]: QualifiedDynamicLoaderTree | DictionaryQualifierType[];\n};\n\n/**\n * Type guard discriminating a qualified dynamic loader map (collections /\n * variants, possibly combined) from a plain dynamic loader map.\n */\nexport const isQualifiedDynamicLoaderMap = (\n value: unknown\n): value is QualifiedDynamicLoaderMap =>\n typeof value === 'object' &&\n value !== null &&\n QUALIFIER_DYNAMIC_TYPES_KEY in value;\n\n/**\n/** One targeted chunk: its stable cache key and lazy loader. */\ntype CollectedChunk = {\n cacheKey: string;\n loader: DynamicDictionaryLoader;\n};\n\ntype CollectedChunks = {\n /** True when the `item` axis is open (collection result → array). */\n itemAxisOpen: boolean;\n /** True when a required coordinate is absent (result → [] or null). */\n missed: boolean;\n /** The chunks the selector targets (in collection order for the item axis). */\n chunks: CollectedChunk[];\n};\n\n/**\n * Walks the loader tree following the selector and collects the chunk loaders\n * it targets — shared by the sync ({@link resolveQualifiedDynamicContent}) and\n * async ({@link resolveQualifiedDynamicContentAsync}) resolvers.\n */\nconst collectQualifiedChunks = (\n loaderMap: QualifiedDynamicLoaderMap,\n key: string,\n locale: string,\n selector: DictionarySelector | undefined\n): CollectedChunks => {\n const qualifierTypes = loaderMap[QUALIFIER_DYNAMIC_TYPES_KEY];\n const localeTree = loaderMap[locale] as\n | QualifiedDynamicLoaderTree\n | undefined;\n\n const itemAxisOpen =\n qualifierTypes.includes('item') && selector?.item === undefined;\n\n if (!localeTree) return { itemAxisOpen, missed: true, chunks: [] };\n\n const chunks: CollectedChunk[] = [];\n\n const walk = (\n node: QualifiedDynamicLoaderTree | DynamicDictionaryLoader,\n dimensions: DictionaryQualifierType[],\n segments: string[]\n ): boolean => {\n if (dimensions.length === 0) {\n chunks.push({\n cacheKey: `${key}.${locale}.${segments.join(COMPOSITE_ID_SEPARATOR)}`,\n loader: node as DynamicDictionaryLoader,\n });\n return true;\n }\n\n const [dimension, ...rest] = dimensions;\n const tree = node as QualifiedDynamicLoaderTree;\n\n if (dimension === 'item' && selector?.item === undefined) {\n // Open collection axis: fan out into every sibling chunk, ordered.\n for (const segment of Object.keys(tree).sort(\n (left, right) => Number(left) - Number(right)\n )) {\n walk(tree[segment]!, rest, [...segments, segment]);\n }\n return true;\n }\n\n const segment =\n dimension === 'variant'\n ? serializeVariant(selector?.variant)\n : String(selector?.item);\n\n const child = tree[segment];\n if (!child) return false;\n\n return walk(child, rest, [...segments, segment]);\n };\n\n const found = walk(localeTree, qualifierTypes, []);\n\n return { itemAxisOpen, missed: !found, chunks };\n};\n\n/**\n * Resolves the content of a qualified dynamic loader map against a selector,\n * loading only the chunk(s) the selector actually targets.\n *\n * Walks the nested loader tree one dimension at a time (canonical order\n * `variant → item`): `variant` defaults to `default` (or descends by the\n * serialized object identity), and `item` either narrows to the selected index\n * or — when no item is given — expands into every sibling chunk (the collection\n * axis). Semantics mirror {@link resolveQualifiedDictionary} so dynamic and\n * static modes behave alike.\n *\n * The Suspense mechanism is injected through `loadChunk` so the same logic\n * serves both the client (suspender cache) and the server (`react.use`). Every\n * targeted loader is started before the first chunk is read, so sibling chunks\n * load in parallel rather than waterfalling.\n *\n * @param loaderMap - The qualified dynamic loader map (entry point default export).\n * @param key - The dictionary key (used to build stable chunk cache keys).\n * @param locale - The resolved locale to load chunks for.\n * @param selector - The selector splitting the qualifier dimensions.\n * @param loadChunk - Reads a started chunk promise, suspending until it resolves.\n * @param transform - Turns a resolved chunk dictionary into final content.\n */\nexport const resolveQualifiedDynamicContent = <Content>(params: {\n loaderMap: QualifiedDynamicLoaderMap;\n key: string;\n locale: string;\n selector: DictionarySelector | undefined;\n loadChunk: (cacheKey: string, promise: Promise<Dictionary>) => Dictionary;\n transform: (dictionary: Dictionary) => Content;\n}): Content | Content[] | null => {\n const { loaderMap, key, locale, selector, loadChunk, transform } = params;\n\n const { itemAxisOpen, missed, chunks } = collectQualifiedChunks(\n loaderMap,\n key,\n locale,\n selector\n );\n\n if (missed) return itemAxisOpen ? [] : null;\n\n // Start every loader before reading, so siblings load in parallel.\n const dictionaries = chunks.map(({ cacheKey, loader }) =>\n loadChunk(cacheKey, loader())\n );\n\n if (itemAxisOpen) return dictionaries.map(transform);\n\n const [dictionary] = dictionaries;\n return dictionary ? transform(dictionary) : null;\n};\n\n/**\n * Async counterpart of {@link resolveQualifiedDynamicContent} for frameworks\n * that load dictionaries with `await` instead of Suspense (Vue, Svelte, Lit,\n * vanilla). Awaits every targeted chunk in parallel, then resolves identically.\n *\n * @param loaderMap - The qualified dynamic loader map.\n * @param key - The dictionary key (used to build stable chunk cache keys).\n * @param locale - The resolved locale to load chunks for.\n * @param selector - The selector splitting the qualifier dimensions.\n * @param transform - Turns a resolved chunk dictionary into final content.\n */\nexport const resolveQualifiedDynamicContentAsync = async <Content>(params: {\n loaderMap: QualifiedDynamicLoaderMap;\n key: string;\n locale: string;\n selector: DictionarySelector | undefined;\n transform: (dictionary: Dictionary) => Content;\n}): Promise<Content | Content[] | null> => {\n const { loaderMap, key, locale, selector, transform } = params;\n\n const { itemAxisOpen, missed, chunks } = collectQualifiedChunks(\n loaderMap,\n key,\n locale,\n selector\n );\n\n if (missed) return itemAxisOpen ? [] : null;\n\n const dictionaries = await Promise.all(chunks.map(({ loader }) => loader()));\n\n if (itemAxisOpen) return dictionaries.map(transform);\n\n const [dictionary] = dictionaries;\n return dictionary ? transform(dictionary) : null;\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"qualifiedDictionary.mjs","names":[],"sources":["../../../src/dictionaryManipulator/qualifiedDictionary.ts"],"sourcesContent":["import type {\n Dictionary,\n DictionaryQualifierType,\n DictionarySelector,\n DictionaryVariantValue,\n QualifiedDictionaryGroup,\n} from '@intlayer/types/dictionary';\nimport type { LocalesValues } from '@intlayer/types/module_augmentation';\n\n/**\n * Canonical order of qualifier dimensions. A key that declares both dimensions\n * always nests them in this order, with `item` innermost so it can act as the\n * collection (array) axis.\n */\nexport const QUALIFIER_ORDER = [\n 'variant',\n 'item',\n] as const satisfies readonly DictionaryQualifierType[];\n\n/**\n * Separator joining per-dimension ids into a composite entry id. Also used as\n * the chunk path separator in dynamic mode.\n */\nexport const COMPOSITE_ID_SEPARATOR = '/';\n\n/**\n * Characters kept verbatim in an encoded qualifier segment. Everything else is\n * percent-encoded so a segment can never contain the composite-id separator\n * (`/`), path-hostile characters (`\\` `:` `*` `?` `\"` `<` `>` `|`, control\n * chars), or characters that would break the generated loader modules (`'`).\n */\nconst SEGMENT_UNSAFE_CHARS = /[^A-Za-z0-9._&=-]/g;\n\n/**\n * Stricter set for the components of an object variant: also encodes `&` and\n * `=` so the `field=value&field=value` serialization stays unambiguous.\n */\nconst COMPONENT_UNSAFE_CHARS = /[^A-Za-z0-9._-]/g;\n\n/** Percent-encodes one UTF-16 code unit as a fixed-width `%XXXX` run. */\nconst percentEncodeChar = (char: string): string =>\n `%${char.charCodeAt(0).toString(16).toUpperCase().padStart(4, '0')}`;\n\nconst encodeSegmentText = (raw: string, unsafeChars: RegExp): string => {\n // Bare '%' cannot be produced by encoding (every encoded run is %XXXX),\n // so it is a safe stand-in for the empty string.\n if (raw === '') return '%';\n\n const encoded = raw.replace(unsafeChars, percentEncodeChar);\n\n // '.' and '..' are path navigation on every filesystem — encode the dots.\n if (encoded === '.' || encoded === '..') {\n return encoded.replace(/\\./g, '%002E');\n }\n\n return encoded;\n};\n\n/**\n * Canonical serialization of a single variant value into its identity string —\n * the variant segment of a composite id, the chunk directory name in dynamic\n * mode, and the runtime matching key.\n *\n * - `undefined` → `'default'` (the implicit fallback variant)\n * - a string → the string itself (a named variant)\n * - an object → its sorted `key=value` pairs joined by `&`\n * (e.g. `{ userId: '123', id: 'abc' }` → `'id=abc&userId=123'`)\n *\n * Characters that are unsafe in file paths or generated code are\n * percent-encoded (fixed-width `%XXXX` runs, injective). Common names —\n * letters, digits, `-` `_` `.` — are left untouched. Both the declaration and\n * the selector go through this function, so encoding never affects matching.\n *\n * Two variants resolve to the same entry iff their serializations are equal, so\n * an object variant in a selector must equal the one declared on the dictionary.\n */\nexport const serializeVariant = (\n variant: DictionaryVariantValue | undefined\n): string => {\n if (variant === undefined) return 'default';\n if (typeof variant === 'string') {\n return encodeSegmentText(variant, SEGMENT_UNSAFE_CHARS);\n }\n\n return Object.keys(variant)\n .sort()\n .map(\n (field) =>\n `${encodeSegmentText(field, COMPONENT_UNSAFE_CHARS)}=${encodeSegmentText(String(variant[field]), COMPONENT_UNSAFE_CHARS)}`\n )\n .join('&');\n};\n\n/**\n * Normalizes the `variant` field of a dictionary into the list of variant ids\n * the declaration registers under. A single value yields one id; an **array**\n * fans out into one id per element (duplicates collapsed). Returns `undefined`\n * when the dictionary does not declare the variant dimension (no `variant`\n * field, or an empty array).\n */\nexport const getVariantIds = (\n variant: Dictionary['variant']\n): string[] | undefined => {\n if (variant === undefined) return undefined;\n\n const values = Array.isArray(variant) ? variant : [variant];\n if (values.length === 0) return undefined;\n\n return [...new Set(values.map(serializeVariant))];\n};\n\n/**\n * Returns the qualifier dimensions declared on a dictionary, in canonical\n * order (`variant → item`). Empty when the dictionary is unqualified\n * (plain dictionary or shared base content of a qualified group).\n */\nexport const getDictionaryQualifierTypes = (\n dictionary: Dictionary\n): DictionaryQualifierType[] => {\n const declaredQualifiers: DictionaryQualifierType[] = [];\n\n if (getVariantIds(dictionary.variant) !== undefined) {\n declaredQualifiers.push('variant');\n }\n if (typeof dictionary.item === 'number') declaredQualifiers.push('item');\n\n return declaredQualifiers;\n};\n\n/**\n * Returns the qualifier identifiers of a dictionary for the given qualifier\n * dimension — the candidate segments of the composite entry ids.\n *\n * - 'variant' → the serialized variant id(s); an array variant yields one id\n * per element (declaration-side fan-out)\n * - 'item' → the item index as a single-element list\n */\nexport const getDictionaryQualifierIds = (\n dictionary: Dictionary,\n qualifierType: DictionaryQualifierType\n): string[] | undefined => {\n if (qualifierType === 'variant') {\n return getVariantIds(dictionary.variant);\n }\n return dictionary.item === undefined ? undefined : [String(dictionary.item)];\n};\n\n/**\n * Builds every composite entry id of a dictionary — the cartesian product of\n * its per-dimension id lists, joined in canonical order. A dictionary with a\n * plain (non-array) variant yields exactly one id; an array variant fans out\n * into one id per element. `undefined` when a dimension of the set is missing.\n */\nexport const getDictionaryCompositeIds = (\n dictionary: Dictionary,\n qualifierTypes: DictionaryQualifierType[]\n): string[] | undefined => {\n let compositeIds: string[] = [''];\n\n for (const qualifierType of qualifierTypes) {\n const ids = getDictionaryQualifierIds(dictionary, qualifierType);\n if (ids === undefined) return undefined;\n\n compositeIds = compositeIds.flatMap((prefix) =>\n ids.map((id) =>\n prefix === '' ? id : `${prefix}${COMPOSITE_ID_SEPARATOR}${id}`\n )\n );\n }\n\n return compositeIds;\n};\n\n/**\n * Tests whether a composite entry id matches a selector across every declared\n * dimension. Segments are compared in their encoded form (both the stored id\n * and the selector go through {@link serializeVariant}). The `item` dimension\n * matches any value when the selector does not provide one (open collection\n * axis).\n */\nconst compositeIdMatchesSelector = (\n compositeId: string,\n qualifierTypes: DictionaryQualifierType[],\n selector: DictionarySelector | undefined\n): boolean => {\n const segments = compositeId.split(COMPOSITE_ID_SEPARATOR);\n\n return qualifierTypes.every((qualifierType, index) => {\n if (qualifierType === 'variant') {\n return segments[index] === serializeVariant(selector?.variant);\n }\n\n // qualifierType === 'item'\n return (\n selector?.item === undefined || segments[index] === String(selector.item)\n );\n });\n};\n\n/**\n * Type guard discriminating a `QualifiedDictionaryGroup` (merge output of a\n * qualified key) from a plain `Dictionary`. Both carry a `content` field; only\n * the group declares `qualifierTypes`, which is therefore the discriminator.\n */\nexport const isQualifiedDictionaryGroup = (\n value: unknown\n): value is QualifiedDictionaryGroup =>\n typeof value === 'object' &&\n value !== null &&\n 'qualifierTypes' in value &&\n Array.isArray((value as { qualifierTypes: unknown }).qualifierTypes) &&\n 'content' in value;\n\n/**\n * Reconstructs a resolvable {@link Dictionary} from a single entry of a\n * qualified group: the content node stored under its composite id, plus the\n * qualifier coordinates decoded from that id (`variant`, `item`).\n *\n * This keeps the resolver's transform code unchanged: it still sees a\n * `{ key, content, variant?, item? }` shape, even though the stored format no\n * longer duplicates those fields per entry. The `variant` coordinate stays in\n * its serialized (encoded) form, e.g. `'id=abc&userId=123'` — matching happens\n * on the composite id segments, never on this reconstructed field.\n */\nexport const reconstructQualifiedEntry = (\n group: QualifiedDictionaryGroup,\n compositeId: string\n): Dictionary => {\n const segments = compositeId.split(COMPOSITE_ID_SEPARATOR);\n\n const entry = {\n key: group.key,\n content: group.content[compositeId],\n } as Dictionary;\n\n group.qualifierTypes.forEach((qualifierType, index) => {\n if (qualifierType === 'variant') {\n entry.variant = segments[index];\n } else if (qualifierType === 'item') {\n entry.item = Number(segments[index]);\n }\n });\n\n return entry;\n};\n\n/**\n * Resolves a dictionary (or qualified dictionary group) against a selector,\n * across every declared dimension.\n *\n * - Plain dictionary → returned as-is (selector ignored)\n * - `item` declared but not selected → every matching entry ordered by index\n * - `item` selected → the matching entry or null\n * - `variant` defaults to the `default` entry when not selected; an object\n * variant resolves only when the selector provides an equal object\n *\n * Dimensions compose: e.g. a variant × item key with `{ variant: 'promo' }`\n * returns every promo item as an array; adding `{ item: 2 }` narrows to one.\n */\nexport const resolveQualifiedDictionary = (\n dictionaryOrGroup: Dictionary | QualifiedDictionaryGroup,\n selector?: DictionarySelector\n): Dictionary | Dictionary[] | null => {\n if (!isQualifiedDictionaryGroup(dictionaryOrGroup)) {\n return dictionaryOrGroup;\n }\n\n const { qualifierTypes, content } = dictionaryOrGroup;\n\n const itemAxisOpen =\n qualifierTypes.includes('item') && selector?.item === undefined;\n\n const matchedEntries = Object.keys(content)\n .filter((compositeId) =>\n compositeIdMatchesSelector(compositeId, qualifierTypes, selector)\n )\n .map((compositeId) =>\n reconstructQualifiedEntry(dictionaryOrGroup, compositeId)\n );\n\n if (itemAxisOpen) {\n return matchedEntries.sort(\n (left, right) => (left.item ?? 0) - (right.item ?? 0)\n );\n }\n\n return matchedEntries[0] ?? null;\n};\n\n/**\n * Splits the second argument of `getIntlayer` / `getDictionary` into the\n * effective locale and the selector object (if any).\n */\nexport const parseDictionarySelector = <L extends LocalesValues>(\n localeOrSelector?: L | DictionarySelector\n): { locale?: L; selector?: DictionarySelector } => {\n if (typeof localeOrSelector === 'object' && localeOrSelector !== null) {\n return {\n locale: localeOrSelector.locale as L | undefined,\n selector: localeOrSelector,\n };\n }\n\n return { locale: localeOrSelector };\n};\n\n/**\n * Builds a stable string identity of a selector (excluding `locale`), suitable\n * for cache keys and memoization dependencies.\n */\nexport const getDictionarySelectorCacheKey = (\n selector?: DictionarySelector\n): string => {\n if (!selector) return '';\n\n return Object.keys(selector)\n .filter((selectorKey) => selectorKey !== 'locale')\n .sort()\n .map((selectorKey) => {\n const value = selector[selectorKey as keyof DictionarySelector];\n const serialized =\n selectorKey === 'variant'\n ? serializeVariant(value as Parameters<typeof serializeVariant>[0])\n : String(value);\n return `${selectorKey}:${serialized}`;\n })\n .join('|');\n};\n\n/**\n * Marker property carrying the ordered qualifier dimensions on a dynamic loader\n * map. Its presence distinguishes a qualified group loader map (a nested tree\n * of chunks) from a plain dynamic loader map (one chunk per `locale`). Prefixed\n * and unlikely to collide with a real locale code.\n */\nexport const QUALIFIER_DYNAMIC_TYPES_KEY = '__intlayerQualifierTypes';\n\n/**\n * A lazily-imported per-locale dictionary chunk loader.\n */\nexport type DynamicDictionaryLoader = () => Promise<Dictionary>;\n\n/**\n * Nested tree of chunk loaders: one nesting level per declared dimension (in\n * canonical order), leaves are loaders.\n */\nexport type QualifiedDynamicLoaderTree = {\n [segment: string]: QualifiedDynamicLoaderTree | DynamicDictionaryLoader;\n};\n\n/**\n * Default export shape of a generated dynamic entry point for a qualified key.\n * One nesting level per dimension under each locale, plus the dimension marker.\n *\n * ```ts\n * {\n * __intlayerQualifierTypes: ['variant', 'item'],\n * en: { promo: { '1': () => import('./json/x/promo/1/en.json'), … }, … },\n * fr: { … },\n * }\n * ```\n */\nexport type QualifiedDynamicLoaderMap = {\n [QUALIFIER_DYNAMIC_TYPES_KEY]: DictionaryQualifierType[];\n [locale: string]: QualifiedDynamicLoaderTree | DictionaryQualifierType[];\n};\n\n/**\n * Type guard discriminating a qualified dynamic loader map (collections /\n * variants, possibly combined) from a plain dynamic loader map.\n */\nexport const isQualifiedDynamicLoaderMap = (\n value: unknown\n): value is QualifiedDynamicLoaderMap =>\n typeof value === 'object' &&\n value !== null &&\n QUALIFIER_DYNAMIC_TYPES_KEY in value;\n\n/**\n/** One targeted chunk: its stable cache key and lazy loader. */\ntype CollectedChunk = {\n cacheKey: string;\n loader: DynamicDictionaryLoader;\n};\n\ntype CollectedChunks = {\n /** True when the `item` axis is open (collection result → array). */\n itemAxisOpen: boolean;\n /** True when a required coordinate is absent (result → [] or null). */\n missed: boolean;\n /** The chunks the selector targets (in collection order for the item axis). */\n chunks: CollectedChunk[];\n};\n\n/**\n * Walks the loader tree following the selector and collects the chunk loaders\n * it targets — shared by the sync ({@link resolveQualifiedDynamicContent}) and\n * async ({@link resolveQualifiedDynamicContentAsync}) resolvers.\n */\nconst collectQualifiedChunks = (\n loaderMap: QualifiedDynamicLoaderMap,\n key: string,\n locale: string,\n selector: DictionarySelector | undefined\n): CollectedChunks => {\n const qualifierTypes = loaderMap[QUALIFIER_DYNAMIC_TYPES_KEY];\n const localeTree = loaderMap[locale] as\n | QualifiedDynamicLoaderTree\n | undefined;\n\n const itemAxisOpen =\n qualifierTypes.includes('item') && selector?.item === undefined;\n\n if (!localeTree) return { itemAxisOpen, missed: true, chunks: [] };\n\n const chunks: CollectedChunk[] = [];\n\n const walk = (\n node: QualifiedDynamicLoaderTree | DynamicDictionaryLoader,\n dimensions: DictionaryQualifierType[],\n segments: string[]\n ): boolean => {\n if (dimensions.length === 0) {\n chunks.push({\n cacheKey: `${key}.${locale}.${segments.join(COMPOSITE_ID_SEPARATOR)}`,\n loader: node as DynamicDictionaryLoader,\n });\n return true;\n }\n\n const [dimension, ...rest] = dimensions;\n const tree = node as QualifiedDynamicLoaderTree;\n\n if (dimension === 'item' && selector?.item === undefined) {\n // Open collection axis: fan out into every sibling chunk, ordered.\n for (const segment of Object.keys(tree).sort(\n (left, right) => Number(left) - Number(right)\n )) {\n walk(tree[segment]!, rest, [...segments, segment]);\n }\n return true;\n }\n\n const segment =\n dimension === 'variant'\n ? serializeVariant(selector?.variant)\n : String(selector?.item);\n\n const child = tree[segment];\n if (!child) return false;\n\n return walk(child, rest, [...segments, segment]);\n };\n\n const found = walk(localeTree, qualifierTypes, []);\n\n return { itemAxisOpen, missed: !found, chunks };\n};\n\n/**\n * Resolves the content of a qualified dynamic loader map against a selector,\n * loading only the chunk(s) the selector actually targets.\n *\n * Walks the nested loader tree one dimension at a time (canonical order\n * `variant → item`): `variant` defaults to `default` (or descends by the\n * serialized object identity), and `item` either narrows to the selected index\n * or — when no item is given — expands into every sibling chunk (the collection\n * axis). Semantics mirror {@link resolveQualifiedDictionary} so dynamic and\n * static modes behave alike.\n *\n * The Suspense mechanism is injected through `loadChunk` so the same logic\n * serves both the client (suspender cache) and the server (`react.use`). Every\n * targeted loader is started before the first chunk is read, so sibling chunks\n * load in parallel rather than waterfalling.\n *\n * @param loaderMap - The qualified dynamic loader map (entry point default export).\n * @param key - The dictionary key (used to build stable chunk cache keys).\n * @param locale - The resolved locale to load chunks for.\n * @param selector - The selector splitting the qualifier dimensions.\n * @param loadChunk - Reads a started chunk promise, suspending until it resolves.\n * @param transform - Turns a resolved chunk dictionary into final content.\n */\nexport const resolveQualifiedDynamicContent = <Content>(params: {\n loaderMap: QualifiedDynamicLoaderMap;\n key: string;\n locale: string;\n selector: DictionarySelector | undefined;\n loadChunk: (cacheKey: string, promise: Promise<Dictionary>) => Dictionary;\n transform: (dictionary: Dictionary) => Content;\n}): Content | Content[] | null => {\n const { loaderMap, key, locale, selector, loadChunk, transform } = params;\n\n const { itemAxisOpen, missed, chunks } = collectQualifiedChunks(\n loaderMap,\n key,\n locale,\n selector\n );\n\n if (missed) return itemAxisOpen ? [] : null;\n\n // Start every loader before reading, so siblings load in parallel.\n const dictionaries = chunks.map(({ cacheKey, loader }) =>\n loadChunk(cacheKey, loader())\n );\n\n if (itemAxisOpen) return dictionaries.map(transform);\n\n const [dictionary] = dictionaries;\n return dictionary ? transform(dictionary) : null;\n};\n\n/**\n * Async counterpart of {@link resolveQualifiedDynamicContent} for frameworks\n * that load dictionaries with `await` instead of Suspense (Vue, Svelte, Lit,\n * vanilla). Awaits every targeted chunk in parallel, then resolves identically.\n *\n * @param loaderMap - The qualified dynamic loader map.\n * @param key - The dictionary key (used to build stable chunk cache keys).\n * @param locale - The resolved locale to load chunks for.\n * @param selector - The selector splitting the qualifier dimensions.\n * @param transform - Turns a resolved chunk dictionary into final content.\n */\nexport const resolveQualifiedDynamicContentAsync = async <Content>(params: {\n loaderMap: QualifiedDynamicLoaderMap;\n key: string;\n locale: string;\n selector: DictionarySelector | undefined;\n transform: (dictionary: Dictionary) => Content;\n}): Promise<Content | Content[] | null> => {\n const { loaderMap, key, locale, selector, transform } = params;\n\n const { itemAxisOpen, missed, chunks } = collectQualifiedChunks(\n loaderMap,\n key,\n locale,\n selector\n );\n\n if (missed) return itemAxisOpen ? [] : null;\n\n const dictionaries = await Promise.all(chunks.map(({ loader }) => loader()));\n\n if (itemAxisOpen) return dictionaries.map(transform);\n\n const [dictionary] = dictionaries;\n return dictionary ? transform(dictionary) : null;\n};\n"],"mappings":"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"}
|