@intlayer/core 7.4.0 → 7.5.0-canary.1

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 (65) hide show
  1. package/dist/cjs/localization/getLocale.cjs +3 -4
  2. package/dist/cjs/localization/getLocale.cjs.map +1 -1
  3. package/dist/cjs/localization/getLocalizedUrl.cjs +2 -1
  4. package/dist/cjs/localization/getLocalizedUrl.cjs.map +1 -1
  5. package/dist/cjs/messageFormat/ICU.cjs +309 -0
  6. package/dist/cjs/messageFormat/ICU.cjs.map +1 -0
  7. package/dist/cjs/messageFormat/i18next.cjs +320 -0
  8. package/dist/cjs/messageFormat/i18next.cjs.map +1 -0
  9. package/dist/cjs/messageFormat/index.cjs +10 -0
  10. package/dist/cjs/messageFormat/verify-icu-format.cjs +65 -0
  11. package/dist/cjs/messageFormat/verify-icu-format.cjs.map +1 -0
  12. package/dist/cjs/messageFormat/vue-i18n.cjs +166 -0
  13. package/dist/cjs/messageFormat/vue-i18n.cjs.map +1 -0
  14. package/dist/esm/localization/getLocale.mjs +1 -2
  15. package/dist/esm/localization/getLocale.mjs.map +1 -1
  16. package/dist/esm/localization/getLocalizedUrl.mjs +2 -1
  17. package/dist/esm/localization/getLocalizedUrl.mjs.map +1 -1
  18. package/dist/esm/messageFormat/ICU.mjs +307 -0
  19. package/dist/esm/messageFormat/ICU.mjs.map +1 -0
  20. package/dist/esm/messageFormat/i18next.mjs +318 -0
  21. package/dist/esm/messageFormat/i18next.mjs.map +1 -0
  22. package/dist/esm/messageFormat/index.mjs +5 -0
  23. package/dist/esm/messageFormat/verify-icu-format.mjs +65 -0
  24. package/dist/esm/messageFormat/verify-icu-format.mjs.map +1 -0
  25. package/dist/esm/messageFormat/vue-i18n.mjs +164 -0
  26. package/dist/esm/messageFormat/vue-i18n.mjs.map +1 -0
  27. package/dist/types/deepTransformPlugins/getFilterMissingTranslationsContent.d.ts +8 -7
  28. package/dist/types/deepTransformPlugins/getFilterMissingTranslationsContent.d.ts.map +1 -1
  29. package/dist/types/deepTransformPlugins/getFilterTranslationsOnlyContent.d.ts +8 -7
  30. package/dist/types/deepTransformPlugins/getFilterTranslationsOnlyContent.d.ts.map +1 -1
  31. package/dist/types/deepTransformPlugins/getFilteredLocalesContent.d.ts +8 -7
  32. package/dist/types/deepTransformPlugins/getFilteredLocalesContent.d.ts.map +1 -1
  33. package/dist/types/dictionaryManipulator/orderDictionaries.d.ts +2 -2
  34. package/dist/types/interpreter/getContent/plugins.d.ts.map +1 -1
  35. package/dist/types/messageFormat/ICU.d.ts +11 -0
  36. package/dist/types/messageFormat/ICU.d.ts.map +1 -0
  37. package/dist/types/messageFormat/i18next.d.ts +9 -0
  38. package/dist/types/messageFormat/i18next.d.ts.map +1 -0
  39. package/dist/types/messageFormat/index.d.ts +4 -0
  40. package/dist/types/messageFormat/verify-icu-format.d.ts +1 -0
  41. package/dist/types/messageFormat/vue-i18n.d.ts +9 -0
  42. package/dist/types/messageFormat/vue-i18n.d.ts.map +1 -0
  43. package/package.json +14 -9
  44. package/dist/cjs/dist/esm/getStorageAttributes.cjs +0 -134
  45. package/dist/cjs/dist/esm/getStorageAttributes.cjs.map +0 -1
  46. package/dist/cjs/dist/esm/localization/localeResolver.cjs +0 -28
  47. package/dist/cjs/dist/esm/localization/localeResolver.cjs.map +0 -1
  48. package/dist/cjs/dist/esm/utils/getCookie.cjs +0 -32
  49. package/dist/cjs/dist/esm/utils/getCookie.cjs.map +0 -1
  50. package/dist/cjs/dist/esm/utils/localeStorage.cjs +0 -61
  51. package/dist/cjs/dist/esm/utils/localeStorage.cjs.map +0 -1
  52. package/dist/cjs/localization/isValidLocale.cjs +0 -54
  53. package/dist/cjs/localization/isValidLocale.cjs.map +0 -1
  54. package/dist/esm/dist/esm/getStorageAttributes.mjs +0 -133
  55. package/dist/esm/dist/esm/getStorageAttributes.mjs.map +0 -1
  56. package/dist/esm/dist/esm/localization/localeResolver.mjs +0 -26
  57. package/dist/esm/dist/esm/localization/localeResolver.mjs.map +0 -1
  58. package/dist/esm/dist/esm/utils/getCookie.mjs +0 -31
  59. package/dist/esm/dist/esm/utils/getCookie.mjs.map +0 -1
  60. package/dist/esm/dist/esm/utils/localeStorage.mjs +0 -59
  61. package/dist/esm/dist/esm/utils/localeStorage.mjs.map +0 -1
  62. package/dist/esm/localization/isValidLocale.mjs +0 -52
  63. package/dist/esm/localization/isValidLocale.mjs.map +0 -1
  64. package/dist/types/localization/isValidLocale.d.ts +0 -35
  65. package/dist/types/localization/isValidLocale.d.ts.map +0 -1
@@ -1,8 +1,7 @@
1
- import { localeResolver } from "../dist/esm/localization/localeResolver.mjs";
2
- import { getLocaleFromStorage } from "../dist/esm/utils/localeStorage.mjs";
3
1
  import { getPreferredLanguages } from "./localeDetector.mjs";
4
2
  import { DefaultValues } from "@intlayer/config/client";
5
3
  import configuration from "@intlayer/config/built";
4
+ import { getLocaleFromStorage, localeResolver } from "@intlayer/core";
6
5
 
7
6
  //#region src/localization/getLocale.ts
8
7
  const getLocale = async (ctx = {}) => {
@@ -1 +1 @@
1
- {"version":3,"file":"getLocale.mjs","names":[],"sources":["../../../src/localization/getLocale.ts"],"sourcesContent":["import configuration from '@intlayer/config/built';\nimport { DefaultValues } from '@intlayer/config/client';\nimport { getLocaleFromStorage, localeResolver } from '@intlayer/core';\nimport type { Locale } from '@intlayer/types';\nimport { getPreferredLanguages } from './localeDetector';\n\nexport type RequestContext = {\n getHeader?: (name: string) => string | null | undefined;\n getCookie?: (name: string) => string | null | undefined;\n};\n\nexport const getLocale = async (ctx: RequestContext = {}): Promise<Locale> => {\n const defaultLocale =\n configuration?.internationalization?.defaultLocale ??\n DefaultValues.Internationalization.DEFAULT_LOCALE;\n const availableLocales =\n configuration?.internationalization?.locales ??\n DefaultValues.Internationalization.LOCALES;\n\n // Try locale from storage (cookie or header)\n const storedLocale = getLocaleFromStorage({\n getCookie: ctx.getCookie,\n getHeader: ctx.getHeader,\n });\n\n if (storedLocale) {\n return storedLocale;\n }\n\n // Fallback to Accept-Language negotiation\n const acceptLanguageHeader = ctx.getHeader?.('accept-language');\n\n if (!acceptLanguageHeader) {\n return defaultLocale;\n }\n\n const preferredLocaleStrings = getPreferredLanguages(\n acceptLanguageHeader,\n availableLocales\n );\n\n const userFallbackLocale = localeResolver(\n preferredLocaleStrings,\n availableLocales,\n defaultLocale\n );\n\n if (userFallbackLocale) {\n return userFallbackLocale;\n }\n\n // Default locale\n return defaultLocale;\n};\n"],"mappings":";;;;;;;AAWA,MAAa,YAAY,OAAO,MAAsB,EAAE,KAAsB;CAC5E,MAAM,gBACJ,eAAe,sBAAsB,iBACrC,cAAc,qBAAqB;CACrC,MAAM,mBACJ,eAAe,sBAAsB,WACrC,cAAc,qBAAqB;CAGrC,MAAM,eAAe,qBAAqB;EACxC,WAAW,IAAI;EACf,WAAW,IAAI;EAChB,CAAC;AAEF,KAAI,aACF,QAAO;CAIT,MAAM,uBAAuB,IAAI,YAAY,kBAAkB;AAE/D,KAAI,CAAC,qBACH,QAAO;CAQT,MAAM,qBAAqB,eALI,sBAC7B,sBACA,iBACD,EAIC,kBACA,cACD;AAED,KAAI,mBACF,QAAO;AAIT,QAAO"}
1
+ {"version":3,"file":"getLocale.mjs","names":[],"sources":["../../../src/localization/getLocale.ts"],"sourcesContent":["import configuration from '@intlayer/config/built';\nimport { DefaultValues } from '@intlayer/config/client';\nimport { getLocaleFromStorage, localeResolver } from '@intlayer/core';\nimport type { Locale } from '@intlayer/types';\nimport { getPreferredLanguages } from './localeDetector';\n\nexport type RequestContext = {\n getHeader?: (name: string) => string | null | undefined;\n getCookie?: (name: string) => string | null | undefined;\n};\n\nexport const getLocale = async (ctx: RequestContext = {}): Promise<Locale> => {\n const defaultLocale =\n configuration?.internationalization?.defaultLocale ??\n DefaultValues.Internationalization.DEFAULT_LOCALE;\n const availableLocales =\n configuration?.internationalization?.locales ??\n DefaultValues.Internationalization.LOCALES;\n\n // Try locale from storage (cookie or header)\n const storedLocale = getLocaleFromStorage({\n getCookie: ctx.getCookie,\n getHeader: ctx.getHeader,\n });\n\n if (storedLocale) {\n return storedLocale;\n }\n\n // Fallback to Accept-Language negotiation\n const acceptLanguageHeader = ctx.getHeader?.('accept-language');\n\n if (!acceptLanguageHeader) {\n return defaultLocale;\n }\n\n const preferredLocaleStrings = getPreferredLanguages(\n acceptLanguageHeader,\n availableLocales\n );\n\n const userFallbackLocale = localeResolver(\n preferredLocaleStrings,\n availableLocales,\n defaultLocale\n );\n\n if (userFallbackLocale) {\n return userFallbackLocale;\n }\n\n // Default locale\n return defaultLocale;\n};\n"],"mappings":";;;;;;AAWA,MAAa,YAAY,OAAO,MAAsB,EAAE,KAAsB;CAC5E,MAAM,gBACJ,eAAe,sBAAsB,iBACrC,cAAc,qBAAqB;CACrC,MAAM,mBACJ,eAAe,sBAAsB,WACrC,cAAc,qBAAqB;CAGrC,MAAM,eAAe,qBAAqB;EACxC,WAAW,IAAI;EACf,WAAW,IAAI;EAChB,CAAC;AAEF,KAAI,aACF,QAAO;CAIT,MAAM,uBAAuB,IAAI,YAAY,kBAAkB;AAE/D,KAAI,CAAC,qBACH,QAAO;CAQT,MAAM,qBAAqB,eALI,sBAC7B,sBACA,iBACD,EAIC,kBACA,cACD;AAED,KAAI,mBACF,QAAO;AAIT,QAAO"}
@@ -63,7 +63,8 @@ const getLocalizedUrl = (url, currentLocale, options = {}) => {
63
63
  }
64
64
  const { prefix } = getPrefix(currentLocale, {
65
65
  defaultLocale,
66
- mode
66
+ mode,
67
+ locales
67
68
  });
68
69
  let localizedPath = `/${prefix}${parsedUrl.pathname}`;
69
70
  localizedPath = localizedPath.replaceAll(/\/+/g, "/");
@@ -1 +1 @@
1
- {"version":3,"file":"getLocalizedUrl.mjs","names":[],"sources":["../../../src/localization/getLocalizedUrl.ts"],"sourcesContent":["import configuration from '@intlayer/config/built';\nimport { DefaultValues } from '@intlayer/config/client';\nimport type { LocalesValues } from '@intlayer/types';\nimport { checkIsURLAbsolute } from '../utils/checkIsURLAbsolute';\nimport { getPathWithoutLocale } from './getPathWithoutLocale';\nimport { getPrefix } from './getPrefix';\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 * @returns The localized URL for the current locale.\n */\nexport const getLocalizedUrl = (\n url: string,\n currentLocale: LocalesValues,\n options: {\n locales?: LocalesValues[];\n defaultLocale?: LocalesValues;\n mode?: 'prefix-no-default' | 'prefix-all' | 'no-prefix' | 'search-params';\n } = {}\n): string => {\n const { defaultLocale, mode, locales } = {\n defaultLocale:\n configuration?.internationalization?.defaultLocale ??\n DefaultValues.Internationalization.DEFAULT_LOCALE,\n mode: configuration?.routing?.mode ?? DefaultValues.Routing.ROUTING_MODE,\n locales:\n configuration?.internationalization?.locales ??\n DefaultValues.Internationalization.LOCALES,\n ...options,\n };\n\n // Remove any existing locale segment from the URL\n const urlWithoutLocale = getPathWithoutLocale(url, locales);\n\n if (mode === 'no-prefix') {\n // No locale prefixing\n return urlWithoutLocale;\n }\n\n // Determine if the original URL is absolute (includes protocol)\n const isAbsoluteUrl = checkIsURLAbsolute(urlWithoutLocale);\n\n // Initialize a URL object if the URL is absolute\n // For relative URLs, use a dummy base to leverage the URL API\n const parsedUrl = isAbsoluteUrl\n ? new URL(urlWithoutLocale)\n : new URL(urlWithoutLocale, 'http://example.com');\n\n // Prepare the base URL (protocol + host) if it's absolute\n const baseUrl = isAbsoluteUrl\n ? `${parsedUrl.protocol}//${parsedUrl.host}`\n : '';\n\n if (mode === 'search-params') {\n // Use search parameters for locale handling\n const searchParams = new URLSearchParams(parsedUrl.search);\n searchParams.set('locale', currentLocale.toString());\n\n const queryString = searchParams.toString();\n const pathWithQuery = queryString\n ? `${parsedUrl.pathname}?${queryString}`\n : parsedUrl.pathname;\n\n if (isAbsoluteUrl) {\n return `${baseUrl}${pathWithQuery}${parsedUrl.hash}`;\n }\n\n return `${pathWithQuery}${parsedUrl.hash}`;\n }\n\n const { prefix } = getPrefix(currentLocale, {\n defaultLocale,\n mode,\n });\n\n // Construct the new pathname with or without the locale prefix\n let localizedPath = `/${prefix}${parsedUrl.pathname}`;\n\n // Remove double slashes\n localizedPath = localizedPath.replaceAll(/\\/+/g, '/');\n\n // Remove trailing slash for non-root paths\n if (localizedPath.length > 1 && localizedPath.endsWith('/')) {\n localizedPath = localizedPath.slice(0, -1);\n }\n\n // Combine with the base URL if the original URL was absolute\n if (isAbsoluteUrl) {\n return `${baseUrl}${localizedPath}${parsedUrl.search}${parsedUrl.hash}`;\n }\n\n return `${localizedPath}${parsedUrl.search}${parsedUrl.hash}`;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2CA,MAAa,mBACX,KACA,eACA,UAII,EAAE,KACK;CACX,MAAM,EAAE,eAAe,MAAM,YAAY;EACvC,eACE,eAAe,sBAAsB,iBACrC,cAAc,qBAAqB;EACrC,MAAM,eAAe,SAAS,QAAQ,cAAc,QAAQ;EAC5D,SACE,eAAe,sBAAsB,WACrC,cAAc,qBAAqB;EACrC,GAAG;EACJ;CAGD,MAAM,mBAAmB,qBAAqB,KAAK,QAAQ;AAE3D,KAAI,SAAS,YAEX,QAAO;CAIT,MAAM,gBAAgB,mBAAmB,iBAAiB;CAI1D,MAAM,YAAY,gBACd,IAAI,IAAI,iBAAiB,GACzB,IAAI,IAAI,kBAAkB,qBAAqB;CAGnD,MAAM,UAAU,gBACZ,GAAG,UAAU,SAAS,IAAI,UAAU,SACpC;AAEJ,KAAI,SAAS,iBAAiB;EAE5B,MAAM,eAAe,IAAI,gBAAgB,UAAU,OAAO;AAC1D,eAAa,IAAI,UAAU,cAAc,UAAU,CAAC;EAEpD,MAAM,cAAc,aAAa,UAAU;EAC3C,MAAM,gBAAgB,cAClB,GAAG,UAAU,SAAS,GAAG,gBACzB,UAAU;AAEd,MAAI,cACF,QAAO,GAAG,UAAU,gBAAgB,UAAU;AAGhD,SAAO,GAAG,gBAAgB,UAAU;;CAGtC,MAAM,EAAE,WAAW,UAAU,eAAe;EAC1C;EACA;EACD,CAAC;CAGF,IAAI,gBAAgB,IAAI,SAAS,UAAU;AAG3C,iBAAgB,cAAc,WAAW,QAAQ,IAAI;AAGrD,KAAI,cAAc,SAAS,KAAK,cAAc,SAAS,IAAI,CACzD,iBAAgB,cAAc,MAAM,GAAG,GAAG;AAI5C,KAAI,cACF,QAAO,GAAG,UAAU,gBAAgB,UAAU,SAAS,UAAU;AAGnE,QAAO,GAAG,gBAAgB,UAAU,SAAS,UAAU"}
1
+ {"version":3,"file":"getLocalizedUrl.mjs","names":[],"sources":["../../../src/localization/getLocalizedUrl.ts"],"sourcesContent":["import configuration from '@intlayer/config/built';\nimport { DefaultValues } from '@intlayer/config/client';\nimport type { LocalesValues } from '@intlayer/types';\nimport { checkIsURLAbsolute } from '../utils/checkIsURLAbsolute';\nimport { getPathWithoutLocale } from './getPathWithoutLocale';\nimport { getPrefix } from './getPrefix';\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 * @returns The localized URL for the current locale.\n */\nexport const getLocalizedUrl = (\n url: string,\n currentLocale: LocalesValues,\n options: {\n locales?: LocalesValues[];\n defaultLocale?: LocalesValues;\n mode?: 'prefix-no-default' | 'prefix-all' | 'no-prefix' | 'search-params';\n } = {}\n): string => {\n const { defaultLocale, mode, locales } = {\n defaultLocale:\n configuration?.internationalization?.defaultLocale ??\n DefaultValues.Internationalization.DEFAULT_LOCALE,\n mode: configuration?.routing?.mode ?? DefaultValues.Routing.ROUTING_MODE,\n locales:\n configuration?.internationalization?.locales ??\n DefaultValues.Internationalization.LOCALES,\n ...options,\n };\n\n // Remove any existing locale segment from the URL\n const urlWithoutLocale = getPathWithoutLocale(url, locales);\n\n if (mode === 'no-prefix') {\n // No locale prefixing\n return urlWithoutLocale;\n }\n\n // Determine if the original URL is absolute (includes protocol)\n const isAbsoluteUrl = checkIsURLAbsolute(urlWithoutLocale);\n\n // Initialize a URL object if the URL is absolute\n // For relative URLs, use a dummy base to leverage the URL API\n const parsedUrl = isAbsoluteUrl\n ? new URL(urlWithoutLocale)\n : new URL(urlWithoutLocale, 'http://example.com');\n\n // Prepare the base URL (protocol + host) if it's absolute\n const baseUrl = isAbsoluteUrl\n ? `${parsedUrl.protocol}//${parsedUrl.host}`\n : '';\n\n if (mode === 'search-params') {\n // Use search parameters for locale handling\n const searchParams = new URLSearchParams(parsedUrl.search);\n searchParams.set('locale', currentLocale.toString());\n\n const queryString = searchParams.toString();\n const pathWithQuery = queryString\n ? `${parsedUrl.pathname}?${queryString}`\n : parsedUrl.pathname;\n\n if (isAbsoluteUrl) {\n return `${baseUrl}${pathWithQuery}${parsedUrl.hash}`;\n }\n\n return `${pathWithQuery}${parsedUrl.hash}`;\n }\n\n const { prefix } = getPrefix(currentLocale, {\n defaultLocale,\n mode,\n locales,\n });\n\n // Construct the new pathname with or without the locale prefix\n let localizedPath = `/${prefix}${parsedUrl.pathname}`;\n\n // Remove double slashes\n localizedPath = localizedPath.replaceAll(/\\/+/g, '/');\n\n // Remove trailing slash for non-root paths\n if (localizedPath.length > 1 && localizedPath.endsWith('/')) {\n localizedPath = localizedPath.slice(0, -1);\n }\n\n // Combine with the base URL if the original URL was absolute\n if (isAbsoluteUrl) {\n return `${baseUrl}${localizedPath}${parsedUrl.search}${parsedUrl.hash}`;\n }\n\n return `${localizedPath}${parsedUrl.search}${parsedUrl.hash}`;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2CA,MAAa,mBACX,KACA,eACA,UAII,EAAE,KACK;CACX,MAAM,EAAE,eAAe,MAAM,YAAY;EACvC,eACE,eAAe,sBAAsB,iBACrC,cAAc,qBAAqB;EACrC,MAAM,eAAe,SAAS,QAAQ,cAAc,QAAQ;EAC5D,SACE,eAAe,sBAAsB,WACrC,cAAc,qBAAqB;EACrC,GAAG;EACJ;CAGD,MAAM,mBAAmB,qBAAqB,KAAK,QAAQ;AAE3D,KAAI,SAAS,YAEX,QAAO;CAIT,MAAM,gBAAgB,mBAAmB,iBAAiB;CAI1D,MAAM,YAAY,gBACd,IAAI,IAAI,iBAAiB,GACzB,IAAI,IAAI,kBAAkB,qBAAqB;CAGnD,MAAM,UAAU,gBACZ,GAAG,UAAU,SAAS,IAAI,UAAU,SACpC;AAEJ,KAAI,SAAS,iBAAiB;EAE5B,MAAM,eAAe,IAAI,gBAAgB,UAAU,OAAO;AAC1D,eAAa,IAAI,UAAU,cAAc,UAAU,CAAC;EAEpD,MAAM,cAAc,aAAa,UAAU;EAC3C,MAAM,gBAAgB,cAClB,GAAG,UAAU,SAAS,GAAG,gBACzB,UAAU;AAEd,MAAI,cACF,QAAO,GAAG,UAAU,gBAAgB,UAAU;AAGhD,SAAO,GAAG,gBAAgB,UAAU;;CAGtC,MAAM,EAAE,WAAW,UAAU,eAAe;EAC1C;EACA;EACA;EACD,CAAC;CAGF,IAAI,gBAAgB,IAAI,SAAS,UAAU;AAG3C,iBAAgB,cAAc,WAAW,QAAQ,IAAI;AAGrD,KAAI,cAAc,SAAS,KAAK,cAAc,SAAS,IAAI,CACzD,iBAAgB,cAAc,MAAM,GAAG,GAAG;AAI5C,KAAI,cACF,QAAO,GAAG,UAAU,gBAAgB,UAAU,SAAS,UAAU;AAGnE,QAAO,GAAG,gBAAgB,UAAU,SAAS,UAAU"}
@@ -0,0 +1,307 @@
1
+ import { deepTransformNode } from "../interpreter/getContent/deepTransform.mjs";
2
+ import { enu as enumeration } from "../transpiler/enumeration/enumeration.mjs";
3
+ import { gender } from "../transpiler/gender/gender.mjs";
4
+ import { insert as insertion } from "../transpiler/insertion/insertion.mjs";
5
+ import { NodeType } from "@intlayer/types";
6
+
7
+ //#region src/messageFormat/ICU.ts
8
+ const parseICU = (text) => {
9
+ let index = 0;
10
+ const parseNodes = () => {
11
+ const nodes = [];
12
+ let currentText = "";
13
+ while (index < text.length) {
14
+ const char = text[index];
15
+ if (char === "{") {
16
+ if (currentText) {
17
+ nodes.push(currentText);
18
+ currentText = "";
19
+ }
20
+ index++;
21
+ nodes.push(parseArgument());
22
+ } else if (char === "}") break;
23
+ else if (char === "'") if (index + 1 < text.length && text[index + 1] === "'") {
24
+ currentText += "'";
25
+ index += 2;
26
+ } else {
27
+ const nextQuote = text.indexOf("'", index + 1);
28
+ if (nextQuote !== -1) {
29
+ currentText += text.substring(index + 1, nextQuote);
30
+ index = nextQuote + 1;
31
+ } else {
32
+ currentText += "'";
33
+ index++;
34
+ }
35
+ }
36
+ else {
37
+ currentText += char;
38
+ index++;
39
+ }
40
+ }
41
+ if (currentText) nodes.push(currentText);
42
+ return nodes;
43
+ };
44
+ const parseArgument = () => {
45
+ let name = "";
46
+ while (index < text.length && /[^,}]/.test(text[index])) {
47
+ name += text[index];
48
+ index++;
49
+ }
50
+ name = name.trim();
51
+ if (index >= text.length) throw new Error("Unclosed argument");
52
+ if (text[index] === "}") {
53
+ index++;
54
+ return {
55
+ type: "argument",
56
+ name
57
+ };
58
+ }
59
+ if (text[index] === ",") {
60
+ index++;
61
+ let type = "";
62
+ while (index < text.length && /[^,}]/.test(text[index])) {
63
+ type += text[index];
64
+ index++;
65
+ }
66
+ type = type.trim();
67
+ if (index >= text.length) throw new Error("Unclosed argument");
68
+ if (text[index] === "}") {
69
+ index++;
70
+ return {
71
+ type: "argument",
72
+ name,
73
+ format: { type }
74
+ };
75
+ }
76
+ if (text[index] === ",") {
77
+ index++;
78
+ if (type === "plural" || type === "select") {
79
+ const options = {};
80
+ while (index < text.length && text[index] !== "}") {
81
+ while (index < text.length && /\s/.test(text[index])) index++;
82
+ let key = "";
83
+ while (index < text.length && /[^{\s]/.test(text[index])) {
84
+ key += text[index];
85
+ index++;
86
+ }
87
+ while (index < text.length && /\s/.test(text[index])) index++;
88
+ if (text[index] !== "{") throw new Error("Expected { after option key");
89
+ index++;
90
+ const value = parseNodes();
91
+ if (text[index] !== "}") throw new Error("Expected } after option value");
92
+ index++;
93
+ options[key] = value;
94
+ while (index < text.length && /\s/.test(text[index])) index++;
95
+ }
96
+ index++;
97
+ if (type === "plural") return {
98
+ type: "plural",
99
+ name,
100
+ options
101
+ };
102
+ else if (type === "select") return {
103
+ type: "select",
104
+ name,
105
+ options
106
+ };
107
+ } else {
108
+ let style = "";
109
+ while (index < text.length && text[index] !== "}") {
110
+ style += text[index];
111
+ index++;
112
+ }
113
+ if (index >= text.length) throw new Error("Unclosed argument");
114
+ style = style.trim();
115
+ index++;
116
+ return {
117
+ type: "argument",
118
+ name,
119
+ format: {
120
+ type,
121
+ style
122
+ }
123
+ };
124
+ }
125
+ }
126
+ }
127
+ throw new Error("Malformed argument");
128
+ };
129
+ return parseNodes();
130
+ };
131
+ const icuNodesToIntlayer = (nodes) => {
132
+ if (nodes.length === 0) return "";
133
+ if (nodes.length === 1 && typeof nodes[0] === "string") return nodes[0];
134
+ if (nodes.every((node) => typeof node === "string" || node.type === "argument")) {
135
+ let str = "";
136
+ for (const node of nodes) if (typeof node === "string") str += node;
137
+ else if (typeof node !== "string" && node.type === "argument") if (node.format) str += `{${node.name}, ${node.format.type}${node.format.style ? `, ${node.format.style}` : ""}}`;
138
+ else str += `{{${node.name}}}`;
139
+ return insertion(str);
140
+ }
141
+ if (nodes.length === 1) {
142
+ const node = nodes[0];
143
+ if (typeof node === "string") return node;
144
+ if (node.type === "argument") {
145
+ if (node.format) return insertion(`{${node.name}, ${node.format.type}${node.format.style ? `, ${node.format.style}` : ""}}`);
146
+ return insertion(`{{${node.name}}}`);
147
+ }
148
+ if (node.type === "plural") {
149
+ const options = {};
150
+ for (const [key, val] of Object.entries(node.options)) {
151
+ let newKey = key;
152
+ if (key.startsWith("=")) newKey = key.substring(1);
153
+ else if (key === "one") newKey = "1";
154
+ else if (key === "two") newKey = "2";
155
+ else if (key === "few") newKey = "<=3";
156
+ else if (key === "many") newKey = ">=4";
157
+ else if (key === "other") newKey = "fallback";
158
+ options[newKey] = icuNodesToIntlayer(val.map((v) => {
159
+ if (typeof v === "string") return v.replace(/#/g, `{{${node.name}}}`);
160
+ return v;
161
+ }));
162
+ }
163
+ options.__intlayer_icu_var = node.name;
164
+ return enumeration(options);
165
+ }
166
+ if (node.type === "select") {
167
+ const options = {};
168
+ for (const [key, val] of Object.entries(node.options)) options[key] = icuNodesToIntlayer(val);
169
+ const optionKeys = Object.keys(options);
170
+ if ((options.male || options.female) && optionKeys.every((k) => [
171
+ "male",
172
+ "female",
173
+ "other",
174
+ "fallback"
175
+ ].includes(k))) return gender({
176
+ fallback: options.other,
177
+ male: options.male,
178
+ female: options.female
179
+ });
180
+ options.__intlayer_icu_var = node.name;
181
+ return enumeration(options);
182
+ }
183
+ }
184
+ return nodes.map((node) => icuNodesToIntlayer([node]));
185
+ };
186
+ const icuToIntlayerPlugin = {
187
+ canHandle: (node) => typeof node === "string" && (node.includes("{") || node.includes("}")),
188
+ transform: (node) => {
189
+ try {
190
+ return icuNodesToIntlayer(parseICU(node));
191
+ } catch {
192
+ return node;
193
+ }
194
+ }
195
+ };
196
+ const intlayerToIcuPlugin = {
197
+ canHandle: (node) => typeof node === "string" && (node.includes("{") || node.includes("}")) || node && typeof node === "object" && (node.nodeType === NodeType.Insertion || node.nodeType === NodeType.Enumeration || node.nodeType === NodeType.Gender || node.nodeType === "composite") || Array.isArray(node),
198
+ transform: (node, props, next) => {
199
+ if (typeof node === "string") return node.replace(/\{\{([^}]+)\}\}/g, "{$1}");
200
+ if (node.nodeType === NodeType.Insertion) return node.insertion.replace(/\{\{([^}]+)\}\}/g, "{$1}");
201
+ if (node.nodeType === NodeType.Enumeration) {
202
+ const options = node.enumeration;
203
+ const transformedOptions = {};
204
+ for (const [key, val] of Object.entries(options)) {
205
+ if (key === "__intlayer_icu_var") continue;
206
+ const childVal = next(val, props);
207
+ transformedOptions[key] = typeof childVal === "string" ? childVal : JSON.stringify(childVal);
208
+ }
209
+ let varName = options.__intlayer_icu_var || "n";
210
+ if (!options.__intlayer_icu_var) {
211
+ const match = (transformedOptions.fallback || transformedOptions.other || Object.values(transformedOptions)[0]).match(/\{([a-zA-Z0-9_]+)\}(?!,)/);
212
+ if (match) varName = match[1];
213
+ }
214
+ const keys = Object.keys(transformedOptions);
215
+ const pluralKeys = [
216
+ "1",
217
+ "2",
218
+ "<=3",
219
+ ">=4",
220
+ "fallback",
221
+ "other",
222
+ "zero",
223
+ "one",
224
+ "two",
225
+ "few",
226
+ "many"
227
+ ];
228
+ const isPlural = keys.every((k) => pluralKeys.includes(k) || /^[<>=]?\d+(\.\d+)?$/.test(k));
229
+ const parts = [];
230
+ if (isPlural) {
231
+ for (const [key, val] of Object.entries(transformedOptions)) {
232
+ let icuKey = key;
233
+ if (key === "fallback") icuKey = "other";
234
+ else if (key === "<=3") icuKey = "few";
235
+ else if (key === ">=4") icuKey = "many";
236
+ else if (/^\d+$/.test(key)) icuKey = `=${key}`;
237
+ else if ([
238
+ "zero",
239
+ "few",
240
+ "many"
241
+ ].includes(key)) icuKey = key;
242
+ else icuKey = "other";
243
+ let strVal = val;
244
+ strVal = strVal.replace(new RegExp(`\\{${varName}\\}`, "g"), "#");
245
+ parts.push(`${icuKey} {${strVal}}`);
246
+ }
247
+ return `{${varName}, plural, ${parts.join(" ")}}`;
248
+ } else {
249
+ const entries = Object.entries(transformedOptions).sort(([keyA], [keyB]) => {
250
+ if (keyA === "fallback" || keyA === "other") return 1;
251
+ if (keyB === "fallback" || keyB === "other") return -1;
252
+ return 0;
253
+ });
254
+ for (const [key, val] of entries) {
255
+ let icuKey = key;
256
+ if (key === "fallback") icuKey = "other";
257
+ parts.push(`${icuKey} {${val}}`);
258
+ }
259
+ return `{${varName}, select, ${parts.join(" ")}}`;
260
+ }
261
+ }
262
+ if (node.nodeType === NodeType.Gender) {
263
+ const options = node.gender;
264
+ const varName = "gender";
265
+ const parts = [];
266
+ const entries = Object.entries(options).sort(([keyA], [keyB]) => {
267
+ if (keyA === "fallback") return 1;
268
+ if (keyB === "fallback") return -1;
269
+ return 0;
270
+ });
271
+ for (const [key, val] of entries) {
272
+ let icuKey = key;
273
+ if (key === "fallback") icuKey = "other";
274
+ const childVal = next(val, props);
275
+ const strVal = typeof childVal === "string" ? childVal : JSON.stringify(childVal);
276
+ parts.push(`${icuKey} {${strVal}}`);
277
+ }
278
+ return `{${varName}, select, ${parts.join(" ")}}`;
279
+ }
280
+ if (Array.isArray(node) || node.nodeType === "composite" && Array.isArray(node.composite)) return (Array.isArray(node) ? node : node.composite).map((item) => next(item, props)).join("");
281
+ return next(node, props);
282
+ }
283
+ };
284
+ const intlayerToICUFormatter = (message) => {
285
+ return deepTransformNode(message, {
286
+ dictionaryKey: "icu",
287
+ keyPath: [],
288
+ plugins: [{
289
+ id: "icu",
290
+ ...intlayerToIcuPlugin
291
+ }]
292
+ });
293
+ };
294
+ const icuToIntlayerFormatter = (message) => {
295
+ return deepTransformNode(message, {
296
+ dictionaryKey: "icu",
297
+ keyPath: [],
298
+ plugins: [{
299
+ id: "icu",
300
+ ...icuToIntlayerPlugin
301
+ }]
302
+ });
303
+ };
304
+
305
+ //#endregion
306
+ export { icuToIntlayerFormatter, intlayerToICUFormatter };
307
+ //# sourceMappingURL=ICU.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ICU.mjs","names":["nodes: ICUNode[]","options: Record<string, ICUNode[]>","insert","options: Record<string, any>","enu","transformedOptions: Record<string, string>"],"sources":["../../../src/messageFormat/ICU.ts"],"sourcesContent":["import { type Dictionary, NodeType } from '@intlayer/types';\nimport { deepTransformNode } from '../interpreter';\nimport { enu, gender, insert } from '../transpiler';\n\n/**\n * ICU MessageFormat Converter\n *\n * This module converts between ICU MessageFormat and Intlayer's internal format.\n *\n * IMPORTANT: Two different formats are used:\n *\n * 1. ICU MessageFormat (external format):\n * - Simple variables: {name}\n * - Formatted variables: {amount, number, currency}\n * - Plural: {count, plural, =0 {none} other {# items}}\n * - Select: {gender, select, male {He} female {She} other {They}}\n *\n * 2. Intlayer Internal Format:\n * - Simple variables: {{name}} (double braces for clarity and to distinguish from literal text)\n * - Formatted variables: {amount, number, currency} (keeps ICU format)\n * - Plural: enu({ 0: 'none', fallback: '{{count}} items' })\n * - Select/Gender: gender({ male: 'He', female: 'She', fallback: 'They' })\n *\n * Conversion flow:\n * - ICU → Intlayer: {name} → {{name}}\n * - Intlayer → ICU: {{name}} → {name}\n *\n * The double braces in Intlayer format serve to:\n * - Distinguish variables from literal text containing braces\n * - Work with getInsertion() runtime function which expects {{var}} patterns\n * - Provide clear visual distinction in content dictionaries\n */\n\n// Types for our AST\ntype ICUNode =\n | string\n | {\n type: 'argument';\n name: string;\n format?: { type: string; style?: string };\n }\n | { type: 'plural'; name: string; options: Record<string, ICUNode[]> }\n | { type: 'select'; name: string; options: Record<string, ICUNode[]> };\n\nexport type JsonValue =\n | string\n | number\n | boolean\n | null\n | JsonValue[]\n | { [key: string]: JsonValue };\n\nconst parseICU = (text: string): ICUNode[] => {\n let index = 0;\n\n const parseNodes = (): ICUNode[] => {\n const nodes: ICUNode[] = [];\n let currentText = '';\n\n while (index < text.length) {\n const char = text[index];\n\n if (char === '{') {\n if (currentText) {\n nodes.push(currentText);\n currentText = '';\n }\n index++; // skip {\n nodes.push(parseArgument());\n } else if (char === '}') {\n // End of current block\n break;\n } else if (char === \"'\") {\n // Escaping\n if (index + 1 < text.length && text[index + 1] === \"'\") {\n currentText += \"'\";\n index += 2;\n } else {\n // Find next quote\n const nextQuote = text.indexOf(\"'\", index + 1);\n if (nextQuote !== -1) {\n // Determine if this is escaping syntax characters\n // For simplicity, we'll treat content between single quotes as literal\n // provided it contains syntax chars.\n // Standard ICU: ' quoted string '\n // If it is just an apostrophe, it should be doubled.\n // But simplified: take content between quotes literally.\n currentText += text.substring(index + 1, nextQuote);\n index = nextQuote + 1;\n } else {\n currentText += \"'\";\n index++;\n }\n }\n } else {\n currentText += char;\n index++;\n }\n }\n\n if (currentText) {\n nodes.push(currentText);\n }\n return nodes;\n };\n\n const parseArgument = (): ICUNode => {\n // We are past '{'\n // Parse name\n let name = '';\n while (index < text.length && /[^,}]/.test(text[index])) {\n name += text[index];\n index++;\n }\n name = name.trim();\n\n if (index >= text.length) throw new Error('Unclosed argument');\n\n if (text[index] === '}') {\n index++;\n return { type: 'argument', name };\n }\n\n // Must be comma\n if (text[index] === ',') {\n index++;\n // Parse type\n let type = '';\n while (index < text.length && /[^,}]/.test(text[index])) {\n type += text[index];\n index++;\n }\n type = type.trim();\n\n if (index >= text.length) throw new Error('Unclosed argument');\n\n if (text[index] === '}') {\n index++;\n return { type: 'argument', name, format: { type } };\n }\n\n if (text[index] === ',') {\n index++;\n\n // If plural or select, parse options\n if (type === 'plural' || type === 'select') {\n // Parse options\n const options: Record<string, ICUNode[]> = {};\n\n while (index < text.length && text[index] !== '}') {\n // skip whitespace\n while (index < text.length && /\\s/.test(text[index])) index++;\n\n // parse key\n let key = '';\n while (index < text.length && /[^{\\s]/.test(text[index])) {\n key += text[index];\n index++;\n }\n\n while (index < text.length && /\\s/.test(text[index])) index++;\n\n if (text[index] !== '{')\n throw new Error('Expected { after option key');\n index++; // skip {\n\n const value = parseNodes();\n\n if (text[index] !== '}')\n throw new Error('Expected } after option value');\n index++; // skip }\n\n options[key] = value;\n\n while (index < text.length && /\\s/.test(text[index])) index++;\n }\n\n index++; // skip closing argument }\n\n if (type === 'plural') {\n return { type: 'plural', name, options };\n } else if (type === 'select') {\n return { type: 'select', name, options };\n }\n } else {\n // Parse style for number/date/time\n let style = '';\n while (index < text.length && text[index] !== '}') {\n style += text[index];\n index++;\n }\n if (index >= text.length) throw new Error('Unclosed argument');\n\n style = style.trim();\n index++; // skip }\n\n return { type: 'argument', name, format: { type, style } };\n }\n }\n }\n\n throw new Error('Malformed argument');\n };\n\n return parseNodes();\n};\n\nconst icuNodesToIntlayer = (nodes: ICUNode[]): any => {\n if (nodes.length === 0) return '';\n if (nodes.length === 1 && typeof nodes[0] === 'string') return nodes[0];\n\n // Check if we can flatten to a single string (insert)\n const canFlatten = nodes.every(\n (node) => typeof node === 'string' || node.type === 'argument'\n );\n if (canFlatten) {\n let str = '';\n for (const node of nodes) {\n if (typeof node === 'string') {\n str += node;\n } else if (typeof node !== 'string' && node.type === 'argument') {\n if (node.format) {\n // Formatted variables keep ICU format: {var, type, style}\n str += `{${node.name}, ${node.format.type}${\n node.format.style ? `, ${node.format.style}` : ''\n }}`;\n } else {\n // Simple variables use Intlayer format: {{var}}\n str += `{{${node.name}}}`;\n }\n }\n }\n return insert(str);\n }\n\n // Mix of string and complex types.\n // If we have just one complex type and it covers everything?\n if (nodes.length === 1) {\n const node = nodes[0];\n\n if (typeof node === 'string') return node; // already handled\n if (node.type === 'argument') {\n if (node.format) {\n // Formatted variables keep ICU format: {var, type, style}\n return insert(\n `{${node.name}, ${node.format.type}${\n node.format.style ? `, ${node.format.style}` : ''\n }}`\n );\n }\n // Simple variables use Intlayer format: {{var}}\n return insert(`{{${node.name}}}`);\n }\n if (node.type === 'plural') {\n const options: Record<string, any> = {};\n\n for (const [key, val] of Object.entries(node.options)) {\n // Map ICU keys to Intlayer keys\n let newKey = key;\n if (key.startsWith('=')) {\n newKey = key.substring(1); // =0 -> 0\n } else if (key === 'one') {\n newKey = '1';\n } else if (key === 'two') {\n newKey = '2';\n } else if (key === 'few') {\n newKey = '<=3';\n } else if (key === 'many') {\n newKey = '>=4';\n } else if (key === 'other') {\n newKey = 'fallback';\n }\n // Handle # in plural value\n // For plural, we need to pass the variable name down or replace #\n // Intlayer uses {{n}} (or whatever var name) for simple variables\n // We should replace # with {{n}} in the string parts of val\n const replacedVal = val.map((v) => {\n if (typeof v === 'string') {\n return v.replace(/#/g, `{{${node.name}}}`);\n }\n return v;\n });\n\n options[newKey] = icuNodesToIntlayer(replacedVal);\n }\n\n // Preserve variable name\n options.__intlayer_icu_var = node.name;\n\n return enu(options);\n }\n if (node.type === 'select') {\n const options: Record<string, any> = {};\n\n for (const [key, val] of Object.entries(node.options)) {\n options[key] = icuNodesToIntlayer(val);\n }\n\n // Check if it looks like gender\n const optionKeys = Object.keys(options);\n // It is gender if it has 'male' OR 'female' AND only contains gender keys (male, female, other)\n const isGender =\n (options.male || options.female) &&\n optionKeys.every((k) =>\n ['male', 'female', 'other', 'fallback'].includes(k)\n );\n\n if (isGender) {\n return gender({\n fallback: options.other,\n male: options.male,\n female: options.female,\n });\n }\n\n // Preserve variable name\n options.__intlayer_icu_var = node.name;\n\n return enu(options);\n }\n }\n\n // If multiple nodes, return array\n return nodes.map((node) => icuNodesToIntlayer([node]));\n};\n\nconst icuToIntlayerPlugin = {\n canHandle: (node: any) =>\n typeof node === 'string' && (node.includes('{') || node.includes('}')),\n transform: (node: any) => {\n try {\n const ast = parseICU(node);\n return icuNodesToIntlayer(ast);\n } catch {\n // If parsing fails, return original string\n return node;\n }\n },\n};\n\nconst intlayerToIcuPlugin = {\n canHandle: (node: any) =>\n (typeof node === 'string' && (node.includes('{') || node.includes('}'))) ||\n (node &&\n typeof node === 'object' &&\n (node.nodeType === NodeType.Insertion ||\n node.nodeType === NodeType.Enumeration ||\n node.nodeType === NodeType.Gender ||\n node.nodeType === 'composite')) ||\n Array.isArray(node),\n transform: (node: any, props: any, next: any) => {\n // Convert Intlayer's double-brace format {{var}} to ICU's single-brace format {var}\n if (typeof node === 'string') {\n return node.replace(/\\{\\{([^}]+)\\}\\}/g, '{$1}');\n }\n\n if (node.nodeType === NodeType.Insertion) {\n // Convert Intlayer format to ICU format:\n // - {{name}} → {name} (simple variable)\n // - {amount, number, currency} → {amount, number, currency} (formatted variable, already in ICU format)\n return node.insertion.replace(/\\{\\{([^}]+)\\}\\}/g, '{$1}');\n }\n\n if (node.nodeType === NodeType.Enumeration) {\n const options = node.enumeration;\n\n // Transform all values first\n const transformedOptions: Record<string, string> = {};\n for (const [key, val] of Object.entries(options)) {\n if (key === '__intlayer_icu_var') continue;\n const childVal = next(val, props);\n transformedOptions[key] =\n typeof childVal === 'string' ? childVal : JSON.stringify(childVal);\n }\n\n // Infer variable name\n let varName = options.__intlayer_icu_var || 'n';\n\n if (!options.__intlayer_icu_var) {\n const fallbackVal =\n transformedOptions.fallback ||\n transformedOptions.other ||\n Object.values(transformedOptions)[0];\n // Match {variable} but avoid {variable, ...} (which are nested ICUs)\n // Actually nested ICU starts with {var, ...\n // Simple variable is {var}\n // We look for {var} that is NOT followed by ,\n const match = fallbackVal.match(/\\{([a-zA-Z0-9_]+)\\}(?!,)/);\n if (match) {\n varName = match[1];\n }\n }\n\n const keys = Object.keys(transformedOptions);\n const pluralKeys = [\n '1',\n '2',\n '<=3',\n '>=4',\n 'fallback',\n 'other',\n 'zero',\n 'one',\n 'two',\n 'few',\n 'many',\n ];\n // Also check for numbers\n const isPlural = keys.every(\n (k) => pluralKeys.includes(k) || /^[<>=]?\\d+(\\.\\d+)?$/.test(k)\n );\n\n const parts = [];\n\n if (isPlural) {\n for (const [key, val] of Object.entries(transformedOptions)) {\n let icuKey = key;\n\n if (key === 'fallback') icuKey = 'other';\n else if (key === '<=3') icuKey = 'few';\n else if (key === '>=4') icuKey = 'many';\n else if (/^\\d+$/.test(key)) icuKey = `=${key}`;\n else if (['zero', 'few', 'many'].includes(key)) icuKey = key;\n else icuKey = 'other';\n\n let strVal = val;\n\n // Replace {varName} with #\n // Note: next() has already converted {{var}} -> {var}\n strVal = strVal.replace(new RegExp(`\\\\{${varName}\\\\}`, 'g'), '#');\n\n parts.push(`${icuKey} {${strVal}}`);\n }\n\n return `{${varName}, plural, ${parts.join(' ')}}`;\n } else {\n // Select\n const entries = Object.entries(transformedOptions).sort(\n ([keyA], [keyB]) => {\n if (keyA === 'fallback' || keyA === 'other') return 1;\n if (keyB === 'fallback' || keyB === 'other') return -1;\n return 0;\n }\n );\n\n for (const [key, val] of entries) {\n let icuKey = key;\n if (key === 'fallback') icuKey = 'other';\n // Do NOT map other keys to 'other'. Keep 'active', 'inactive', etc.\n\n parts.push(`${icuKey} {${val}}`);\n }\n return `{${varName}, select, ${parts.join(' ')}}`;\n }\n }\n\n if (node.nodeType === NodeType.Gender) {\n const options = node.gender;\n const varName = 'gender';\n const parts = [];\n\n const entries = Object.entries(options).sort(([keyA], [keyB]) => {\n if (keyA === 'fallback') return 1;\n if (keyB === 'fallback') return -1;\n return 0;\n });\n\n for (const [key, val] of entries) {\n let icuKey = key;\n if (key === 'fallback') icuKey = 'other';\n\n const childVal = next(val, props);\n const strVal =\n typeof childVal === 'string' ? childVal : JSON.stringify(childVal);\n\n parts.push(`${icuKey} {${strVal}}`);\n }\n return `{${varName}, select, ${parts.join(' ')}}`;\n }\n\n if (\n Array.isArray(node) ||\n (node.nodeType === 'composite' && Array.isArray(node.composite))\n ) {\n // handle array/composite\n const arr = Array.isArray(node) ? node : node.composite;\n const items = arr.map((item: any) => next(item, props));\n return items.join('');\n }\n\n return next(node, props);\n },\n};\n\nexport const intlayerToICUFormatter = (\n message: Dictionary['content']\n): JsonValue => {\n return deepTransformNode(message, {\n dictionaryKey: 'icu',\n keyPath: [],\n plugins: [{ id: 'icu', ...intlayerToIcuPlugin }],\n });\n};\n\nexport const icuToIntlayerFormatter = (\n message: Dictionary['content']\n): JsonValue => {\n return deepTransformNode(message, {\n dictionaryKey: 'icu',\n keyPath: [],\n plugins: [{ id: 'icu', ...icuToIntlayerPlugin }],\n });\n};\n"],"mappings":"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"}