{"version":3,"file":"createSyncPlugin.mjs","names":[],"sources":["../../../src/syncPluginKit/createSyncPlugin.ts"],"sourcesContent":["import { relative, resolve } from 'node:path';\nimport { colorizePath, getAppLogger } from '@intlayer/config/logger';\nimport type { Locale } from '@intlayer/types/allLocales';\nimport type { Dictionary, LocalDictionaryId } from '@intlayer/types/dictionary';\nimport type { Plugin } from '@intlayer/types/plugin';\nimport type {\n  CreateSyncPluginOptions,\n  SplitKeysMode,\n  SyncContent,\n  SyncPluginContext,\n} from './types';\n\n/** Separator between the dictionary segment and the key remainder of a flat id. */\nconst KEY_PREFIX_SEPARATOR = '.';\n\n/**\n * Groups a flat map of dotted ids by their first segment.\n *\n * `{ 'footer.github': 'GitHub', 'footer.contact': 'Contact', 'banner': '!' }`\n * becomes `{ footer: { github: 'GitHub', contact: 'Contact' }, banner: '!' }`.\n *\n * An id without a separator names a dictionary whose content is the value\n * itself — the shape the optimize pass binds with an empty key remainder.\n * Nested (non-flat) values are passed through under their own key, so a\n * catalog mixing both shapes still round-trips.\n */\nconst groupByKeyPrefix = (\n  content: SyncContent\n): Record<string, SyncContent | unknown> => {\n  const grouped: Record<string, SyncContent | unknown> = {};\n\n  for (const [id, value] of Object.entries(content)) {\n    const separatorIndex = id.indexOf(KEY_PREFIX_SEPARATOR);\n\n    if (separatorIndex === -1) {\n      grouped[id] = value;\n      continue;\n    }\n\n    const prefix = id.slice(0, separatorIndex);\n    const remainder = id.slice(separatorIndex + 1);\n    const bucket = grouped[prefix];\n\n    // A scalar already sits here (`'a'` seen before `'a.b'`): keep the scalar\n    // and leave the dotted id whole, rather than silently dropping either.\n    if (bucket !== undefined && typeof bucket !== 'object') {\n      grouped[id] = value;\n      continue;\n    }\n\n    grouped[prefix] = { ...((bucket ?? {}) as object), [remainder]: value };\n  }\n\n  return grouped;\n};\n\n/**\n * Inverse of {@link groupByKeyPrefix} for one dictionary: re-joins a grouped\n * bucket back into the flat dotted ids the source file stores.\n */\nconst flattenKeyPrefix = (\n  prefix: string,\n  content: unknown\n): Record<string, unknown> => {\n  if (\n    content === null ||\n    typeof content !== 'object' ||\n    Array.isArray(content)\n  ) {\n    // Dot-less id — the dictionary content *is* the message.\n    return { [prefix]: content };\n  }\n\n  const flattened: Record<string, unknown> = {};\n  for (const [key, value] of Object.entries(content)) {\n    flattened[`${prefix}${KEY_PREFIX_SEPARATOR}${key}`] = value;\n  }\n  return flattened;\n};\n\n/**\n * Content that carries nothing to persist: `undefined`, `null`, or an object\n * without any own key. Writing it back would erase the target.\n */\nconst isEmptyContent = (content: unknown): boolean =>\n  typeof content === 'undefined' ||\n  content === null ||\n  (typeof content === 'object' && Object.keys(content).length === 0);\n\n/**\n * Deep-clone formatted output into plain JSON data, stripping prototypes and\n * non-serializable values before the emptiness check and the write.\n */\nconst toPlainContent = (content: unknown): unknown =>\n  typeof content === 'undefined'\n    ? undefined\n    : JSON.parse(JSON.stringify(content));\n\n/**\n * Build an Intlayer {@link Plugin} from a transport adapter.\n *\n * The factory implements the three plugin hooks once — ingestion\n * (`loadDictionaries`), content-declaration reformatting (`formatOutput`) and\n * write-back (`afterBuild`) — so adapters only describe *where* content lives\n * (filesystem, TMS such as Crowdin, extra CMS…) and codecs only describe the\n * payload format. Write-back only processes dictionaries whose `location`\n * matches this plugin instance, so adapters never see foreign content.\n */\nexport const createSyncPlugin = (options: CreateSyncPluginOptions): Plugin => {\n  const {\n    name,\n    adapter,\n    direction = 'both',\n    location,\n    priority = 0,\n    format,\n    localeOverride,\n  } = options;\n\n  let splitKeysPromise: Promise<SplitKeysMode> | undefined;\n\n  /**\n   * `splitKeys` resolution is asynchronous (adapter auto-detection may parse\n   * an async source pattern), so it is resolved lazily and memoized.\n   */\n  const resolveSplitKeys = (): Promise<SplitKeysMode> => {\n    splitKeysPromise ??= (async () =>\n      options.splitKeys ?? (await adapter.detectSplitKeys?.()) ?? false)();\n\n    return splitKeysPromise;\n  };\n\n  const loadDictionaries: Plugin['loadDictionaries'] = async ({\n    configuration,\n  }) => {\n    const context: SyncPluginContext = { configuration };\n    const appLogger = getAppLogger(configuration);\n\n    const entries = await adapter.list(context);\n\n    if (entries.length === 0) {\n      const sourceDescription =\n        (await adapter.describeSource?.(context)) ?? name;\n\n      appLogger(\n        `[${name}] No dictionaries found at locations matching source pattern: ${colorizePath(sourceDescription)}`,\n        { level: 'warn' }\n      );\n    }\n\n    const shouldSplitByKeys = await resolveSplitKeys();\n    const { baseDir } = configuration.system;\n    const { defaultLocale } = configuration.internationalization;\n\n    // Sync plugins advertise the source pattern (with {{key}}/{{locale}}\n    // markers) as fill target; pull-only plugins fill each entry in place.\n    let patternFill: string | undefined;\n\n    if (direction === 'both' && adapter.getFillPattern) {\n      const fillPattern = await adapter.getFillPattern(context);\n      patternFill = relative(baseDir, resolve(baseDir, fillPattern));\n    }\n\n    const dictionaries: Dictionary[] = [];\n\n    for (const entry of entries) {\n      const content = (await adapter.read(entry, context)) ?? {};\n\n      const relativeFilePath = entry.filePath\n        ? relative(baseDir, entry.filePath)\n        : undefined;\n\n      const usedLocale = (localeOverride ?? entry.locale) as Locale;\n      const filled = usedLocale !== defaultLocale ? true : undefined;\n      const fill = patternFill ?? relativeFilePath;\n      const identifier = relativeFilePath ?? entry.uri;\n\n      // One entry groups several namespaces: emit one dictionary per\n      // namespace. `true` reads them from the first-level keys (`Hero`,\n      // `Nav`, …); `'key-prefix'` derives them from the first dot-segment of\n      // a flat catalog's dotted ids (`footer.github` → `footer`).\n      if (shouldSplitByKeys) {\n        const splitContent =\n          shouldSplitByKeys === 'key-prefix'\n            ? groupByKeyPrefix(content)\n            : content;\n\n        for (const [namespaceKey, namespaceContent] of Object.entries(\n          splitContent\n        )) {\n          dictionaries.push({\n            key: namespaceKey,\n            locale: usedLocale,\n            fill,\n            format,\n            localId:\n              `${namespaceKey}::${location}::${identifier}` as LocalDictionaryId,\n            location: location as Dictionary['location'],\n            filled,\n            content: namespaceContent as SyncContent,\n            filePath: relativeFilePath,\n            priority,\n          } as Dictionary);\n        }\n        continue;\n      }\n\n      dictionaries.push({\n        key: entry.key,\n        locale: usedLocale,\n        fill,\n        format,\n        localId:\n          `${entry.key}::${location}::${identifier}` as LocalDictionaryId,\n        location: location as Dictionary['location'],\n        filled,\n        content,\n        filePath: relativeFilePath,\n        priority,\n      } as Dictionary);\n    }\n\n    return dictionaries;\n  };\n\n  if (direction === 'pull') {\n    return { name, loadDictionaries };\n  }\n\n  const formatOutput: Plugin['formatOutput'] = async ({\n    dictionary,\n    configuration,\n  }) => {\n    if (!dictionary.filePath || !dictionary.locale) return dictionary;\n\n    // In split mode several namespaces share the same target; the target is\n    // re-assembled in `afterBuild`. Skip here to avoid overwriting the whole\n    // target with a single namespace.\n    if (await resolveSplitKeys()) return dictionary;\n\n    // Ownership check: only reformat declarations this adapter can identify\n    // as its own canonical target.\n    if (!adapter.resolveUri) return dictionary;\n\n    const canonicalUri = await adapter.resolveUri(\n      { key: dictionary.key, locale: dictionary.locale as Locale },\n      { configuration }\n    );\n\n    const { baseDir } = configuration.system;\n\n    if (\n      resolve(baseDir, canonicalUri) !== resolve(baseDir, dictionary.filePath)\n    ) {\n      return dictionary;\n    }\n\n    // Lazy import to keep the module graph light when configs are transpiled\n    const { formatDictionaryOutput } = await import('../formatDictionary');\n\n    return formatDictionaryOutput(dictionary as Dictionary, format).content;\n  };\n\n  const afterBuild: Plugin['afterBuild'] = async ({\n    dictionaries,\n    configuration,\n  }) => {\n    // Lazy imports to keep the module graph light when configs are transpiled\n    const { getPerLocaleDictionary } = await import('@intlayer/core/plugins');\n    const { formatDictionaryOutput } = await import('../formatDictionary');\n    const { parallelize } = await import('../utils/parallelize');\n\n    const context: SyncPluginContext = { configuration };\n    const { locales } = configuration.internationalization;\n\n    // Only ever hand the adapter dictionaries owned by THIS plugin instance.\n    const ownedDictionaries = Object.entries(dictionaries.mergedDictionaries)\n      .map(([key, result]) => ({\n        key,\n        dictionary: result.dictionary as Dictionary,\n      }))\n      .filter(({ dictionary }) => dictionary.location === location);\n\n    const splitMode = await resolveSplitKeys();\n\n    if (splitMode) {\n      // Split mode: every namespace dictionary writes back into the same\n      // per-locale target. Re-assemble them under their top-level key and\n      // write each target once, instead of one write per key (which would\n      // overwrite).\n      const mergedContentByLocale = {} as Record<Locale, SyncContent>;\n      const writeKeyByLocale = {} as Record<Locale, string>;\n\n      for (const { key, dictionary } of ownedDictionaries) {\n        for (const locale of locales) {\n          const localizedDictionary = getPerLocaleDictionary(\n            dictionary,\n            locale\n          );\n\n          const formattedOutput = formatDictionaryOutput(\n            localizedDictionary,\n            format\n          );\n\n          const content = toPlainContent(formattedOutput.content);\n\n          if (isEmptyContent(content)) continue;\n\n          mergedContentByLocale[locale] ??= {};\n\n          // `'key-prefix'` split the flat dotted ids apart on read; restore\n          // them so the source file keeps the shape its library expects,\n          // instead of gaining a nested object per prefix.\n          if (splitMode === 'key-prefix') {\n            Object.assign(\n              mergedContentByLocale[locale],\n              flattenKeyPrefix(key, content)\n            );\n          } else {\n            mergedContentByLocale[locale][key] = content;\n          }\n\n          writeKeyByLocale[locale] = key;\n        }\n      }\n\n      await parallelize(\n        Object.keys(mergedContentByLocale) as Locale[],\n        async (locale) => {\n          await adapter.write(\n            { key: writeKeyByLocale[locale], locale },\n            mergedContentByLocale[locale],\n            context\n          );\n        }\n      );\n\n      return;\n    }\n\n    const writeTasks = ownedDictionaries.flatMap(({ key, dictionary }) =>\n      locales.map((locale) => ({ key, dictionary, locale }))\n    );\n\n    await parallelize(writeTasks, async ({ key, dictionary, locale }) => {\n      const localizedDictionary = getPerLocaleDictionary(dictionary, locale);\n\n      const formattedOutput = formatDictionaryOutput(\n        localizedDictionary,\n        format\n      );\n\n      const content = toPlainContent(formattedOutput.content);\n\n      if (isEmptyContent(content)) return;\n\n      await adapter.write({ key, locale }, content as SyncContent, context);\n    });\n  };\n\n  return { name, loadDictionaries, formatOutput, afterBuild };\n};\n"],"mappings":"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"}