#
tokens: 46552/50000 8/626 files (page 13/20)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 13 of 20. Use http://codebase.md/lingodotdev/lingo.dev?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .changeset
│   ├── config.json
│   └── README.md
├── .claude
│   ├── agents
│   │   └── code-architect-reviewer.md
│   └── commands
│       ├── analyze-bucket-type.md
│       └── create-bucket-docs.md
├── .editorconfig
├── .github
│   ├── dependabot.yml
│   └── workflows
│       ├── docker.yml
│       ├── lingodotdev.yml
│       ├── pr-check.yml
│       ├── pr-lint.yml
│       └── release.yml
├── .gitignore
├── .husky
│   └── commit-msg
├── .npmrc
├── .prettierignore
├── .prettierrc
├── .vscode
│   ├── extensions.json
│   ├── launch.json
│   └── settings.json
├── action.yml
├── CLAUDE.md
├── CODE_OF_CONDUCT.md
├── commitlint.config.js
├── composer.json
├── content
│   ├── banner.compiler.png
│   ├── banner.dark.png
│   └── banner.launch.png
├── CONTRIBUTING.md
├── DEBUGGING.md
├── demo
│   ├── adonisjs
│   │   ├── .editorconfig
│   │   ├── .env.example
│   │   ├── .gitignore
│   │   ├── ace.js
│   │   ├── adonisrc.ts
│   │   ├── app
│   │   │   ├── exceptions
│   │   │   │   └── handler.ts
│   │   │   └── middleware
│   │   │       └── container_bindings_middleware.ts
│   │   ├── bin
│   │   │   ├── console.ts
│   │   │   ├── server.ts
│   │   │   └── test.ts
│   │   ├── CHANGELOG.md
│   │   ├── config
│   │   │   ├── app.ts
│   │   │   ├── bodyparser.ts
│   │   │   ├── cors.ts
│   │   │   ├── hash.ts
│   │   │   ├── inertia.ts
│   │   │   ├── logger.ts
│   │   │   ├── session.ts
│   │   │   ├── shield.ts
│   │   │   ├── static.ts
│   │   │   └── vite.ts
│   │   ├── eslint.config.js
│   │   ├── inertia
│   │   │   ├── app
│   │   │   │   ├── app.tsx
│   │   │   │   └── ssr.tsx
│   │   │   ├── css
│   │   │   │   └── app.css
│   │   │   ├── lingo
│   │   │   │   ├── dictionary.js
│   │   │   │   └── meta.json
│   │   │   ├── pages
│   │   │   │   ├── errors
│   │   │   │   │   ├── not_found.tsx
│   │   │   │   │   └── server_error.tsx
│   │   │   │   └── home.tsx
│   │   │   └── tsconfig.json
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── resources
│   │   │   └── views
│   │   │       └── inertia_layout.edge
│   │   ├── start
│   │   │   ├── env.ts
│   │   │   ├── kernel.ts
│   │   │   └── routes.ts
│   │   ├── tests
│   │   │   └── bootstrap.ts
│   │   ├── tsconfig.json
│   │   └── vite.config.ts
│   ├── next-app
│   │   ├── .gitignore
│   │   ├── CHANGELOG.md
│   │   ├── eslint.config.mjs
│   │   ├── next.config.ts
│   │   ├── package.json
│   │   ├── postcss.config.mjs
│   │   ├── public
│   │   │   ├── file.svg
│   │   │   ├── globe.svg
│   │   │   ├── next.svg
│   │   │   ├── vercel.svg
│   │   │   └── window.svg
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── app
│   │   │   │   ├── client-component.tsx
│   │   │   │   ├── favicon.ico
│   │   │   │   ├── globals.css
│   │   │   │   ├── layout.tsx
│   │   │   │   ├── lingo-dot-dev.tsx
│   │   │   │   ├── page.tsx
│   │   │   │   └── test
│   │   │   │       └── page.tsx
│   │   │   ├── components
│   │   │   │   ├── hero-actions.tsx
│   │   │   │   ├── hero-subtitle.tsx
│   │   │   │   ├── hero-title.tsx
│   │   │   │   └── index.ts
│   │   │   └── lingo
│   │   │       ├── dictionary.js
│   │   │       └── meta.json
│   │   └── tsconfig.json
│   ├── react-router-app
│   │   ├── .dockerignore
│   │   ├── .gitignore
│   │   ├── app
│   │   │   ├── app.css
│   │   │   ├── lingo
│   │   │   │   ├── dictionary.js
│   │   │   │   └── meta.json
│   │   │   ├── root.tsx
│   │   │   ├── routes
│   │   │   │   ├── home.tsx
│   │   │   │   └── test.tsx
│   │   │   ├── routes.ts
│   │   │   └── welcome
│   │   │       ├── lingo-dot-dev.tsx
│   │   │       ├── logo-dark.svg
│   │   │       ├── logo-light.svg
│   │   │       └── welcome.tsx
│   │   ├── Dockerfile
│   │   ├── package.json
│   │   ├── public
│   │   │   └── favicon.ico
│   │   ├── react-router.config.ts
│   │   ├── README.md
│   │   ├── tsconfig.json
│   │   └── vite.config.ts
│   └── vite-project
│       ├── .gitignore
│       ├── CHANGELOG.md
│       ├── eslint.config.js
│       ├── index.html
│       ├── package.json
│       ├── public
│       │   └── vite.svg
│       ├── README.md
│       ├── src
│       │   ├── App.css
│       │   ├── App.tsx
│       │   ├── assets
│       │   │   └── react.svg
│       │   ├── components
│       │   │   └── test.tsx
│       │   ├── index.css
│       │   ├── lingo
│       │   │   ├── dictionary.js
│       │   │   └── meta.json
│       │   ├── lingo-dot-dev.tsx
│       │   ├── main.tsx
│       │   └── vite-env.d.ts
│       ├── tsconfig.app.json
│       ├── tsconfig.json
│       ├── tsconfig.node.json
│       └── vite.config.ts
├── Dockerfile
├── i18n.json
├── i18n.lock
├── integrations
│   └── directus
│       ├── .gitignore
│       ├── CHANGELOG.md
│       ├── docker-compose.yml
│       ├── Dockerfile
│       ├── package.json
│       ├── README.md
│       ├── src
│       │   ├── api.ts
│       │   ├── app.ts
│       │   └── index.spec.ts
│       ├── tsconfig.json
│       ├── tsconfig.test.json
│       └── tsup.config.ts
├── ISSUE_TEMPLATE.md
├── legacy
│   ├── cli
│   │   ├── bin
│   │   │   └── cli.mjs
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   └── readme.md
│   └── sdk
│       ├── CHANGELOG.md
│       ├── index.d.ts
│       ├── index.js
│       ├── package.json
│       └── README.md
├── LICENSE.md
├── mcp.md
├── package.json
├── packages
│   ├── cli
│   │   ├── assets
│   │   │   ├── failure.mp3
│   │   │   └── success.mp3
│   │   ├── bin
│   │   │   └── cli.mjs
│   │   ├── CHANGELOG.md
│   │   ├── demo
│   │   │   ├── android
│   │   │   │   ├── en
│   │   │   │   │   └── example.xml
│   │   │   │   ├── es
│   │   │   │   │   └── example.xml
│   │   │   │   ├── i18n.json
│   │   │   │   └── i18n.lock
│   │   │   ├── csv
│   │   │   │   ├── example.csv
│   │   │   │   ├── i18n.json
│   │   │   │   └── i18n.lock
│   │   │   ├── demo.spec.ts
│   │   │   ├── ejs
│   │   │   │   ├── en
│   │   │   │   │   └── example.ejs
│   │   │   │   ├── es
│   │   │   │   │   └── example.ejs
│   │   │   │   ├── i18n.json
│   │   │   │   └── i18n.lock
│   │   │   ├── flutter
│   │   │   │   ├── en
│   │   │   │   │   └── example.arb
│   │   │   │   ├── es
│   │   │   │   │   └── example.arb
│   │   │   │   ├── i18n.json
│   │   │   │   └── i18n.lock
│   │   │   ├── html
│   │   │   │   ├── en
│   │   │   │   │   └── example.html
│   │   │   │   ├── es
│   │   │   │   │   └── example.html
│   │   │   │   ├── i18n.json
│   │   │   │   └── i18n.lock
│   │   │   ├── json
│   │   │   │   ├── en
│   │   │   │   │   └── example.json
│   │   │   │   ├── es
│   │   │   │   │   └── example.json
│   │   │   │   ├── i18n.json
│   │   │   │   └── i18n.lock
│   │   │   ├── json-dictionary
│   │   │   │   ├── example.json
│   │   │   │   ├── i18n.json
│   │   │   │   └── i18n.lock
│   │   │   ├── json5
│   │   │   │   ├── en
│   │   │   │   │   └── example.json5
│   │   │   │   ├── es
│   │   │   │   │   └── example.json5
│   │   │   │   ├── i18n.json
│   │   │   │   └── i18n.lock
│   │   │   ├── jsonc
│   │   │   │   ├── en
│   │   │   │   │   └── example.jsonc
│   │   │   │   ├── es
│   │   │   │   │   └── example.jsonc
│   │   │   │   ├── i18n.json
│   │   │   │   ├── i18n.lock
│   │   │   │   └── ru
│   │   │   │       └── example.jsonc
│   │   │   ├── markdoc
│   │   │   │   ├── en
│   │   │   │   │   └── example.markdoc
│   │   │   │   ├── es
│   │   │   │   │   └── example.markdoc
│   │   │   │   ├── i18n.json
│   │   │   │   └── i18n.lock
│   │   │   ├── markdown
│   │   │   │   ├── en
│   │   │   │   │   └── example.md
│   │   │   │   ├── es
│   │   │   │   │   └── example.md
│   │   │   │   ├── i18n.json
│   │   │   │   └── i18n.lock
│   │   │   ├── mdx
│   │   │   │   ├── en
│   │   │   │   │   └── example.mdx
│   │   │   │   ├── es
│   │   │   │   │   └── example.mdx
│   │   │   │   ├── i18n.json
│   │   │   │   └── i18n.lock
│   │   │   ├── php
│   │   │   │   ├── en
│   │   │   │   │   └── example.php
│   │   │   │   ├── es
│   │   │   │   │   └── example.php
│   │   │   │   ├── i18n.json
│   │   │   │   └── i18n.lock
│   │   │   ├── po
│   │   │   │   ├── en
│   │   │   │   │   └── example.po
│   │   │   │   ├── es
│   │   │   │   │   └── example.po
│   │   │   │   ├── i18n.json
│   │   │   │   └── i18n.lock
│   │   │   ├── properties
│   │   │   │   ├── en
│   │   │   │   │   └── example.properties
│   │   │   │   ├── es
│   │   │   │   │   └── example.properties
│   │   │   │   ├── i18n.json
│   │   │   │   └── i18n.lock
│   │   │   ├── run_i18n.sh
│   │   │   ├── srt
│   │   │   │   ├── en
│   │   │   │   │   └── example.srt
│   │   │   │   ├── es
│   │   │   │   │   └── example.srt
│   │   │   │   ├── i18n.json
│   │   │   │   └── i18n.lock
│   │   │   ├── txt
│   │   │   │   ├── en
│   │   │   │   │   └── example.txt
│   │   │   │   ├── es
│   │   │   │   │   └── example.txt
│   │   │   │   ├── i18n.json
│   │   │   │   └── i18n.lock
│   │   │   ├── typescript
│   │   │   │   ├── en
│   │   │   │   │   └── example.ts
│   │   │   │   ├── es
│   │   │   │   │   └── example.ts
│   │   │   │   ├── i18n.json
│   │   │   │   └── i18n.lock
│   │   │   ├── vtt
│   │   │   │   ├── en
│   │   │   │   │   └── example.vtt
│   │   │   │   ├── es
│   │   │   │   │   └── example.vtt
│   │   │   │   ├── i18n.json
│   │   │   │   └── i18n.lock
│   │   │   ├── vue-json
│   │   │   │   ├── example.vue
│   │   │   │   ├── i18n.json
│   │   │   │   └── i18n.lock
│   │   │   ├── xcode-strings
│   │   │   │   ├── en
│   │   │   │   │   └── example.strings
│   │   │   │   ├── es
│   │   │   │   │   └── example.strings
│   │   │   │   ├── i18n.json
│   │   │   │   └── i18n.lock
│   │   │   ├── xcode-stringsdict
│   │   │   │   ├── en
│   │   │   │   │   └── example.stringsdict
│   │   │   │   ├── es
│   │   │   │   │   └── example.stringsdict
│   │   │   │   ├── i18n.json
│   │   │   │   └── i18n.lock
│   │   │   ├── xcode-xcstrings
│   │   │   │   ├── example.xcstrings
│   │   │   │   ├── i18n.json
│   │   │   │   └── i18n.lock
│   │   │   ├── xcode-xcstrings-v2
│   │   │   │   ├── complex-example.xcstrings
│   │   │   │   ├── example.xcstrings
│   │   │   │   ├── i18n.json
│   │   │   │   └── i18n.lock
│   │   │   ├── xliff
│   │   │   │   ├── en
│   │   │   │   │   ├── example-v1.2.xliff
│   │   │   │   │   └── example-v2.xliff
│   │   │   │   ├── es
│   │   │   │   │   ├── example-v1.2.xliff
│   │   │   │   │   ├── example-v2.xliff
│   │   │   │   │   └── example.xliff
│   │   │   │   ├── i18n.json
│   │   │   │   └── i18n.lock
│   │   │   ├── xml
│   │   │   │   ├── en
│   │   │   │   │   └── example.xml
│   │   │   │   ├── es
│   │   │   │   │   └── example.xml
│   │   │   │   ├── i18n.json
│   │   │   │   └── i18n.lock
│   │   │   ├── yaml
│   │   │   │   ├── en
│   │   │   │   │   └── example.yml
│   │   │   │   ├── es
│   │   │   │   │   └── example.yml
│   │   │   │   ├── i18n.json
│   │   │   │   └── i18n.lock
│   │   │   └── yaml-root-key
│   │   │       ├── en
│   │   │       │   └── example.yml
│   │   │       ├── es
│   │   │       │   └── example.yml
│   │   │       ├── i18n.json
│   │   │       └── i18n.lock
│   │   ├── i18n.json
│   │   ├── i18n.lock
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── cli
│   │   │   │   ├── cmd
│   │   │   │   │   ├── auth.ts
│   │   │   │   │   ├── ci
│   │   │   │   │   │   ├── flows
│   │   │   │   │   │   │   ├── _base.ts
│   │   │   │   │   │   │   ├── in-branch.ts
│   │   │   │   │   │   │   └── pull-request.ts
│   │   │   │   │   │   ├── index.ts
│   │   │   │   │   │   └── platforms
│   │   │   │   │   │       ├── _base.ts
│   │   │   │   │   │       ├── bitbucket.ts
│   │   │   │   │   │       ├── github.ts
│   │   │   │   │   │       ├── gitlab.ts
│   │   │   │   │   │       └── index.ts
│   │   │   │   │   ├── cleanup.ts
│   │   │   │   │   ├── config
│   │   │   │   │   │   ├── get.ts
│   │   │   │   │   │   ├── index.ts
│   │   │   │   │   │   ├── set.ts
│   │   │   │   │   │   └── unset.ts
│   │   │   │   │   ├── i18n.ts
│   │   │   │   │   ├── init.ts
│   │   │   │   │   ├── lockfile.ts
│   │   │   │   │   ├── login.ts
│   │   │   │   │   ├── logout.ts
│   │   │   │   │   ├── may-the-fourth.ts
│   │   │   │   │   ├── mcp.ts
│   │   │   │   │   ├── purge.ts
│   │   │   │   │   ├── run
│   │   │   │   │   │   ├── _const.ts
│   │   │   │   │   │   ├── _types.ts
│   │   │   │   │   │   ├── _utils.ts
│   │   │   │   │   │   ├── execute.spec.ts
│   │   │   │   │   │   ├── execute.ts
│   │   │   │   │   │   ├── frozen.ts
│   │   │   │   │   │   ├── index.ts
│   │   │   │   │   │   ├── plan.ts
│   │   │   │   │   │   ├── setup.ts
│   │   │   │   │   │   └── watch.ts
│   │   │   │   │   ├── show
│   │   │   │   │   │   ├── _shared-key-command.ts
│   │   │   │   │   │   ├── config.ts
│   │   │   │   │   │   ├── files.ts
│   │   │   │   │   │   ├── ignored-keys.ts
│   │   │   │   │   │   ├── index.ts
│   │   │   │   │   │   ├── locale.ts
│   │   │   │   │   │   └── locked-keys.ts
│   │   │   │   │   └── status.ts
│   │   │   │   ├── constants.ts
│   │   │   │   ├── index.spec.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── loaders
│   │   │   │   │   ├── _types.ts
│   │   │   │   │   ├── _utils.ts
│   │   │   │   │   ├── android.spec.ts
│   │   │   │   │   ├── android.ts
│   │   │   │   │   ├── csv.spec.ts
│   │   │   │   │   ├── csv.ts
│   │   │   │   │   ├── dato
│   │   │   │   │   │   ├── _base.ts
│   │   │   │   │   │   ├── _utils.ts
│   │   │   │   │   │   ├── api.ts
│   │   │   │   │   │   ├── extract.ts
│   │   │   │   │   │   ├── filter.ts
│   │   │   │   │   │   └── index.ts
│   │   │   │   │   ├── ejs.spec.ts
│   │   │   │   │   ├── ejs.ts
│   │   │   │   │   ├── ensure-key-order.spec.ts
│   │   │   │   │   ├── ensure-key-order.ts
│   │   │   │   │   ├── flat.spec.ts
│   │   │   │   │   ├── flat.ts
│   │   │   │   │   ├── flutter.spec.ts
│   │   │   │   │   ├── flutter.ts
│   │   │   │   │   ├── formatters
│   │   │   │   │   │   ├── _base.ts
│   │   │   │   │   │   ├── biome.ts
│   │   │   │   │   │   ├── index.ts
│   │   │   │   │   │   └── prettier.ts
│   │   │   │   │   ├── html.ts
│   │   │   │   │   ├── icu-safety.spec.ts
│   │   │   │   │   ├── ignored-keys-buckets.spec.ts
│   │   │   │   │   ├── ignored-keys.spec.ts
│   │   │   │   │   ├── ignored-keys.ts
│   │   │   │   │   ├── index.spec.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── inject-locale.spec.ts
│   │   │   │   │   ├── inject-locale.ts
│   │   │   │   │   ├── json-dictionary.spec.ts
│   │   │   │   │   ├── json-dictionary.ts
│   │   │   │   │   ├── json-sorting.test.ts
│   │   │   │   │   ├── json-sorting.ts
│   │   │   │   │   ├── json.ts
│   │   │   │   │   ├── json5.spec.ts
│   │   │   │   │   ├── json5.ts
│   │   │   │   │   ├── jsonc.spec.ts
│   │   │   │   │   ├── jsonc.ts
│   │   │   │   │   ├── locked-keys.spec.ts
│   │   │   │   │   ├── locked-keys.ts
│   │   │   │   │   ├── locked-patterns.spec.ts
│   │   │   │   │   ├── locked-patterns.ts
│   │   │   │   │   ├── markdoc.spec.ts
│   │   │   │   │   ├── markdoc.ts
│   │   │   │   │   ├── markdown.ts
│   │   │   │   │   ├── mdx.spec.ts
│   │   │   │   │   ├── mdx.ts
│   │   │   │   │   ├── mdx2
│   │   │   │   │   │   ├── _types.ts
│   │   │   │   │   │   ├── _utils.ts
│   │   │   │   │   │   ├── code-placeholder.spec.ts
│   │   │   │   │   │   ├── code-placeholder.ts
│   │   │   │   │   │   ├── frontmatter-split.spec.ts
│   │   │   │   │   │   ├── frontmatter-split.ts
│   │   │   │   │   │   ├── localizable-document.spec.ts
│   │   │   │   │   │   ├── localizable-document.ts
│   │   │   │   │   │   ├── section-split.spec.ts
│   │   │   │   │   │   ├── section-split.ts
│   │   │   │   │   │   └── sections-split-2.ts
│   │   │   │   │   ├── passthrough.ts
│   │   │   │   │   ├── php.ts
│   │   │   │   │   ├── plutil-json-loader.ts
│   │   │   │   │   ├── po
│   │   │   │   │   │   ├── _types.ts
│   │   │   │   │   │   ├── index.spec.ts
│   │   │   │   │   │   └── index.ts
│   │   │   │   │   ├── properties.ts
│   │   │   │   │   ├── root-key.ts
│   │   │   │   │   ├── srt.ts
│   │   │   │   │   ├── sync.ts
│   │   │   │   │   ├── text-file.ts
│   │   │   │   │   ├── txt.ts
│   │   │   │   │   ├── typescript
│   │   │   │   │   │   ├── cjs-interop.ts
│   │   │   │   │   │   ├── index.spec.ts
│   │   │   │   │   │   └── index.ts
│   │   │   │   │   ├── unlocalizable.spec.ts
│   │   │   │   │   ├── unlocalizable.ts
│   │   │   │   │   ├── variable
│   │   │   │   │   │   ├── index.spec.ts
│   │   │   │   │   │   └── index.ts
│   │   │   │   │   ├── vtt.ts
│   │   │   │   │   ├── vue-json.ts
│   │   │   │   │   ├── xcode-strings
│   │   │   │   │   │   ├── escape.ts
│   │   │   │   │   │   ├── parser.ts
│   │   │   │   │   │   ├── tokenizer.ts
│   │   │   │   │   │   └── types.ts
│   │   │   │   │   ├── xcode-strings.spec.ts
│   │   │   │   │   ├── xcode-strings.ts
│   │   │   │   │   ├── xcode-stringsdict.ts
│   │   │   │   │   ├── xcode-xcstrings-icu.spec.ts
│   │   │   │   │   ├── xcode-xcstrings-icu.ts
│   │   │   │   │   ├── xcode-xcstrings-lock-compatibility.spec.ts
│   │   │   │   │   ├── xcode-xcstrings-v2-loader.ts
│   │   │   │   │   ├── xcode-xcstrings.spec.ts
│   │   │   │   │   ├── xcode-xcstrings.ts
│   │   │   │   │   ├── xliff.spec.ts
│   │   │   │   │   ├── xliff.ts
│   │   │   │   │   ├── xml.ts
│   │   │   │   │   └── yaml.ts
│   │   │   │   ├── localizer
│   │   │   │   │   ├── _types.ts
│   │   │   │   │   ├── explicit.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── lingodotdev.ts
│   │   │   │   ├── processor
│   │   │   │   │   ├── _base.ts
│   │   │   │   │   ├── basic.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── lingo.ts
│   │   │   │   └── utils
│   │   │   │       ├── auth.ts
│   │   │   │       ├── buckets.spec.ts
│   │   │   │       ├── buckets.ts
│   │   │   │       ├── cache.ts
│   │   │   │       ├── cloudflare-status.ts
│   │   │   │       ├── config.ts
│   │   │   │       ├── delta.spec.ts
│   │   │   │       ├── delta.ts
│   │   │   │       ├── ensure-patterns.ts
│   │   │   │       ├── errors.ts
│   │   │   │       ├── exec.spec.ts
│   │   │   │       ├── exec.ts
│   │   │   │       ├── exit-gracefully.spec.ts
│   │   │   │       ├── exit-gracefully.ts
│   │   │   │       ├── exp-backoff.ts
│   │   │   │       ├── find-locale-paths.spec.ts
│   │   │   │       ├── find-locale-paths.ts
│   │   │   │       ├── fs.ts
│   │   │   │       ├── init-ci-cd.ts
│   │   │   │       ├── key-matching.spec.ts
│   │   │   │       ├── key-matching.ts
│   │   │   │       ├── lockfile.ts
│   │   │   │       ├── md5.ts
│   │   │   │       ├── observability.ts
│   │   │   │       ├── plutil-formatter.spec.ts
│   │   │   │       ├── plutil-formatter.ts
│   │   │   │       ├── settings.ts
│   │   │   │       ├── ui.ts
│   │   │   │       └── update-gitignore.ts
│   │   │   ├── compiler
│   │   │   │   └── index.ts
│   │   │   ├── locale-codes
│   │   │   │   └── index.ts
│   │   │   ├── react
│   │   │   │   ├── client.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── react-router.ts
│   │   │   │   └── rsc.ts
│   │   │   ├── sdk
│   │   │   │   └── index.ts
│   │   │   └── spec
│   │   │       └── index.ts
│   │   ├── tests
│   │   │   └── mock-storage.ts
│   │   ├── troubleshooting.md
│   │   ├── tsconfig.json
│   │   ├── tsconfig.test.json
│   │   ├── tsup.config.ts
│   │   ├── types
│   │   │   ├── vtt.d.ts
│   │   │   └── xliff.d.ts
│   │   ├── vitest.config.ts
│   │   └── WATCH_MODE.md
│   ├── compiler
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── _base.ts
│   │   │   ├── _const.ts
│   │   │   ├── _loader-utils.spec.ts
│   │   │   ├── _loader-utils.ts
│   │   │   ├── _utils.spec.ts
│   │   │   ├── _utils.ts
│   │   │   ├── client-dictionary-loader.ts
│   │   │   ├── i18n-directive.spec.ts
│   │   │   ├── i18n-directive.ts
│   │   │   ├── index.spec.ts
│   │   │   ├── index.ts
│   │   │   ├── jsx-attribute-flag.spec.ts
│   │   │   ├── jsx-attribute-flag.ts
│   │   │   ├── jsx-attribute-scope-inject.spec.ts
│   │   │   ├── jsx-attribute-scope-inject.ts
│   │   │   ├── jsx-attribute-scopes-export.spec.ts
│   │   │   ├── jsx-attribute-scopes-export.ts
│   │   │   ├── jsx-attribute.spec.ts
│   │   │   ├── jsx-attribute.ts
│   │   │   ├── jsx-fragment.spec.ts
│   │   │   ├── jsx-fragment.ts
│   │   │   ├── jsx-html-lang.spec.ts
│   │   │   ├── jsx-html-lang.ts
│   │   │   ├── jsx-provider.spec.ts
│   │   │   ├── jsx-provider.ts
│   │   │   ├── jsx-remove-attributes.spec.ts
│   │   │   ├── jsx-remove-attributes.ts
│   │   │   ├── jsx-root-flag.spec.ts
│   │   │   ├── jsx-root-flag.ts
│   │   │   ├── jsx-scope-flag.spec.ts
│   │   │   ├── jsx-scope-flag.ts
│   │   │   ├── jsx-scope-inject.spec.ts
│   │   │   ├── jsx-scope-inject.ts
│   │   │   ├── jsx-scopes-export.spec.ts
│   │   │   ├── jsx-scopes-export.ts
│   │   │   ├── lib
│   │   │   │   └── lcp
│   │   │   │       ├── api
│   │   │   │       │   ├── index.ts
│   │   │   │       │   ├── prompt.spec.ts
│   │   │   │       │   ├── prompt.ts
│   │   │   │       │   ├── provider-details.spec.ts
│   │   │   │       │   ├── provider-details.ts
│   │   │   │       │   ├── shots.ts
│   │   │   │       │   ├── xml2obj.spec.ts
│   │   │   │       │   └── xml2obj.ts
│   │   │   │       ├── api.spec.ts
│   │   │   │       ├── cache.spec.ts
│   │   │   │       ├── cache.ts
│   │   │   │       ├── index.spec.ts
│   │   │   │       ├── index.ts
│   │   │   │       ├── schema.ts
│   │   │   │       ├── server.spec.ts
│   │   │   │       └── server.ts
│   │   │   ├── lingo-turbopack-loader.ts
│   │   │   ├── react-router-dictionary-loader.ts
│   │   │   ├── rsc-dictionary-loader.ts
│   │   │   └── utils
│   │   │       ├── ast-key.spec.ts
│   │   │       ├── ast-key.ts
│   │   │       ├── create-locale-import-map.spec.ts
│   │   │       ├── create-locale-import-map.ts
│   │   │       ├── env.spec.ts
│   │   │       ├── env.ts
│   │   │       ├── hash.spec.ts
│   │   │       ├── hash.ts
│   │   │       ├── index.spec.ts
│   │   │       ├── index.ts
│   │   │       ├── invokations.spec.ts
│   │   │       ├── invokations.ts
│   │   │       ├── jsx-attribute-scope.ts
│   │   │       ├── jsx-attribute.spec.ts
│   │   │       ├── jsx-attribute.ts
│   │   │       ├── jsx-content-whitespace.spec.ts
│   │   │       ├── jsx-content.spec.ts
│   │   │       ├── jsx-content.ts
│   │   │       ├── jsx-element.spec.ts
│   │   │       ├── jsx-element.ts
│   │   │       ├── jsx-expressions.test.ts
│   │   │       ├── jsx-expressions.ts
│   │   │       ├── jsx-functions.spec.ts
│   │   │       ├── jsx-functions.ts
│   │   │       ├── jsx-scope.spec.ts
│   │   │       ├── jsx-scope.ts
│   │   │       ├── jsx-variables.spec.ts
│   │   │       ├── jsx-variables.ts
│   │   │       ├── llm-api-key.ts
│   │   │       ├── llm-api-keys.spec.ts
│   │   │       ├── locales.spec.ts
│   │   │       ├── locales.ts
│   │   │       ├── module-params.spec.ts
│   │   │       ├── module-params.ts
│   │   │       ├── observability.spec.ts
│   │   │       ├── observability.ts
│   │   │       ├── rc.spec.ts
│   │   │       └── rc.ts
│   │   ├── tsconfig.json
│   │   ├── tsup.config.ts
│   │   └── vitest.config.ts
│   ├── locales
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── constants.ts
│   │   │   ├── index.ts
│   │   │   ├── names
│   │   │   │   ├── index.spec.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── integration.spec.ts
│   │   │   │   └── loader.ts
│   │   │   ├── parser.spec.ts
│   │   │   ├── parser.ts
│   │   │   ├── types.ts
│   │   │   ├── validation.spec.ts
│   │   │   └── validation.ts
│   │   ├── tsconfig.json
│   │   └── tsup.config.ts
│   ├── react
│   │   ├── build.config.ts
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── client
│   │   │   │   ├── attribute-component.spec.tsx
│   │   │   │   ├── attribute-component.tsx
│   │   │   │   ├── component.lingo-component.spec.tsx
│   │   │   │   ├── component.spec.tsx
│   │   │   │   ├── component.tsx
│   │   │   │   ├── context.spec.tsx
│   │   │   │   ├── context.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── loader.spec.ts
│   │   │   │   ├── loader.ts
│   │   │   │   ├── locale-switcher.spec.tsx
│   │   │   │   ├── locale-switcher.tsx
│   │   │   │   ├── locale.spec.ts
│   │   │   │   ├── locale.ts
│   │   │   │   ├── provider.spec.tsx
│   │   │   │   ├── provider.tsx
│   │   │   │   ├── utils.spec.ts
│   │   │   │   └── utils.ts
│   │   │   ├── core
│   │   │   │   ├── attribute-component.spec.tsx
│   │   │   │   ├── attribute-component.tsx
│   │   │   │   ├── component.spec.tsx
│   │   │   │   ├── component.tsx
│   │   │   │   ├── const.ts
│   │   │   │   ├── get-dictionary.spec.ts
│   │   │   │   ├── get-dictionary.ts
│   │   │   │   └── index.ts
│   │   │   ├── react-router
│   │   │   │   ├── index.ts
│   │   │   │   ├── loader.spec.ts
│   │   │   │   └── loader.ts
│   │   │   ├── rsc
│   │   │   │   ├── attribute-component.tsx
│   │   │   │   ├── component.lingo-component.spec.tsx
│   │   │   │   ├── component.spec.tsx
│   │   │   │   ├── component.tsx
│   │   │   │   ├── index.ts
│   │   │   │   ├── loader.spec.ts
│   │   │   │   ├── loader.ts
│   │   │   │   ├── provider.spec.tsx
│   │   │   │   ├── provider.tsx
│   │   │   │   ├── utils.spec.ts
│   │   │   │   └── utils.ts
│   │   │   └── test
│   │   │       └── setup.ts
│   │   ├── tsconfig.json
│   │   └── vitest.config.ts
│   ├── sdk
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── abort-controller.specs.ts
│   │   │   ├── index.spec.ts
│   │   │   └── index.ts
│   │   ├── tsconfig.json
│   │   ├── tsconfig.test.json
│   │   └── tsup.config.ts
│   └── spec
│       ├── CHANGELOG.md
│       ├── package.json
│       ├── README.md
│       ├── src
│       │   ├── config.spec.ts
│       │   ├── config.ts
│       │   ├── formats.ts
│       │   ├── index.spec.ts
│       │   ├── index.ts
│       │   ├── json-schema.ts
│       │   ├── locales.spec.ts
│       │   └── locales.ts
│       ├── tsconfig.json
│       ├── tsconfig.test.json
│       └── tsup.config.ts
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── readme
│   ├── ar.md
│   ├── bn.md
│   ├── de.md
│   ├── en.md
│   ├── es.md
│   ├── fa.md
│   ├── fr.md
│   ├── he.md
│   ├── hi.md
│   ├── it.md
│   ├── ja.md
│   ├── ko.md
│   ├── pl.md
│   ├── pt-BR.md
│   ├── ru.md
│   ├── tr.md
│   ├── uk-UA.md
│   └── zh-Hans.md
├── readme.md
├── scripts
│   ├── docs
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── generate-cli-docs.ts
│   │   │   ├── generate-config-docs.ts
│   │   │   ├── json-schema
│   │   │   │   ├── markdown-renderer.test.ts
│   │   │   │   ├── markdown-renderer.ts
│   │   │   │   ├── parser.test.ts
│   │   │   │   ├── parser.ts
│   │   │   │   └── types.ts
│   │   │   ├── utils.test.ts
│   │   │   └── utils.ts
│   │   ├── tsconfig.json
│   │   └── vitest.config.ts
│   └── packagist-publish.php
└── turbo.json
```

# Files

--------------------------------------------------------------------------------
/scripts/docs/src/json-schema/markdown-renderer.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, expect, it } from "vitest";
  2 | import type { RootContent } from "mdast";
  3 | import {
  4 |   makeHeadingNode,
  5 |   makeDescriptionNode,
  6 |   makeTypeBulletNode,
  7 |   makeRequiredBulletNode,
  8 |   makeDefaultBulletNode,
  9 |   makeEnumBulletNode,
 10 |   makeAllowedKeysBulletNode,
 11 |   makeBullets,
 12 |   renderPropertyToMarkdown,
 13 |   renderPropertiesToMarkdown,
 14 |   renderMarkdown,
 15 | } from "./markdown-renderer";
 16 | import type { PropertyInfo } from "./types";
 17 | 
 18 | describe("makeHeadingNode", () => {
 19 |   it("should create heading with correct depth for top-level property", () => {
 20 |     const node = makeHeadingNode("version");
 21 |     expect(node).toEqual({
 22 |       type: "heading",
 23 |       depth: 2,
 24 |       children: [{ type: "inlineCode", value: "version" }],
 25 |     });
 26 |   });
 27 | 
 28 |   it("should create deeper heading for nested property", () => {
 29 |     const node = makeHeadingNode("config.debug.level");
 30 |     expect(node).toEqual({
 31 |       type: "heading",
 32 |       depth: 4,
 33 |       children: [{ type: "inlineCode", value: "config.debug.level" }],
 34 |     });
 35 |   });
 36 | 
 37 |   it("should cap heading depth at 6", () => {
 38 |     const node = makeHeadingNode("a.b.c.d.e.f.g.h");
 39 |     expect(node).toEqual({
 40 |       type: "heading",
 41 |       depth: 6,
 42 |       children: [{ type: "inlineCode", value: "a.b.c.d.e.f.g.h" }],
 43 |     });
 44 |   });
 45 | });
 46 | 
 47 | describe("makeDescriptionNode", () => {
 48 |   it("should create paragraph node for description", () => {
 49 |     const node = makeDescriptionNode("This is a description");
 50 |     expect(node).toEqual({
 51 |       type: "paragraph",
 52 |       children: [{ type: "text", value: "This is a description" }],
 53 |     });
 54 |   });
 55 | 
 56 |   it("should return null for empty description", () => {
 57 |     expect(makeDescriptionNode("")).toBeNull();
 58 |     expect(makeDescriptionNode(undefined)).toBeNull();
 59 |   });
 60 | });
 61 | 
 62 | describe("makeTypeBulletNode", () => {
 63 |   it("should create list item with type information", () => {
 64 |     const node = makeTypeBulletNode("string");
 65 |     expect(node).toEqual({
 66 |       type: "listItem",
 67 |       children: [
 68 |         {
 69 |           type: "paragraph",
 70 |           children: [
 71 |             { type: "text", value: "Type: " },
 72 |             { type: "inlineCode", value: "string" },
 73 |           ],
 74 |         },
 75 |       ],
 76 |     });
 77 |   });
 78 | });
 79 | 
 80 | describe("makeRequiredBulletNode", () => {
 81 |   it("should create list item for required property", () => {
 82 |     const node = makeRequiredBulletNode(true);
 83 |     expect(node).toEqual({
 84 |       type: "listItem",
 85 |       children: [
 86 |         {
 87 |           type: "paragraph",
 88 |           children: [
 89 |             { type: "text", value: "Required: " },
 90 |             { type: "inlineCode", value: "yes" },
 91 |           ],
 92 |         },
 93 |       ],
 94 |     });
 95 |   });
 96 | 
 97 |   it("should create list item for optional property", () => {
 98 |     const node = makeRequiredBulletNode(false);
 99 |     expect(node).toEqual({
100 |       type: "listItem",
101 |       children: [
102 |         {
103 |           type: "paragraph",
104 |           children: [
105 |             { type: "text", value: "Required: " },
106 |             { type: "inlineCode", value: "no" },
107 |           ],
108 |         },
109 |       ],
110 |     });
111 |   });
112 | });
113 | 
114 | describe("makeDefaultBulletNode", () => {
115 |   it("should create list item for default value", () => {
116 |     const node = makeDefaultBulletNode("default value");
117 |     expect(node).toEqual({
118 |       type: "listItem",
119 |       children: [
120 |         {
121 |           type: "paragraph",
122 |           children: [
123 |             { type: "text", value: "Default: " },
124 |             { type: "inlineCode", value: '"default value"' },
125 |           ],
126 |         },
127 |       ],
128 |     });
129 |   });
130 | 
131 |   it("should handle numeric default", () => {
132 |     const node = makeDefaultBulletNode(42);
133 |     expect(node).toBeDefined();
134 |     if (
135 |       node &&
136 |       "children" in node &&
137 |       node.children[0] &&
138 |       "children" in node.children[0]
139 |     ) {
140 |       expect(node.children[0].children[1]).toEqual({
141 |         type: "inlineCode",
142 |         value: "42",
143 |       });
144 |     }
145 |   });
146 | 
147 |   it("should return null for undefined default", () => {
148 |     expect(makeDefaultBulletNode(undefined)).toBeNull();
149 |   });
150 | });
151 | 
152 | describe("makeEnumBulletNode", () => {
153 |   it("should create list item with enum values", () => {
154 |     const node = makeEnumBulletNode(["red", "green", "blue"]);
155 |     expect(node).toEqual({
156 |       type: "listItem",
157 |       children: [
158 |         {
159 |           type: "paragraph",
160 |           children: [{ type: "text", value: "Allowed values:" }],
161 |         },
162 |         {
163 |           type: "list",
164 |           ordered: false,
165 |           spread: false,
166 |           children: [
167 |             {
168 |               type: "listItem",
169 |               children: [
170 |                 {
171 |                   type: "paragraph",
172 |                   children: [{ type: "inlineCode", value: "red" }],
173 |                 },
174 |               ],
175 |             },
176 |             {
177 |               type: "listItem",
178 |               children: [
179 |                 {
180 |                   type: "paragraph",
181 |                   children: [{ type: "inlineCode", value: "green" }],
182 |                 },
183 |               ],
184 |             },
185 |             {
186 |               type: "listItem",
187 |               children: [
188 |                 {
189 |                   type: "paragraph",
190 |                   children: [{ type: "inlineCode", value: "blue" }],
191 |                 },
192 |               ],
193 |             },
194 |           ],
195 |         },
196 |       ],
197 |     });
198 |   });
199 | 
200 |   it("should return null for empty array", () => {
201 |     expect(makeEnumBulletNode([])).toBeNull();
202 |     expect(makeEnumBulletNode(undefined)).toBeNull();
203 |   });
204 | });
205 | 
206 | describe("makeAllowedKeysBulletNode", () => {
207 |   it("should create list item with allowed keys", () => {
208 |     const node = makeAllowedKeysBulletNode(["key1", "key2"]);
209 |     expect(node).toEqual({
210 |       type: "listItem",
211 |       children: [
212 |         {
213 |           type: "paragraph",
214 |           children: [{ type: "text", value: "Allowed keys:" }],
215 |         },
216 |         {
217 |           type: "list",
218 |           ordered: false,
219 |           spread: false,
220 |           children: [
221 |             {
222 |               type: "listItem",
223 |               children: [
224 |                 {
225 |                   type: "paragraph",
226 |                   children: [{ type: "inlineCode", value: "key1" }],
227 |                 },
228 |               ],
229 |             },
230 |             {
231 |               type: "listItem",
232 |               children: [
233 |                 {
234 |                   type: "paragraph",
235 |                   children: [{ type: "inlineCode", value: "key2" }],
236 |                 },
237 |               ],
238 |             },
239 |           ],
240 |         },
241 |       ],
242 |     });
243 |   });
244 | 
245 |   it("should return null for empty/undefined array", () => {
246 |     expect(makeAllowedKeysBulletNode([])).toBeNull();
247 |     expect(makeAllowedKeysBulletNode(undefined)).toBeNull();
248 |   });
249 | });
250 | 
251 | describe("makeBullets", () => {
252 |   it("should create all relevant bullets for a property", () => {
253 |     const property: PropertyInfo = {
254 |       name: "test",
255 |       fullPath: "test",
256 |       type: "string",
257 |       required: true,
258 |       defaultValue: "default",
259 |       allowedValues: ["a", "b"],
260 |       allowedKeys: ["key1"],
261 |     };
262 | 
263 |     const bullets = makeBullets(property);
264 |     expect(bullets).toHaveLength(5); // type, required, default, enum, allowedKeys
265 |   });
266 | 
267 |   it("should only create necessary bullets", () => {
268 |     const property: PropertyInfo = {
269 |       name: "test",
270 |       fullPath: "test",
271 |       type: "string",
272 |       required: false,
273 |     };
274 | 
275 |     const bullets = makeBullets(property);
276 |     expect(bullets).toHaveLength(2); // only type and required
277 |   });
278 | });
279 | 
280 | describe("renderPropertyToMarkdown", () => {
281 |   it("should render simple property", () => {
282 |     const property: PropertyInfo = {
283 |       name: "version",
284 |       fullPath: "version",
285 |       type: "string",
286 |       required: true,
287 |       description: "The version number",
288 |     };
289 | 
290 |     const nodes = renderPropertyToMarkdown(property);
291 |     expect(nodes).toHaveLength(3); // heading, description, bullets list
292 |     expect(nodes[0].type).toBe("heading");
293 |     expect(nodes[1].type).toBe("paragraph");
294 |     expect(nodes[2].type).toBe("list");
295 |   });
296 | 
297 |   it("should render property without description", () => {
298 |     const property: PropertyInfo = {
299 |       name: "test",
300 |       fullPath: "test",
301 |       type: "string",
302 |       required: false,
303 |     };
304 | 
305 |     const nodes = renderPropertyToMarkdown(property);
306 |     expect(nodes).toHaveLength(2); // heading, bullets list (no description)
307 |   });
308 | 
309 |   it("should render property with children", () => {
310 |     const property: PropertyInfo = {
311 |       name: "config",
312 |       fullPath: "config",
313 |       type: "object",
314 |       required: true,
315 |       children: [
316 |         {
317 |           name: "debug",
318 |           fullPath: "config.debug",
319 |           type: "boolean",
320 |           required: false,
321 |         },
322 |       ],
323 |     };
324 | 
325 |     const nodes = renderPropertyToMarkdown(property);
326 |     expect(nodes.length).toBeGreaterThan(2); // includes child nodes
327 | 
328 |     // Find child heading node
329 |     const childHeading = nodes.find(
330 |       (node: RootContent) =>
331 |         node.type === "heading" &&
332 |         node.type === "heading" &&
333 |         "children" in node &&
334 |         node.children[0] &&
335 |         "value" in node.children[0] &&
336 |         node.children[0].value === "config.debug",
337 |     );
338 |     expect(childHeading).toBeDefined();
339 |   });
340 | });
341 | 
342 | describe("renderPropertiesToMarkdown", () => {
343 |   it("should render complete document with header", () => {
344 |     const properties: PropertyInfo[] = [
345 |       {
346 |         name: "version",
347 |         fullPath: "version",
348 |         type: "string",
349 |         required: true,
350 |       },
351 |     ];
352 | 
353 |     const nodes = renderPropertiesToMarkdown(properties);
354 |     expect(nodes[0]).toEqual({
355 |       type: "paragraph",
356 |       children: [
357 |         {
358 |           type: "text",
359 |           value:
360 |             "This page describes the complete list of properties that are available within the ",
361 |         },
362 |         { type: "inlineCode", value: "i18n.json" },
363 |         {
364 |           type: "text",
365 |           value: " configuration file. This file is used by ",
366 |         },
367 |         {
368 |           type: "strong",
369 |           children: [{ type: "text", value: "Lingo.dev CLI" }],
370 |         },
371 |         {
372 |           type: "text",
373 |           value: " to configure the behavior of the translation pipeline.",
374 |         },
375 |       ],
376 |     });
377 |     expect(nodes[1].type).toBe("heading"); // version heading
378 |   });
379 | 
380 |   it("should add spacing between top-level properties", () => {
381 |     const properties: PropertyInfo[] = [
382 |       {
383 |         name: "prop1",
384 |         fullPath: "prop1",
385 |         type: "string",
386 |         required: true,
387 |       },
388 |       {
389 |         name: "prop2",
390 |         fullPath: "prop2",
391 |         type: "string",
392 |         required: false,
393 |       },
394 |     ];
395 | 
396 |     const nodes = renderPropertiesToMarkdown(properties);
397 |     // Should have spacing paragraphs between properties
398 |     const spacingNodes = nodes.filter(
399 |       (node: RootContent) =>
400 |         node.type === "paragraph" &&
401 |         "children" in node &&
402 |         node.children[0] &&
403 |         "value" in node.children[0] &&
404 |         node.children[0].value === "",
405 |     );
406 |     expect(spacingNodes).toHaveLength(2); // One after each property
407 |   });
408 | });
409 | 
410 | describe("renderMarkdown", () => {
411 |   it("should generate valid markdown string", () => {
412 |     const properties: PropertyInfo[] = [
413 |       {
414 |         name: "version",
415 |         fullPath: "version",
416 |         type: "string",
417 |         required: true,
418 |         description: "The version",
419 |       },
420 |     ];
421 | 
422 |     const markdown = renderMarkdown(properties);
423 |     expect(typeof markdown).toBe("string");
424 |     expect(markdown).toContain("---\ntitle: i18n.json properties\n---");
425 |     expect(markdown).toContain(
426 |       "This page describes the complete list of properties",
427 |     );
428 |     expect(markdown).toContain("## `version`");
429 |     expect(markdown).toContain("The version");
430 |     expect(markdown).toContain("* Type: `string`");
431 |     expect(markdown).toContain("* Required: `yes`");
432 |   });
433 | 
434 |   it("should handle empty properties array", () => {
435 |     const markdown = renderMarkdown([]);
436 |     expect(markdown).toContain("---\ntitle: i18n.json properties\n---");
437 |     expect(markdown).toContain("This page describes the complete list");
438 |   });
439 | });
440 | 
```

--------------------------------------------------------------------------------
/packages/cli/src/cli/loaders/xcode-xcstrings-icu.spec.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect } from "vitest";
  2 | import {
  3 |   xcstringsToPluralWithMeta,
  4 |   pluralWithMetaToXcstrings,
  5 |   type PluralWithMetadata,
  6 | } from "./xcode-xcstrings-icu";
  7 | 
  8 | describe("loaders/xcode-xcstrings-icu", () => {
  9 |   describe("xcstringsToPluralWithMeta", () => {
 10 |     it("should convert simple plural forms to ICU", () => {
 11 |       const input = {
 12 |         one: "1 item",
 13 |         other: "%d items",
 14 |       };
 15 | 
 16 |       const result = xcstringsToPluralWithMeta(input, "en");
 17 | 
 18 |       expect(result.icu).toBe("{count, plural, one {1 item} other {# items}}");
 19 |       expect(result._meta).toEqual({
 20 |         variables: {
 21 |           count: {
 22 |             format: "%d",
 23 |             role: "plural",
 24 |           },
 25 |         },
 26 |       });
 27 |     });
 28 | 
 29 |     it("should convert optional zero form to exact match =0 for English", () => {
 30 |       const input = {
 31 |         zero: "No items",
 32 |         one: "1 item",
 33 |         other: "%d items",
 34 |       };
 35 | 
 36 |       const result = xcstringsToPluralWithMeta(input, "en");
 37 | 
 38 |       // English required forms: one, other
 39 |       // "zero" is optional, so it becomes "=0"
 40 |       expect(result.icu).toBe(
 41 |         "{count, plural, =0 {No items} one {1 item} other {# items}}",
 42 |       );
 43 |       expect(result._meta?.variables.count.format).toBe("%d");
 44 |     });
 45 | 
 46 |     it("should convert optional zero form to exact match =0 for Russian", () => {
 47 |       const input = {
 48 |         zero: "Нет элементов",
 49 |         one: "1 элемент",
 50 |         few: "%d элемента",
 51 |         many: "%d элементов",
 52 |         other: "%d элемента",
 53 |       };
 54 | 
 55 |       const result = xcstringsToPluralWithMeta(input, "ru");
 56 | 
 57 |       // Russian required forms: one, few, many, other
 58 |       // "zero" is optional, so it becomes "=0"
 59 |       expect(result.icu).toBe(
 60 |         "{count, plural, =0 {Нет элементов} one {1 элемент} few {# элемента} many {# элементов} other {# элемента}}",
 61 |       );
 62 |       expect(result._meta?.variables.count.format).toBe("%d");
 63 |     });
 64 | 
 65 |     it("should preserve float format specifiers", () => {
 66 |       const input = {
 67 |         one: "%.1f mile",
 68 |         other: "%.1f miles",
 69 |       };
 70 | 
 71 |       const result = xcstringsToPluralWithMeta(input, "en");
 72 | 
 73 |       expect(result.icu).toBe("{count, plural, one {# mile} other {# miles}}");
 74 |       expect(result._meta).toEqual({
 75 |         variables: {
 76 |           count: {
 77 |             format: "%.1f",
 78 |             role: "plural",
 79 |           },
 80 |         },
 81 |       });
 82 |     });
 83 | 
 84 |     it("should preserve %lld format specifier", () => {
 85 |       const input = {
 86 |         one: "1 photo",
 87 |         other: "%lld photos",
 88 |       };
 89 | 
 90 |       const result = xcstringsToPluralWithMeta(input, "en");
 91 | 
 92 |       expect(result.icu).toBe(
 93 |         "{count, plural, one {1 photo} other {# photos}}",
 94 |       );
 95 |       expect(result._meta).toEqual({
 96 |         variables: {
 97 |           count: {
 98 |             format: "%lld",
 99 |             role: "plural",
100 |           },
101 |         },
102 |       });
103 |     });
104 | 
105 |     it("should handle multiple variables", () => {
106 |       const input = {
107 |         one: "%@ uploaded 1 photo",
108 |         other: "%@ uploaded %d photos",
109 |       };
110 | 
111 |       const result = xcstringsToPluralWithMeta(input, "en");
112 | 
113 |       expect(result.icu).toBe(
114 |         "{count, plural, one {{var0} uploaded 1 photo} other {{var0} uploaded # photos}}",
115 |       );
116 |       expect(result._meta).toEqual({
117 |         variables: {
118 |           var0: {
119 |             format: "%@",
120 |             role: "other",
121 |           },
122 |           count: {
123 |             format: "%d",
124 |             role: "plural",
125 |           },
126 |         },
127 |       });
128 |     });
129 | 
130 |     it("should handle three variables", () => {
131 |       const input = {
132 |         one: "%@ uploaded 1 photo to %@",
133 |         other: "%@ uploaded %d photos to %@",
134 |       };
135 | 
136 |       const result = xcstringsToPluralWithMeta(input, "en");
137 | 
138 |       // Note: This is a known limitation - when forms have different numbers of placeholders,
139 |       // the conversion may not be perfect. The "one" form has 2 placeholders but we map 3 variables.
140 |       // In practice, this edge case is rare as plural forms usually have consistent placeholder counts.
141 |       expect(result.icu).toContain("{var0} uploaded");
142 |       expect(result._meta?.variables).toEqual({
143 |         var0: { format: "%@", role: "other" },
144 |         count: { format: "%d", role: "plural" },
145 |         var1: { format: "%@", role: "other" },
146 |       });
147 |     });
148 | 
149 |     it("should handle %.2f precision", () => {
150 |       const input = {
151 |         one: "%.2f kilometer",
152 |         other: "%.2f kilometers",
153 |       };
154 | 
155 |       const result = xcstringsToPluralWithMeta(input, "en");
156 | 
157 |       expect(result.icu).toBe(
158 |         "{count, plural, one {# kilometer} other {# kilometers}}",
159 |       );
160 |       expect(result._meta?.variables.count.format).toBe("%.2f");
161 |     });
162 | 
163 |     it("should throw error for empty input", () => {
164 |       expect(() => xcstringsToPluralWithMeta({}, "en")).toThrow(
165 |         "pluralForms cannot be empty",
166 |       );
167 |     });
168 |   });
169 | 
170 |   describe("pluralWithMetaToXcstrings", () => {
171 |     it("should convert ICU back to xcstrings format", () => {
172 |       const input: PluralWithMetadata = {
173 |         icu: "{count, plural, one {1 item} other {# items}}",
174 |         _meta: {
175 |           variables: {
176 |             count: {
177 |               format: "%d",
178 |               role: "plural",
179 |             },
180 |           },
181 |         },
182 |       };
183 | 
184 |       const result = pluralWithMetaToXcstrings(input);
185 | 
186 |       expect(result).toEqual({
187 |         one: "1 item",
188 |         other: "%d items",
189 |       });
190 |     });
191 | 
192 |     it("should restore float format specifiers", () => {
193 |       const input: PluralWithMetadata = {
194 |         icu: "{count, plural, one {# mile} other {# miles}}",
195 |         _meta: {
196 |           variables: {
197 |             count: {
198 |               format: "%.1f",
199 |               role: "plural",
200 |             },
201 |           },
202 |         },
203 |       };
204 | 
205 |       const result = pluralWithMetaToXcstrings(input);
206 | 
207 |       expect(result).toEqual({
208 |         one: "%.1f mile",
209 |         other: "%.1f miles",
210 |       });
211 |     });
212 | 
213 |     it("should restore %lld format", () => {
214 |       const input: PluralWithMetadata = {
215 |         icu: "{count, plural, one {1 photo} other {# photos}}",
216 |         _meta: {
217 |           variables: {
218 |             count: {
219 |               format: "%lld",
220 |               role: "plural",
221 |             },
222 |           },
223 |         },
224 |       };
225 | 
226 |       const result = pluralWithMetaToXcstrings(input);
227 | 
228 |       expect(result).toEqual({
229 |         one: "1 photo",
230 |         other: "%lld photos",
231 |       });
232 |     });
233 | 
234 |     it("should handle multiple variables", () => {
235 |       const input: PluralWithMetadata = {
236 |         icu: "{count, plural, one {{userName} uploaded 1 photo} other {{userName} uploaded # photos}}",
237 |         _meta: {
238 |           variables: {
239 |             userName: { format: "%@", role: "other" },
240 |             count: { format: "%d", role: "plural" },
241 |           },
242 |         },
243 |       };
244 | 
245 |       const result = pluralWithMetaToXcstrings(input);
246 | 
247 |       expect(result).toEqual({
248 |         one: "%@ uploaded 1 photo",
249 |         other: "%@ uploaded %d photos",
250 |       });
251 |     });
252 | 
253 |     it("should convert exact match =0 back to zero form", () => {
254 |       const input: PluralWithMetadata = {
255 |         icu: "{count, plural, =0 {No items} one {1 item} other {# items}}",
256 |         _meta: {
257 |           variables: {
258 |             count: { format: "%d", role: "plural" },
259 |           },
260 |         },
261 |       };
262 | 
263 |       const result = pluralWithMetaToXcstrings(input);
264 | 
265 |       expect(result).toEqual({
266 |         zero: "No items",
267 |         one: "1 item",
268 |         other: "%d items",
269 |       });
270 |     });
271 | 
272 |     it("should use default format when metadata is missing", () => {
273 |       const input: PluralWithMetadata = {
274 |         icu: "{count, plural, one {1 item} other {# items}}",
275 |       };
276 | 
277 |       const result = pluralWithMetaToXcstrings(input);
278 | 
279 |       expect(result).toEqual({
280 |         one: "1 item",
281 |         other: "%lld items",
282 |       });
283 |     });
284 | 
285 |     it("should throw error for invalid ICU format", () => {
286 |       const input: PluralWithMetadata = {
287 |         icu: "not valid ICU",
288 |       };
289 | 
290 |       expect(() => pluralWithMetaToXcstrings(input)).toThrow();
291 |     });
292 |   });
293 | 
294 |   describe("round-trip conversion", () => {
295 |     it("should preserve format through round-trip", () => {
296 |       const original = {
297 |         one: "1 item",
298 |         other: "%d items",
299 |       };
300 | 
301 |       const icu = xcstringsToPluralWithMeta(original, "en");
302 |       const restored = pluralWithMetaToXcstrings(icu);
303 | 
304 |       expect(restored).toEqual(original);
305 |     });
306 | 
307 |     it("should preserve float precision through round-trip", () => {
308 |       const original = {
309 |         one: "%.2f mile",
310 |         other: "%.2f miles",
311 |       };
312 | 
313 |       const icu = xcstringsToPluralWithMeta(original, "en");
314 |       const restored = pluralWithMetaToXcstrings(icu);
315 | 
316 |       expect(restored).toEqual(original);
317 |     });
318 | 
319 |     it("should preserve multiple variables through round-trip", () => {
320 |       const original = {
321 |         one: "%@ uploaded 1 photo",
322 |         other: "%@ uploaded %d photos",
323 |       };
324 | 
325 |       const icu = xcstringsToPluralWithMeta(original, "en");
326 |       const restored = pluralWithMetaToXcstrings(icu);
327 | 
328 |       expect(restored).toEqual(original);
329 |     });
330 | 
331 |     it("should preserve zero form through round-trip", () => {
332 |       const original = {
333 |         zero: "No items",
334 |         one: "1 item",
335 |         other: "%lld items",
336 |       };
337 | 
338 |       const icu = xcstringsToPluralWithMeta(original, "en");
339 |       const restored = pluralWithMetaToXcstrings(icu);
340 | 
341 |       expect(restored).toEqual(original);
342 |     });
343 |   });
344 | 
345 |   describe("translation simulation", () => {
346 |     it("should handle English to Russian translation", () => {
347 |       // Source (English)
348 |       const englishForms = {
349 |         one: "1 item",
350 |         other: "%d items",
351 |       };
352 | 
353 |       const englishICU = xcstringsToPluralWithMeta(englishForms, "en");
354 | 
355 |       // Simulate backend translation (English → Russian)
356 |       // Backend expands 2 forms to 4 forms
357 |       const russianICU: PluralWithMetadata = {
358 |         icu: "{count, plural, one {# элемент} few {# элемента} many {# элементов} other {# элемента}}",
359 |         _meta: englishICU._meta, // Metadata preserved
360 |       };
361 | 
362 |       const russianForms = pluralWithMetaToXcstrings(russianICU);
363 | 
364 |       expect(russianForms).toEqual({
365 |         one: "%d элемент",
366 |         few: "%d элемента",
367 |         many: "%d элементов",
368 |         other: "%d элемента",
369 |       });
370 |     });
371 | 
372 |     it("should handle Chinese to Arabic translation", () => {
373 |       // Source (Chinese - no plurals)
374 |       const chineseForms = {
375 |         other: "%d 个项目",
376 |       };
377 | 
378 |       const chineseICU = xcstringsToPluralWithMeta(chineseForms, "zh");
379 | 
380 |       // Simulate backend translation (Chinese → Arabic)
381 |       // Backend expands 1 form to 6 forms
382 |       const arabicICU: PluralWithMetadata = {
383 |         icu: "{count, plural, zero {لا توجد مشاريع} one {مشروع واحد} two {مشروعان} few {# مشاريع} many {# مشروعًا} other {# مشروع}}",
384 |         _meta: chineseICU._meta,
385 |       };
386 | 
387 |       const arabicForms = pluralWithMetaToXcstrings(arabicICU);
388 | 
389 |       expect(arabicForms).toEqual({
390 |         zero: "لا توجد مشاريع",
391 |         one: "مشروع واحد",
392 |         two: "مشروعان",
393 |         few: "%d مشاريع",
394 |         many: "%d مشروعًا",
395 |         other: "%d مشروع",
396 |       });
397 |     });
398 | 
399 |     it("should handle variable reordering in translation", () => {
400 |       // Source (English)
401 |       const englishForms = {
402 |         one: "%@ uploaded 1 photo",
403 |         other: "%@ uploaded %d photos",
404 |       };
405 | 
406 |       const englishICU = xcstringsToPluralWithMeta(englishForms, "en");
407 | 
408 |       // Simulate backend translation with variable reordering
409 |       const russianICU: PluralWithMetadata = {
410 |         icu: "{count, plural, one {{var0} загрузил 1 фото} few {{var0} загрузил # фото} many {{var0} загрузил # фотографий} other {{var0} загрузил # фотографии}}",
411 |         _meta: englishICU._meta, // Metadata preserved
412 |       };
413 | 
414 |       const russianForms = pluralWithMetaToXcstrings(russianICU);
415 | 
416 |       expect(russianForms).toEqual({
417 |         one: "%@ загрузил 1 фото",
418 |         few: "%@ загрузил %d фото",
419 |         many: "%@ загрузил %d фотографий",
420 |         other: "%@ загрузил %d фотографии",
421 |       });
422 |     });
423 |   });
424 | });
425 | 
```

--------------------------------------------------------------------------------
/packages/cli/src/cli/cmd/run/execute.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import chalk from "chalk";
  2 | import { Listr, ListrTask } from "listr2";
  3 | import pLimit, { LimitFunction } from "p-limit";
  4 | import _ from "lodash";
  5 | import { minimatch } from "minimatch";
  6 | 
  7 | import { colors } from "../../constants";
  8 | import { CmdRunContext, CmdRunTask, CmdRunTaskResult } from "./_types";
  9 | import { commonTaskRendererOptions } from "./_const";
 10 | import createBucketLoader from "../../loaders";
 11 | import { createDeltaProcessor, Delta } from "../../utils/delta";
 12 | 
 13 | const MAX_WORKER_COUNT = 10;
 14 | 
 15 | export default async function execute(input: CmdRunContext) {
 16 |   const effectiveConcurrency = Math.min(
 17 |     input.flags.concurrency,
 18 |     input.tasks.length,
 19 |     MAX_WORKER_COUNT,
 20 |   );
 21 |   console.log(chalk.hex(colors.orange)(`[Localization]`));
 22 | 
 23 |   return new Listr<CmdRunContext>(
 24 |     [
 25 |       {
 26 |         title: "Initializing localization engine",
 27 |         task: async (ctx, task) => {
 28 |           task.title = `Localization engine ${chalk.hex(colors.green)(
 29 |             "ready",
 30 |           )} (${ctx.localizer!.id})`;
 31 |         },
 32 |       },
 33 |       {
 34 |         title: `Processing localization tasks ${chalk.dim(
 35 |           `(tasks: ${input.tasks.length}, concurrency: ${effectiveConcurrency})`,
 36 |         )}`,
 37 |         task: async (ctx, task) => {
 38 |           if (input.tasks.length < 1) {
 39 |             task.title = `Skipping, nothing to localize.`;
 40 |             task.skip();
 41 |             return;
 42 |           }
 43 | 
 44 |           // Preload checksums for all unique bucket path patterns before starting any workers
 45 |           const initialChecksumsMap = new Map<string, Record<string, string>>();
 46 |           const uniqueBucketPatterns = _.uniq(
 47 |             ctx.tasks.map((t) => t.bucketPathPattern),
 48 |           );
 49 |           for (const bucketPathPattern of uniqueBucketPatterns) {
 50 |             const deltaProcessor = createDeltaProcessor(bucketPathPattern);
 51 |             const checksums = await deltaProcessor.loadChecksums();
 52 |             initialChecksumsMap.set(bucketPathPattern, checksums);
 53 |           }
 54 | 
 55 |           const i18nLimiter = pLimit(effectiveConcurrency);
 56 |           const ioLimiter = pLimit(1);
 57 | 
 58 |           const perFileIoLimiters = new Map<string, LimitFunction>();
 59 |           const getFileIoLimiter = (
 60 |             bucketPathPattern: string,
 61 |           ): LimitFunction => {
 62 |             const lockKey = bucketPathPattern;
 63 | 
 64 |             if (!perFileIoLimiters.has(lockKey)) {
 65 |               perFileIoLimiters.set(lockKey, pLimit(1));
 66 |             }
 67 |             return perFileIoLimiters.get(lockKey)!;
 68 |           };
 69 | 
 70 |           const workersCount = effectiveConcurrency;
 71 | 
 72 |           const workerTasks: ListrTask[] = [];
 73 |           for (let i = 0; i < workersCount; i++) {
 74 |             const assignedTasks = ctx.tasks.filter(
 75 |               (_, idx) => idx % workersCount === i,
 76 |             );
 77 |             workerTasks.push(
 78 |               createWorkerTask({
 79 |                 ctx,
 80 |                 assignedTasks,
 81 |                 ioLimiter,
 82 |                 i18nLimiter,
 83 |                 initialChecksumsMap,
 84 |                 getFileIoLimiter,
 85 |                 onDone() {
 86 |                   task.title = createExecutionProgressMessage(ctx);
 87 |                 },
 88 |               }),
 89 |             );
 90 |           }
 91 | 
 92 |           return task.newListr(workerTasks, {
 93 |             concurrent: true,
 94 |             exitOnError: false,
 95 |             rendererOptions: {
 96 |               ...commonTaskRendererOptions,
 97 |               collapseSubtasks: true,
 98 |             },
 99 |           });
100 |         },
101 |       },
102 |     ],
103 |     {
104 |       exitOnError: false,
105 |       rendererOptions: commonTaskRendererOptions,
106 |     },
107 |   ).run(input);
108 | }
109 | 
110 | function createWorkerStatusMessage(args: {
111 |   assignedTask: CmdRunTask;
112 |   percentage: number;
113 | }) {
114 |   const displayPath = args.assignedTask.bucketPathPattern.replace(
115 |     "[locale]",
116 |     args.assignedTask.targetLocale,
117 |   );
118 |   return `[${chalk.hex(colors.yellow)(
119 |     `${args.percentage}%`,
120 |   )}] Processing: ${chalk.dim(displayPath)} (${chalk.hex(colors.yellow)(
121 |     args.assignedTask.sourceLocale,
122 |   )} -> ${chalk.hex(colors.yellow)(args.assignedTask.targetLocale)})`;
123 | }
124 | 
125 | function createExecutionProgressMessage(ctx: CmdRunContext) {
126 |   const succeededTasksCount = countTasks(
127 |     ctx,
128 |     (_t, result) => result.status === "success",
129 |   );
130 |   const failedTasksCount = countTasks(
131 |     ctx,
132 |     (_t, result) => result.status === "error",
133 |   );
134 |   const skippedTasksCount = countTasks(
135 |     ctx,
136 |     (_t, result) => result.status === "skipped",
137 |   );
138 | 
139 |   return `Processed ${chalk.green(succeededTasksCount)}/${
140 |     ctx.tasks.length
141 |   }, Failed ${chalk.red(failedTasksCount)}, Skipped ${chalk.dim(
142 |     skippedTasksCount,
143 |   )}`;
144 | }
145 | 
146 | function createLoaderForTask(assignedTask: CmdRunTask) {
147 |   const bucketLoader = createBucketLoader(
148 |     assignedTask.bucketType,
149 |     assignedTask.bucketPathPattern,
150 |     {
151 |       defaultLocale: assignedTask.sourceLocale,
152 |       injectLocale: assignedTask.injectLocale,
153 |       formatter: assignedTask.formatter,
154 |     },
155 |     assignedTask.lockedKeys,
156 |     assignedTask.lockedPatterns,
157 |     assignedTask.ignoredKeys,
158 |   );
159 |   bucketLoader.setDefaultLocale(assignedTask.sourceLocale);
160 | 
161 |   return bucketLoader;
162 | }
163 | 
164 | function createWorkerTask(args: {
165 |   ctx: CmdRunContext;
166 |   assignedTasks: CmdRunTask[];
167 |   ioLimiter: LimitFunction;
168 |   i18nLimiter: LimitFunction;
169 |   onDone: () => void;
170 |   initialChecksumsMap: Map<string, Record<string, string>>;
171 |   getFileIoLimiter: (bucketPathPattern: string) => LimitFunction;
172 | }): ListrTask {
173 |   return {
174 |     title: "Initializing...",
175 |     task: async (_subCtx: any, subTask: any) => {
176 |       for (const assignedTask of args.assignedTasks) {
177 |         subTask.title = createWorkerStatusMessage({
178 |           assignedTask,
179 |           percentage: 0,
180 |         });
181 |         const bucketLoader = createLoaderForTask(assignedTask);
182 |         const deltaProcessor = createDeltaProcessor(
183 |           assignedTask.bucketPathPattern,
184 |         );
185 | 
186 |         // Get initial checksums from the preloaded map
187 |         const initialChecksums =
188 |           args.initialChecksumsMap.get(assignedTask.bucketPathPattern) || {};
189 | 
190 |         const taskResult = await args.i18nLimiter(async () => {
191 |           try {
192 |             // Pull operations must be serialized per-file for single-file formats
193 |             // where multiple locales share the same file (e.g., xcode-xcstrings)
194 |             const fileIoLimiter = args.getFileIoLimiter(
195 |               assignedTask.bucketPathPattern,
196 |             );
197 |             const sourceData = await fileIoLimiter(async () =>
198 |               bucketLoader.pull(assignedTask.sourceLocale),
199 |             );
200 |             const hints = await fileIoLimiter(async () =>
201 |               bucketLoader.pullHints(),
202 |             );
203 |             const targetData = await fileIoLimiter(async () =>
204 |               bucketLoader.pull(assignedTask.targetLocale),
205 |             );
206 |             const delta = await deltaProcessor.calculateDelta({
207 |               sourceData,
208 |               targetData,
209 |               checksums: initialChecksums,
210 |             });
211 | 
212 |             const processableData = _.chain(sourceData)
213 |               .entries()
214 |               .filter(
215 |                 ([key, value]) =>
216 |                   delta.added.includes(key) ||
217 |                   delta.updated.includes(key) ||
218 |                   !!args.ctx.flags.force,
219 |               )
220 |               .filter(
221 |                 ([key]) =>
222 |                   !assignedTask.onlyKeys.length ||
223 |                   assignedTask.onlyKeys?.some((pattern) =>
224 |                     minimatch(key, pattern),
225 |                   ),
226 |               )
227 |               .fromPairs()
228 |               .value();
229 | 
230 |             if (!Object.keys(processableData).length) {
231 |               await fileIoLimiter(async () => {
232 |                 // re-push in case some of the unlocalizable / meta data changed
233 |                 await bucketLoader.push(assignedTask.targetLocale, targetData);
234 |               });
235 |               return {
236 |                 status: "skipped",
237 |                 pathPattern: assignedTask.bucketPathPattern,
238 |                 sourceLocale: assignedTask.sourceLocale,
239 |                 targetLocale: assignedTask.targetLocale,
240 |               } satisfies CmdRunTaskResult;
241 |             }
242 | 
243 |             const relevantHints = _.pick(hints, Object.keys(processableData));
244 |             const processedTargetData = await args.ctx.localizer!.localize(
245 |               {
246 |                 sourceLocale: assignedTask.sourceLocale,
247 |                 targetLocale: assignedTask.targetLocale,
248 |                 sourceData,
249 |                 targetData,
250 |                 processableData,
251 |                 hints: relevantHints,
252 |               },
253 |               async (progress, _sourceChunk, processedChunk) => {
254 |                 // write translated chunks as they are received from LLM
255 |                 await fileIoLimiter(async () => {
256 |                   // pull the latest source data before pushing for buckets that store all locales in a single file
257 |                   await bucketLoader.pull(assignedTask.sourceLocale);
258 |                   // pull the latest target data to include all already processed chunks
259 |                   const latestTargetData = await bucketLoader.pull(
260 |                     assignedTask.targetLocale,
261 |                   );
262 | 
263 |                   // add the new chunk to target data
264 |                   const _partialData = _.merge(
265 |                     {},
266 |                     latestTargetData,
267 |                     processedChunk,
268 |                   );
269 |                   // process renamed keys
270 |                   const finalChunkTargetData = processRenamedKeys(
271 |                     delta,
272 |                     _partialData,
273 |                   );
274 |                   // push final chunk to the target locale
275 |                   await bucketLoader.push(
276 |                     assignedTask.targetLocale,
277 |                     finalChunkTargetData,
278 |                   );
279 |                 });
280 | 
281 |                 subTask.title = createWorkerStatusMessage({
282 |                   assignedTask,
283 |                   percentage: progress,
284 |                 });
285 |               },
286 |             );
287 | 
288 |             const finalTargetData = _.merge(
289 |               {},
290 |               sourceData,
291 |               targetData,
292 |               processedTargetData,
293 |             );
294 |             const finalRenamedTargetData = processRenamedKeys(
295 |               delta,
296 |               finalTargetData,
297 |             );
298 | 
299 |             await fileIoLimiter(async () => {
300 |               // not all localizers have progress callback (eg. explicit localizer),
301 |               // the final target data might not be pushed yet - push now to ensure it's up to date
302 |               await bucketLoader.pull(assignedTask.sourceLocale);
303 |               await bucketLoader.push(
304 |                 assignedTask.targetLocale,
305 |                 finalRenamedTargetData,
306 |               );
307 | 
308 |               const checksums =
309 |                 await deltaProcessor.createChecksums(sourceData);
310 |               if (!args.ctx.flags.targetLocale?.length) {
311 |                 await deltaProcessor.saveChecksums(checksums);
312 |               }
313 |             });
314 | 
315 |             return {
316 |               status: "success",
317 |               pathPattern: assignedTask.bucketPathPattern,
318 |               sourceLocale: assignedTask.sourceLocale,
319 |               targetLocale: assignedTask.targetLocale,
320 |             } satisfies CmdRunTaskResult;
321 |           } catch (error) {
322 |             return {
323 |               status: "error",
324 |               error: error as Error,
325 |               pathPattern: assignedTask.bucketPathPattern,
326 |               sourceLocale: assignedTask.sourceLocale,
327 |               targetLocale: assignedTask.targetLocale,
328 |             } satisfies CmdRunTaskResult;
329 |           }
330 |         });
331 | 
332 |         args.ctx.results.set(assignedTask, taskResult);
333 |       }
334 | 
335 |       subTask.title = "Done";
336 |     },
337 |   };
338 | }
339 | 
340 | function countTasks(
341 |   ctx: CmdRunContext,
342 |   predicate: (task: CmdRunTask, result: CmdRunTaskResult) => boolean,
343 | ) {
344 |   return Array.from(ctx.results.entries()).filter(([task, result]) =>
345 |     predicate(task, result),
346 |   ).length;
347 | }
348 | 
349 | function processRenamedKeys(delta: Delta, targetData: Record<string, string>) {
350 |   return _.chain(targetData)
351 |     .entries()
352 |     .map(([key, value]) => {
353 |       const renaming = delta.renamed.find(([oldKey]) => oldKey === key);
354 |       if (!renaming) {
355 |         return [key, value];
356 |       }
357 |       return [renaming[1], value];
358 |     })
359 |     .fromPairs()
360 |     .value();
361 | }
362 | 
```

--------------------------------------------------------------------------------
/packages/spec/src/config.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import Z from "zod";
  2 | import { localeCodeSchema } from "./locales";
  3 | import { bucketTypeSchema } from "./formats";
  4 | 
  5 | // common
  6 | export const localeSchema = Z.object({
  7 |   source: localeCodeSchema.describe(
  8 |     "Primary source locale code of your content (e.g. 'en', 'en-US', 'pt_BR', or 'pt-rBR'). Must be one of the supported locale codes – either a short ISO-639 language code or a full locale identifier using '-', '_' or Android '-r' notation.",
  9 |   ),
 10 |   targets: Z.array(localeCodeSchema).describe(
 11 |     "List of target locale codes to translate to.",
 12 |   ),
 13 | }).describe("Locale configuration block.");
 14 | 
 15 | // factories
 16 | type ConfigDefinition<
 17 |   T extends Z.ZodRawShape,
 18 |   _P extends Z.ZodRawShape = any,
 19 | > = {
 20 |   schema: Z.ZodObject<T>;
 21 |   defaultValue: Z.infer<Z.ZodObject<T>>;
 22 |   parse: (rawConfig: unknown) => Z.infer<Z.ZodObject<T>>;
 23 | };
 24 | const createConfigDefinition = <
 25 |   T extends Z.ZodRawShape,
 26 |   _P extends Z.ZodRawShape = any,
 27 | >(
 28 |   definition: ConfigDefinition<T, _P>,
 29 | ) => definition;
 30 | 
 31 | type ConfigDefinitionExtensionParams<
 32 |   T extends Z.ZodRawShape,
 33 |   P extends Z.ZodRawShape,
 34 | > = {
 35 |   createSchema: (baseSchema: Z.ZodObject<P>) => Z.ZodObject<T>;
 36 |   createDefaultValue: (
 37 |     baseDefaultValue: Z.infer<Z.ZodObject<P>>,
 38 |   ) => Z.infer<Z.ZodObject<T>>;
 39 |   createUpgrader: (
 40 |     config: Z.infer<Z.ZodObject<P>>,
 41 |     schema: Z.ZodObject<T>,
 42 |     defaultValue: Z.infer<Z.ZodObject<T>>,
 43 |   ) => Z.infer<Z.ZodObject<T>>;
 44 | };
 45 | const extendConfigDefinition = <
 46 |   T extends Z.ZodRawShape,
 47 |   P extends Z.ZodRawShape,
 48 | >(
 49 |   definition: ConfigDefinition<P, any>,
 50 |   params: ConfigDefinitionExtensionParams<T, P>,
 51 | ) => {
 52 |   const schema = params.createSchema(definition.schema);
 53 |   const defaultValue = params.createDefaultValue(definition.defaultValue);
 54 |   const upgrader = (config: Z.infer<Z.ZodObject<P>>) =>
 55 |     params.createUpgrader(config, schema, defaultValue);
 56 | 
 57 |   return createConfigDefinition({
 58 |     schema,
 59 |     defaultValue,
 60 |     parse: (rawConfig) => {
 61 |       const safeResult = schema.safeParse(rawConfig);
 62 |       if (safeResult.success) {
 63 |         return safeResult.data;
 64 |       }
 65 | 
 66 |       const localeErrors = safeResult.error.errors
 67 |         .filter((issue) => issue.message.includes("Invalid locale code"))
 68 |         .map((issue) => {
 69 |           let unsupportedLocale = "";
 70 |           const path = issue.path;
 71 | 
 72 |           const config = rawConfig as { locale?: { [key: string]: any } };
 73 | 
 74 |           if (config.locale) {
 75 |             unsupportedLocale = path.reduce<any>((acc, key) => {
 76 |               if (acc && typeof acc === "object" && key in acc) {
 77 |                 return acc[key];
 78 |               }
 79 |               return acc;
 80 |             }, config.locale);
 81 |           }
 82 | 
 83 |           return `Unsupported locale: ${unsupportedLocale}`;
 84 |         });
 85 | 
 86 |       if (localeErrors.length > 0) {
 87 |         throw new Error(`\n${localeErrors.join("\n")}`);
 88 |       }
 89 | 
 90 |       const baseConfig = definition.parse(rawConfig);
 91 |       const result = upgrader(baseConfig);
 92 |       return result;
 93 |     },
 94 |   });
 95 | };
 96 | 
 97 | // any -> v0
 98 | const configV0Schema = Z.object({
 99 |   version: Z.union([Z.number(), Z.string()])
100 |     .default(0)
101 |     .describe("The version number of the schema."),
102 | });
103 | export const configV0Definition = createConfigDefinition({
104 |   schema: configV0Schema,
105 |   defaultValue: { version: 0 },
106 |   parse: (rawConfig) => {
107 |     return configV0Schema.parse(rawConfig);
108 |   },
109 | });
110 | 
111 | // v0 -> v1
112 | export const configV1Definition = extendConfigDefinition(configV0Definition, {
113 |   createSchema: (baseSchema) =>
114 |     baseSchema.extend({
115 |       locale: localeSchema,
116 |       buckets: Z.record(Z.string(), bucketTypeSchema)
117 |         .default({})
118 |         .describe(
119 |           "Mapping of source file paths (glob patterns) to bucket types.",
120 |         )
121 |         .optional(),
122 |     }),
123 |   createDefaultValue: () => ({
124 |     version: 1,
125 |     locale: {
126 |       source: "en" as const,
127 |       targets: ["es" as const],
128 |     },
129 |     buckets: {},
130 |   }),
131 |   createUpgrader: () => ({
132 |     version: 1,
133 |     locale: {
134 |       source: "en" as const,
135 |       targets: ["es" as const],
136 |     },
137 |     buckets: {},
138 |   }),
139 | });
140 | 
141 | // v1 -> v1.1
142 | export const configV1_1Definition = extendConfigDefinition(configV1Definition, {
143 |   createSchema: (baseSchema) =>
144 |     baseSchema.extend({
145 |       buckets: Z.record(
146 |         bucketTypeSchema,
147 |         Z.object({
148 |           include: Z.array(Z.string())
149 |             .default([])
150 |             .describe(
151 |               "File paths or glob patterns to include for this bucket.",
152 |             ),
153 |           exclude: Z.array(Z.string())
154 |             .default([])
155 |             .optional()
156 |             .describe(
157 |               "File paths or glob patterns to exclude from this bucket.",
158 |             ),
159 |         }),
160 |       ).default({}),
161 |     }),
162 |   createDefaultValue: (baseDefaultValue) => ({
163 |     ...baseDefaultValue,
164 |     version: 1.1,
165 |     buckets: {},
166 |   }),
167 |   createUpgrader: (oldConfig, schema) => {
168 |     const upgradedConfig: Z.infer<typeof schema> = {
169 |       ...oldConfig,
170 |       version: 1.1,
171 |       buckets: {},
172 |     };
173 | 
174 |     // Transform buckets from v1 to v1.1 format
175 |     if (oldConfig.buckets) {
176 |       for (const [bucketPath, bucketType] of Object.entries(
177 |         oldConfig.buckets,
178 |       )) {
179 |         if (!upgradedConfig.buckets[bucketType]) {
180 |           upgradedConfig.buckets[bucketType] = {
181 |             include: [],
182 |           };
183 |         }
184 |         upgradedConfig.buckets[bucketType]?.include.push(bucketPath);
185 |       }
186 |     }
187 | 
188 |     return upgradedConfig;
189 |   },
190 | });
191 | 
192 | // v1.1 -> v1.2
193 | // Changes: Add "extraSource" optional field to the locale node of the config
194 | export const configV1_2Definition = extendConfigDefinition(
195 |   configV1_1Definition,
196 |   {
197 |     createSchema: (baseSchema) =>
198 |       baseSchema.extend({
199 |         locale: localeSchema.extend({
200 |           extraSource: localeCodeSchema
201 |             .optional()
202 |             .describe(
203 |               "Optional extra source locale code used as fallback during translation.",
204 |             ),
205 |         }),
206 |       }),
207 |     createDefaultValue: (baseDefaultValue) => ({
208 |       ...baseDefaultValue,
209 |       version: 1.2,
210 |     }),
211 |     createUpgrader: (oldConfig) => ({
212 |       ...oldConfig,
213 |       version: 1.2,
214 |     }),
215 |   },
216 | );
217 | 
218 | // v1.2 -> v1.3
219 | // Changes: Support both string paths and {path, delimiter} objects in bucket include/exclude arrays
220 | export const bucketItemSchema = Z.object({
221 |   path: Z.string().describe("Path pattern containing a [locale] placeholder."),
222 |   delimiter: Z.union([Z.literal("-"), Z.literal("_"), Z.literal(null)])
223 |     .optional()
224 |     .describe(
225 |       "Delimiter that replaces the [locale] placeholder in the path (default: no delimiter).",
226 |     ),
227 | }).describe(
228 |   "Bucket path item. Either a string path or an object specifying path and delimiter.",
229 | );
230 | export type BucketItem = Z.infer<typeof bucketItemSchema>;
231 | 
232 | // Define a base bucket value schema that can be reused and extended
233 | export const bucketValueSchemaV1_3 = Z.object({
234 |   include: Z.array(Z.union([Z.string(), bucketItemSchema]))
235 |     .default([])
236 |     .describe("Glob patterns or bucket items to include for this bucket."),
237 |   exclude: Z.array(Z.union([Z.string(), bucketItemSchema]))
238 |     .default([])
239 |     .optional()
240 |     .describe("Glob patterns or bucket items to exclude from this bucket."),
241 |   injectLocale: Z.array(Z.string())
242 |     .optional()
243 |     .describe(
244 |       "Keys within files where the current locale should be injected or removed.",
245 |     ),
246 | }).describe("Configuration options for a translation bucket.");
247 | 
248 | export const configV1_3Definition = extendConfigDefinition(
249 |   configV1_2Definition,
250 |   {
251 |     createSchema: (baseSchema) =>
252 |       baseSchema.extend({
253 |         buckets: Z.record(bucketTypeSchema, bucketValueSchemaV1_3).default({}),
254 |       }),
255 |     createDefaultValue: (baseDefaultValue) => ({
256 |       ...baseDefaultValue,
257 |       version: 1.3,
258 |     }),
259 |     createUpgrader: (oldConfig) => ({
260 |       ...oldConfig,
261 |       version: 1.3,
262 |     }),
263 |   },
264 | );
265 | 
266 | const configSchema = "https://lingo.dev/schema/i18n.json";
267 | 
268 | // v1.3 -> v1.4
269 | // Changes: Add $schema to the config
270 | export const configV1_4Definition = extendConfigDefinition(
271 |   configV1_3Definition,
272 |   {
273 |     createSchema: (baseSchema) =>
274 |       baseSchema.extend({
275 |         $schema: Z.string().default(configSchema),
276 |       }),
277 |     createDefaultValue: (baseDefaultValue) => ({
278 |       ...baseDefaultValue,
279 |       version: 1.4,
280 |       $schema: configSchema,
281 |     }),
282 |     createUpgrader: (oldConfig) => ({
283 |       ...oldConfig,
284 |       version: 1.4,
285 |       $schema: configSchema,
286 |     }),
287 |   },
288 | );
289 | 
290 | // v1.4 -> v1.5
291 | // Changes: add "provider" field to the config
292 | const providerSchema = Z.object({
293 |   id: Z.enum([
294 |     "openai",
295 |     "anthropic",
296 |     "google",
297 |     "ollama",
298 |     "openrouter",
299 |     "mistral",
300 |   ]).describe("Identifier of the translation provider service."),
301 |   model: Z.string().describe("Model name to use for translations."),
302 |   prompt: Z.string().describe(
303 |     "Prompt template used when requesting translations.",
304 |   ),
305 |   baseUrl: Z.string()
306 |     .optional()
307 |     .describe("Custom base URL for the provider API (optional)."),
308 | }).describe("Configuration for the machine-translation provider.");
309 | export const configV1_5Definition = extendConfigDefinition(
310 |   configV1_4Definition,
311 |   {
312 |     createSchema: (baseSchema) =>
313 |       baseSchema.extend({
314 |         provider: providerSchema.optional(),
315 |       }),
316 |     createDefaultValue: (baseDefaultValue) => ({
317 |       ...baseDefaultValue,
318 |       version: 1.5,
319 |     }),
320 |     createUpgrader: (oldConfig) => ({
321 |       ...oldConfig,
322 |       version: 1.5,
323 |     }),
324 |   },
325 | );
326 | 
327 | // v1.5 -> v1.6
328 | // Changes: Add "lockedKeys" string array to bucket config
329 | export const bucketValueSchemaV1_6 = bucketValueSchemaV1_3.extend({
330 |   lockedKeys: Z.array(Z.string())
331 |     .default([])
332 |     .optional()
333 |     .describe(
334 |       "Keys that must remain unchanged and should never be overwritten by translations.",
335 |     ),
336 | });
337 | 
338 | export const configV1_6Definition = extendConfigDefinition(
339 |   configV1_5Definition,
340 |   {
341 |     createSchema: (baseSchema) =>
342 |       baseSchema.extend({
343 |         buckets: Z.record(bucketTypeSchema, bucketValueSchemaV1_6).default({}),
344 |       }),
345 |     createDefaultValue: (baseDefaultValue) => ({
346 |       ...baseDefaultValue,
347 |       version: 1.6,
348 |     }),
349 |     createUpgrader: (oldConfig) => ({
350 |       ...oldConfig,
351 |       version: 1.6,
352 |     }),
353 |   },
354 | );
355 | 
356 | // Changes: Add "lockedPatterns" string array of regex patterns to bucket config
357 | export const bucketValueSchemaV1_7 = bucketValueSchemaV1_6.extend({
358 |   lockedPatterns: Z.array(Z.string())
359 |     .default([])
360 |     .optional()
361 |     .describe(
362 |       "Regular expression patterns whose matched content should remain locked during translation.",
363 |     ),
364 | });
365 | 
366 | export const configV1_7Definition = extendConfigDefinition(
367 |   configV1_6Definition,
368 |   {
369 |     createSchema: (baseSchema) =>
370 |       baseSchema.extend({
371 |         buckets: Z.record(bucketTypeSchema, bucketValueSchemaV1_7).default({}),
372 |       }),
373 |     createDefaultValue: (baseDefaultValue) => ({
374 |       ...baseDefaultValue,
375 |       version: 1.7,
376 |     }),
377 |     createUpgrader: (oldConfig) => ({
378 |       ...oldConfig,
379 |       version: 1.7,
380 |     }),
381 |   },
382 | );
383 | 
384 | // v1.7 -> v1.8
385 | // Changes: Add "ignoredKeys" string array to bucket config
386 | export const bucketValueSchemaV1_8 = bucketValueSchemaV1_7.extend({
387 |   ignoredKeys: Z.array(Z.string())
388 |     .default([])
389 |     .optional()
390 |     .describe(
391 |       "Keys that should be completely ignored by translation processes.",
392 |     ),
393 | });
394 | 
395 | export const configV1_8Definition = extendConfigDefinition(
396 |   configV1_7Definition,
397 |   {
398 |     createSchema: (baseSchema) =>
399 |       baseSchema.extend({
400 |         buckets: Z.record(bucketTypeSchema, bucketValueSchemaV1_8).default({}),
401 |       }),
402 |     createDefaultValue: (baseDefaultValue) => ({
403 |       ...baseDefaultValue,
404 |       version: 1.8,
405 |     }),
406 |     createUpgrader: (oldConfig) => ({
407 |       ...oldConfig,
408 |       version: 1.8,
409 |     }),
410 |   },
411 | );
412 | 
413 | // v1.8 -> v1.9
414 | // Changes: Add "formatter" field to top-level config
415 | export const configV1_9Definition = extendConfigDefinition(
416 |   configV1_8Definition,
417 |   {
418 |     createSchema: (baseSchema) =>
419 |       baseSchema.extend({
420 |         formatter: Z.enum(["prettier", "biome"])
421 |           .optional()
422 |           .describe(
423 |             "Code formatter to use for all buckets. Defaults to 'prettier' if not specified and a prettier config is found.",
424 |           ),
425 |       }),
426 |     createDefaultValue: (baseDefaultValue) => ({
427 |       ...baseDefaultValue,
428 |       version: 1.9,
429 |     }),
430 |     createUpgrader: (oldConfig) => ({
431 |       ...oldConfig,
432 |       version: 1.9,
433 |     }),
434 |   },
435 | );
436 | 
437 | // v1.9 -> v1.10
438 | // Changes: Add "settings" field to provider config for model-specific parameters
439 | const modelSettingsSchema = Z.object({
440 |   temperature: Z.number()
441 |     .min(0)
442 |     .max(2)
443 |     .optional()
444 |     .describe(
445 |       "Controls randomness in model outputs (0=deterministic, 2=very random). Some models like GPT-5 require temperature=1.",
446 |     ),
447 | })
448 |   .optional()
449 |   .describe("Model-specific settings for translation requests.");
450 | 
451 | const providerSchemaV1_10 = Z.object({
452 |   id: Z.enum([
453 |     "openai",
454 |     "anthropic",
455 |     "google",
456 |     "ollama",
457 |     "openrouter",
458 |     "mistral",
459 |   ]).describe("Identifier of the translation provider service."),
460 |   model: Z.string().describe("Model name to use for translations."),
461 |   prompt: Z.string().describe(
462 |     "Prompt template used when requesting translations.",
463 |   ),
464 |   baseUrl: Z.string()
465 |     .optional()
466 |     .describe("Custom base URL for the provider API (optional)."),
467 |   settings: modelSettingsSchema,
468 | }).describe("Configuration for the machine-translation provider.");
469 | 
470 | export const configV1_10Definition = extendConfigDefinition(
471 |   configV1_9Definition,
472 |   {
473 |     createSchema: (baseSchema) =>
474 |       baseSchema.extend({
475 |         provider: providerSchemaV1_10.optional(),
476 |       }),
477 |     createDefaultValue: (baseDefaultValue) => ({
478 |       ...baseDefaultValue,
479 |       version: "1.10",
480 |     }),
481 |     createUpgrader: (oldConfig) => ({
482 |       ...oldConfig,
483 |       version: "1.10",
484 |     }),
485 |   },
486 | );
487 | 
488 | // exports
489 | export const LATEST_CONFIG_DEFINITION = configV1_10Definition;
490 | 
491 | export type I18nConfig = Z.infer<(typeof LATEST_CONFIG_DEFINITION)["schema"]>;
492 | 
493 | export function parseI18nConfig(rawConfig: unknown) {
494 |   try {
495 |     const result = LATEST_CONFIG_DEFINITION.parse(rawConfig);
496 |     return result;
497 |   } catch (error: any) {
498 |     throw new Error(`Failed to parse config: ${error.message}`);
499 |   }
500 | }
501 | 
502 | export const defaultConfig = LATEST_CONFIG_DEFINITION.defaultValue;
503 | 
```

--------------------------------------------------------------------------------
/packages/cli/src/cli/loaders/mdx2/section-split.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Optimized version of the section joining algorithm
  3 |  *
  4 |  * This implementation focuses on performance and maintainability:
  5 |  * 1. Uses a lookup table for faster section type determination
  6 |  * 2. Uses a matrix for faster spacing determination
  7 |  * 3. Reduces string concatenations by using an array and joining at the end
  8 |  * 4. Adds detailed comments for better maintainability
  9 |  */
 10 | 
 11 | import { unified } from "unified";
 12 | import _ from "lodash";
 13 | import remarkParse from "remark-parse";
 14 | import remarkGfm from "remark-gfm";
 15 | import remarkMdx from "remark-mdx";
 16 | import { VFile } from "vfile";
 17 | import { Root, RootContent } from "mdast";
 18 | import { PlaceholderedMdx, SectionedMdx } from "./_types";
 19 | import { traverseMdast } from "./_utils";
 20 | import { createLoader } from "../_utils";
 21 | import { ILoader } from "../_types";
 22 | 
 23 | /**
 24 |  * MDX Section Splitter
 25 |  *
 26 |  * This module splits MDX content into logical sections, with special handling for JSX/HTML tags.
 27 |  *
 28 |  * Key features:
 29 |  * - Splits content at headings (h1-h6)
 30 |  * - Treats JSX/HTML opening tags as separate sections
 31 |  * - Treats JSX/HTML closing tags as separate sections
 32 |  * - Treats self-closing JSX/HTML tags as separate sections
 33 |  * - Handles nested components properly
 34 |  * - Preserves content between tags as separate sections
 35 |  * - Intelligently joins sections with appropriate spacing
 36 |  */
 37 | 
 38 | // Create a parser instance for GitHub-flavoured Markdown and MDX JSX
 39 | const parser = unified().use(remarkParse).use(remarkGfm).use(remarkMdx);
 40 | 
 41 | // Interface for section boundaries
 42 | interface Boundary {
 43 |   /** 0-based offset into content where the boundary begins */
 44 |   start: number;
 45 |   /** 0-based offset into content where the boundary ends */
 46 |   end: number;
 47 |   /** Whether the boundary node itself should be isolated as its own section */
 48 |   isolateSelf: boolean;
 49 | }
 50 | 
 51 | // Section types for intelligent joining
 52 | enum SectionType {
 53 |   HEADING = 0,
 54 |   JSX_OPENING_TAG = 1,
 55 |   JSX_CLOSING_TAG = 2,
 56 |   JSX_SELF_CLOSING_TAG = 3,
 57 |   CONTENT = 4,
 58 |   UNKNOWN = 5,
 59 | }
 60 | 
 61 | // Spacing matrix for fast lookup
 62 | // [prevType][currentType] = spacing
 63 | const SPACING_MATRIX = [
 64 |   // HEADING as previous type
 65 |   ["\n\n", "\n\n", "\n\n", "\n\n", "\n\n", "\n\n"],
 66 |   // JSX_OPENING_TAG as previous type
 67 |   ["\n\n", "\n", "\n", "\n", "\n", "\n\n"],
 68 |   // JSX_CLOSING_TAG as previous type
 69 |   ["\n\n", "\n", "\n", "\n", "\n\n", "\n\n"],
 70 |   // JSX_SELF_CLOSING_TAG as previous type
 71 |   ["\n\n", "\n", "\n", "\n", "\n", "\n\n"],
 72 |   // CONTENT as previous type
 73 |   ["\n\n", "\n\n", "\n", "\n\n", "\n\n", "\n\n"],
 74 |   // UNKNOWN as previous type
 75 |   ["\n\n", "\n\n", "\n\n", "\n\n", "\n\n", "\n\n"],
 76 | ];
 77 | 
 78 | /**
 79 |  * Creates a loader that splits MDX content into logical sections.
 80 |  *
 81 |  * A new section starts at:
 82 |  *  • Any heading (level 1-6)
 83 |  *  • Any JSX/HTML opening tag (<Component> or <div> etc.)
 84 |  *  • Any JSX/HTML closing tag (</Component> or </div> etc.)
 85 |  *  • Any self-closing JSX/HTML tag (<Component /> or <br /> etc.)
 86 |  */
 87 | export default function createMdxSectionSplitLoader(): ILoader<
 88 |   PlaceholderedMdx,
 89 |   SectionedMdx
 90 | > {
 91 |   return createLoader({
 92 |     async pull(_locale, input) {
 93 |       // Extract input or use defaults
 94 |       const {
 95 |         frontmatter = {},
 96 |         content = "",
 97 |         codePlaceholders = {},
 98 |       } = input ||
 99 |       ({
100 |         frontmatter: {},
101 |         content: "",
102 |         codePlaceholders: {},
103 |       } as PlaceholderedMdx);
104 | 
105 |       // Skip processing for empty content
106 |       if (!content.trim()) {
107 |         return {
108 |           frontmatter,
109 |           sections: {},
110 |         };
111 |       }
112 | 
113 |       // Parse the content to get the AST
114 |       const file = new VFile(content);
115 |       const ast = parser.parse(file) as Root;
116 | 
117 |       // Process the AST to find section boundaries
118 |       const boundaries = findSectionBoundaries(ast, content);
119 | 
120 |       // Build sections from boundaries
121 |       const sections = createSectionsFromBoundaries(boundaries, content);
122 | 
123 |       return {
124 |         frontmatter,
125 |         sections,
126 |       };
127 |     },
128 | 
129 |     async push(_locale, data, originalInput, _originalLocale) {
130 |       // Get sections as array
131 |       const sectionsArray = Object.values(data.sections);
132 | 
133 |       // If no sections, return empty content
134 |       if (sectionsArray.length === 0) {
135 |         return {
136 |           frontmatter: data.frontmatter,
137 |           content: "",
138 |           codePlaceholders: originalInput?.codePlaceholders ?? {},
139 |         };
140 |       }
141 | 
142 |       // Optimize by pre-allocating result array and determining section types once
143 |       const resultParts: string[] = new Array(sectionsArray.length * 2 - 1);
144 |       const sectionTypes: SectionType[] = new Array(sectionsArray.length);
145 | 
146 |       // Determine section types for all sections
147 |       for (let i = 0; i < sectionsArray.length; i++) {
148 |         sectionTypes[i] = determineJsxSectionType(sectionsArray[i]);
149 |       }
150 | 
151 |       // Add first section without spacing
152 |       resultParts[0] = sectionsArray[0];
153 | 
154 |       // Add remaining sections with appropriate spacing
155 |       for (let i = 1, j = 1; i < sectionsArray.length; i++, j += 2) {
156 |         const prevType = sectionTypes[i - 1];
157 |         const currentType = sectionTypes[i];
158 | 
159 |         // Get spacing from matrix for better performance
160 |         resultParts[j] = SPACING_MATRIX[prevType][currentType];
161 |         resultParts[j + 1] = sectionsArray[i];
162 |       }
163 | 
164 |       // Join all parts into final content
165 |       const content = resultParts.join("");
166 | 
167 |       return {
168 |         frontmatter: data.frontmatter,
169 |         content,
170 |         codePlaceholders: originalInput?.codePlaceholders ?? {},
171 |       };
172 |     },
173 |   });
174 | }
175 | 
176 | /**
177 |  * Determines the type of a section based on its content.
178 |  * Optimized with regex caching and early returns.
179 |  */
180 | function determineJsxSectionType(section: string): SectionType {
181 |   section = section.trim();
182 | 
183 |   // Early returns for common cases
184 |   if (!section) return SectionType.UNKNOWN;
185 | 
186 |   const firstChar = section.charAt(0);
187 |   const lastChar = section.charAt(section.length - 1);
188 | 
189 |   // Check for headings (starts with #)
190 |   if (firstChar === "#") {
191 |     // Ensure it's a proper heading with space after #
192 |     if (/^#{1,6}\s/.test(section)) {
193 |       return SectionType.HEADING;
194 |     }
195 |   }
196 | 
197 |   // Check for JSX/HTML tags (starts with <)
198 |   if (firstChar === "<") {
199 |     // Self-closing tag (ends with />)
200 |     if (section.endsWith("/>")) {
201 |       return SectionType.JSX_SELF_CLOSING_TAG;
202 |     }
203 | 
204 |     // Closing tag (starts with </)
205 |     if (section.startsWith("</")) {
206 |       return SectionType.JSX_CLOSING_TAG;
207 |     }
208 | 
209 |     // Opening tag (ends with >)
210 |     if (lastChar === ">") {
211 |       return SectionType.JSX_OPENING_TAG;
212 |     }
213 |   }
214 | 
215 |   // Default to content
216 |   return SectionType.CONTENT;
217 | }
218 | 
219 | /**
220 |  * Determines if a node is a JSX or HTML element.
221 |  */
222 | function isJsxOrHtml(node: Root | RootContent): boolean {
223 |   return (
224 |     node.type === "mdxJsxFlowElement" ||
225 |     node.type === "mdxJsxTextElement" ||
226 |     node.type === "html"
227 |   );
228 | }
229 | 
230 | /**
231 |  * Finds the end position of an opening tag in a text string.
232 |  * Optimized to handle nested angle brackets correctly.
233 |  */
234 | function findOpeningTagEnd(text: string): number {
235 |   let depth = 0;
236 |   let inQuotes = false;
237 |   let quoteChar = "";
238 | 
239 |   for (let i = 0; i < text.length; i++) {
240 |     const char = text[i];
241 | 
242 |     // Handle quotes (to avoid counting angle brackets inside attribute values)
243 |     if ((char === '"' || char === "'") && (i === 0 || text[i - 1] !== "\\")) {
244 |       if (!inQuotes) {
245 |         inQuotes = true;
246 |         quoteChar = char;
247 |       } else if (char === quoteChar) {
248 |         inQuotes = false;
249 |       }
250 |     }
251 | 
252 |     // Only count angle brackets when not in quotes
253 |     if (!inQuotes) {
254 |       if (char === "<") depth++;
255 |       if (char === ">") {
256 |         depth--;
257 |         if (depth === 0) return i + 1;
258 |       }
259 |     }
260 |   }
261 |   return -1;
262 | }
263 | 
264 | /**
265 |  * Finds the start position of a closing tag in a text string.
266 |  * Optimized to handle nested components correctly.
267 |  */
268 | function findClosingTagStart(text: string): number {
269 |   // Extract the tag name from the opening tag to match the correct closing tag
270 |   const openTagMatch = /<([^\s/>]+)/.exec(text);
271 |   if (!openTagMatch) return -1;
272 | 
273 |   const tagName = openTagMatch[1];
274 |   const closingTagRegex = new RegExp(`</${tagName}\\s*>`, "g");
275 | 
276 |   // Find the last occurrence of the closing tag
277 |   let lastMatch = null;
278 |   let match;
279 | 
280 |   while ((match = closingTagRegex.exec(text)) !== null) {
281 |     lastMatch = match;
282 |   }
283 | 
284 |   return lastMatch ? lastMatch.index : -1;
285 | }
286 | 
287 | /**
288 |  * Processes a JSX/HTML node to extract opening and closing tags as separate boundaries.
289 |  */
290 | function processJsxNode(
291 |   node: RootContent,
292 |   content: string,
293 |   boundaries: Boundary[],
294 | ): void {
295 |   // Skip nodes without valid position information
296 |   if (
297 |     !node.position ||
298 |     typeof node.position.start.offset !== "number" ||
299 |     typeof node.position.end.offset !== "number"
300 |   ) {
301 |     return;
302 |   }
303 | 
304 |   const nodeStart = node.position.start.offset;
305 |   const nodeEnd = node.position.end.offset;
306 |   const nodeContent = content.slice(nodeStart, nodeEnd);
307 | 
308 |   // Handle HTML nodes using regex
309 |   if (node.type === "html") {
310 |     extractHtmlTags(nodeStart, nodeContent, boundaries);
311 |     return;
312 |   }
313 | 
314 |   // Handle MDX JSX elements
315 |   if (node.type === "mdxJsxFlowElement" || node.type === "mdxJsxTextElement") {
316 |     const isSelfClosing = (node as any).selfClosing === true;
317 | 
318 |     if (isSelfClosing) {
319 |       // Self-closing tag - treat as a single section
320 |       boundaries.push({
321 |         start: nodeStart,
322 |         end: nodeEnd,
323 |         isolateSelf: true,
324 |       });
325 |     } else {
326 |       extractJsxTags(node, nodeContent, boundaries);
327 | 
328 |       // Process children recursively to handle nested components
329 |       if ((node as any).children) {
330 |         for (const child of (node as any).children) {
331 |           if (isJsxOrHtml(child)) {
332 |             processJsxNode(child, content, boundaries);
333 |           }
334 |         }
335 |       }
336 |     }
337 |   }
338 | }
339 | 
340 | /**
341 |  * Extracts HTML tags using regex and adds them as boundaries.
342 |  * Optimized with a more precise regex pattern.
343 |  */
344 | function extractHtmlTags(
345 |   nodeStart: number,
346 |   nodeContent: string,
347 |   boundaries: Boundary[],
348 | ): void {
349 |   // More precise regex for HTML tags that handles attributes better
350 |   const tagRegex =
351 |     /<\/?[a-zA-Z][a-zA-Z0-9:._-]*(?:\s+[a-zA-Z:_][a-zA-Z0-9:._-]*(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^'">\s]+))?)*\s*\/?>/g;
352 |   let match;
353 | 
354 |   while ((match = tagRegex.exec(nodeContent)) !== null) {
355 |     const tagStart = nodeStart + match.index;
356 |     const tagEnd = tagStart + match[0].length;
357 | 
358 |     boundaries.push({
359 |       start: tagStart,
360 |       end: tagEnd,
361 |       isolateSelf: true,
362 |     });
363 |   }
364 | }
365 | 
366 | /**
367 |  * Extracts opening and closing JSX tags and adds them as boundaries.
368 |  */
369 | function extractJsxTags(
370 |   node: RootContent,
371 |   nodeContent: string,
372 |   boundaries: Boundary[],
373 | ): void {
374 |   const nodeStart = node.position!.start.offset;
375 |   const nodeEnd = node.position!.end.offset;
376 | 
377 |   if (!nodeStart || !nodeEnd) {
378 |     return;
379 |   }
380 | 
381 |   // Find the opening tag
382 |   const openingTagEnd = findOpeningTagEnd(nodeContent);
383 |   if (openingTagEnd > 0) {
384 |     boundaries.push({
385 |       start: nodeStart,
386 |       end: nodeStart + openingTagEnd,
387 |       isolateSelf: true,
388 |     });
389 |   }
390 | 
391 |   // Find the closing tag
392 |   const closingTagStart = findClosingTagStart(nodeContent);
393 |   if (closingTagStart > 0 && closingTagStart < nodeContent.length) {
394 |     boundaries.push({
395 |       start: nodeStart + closingTagStart,
396 |       end: nodeEnd,
397 |       isolateSelf: true,
398 |     });
399 |   }
400 | }
401 | 
402 | /**
403 |  * Finds all section boundaries in the AST.
404 |  */
405 | function findSectionBoundaries(ast: Root, content: string): Boundary[] {
406 |   const boundaries: Boundary[] = [];
407 | 
408 |   // Use a Map to cache node positions for faster lookups
409 |   const nodePositions = new Map<RootContent, { start: number; end: number }>();
410 | 
411 |   // Pre-process nodes to cache their positions
412 |   traverseMdast(ast, (node: any) => {
413 |     if (
414 |       node.position &&
415 |       typeof node.position.start.offset === "number" &&
416 |       typeof node.position.end.offset === "number"
417 |     ) {
418 |       nodePositions.set(node, {
419 |         start: node.position.start.offset,
420 |         end: node.position.end.offset,
421 |       });
422 |     }
423 |   });
424 | 
425 |   for (const child of ast.children) {
426 |     const position = nodePositions.get(child);
427 |     if (!position) continue;
428 | 
429 |     if (child.type === "heading") {
430 |       // Heading marks the beginning of a new section including itself
431 |       boundaries.push({
432 |         start: position.start,
433 |         end: position.end,
434 |         isolateSelf: false,
435 |       });
436 |     } else if (isJsxOrHtml(child)) {
437 |       // Process JSX/HTML nodes to extract tags as separate sections
438 |       processJsxNode(child, content, boundaries);
439 |     }
440 |   }
441 | 
442 |   // Sort boundaries by start position
443 |   return boundaries.sort((a, b) => a.start - b.start);
444 | }
445 | 
446 | /**
447 |  * Creates sections from the identified boundaries.
448 |  * Optimized to reduce unnecessary string operations.
449 |  */
450 | function createSectionsFromBoundaries(
451 |   boundaries: Boundary[],
452 |   content: string,
453 | ): Record<string, string> {
454 |   const sections: Record<string, string> = {};
455 | 
456 |   // Early return for empty content or no boundaries
457 |   if (!content.trim() || boundaries.length === 0) {
458 |     const trimmed = content.trim();
459 |     if (trimmed) {
460 |       sections["0"] = trimmed;
461 |     }
462 |     return sections;
463 |   }
464 | 
465 |   let idx = 0;
466 |   let lastEnd = 0;
467 | 
468 |   // Pre-allocate array with estimated capacity
469 |   const sectionsArray: string[] = [];
470 | 
471 |   // Process each boundary and the content between boundaries
472 |   for (let i = 0; i < boundaries.length; i++) {
473 |     const { start, end, isolateSelf } = boundaries[i];
474 | 
475 |     // Capture content before this boundary if any
476 |     if (start > lastEnd) {
477 |       const segment = content.slice(lastEnd, start).trim();
478 |       if (segment) {
479 |         sectionsArray.push(segment);
480 |       }
481 |     }
482 | 
483 |     if (isolateSelf) {
484 |       // Extract the boundary itself as a section
485 |       const segment = content.slice(start, end).trim();
486 |       if (segment) {
487 |         sectionsArray.push(segment);
488 |       }
489 |       lastEnd = end;
490 |     } else {
491 |       // For non-isolated boundaries (like headings), include them with following content
492 |       const nextStart =
493 |         i + 1 < boundaries.length ? boundaries[i + 1].start : content.length;
494 |       const segment = content.slice(start, nextStart).trim();
495 |       if (segment) {
496 |         sectionsArray.push(segment);
497 |       }
498 |       lastEnd = nextStart;
499 |     }
500 |   }
501 | 
502 |   // Capture any content after the last boundary
503 |   if (lastEnd < content.length) {
504 |     const segment = content.slice(lastEnd).trim();
505 |     if (segment) {
506 |       sectionsArray.push(segment);
507 |     }
508 |   }
509 | 
510 |   // Convert array to object with sequential keys
511 |   sectionsArray.forEach((section, index) => {
512 |     sections[index.toString()] = section;
513 |   });
514 | 
515 |   return sections;
516 | }
517 | 
```

--------------------------------------------------------------------------------
/packages/compiler/src/jsx-scope-inject.spec.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect } from "vitest";
  2 | import { lingoJsxScopeInjectMutation } from "./jsx-scope-inject";
  3 | import { createPayload, createOutput, defaultParams } from "./_base";
  4 | import * as parser from "@babel/parser";
  5 | import generate from "@babel/generator";
  6 | 
  7 | // Helper function to run mutation and get result
  8 | function runMutation(code: string, rsc = false) {
  9 |   const params = { ...defaultParams, rsc };
 10 |   const input = createPayload({ code, params, relativeFilePath: "test" });
 11 |   const mutated = lingoJsxScopeInjectMutation(input);
 12 |   if (!mutated) throw new Error("Mutation returned null");
 13 |   return createOutput(mutated).code;
 14 | }
 15 | 
 16 | // Helper function to normalize code for comparison
 17 | function normalizeCode(code: string) {
 18 |   const ast = parser.parse(code, {
 19 |     sourceType: "module",
 20 |     plugins: ["jsx", "typescript"],
 21 |   });
 22 |   return generate(ast).code;
 23 | }
 24 | 
 25 | describe("lingoJsxScopeInjectMutation", () => {
 26 |   describe("skip", () => {
 27 |     it("should skip if data-lingo-skip is truthy", () => {
 28 |       const input = `
 29 | function Component() {
 30 |   return <div data-jsx-scope data-lingo-skip>
 31 |     <p>Hello world!</p>
 32 |   </div>;
 33 | }
 34 |     `;
 35 | 
 36 |       const expected = `
 37 | function Component() {
 38 |   return <div data-jsx-scope data-lingo-skip>
 39 |     <p>Hello world!</p>
 40 |   </div>;
 41 | }
 42 |     `;
 43 | 
 44 |       const result = runMutation(input);
 45 |       expect(normalizeCode(result)).toBe(normalizeCode(expected));
 46 |     });
 47 |   });
 48 | 
 49 |   describe("transform", () => {
 50 |     it("should transform elements with data-jsx-scope into LingoComponent", () => {
 51 |       const input = `
 52 | function Component() {
 53 |   return <div>
 54 |     <p data-jsx-scope="0/my/custom/path/1" className="text-foreground">Hello world!</p>
 55 |   </div>;
 56 | }
 57 | `.trim();
 58 | 
 59 |       const expected = `
 60 | import { LingoComponent } from "lingo.dev/react/client";
 61 | function Component() {
 62 |   return <div>
 63 |     <LingoComponent data-jsx-scope="0/my/custom/path/1" className="text-foreground" $as="p" $fileKey="test" $entryKey="0/my/custom/path/1" />
 64 |   </div>;
 65 | }
 66 | `.trim();
 67 | 
 68 |       const result = runMutation(input);
 69 | 
 70 |       // We normalize both the expected and result to handle formatting differences
 71 |       expect(normalizeCode(result)).toBe(normalizeCode(expected));
 72 |     });
 73 | 
 74 |     it("should transform JSX elements differently for server components", () => {
 75 |       const input = `
 76 | function Component() {
 77 |   return <div>
 78 |     <p data-jsx-scope="0/body/0/argument/1" className="text-foreground">Hello world!</p>
 79 |   </div>;
 80 | }
 81 | `.trim();
 82 | 
 83 |       const expected = `
 84 | import { LingoComponent, loadDictionary } from "lingo.dev/react/rsc";
 85 | function Component() {
 86 |   return <div>
 87 |     <LingoComponent data-jsx-scope="0/body/0/argument/1" className="text-foreground" $as="p" $fileKey="test" $entryKey="0/body/0/argument/1" $loadDictionary={locale => loadDictionary(locale)} />
 88 |   </div>;
 89 | }
 90 | `.trim();
 91 | 
 92 |       const result = runMutation(input, true);
 93 | 
 94 |       expect(normalizeCode(result)).toBe(normalizeCode(expected));
 95 |     });
 96 | 
 97 |     it("should skip transformation if no JSX scopes are present", () => {
 98 |       const input = `
 99 | function Component() {
100 |   return <div>
101 |     <p className="text-foreground">Hello world!</p>
102 |   </div>;
103 | }
104 | `.trim();
105 | 
106 |       // Input should match output exactly
107 |       const result = runMutation(input);
108 |       expect(normalizeCode(result)).toBe(normalizeCode(input));
109 |     });
110 | 
111 |     it("should preserve JSX expression attributes", () => {
112 |       const input = `
113 | function Component({ dynamicClass }) {
114 |   return <div>
115 |     <p data-jsx-scope="0/body/0/argument/1" className={dynamicClass}>Hello world!</p>
116 |   </div>;
117 | }
118 | `.trim();
119 | 
120 |       const expected = `
121 | import { LingoComponent } from "lingo.dev/react/client";
122 | function Component({
123 |   dynamicClass
124 | }) {
125 |   return <div>
126 |     <LingoComponent data-jsx-scope="0/body/0/argument/1" className={dynamicClass} $as="p" $fileKey="test" $entryKey="0/body/0/argument/1" />
127 |   </div>;
128 | }
129 | `.trim();
130 | 
131 |       const result = runMutation(input);
132 |       expect(normalizeCode(result)).toBe(normalizeCode(expected));
133 |     });
134 | 
135 |     it("should handle boolean attributes correctly", () => {
136 |       const input = `
137 | function Component() {
138 |   return <div>
139 |     <button data-jsx-scope="0/body/0/argument/1" disabled>Click me</button>
140 |   </div>;
141 | }
142 | `.trim();
143 | 
144 |       const expected = `
145 | import { LingoComponent } from "lingo.dev/react/client";
146 | function Component() {
147 |   return <div>
148 |     <LingoComponent data-jsx-scope="0/body/0/argument/1" disabled $as="button" $fileKey="test" $entryKey="0/body/0/argument/1" />
149 |   </div>;
150 | }
151 | `.trim();
152 | 
153 |       const result = runMutation(input);
154 |       expect(normalizeCode(result)).toBe(normalizeCode(expected));
155 |     });
156 |   });
157 | 
158 |   describe("variables", () => {
159 |     it("should handle JSX variables in elements with data-jsx-scope", () => {
160 |       const input = `
161 | function Component({ count, category }) {
162 |   return <div>
163 |     <p data-jsx-scope="0/body/0/argument/1" className="text-foreground">You have {count} items in {category}.</p>
164 |   </div>;
165 | }
166 | `.trim();
167 | 
168 |       const expected = `
169 | import { LingoComponent } from "lingo.dev/react/client";
170 | function Component({ count, category }) {
171 |   return <div>
172 |     <LingoComponent
173 |       data-jsx-scope="0/body/0/argument/1"
174 |       className="text-foreground"
175 |       $as="p"
176 |       $fileKey="test"
177 |       $entryKey="0/body/0/argument/1"
178 |       $variables={{ "count": count, "category": category }}
179 |     />
180 |   </div>;
181 | }
182 | `.trim();
183 | 
184 |       const result = runMutation(input);
185 |       expect(normalizeCode(result)).toBe(normalizeCode(expected));
186 |     });
187 | 
188 |     it("should handle JSX variables for server components", () => {
189 |       const input = `
190 | function Component({ count, category }) {
191 |   return <div>
192 |     <p data-jsx-scope="0/body/0/argument/1" className="text-foreground">You have {count} items in {category}.</p>
193 |   </div>;
194 | }
195 | `.trim();
196 | 
197 |       const expected = `
198 | import { LingoComponent, loadDictionary } from "lingo.dev/react/rsc";
199 | function Component({ count, category }) {
200 |   return <div>
201 |     <LingoComponent
202 |       data-jsx-scope="0/body/0/argument/1"
203 |       className="text-foreground"
204 |       $as="p"
205 |       $fileKey="test"
206 |       $entryKey="0/body/0/argument/1"
207 |       $variables={{ "count": count, "category": category }}
208 |       $loadDictionary={locale => loadDictionary(locale)}
209 |     />
210 |   </div>;
211 | }
212 | `.trim();
213 | 
214 |       const result = runMutation(input, true);
215 |       expect(normalizeCode(result)).toBe(normalizeCode(expected));
216 |     });
217 | 
218 |     it("should handle nested JSX elements with variables", () => {
219 |       const input = `
220 | function Component({ count, user }) {
221 |   return <div>
222 |     <div data-jsx-scope="0/body/0/argument/1">
223 |       Welcome {user.name}, you have {count} notifications.
224 |     </div>
225 |   </div>;
226 | }
227 | `.trim();
228 | 
229 |       const expected = `
230 | import { LingoComponent } from "lingo.dev/react/client";
231 | function Component({ count, user }) {
232 |   return <div>
233 |     <LingoComponent
234 |       data-jsx-scope="0/body/0/argument/1"
235 |       $as="div"
236 |       $fileKey="test"
237 |       $entryKey="0/body/0/argument/1"
238 |       $variables={{ "user.name": user.name, "count": count }}
239 |     />
240 |   </div>;
241 | }
242 | `.trim();
243 | 
244 |       const result = runMutation(input);
245 |       expect(normalizeCode(result)).toBe(normalizeCode(expected));
246 |     });
247 |   });
248 | 
249 |   describe("elements", () => {
250 |     it("should handle nested JSX elements", () => {
251 |       const input = `
252 | function Component() {
253 |   return <div>
254 |     <div data-jsx-scope="0/body/0/argument/1">
255 |       <p>Hello</p>
256 |       <span>World</span>
257 |     </div>
258 |   </div>;
259 | }
260 | `.trim();
261 | 
262 |       const expected = `
263 | import { LingoComponent } from "lingo.dev/react/client";
264 | function Component() {
265 |   return <div>
266 |     <LingoComponent
267 |       data-jsx-scope="0/body/0/argument/1"
268 |       $as="div"
269 |       $fileKey="test"
270 |       $entryKey="0/body/0/argument/1"
271 |       $elements={[
272 |         ({
273 |   children
274 | }) => <p>{children}</p>,
275 |         ({
276 |   children
277 | }) => <span>{children}</span>
278 |       ]}
279 |     />
280 |   </div>;
281 | }
282 | `.trim();
283 | 
284 |       const result = runMutation(input);
285 |       expect(normalizeCode(result)).toBe(normalizeCode(expected));
286 |     });
287 | 
288 |     it("should handle deeply nested JSX elements", () => {
289 |       const input = `
290 | function Component() {
291 |   return <div>
292 |     <div data-jsx-scope="0/body/0/argument/1">
293 |       <p>
294 |         <span>
295 |           <strong>Deeply</strong>
296 |         </span>
297 |         nested
298 |       </p>
299 |     </div>
300 |   </div>;
301 | }
302 | `.trim();
303 | 
304 |       const expected = `
305 | import { LingoComponent } from "lingo.dev/react/client";
306 | function Component() {
307 |   return <div>
308 |     <LingoComponent
309 |       data-jsx-scope="0/body/0/argument/1"
310 |       $as="div"
311 |       $fileKey="test"
312 |       $entryKey="0/body/0/argument/1"
313 |       $elements={[
314 |         ({
315 |   children
316 | }) => <p>{children}</p>,
317 |         ({
318 |   children
319 | }) => <span>{children}</span>,
320 |         ({
321 |   children
322 | }) => <strong>{children}</strong>
323 |       ]}
324 |     />
325 |   </div>;
326 | }
327 | `.trim();
328 | 
329 |       const result = runMutation(input);
330 |       expect(normalizeCode(result)).toBe(normalizeCode(expected));
331 |     });
332 | 
333 |     it("should handle nested elements with variables", () => {
334 |       const input = `
335 | function Component({ name }) {
336 |   return <div>
337 |     <div data-jsx-scope="0/body/0/argument/1">
338 |       <p>Hello {name}</p>
339 |       <span>Welcome back!</span>
340 |     </div>
341 |   </div>;
342 | }
343 | `.trim();
344 | 
345 |       const expected = `
346 | import { LingoComponent } from "lingo.dev/react/client";
347 | function Component({ name }) {
348 |   return <div>
349 |     <LingoComponent
350 |       data-jsx-scope="0/body/0/argument/1"
351 |       $as="div"
352 |       $fileKey="test"
353 |       $entryKey="0/body/0/argument/1"
354 |       $variables={{ "name": name }}
355 |       $elements={[
356 |         ({
357 |   children
358 | }) => <p>{children}</p>,
359 |         ({
360 |   children
361 | }) => <span>{children}</span>
362 |       ]}
363 |     />
364 |   </div>;
365 | }
366 | `.trim();
367 | 
368 |       const result = runMutation(input);
369 |       expect(normalizeCode(result)).toBe(normalizeCode(expected));
370 |     });
371 |   });
372 | 
373 |   describe("functions", () => {
374 |     it("should handle simple function calls", () => {
375 |       const input = `
376 | function Component() {
377 |   return <div>
378 |     <p data-jsx-scope="0/body/0/argument/1">Hello {getName(user)}, you have {getCount()} items</p>
379 |   </div>;
380 | }
381 | `.trim();
382 | 
383 |       const expected = `
384 | import { LingoComponent } from "lingo.dev/react/client";
385 | function Component() {
386 |   return <div>
387 |     <LingoComponent
388 |       data-jsx-scope="0/body/0/argument/1"
389 |       $as="p"
390 |       $fileKey="test"
391 |       $entryKey="0/body/0/argument/1"
392 |       $functions={{
393 |         "getName": [getName(user)],
394 |         "getCount": [getCount()]
395 |       }}
396 |     />
397 |   </div>;
398 | }
399 | `.trim();
400 | 
401 |       const result = runMutation(input);
402 |       expect(normalizeCode(result)).toBe(normalizeCode(expected));
403 |     });
404 | 
405 |     it("should handle function calls with variables and nested elements", () => {
406 |       const input = `
407 | function Component({ user }) {
408 |   return <div>
409 |     <div data-jsx-scope="0/body/0/argument/1">
410 |       <p>{formatName(getName(user))}</p> has <em>{count}</em>
411 |       <span>Last seen: {formatDate(user.lastSeen)}</span>
412 |     </div>
413 |   </div>;
414 | }
415 | `.trim();
416 | 
417 |       const expected = `
418 | import { LingoComponent } from "lingo.dev/react/client";
419 | function Component({ user }) {
420 |   return <div>
421 |     <LingoComponent
422 |       data-jsx-scope="0/body/0/argument/1"
423 |       $as="div"
424 |       $fileKey="test"
425 |       $entryKey="0/body/0/argument/1"
426 |       $variables={{ "count": count }}
427 |       $elements={[
428 |         ({ children }) => <p>{children}</p>,
429 |         ({ children }) => <em>{children}</em>,
430 |         ({ children }) => <span>{children}</span>
431 |       ]}
432 |       $functions={{
433 |         "formatName": [formatName(getName(user))],
434 |         "formatDate": [formatDate(user.lastSeen)]
435 |       }}
436 |     />
437 |   </div>;
438 | }
439 | `.trim();
440 | 
441 |       const result = runMutation(input);
442 |       expect(normalizeCode(result)).toBe(normalizeCode(expected));
443 |     });
444 |   });
445 | 
446 |   describe("expressions", () => {
447 |     it("should extract simple expressions", () => {
448 |       const input = `
449 | function Component() {
450 |   return <div>
451 |     <p data-jsx-scope="0/body/0/argument/1">Result: {count + 1}</p>
452 |   </div>;
453 | }
454 | `.trim();
455 | 
456 |       const expected = `
457 | import { LingoComponent } from "lingo.dev/react/client";
458 | function Component() {
459 |   return <div>
460 |     <LingoComponent
461 |       data-jsx-scope="0/body/0/argument/1"
462 |       $as="p"
463 |       $fileKey="test"
464 |       $entryKey="0/body/0/argument/1"
465 |       $expressions={[
466 |         count + 1
467 |       ]}
468 |     />
469 |   </div>;
470 | }
471 | `.trim();
472 | 
473 |       const result = runMutation(input);
474 |       expect(normalizeCode(result)).toBe(normalizeCode(expected));
475 |     });
476 | 
477 |     it("should extract multiple expressions", () => {
478 |       const input = `
479 | function Component() {
480 |   return <div>
481 |     <p data-jsx-scope="0/body/0/argument/1">First: {count * 2}, Second: {value > 0}</p>
482 |   </div>;
483 | }
484 | `.trim();
485 | 
486 |       const expected = `
487 | import { LingoComponent } from "lingo.dev/react/client";
488 | function Component() {
489 |   return <div>
490 |     <LingoComponent
491 |       data-jsx-scope="0/body/0/argument/1"
492 |       $as="p"
493 |       $fileKey="test"
494 |       $entryKey="0/body/0/argument/1"
495 |       $expressions={[
496 |         count * 2,
497 |         value > 0
498 |       ]}
499 |     />
500 |   </div>;
501 | }
502 | `.trim();
503 | 
504 |       const result = runMutation(input);
505 |       expect(normalizeCode(result)).toBe(normalizeCode(expected));
506 |     });
507 | 
508 |     it("should handle mixed variables, functions and expressions", () => {
509 |       const input = `
510 | function Component() {
511 |   return <div>
512 |     <p data-jsx-scope="0/body/0/argument/1">
513 |       {count + 1} items by {user.name}, processed by {getName()}}
514 |     </p>
515 |   </div>;
516 | }
517 | `.trim();
518 | 
519 |       const expected = `
520 | import { LingoComponent } from "lingo.dev/react/client";
521 | function Component() {
522 |   return <div>
523 |     <LingoComponent
524 |       data-jsx-scope="0/body/0/argument/1"
525 |       $as="p"
526 |       $fileKey="test"
527 |       $entryKey="0/body/0/argument/1"
528 |       $variables={{
529 |         "user.name": user.name
530 |       }}
531 |       $functions={{
532 |         "getName": [getName()],
533 |       }}
534 |       $expressions={[
535 |         count + 1
536 |       ]}
537 |     />
538 |   </div>;
539 | }
540 | `.trim();
541 | 
542 |       const result = runMutation(input);
543 |       expect(normalizeCode(result)).toBe(normalizeCode(expected));
544 |     });
545 | 
546 |     it("should handle expressions in nested elements", () => {
547 |       const input = `
548 | function Component() {
549 |   return <div>
550 |     <div data-jsx-scope="0/body/0/argument/1">
551 |       <p>Count: {items.length + offset}</p>
552 |       <span>Active: {items.filter(i => i.active).length > 0}</span>
553 |     </div>
554 |   </div>;
555 | }
556 | `.trim();
557 | 
558 |       const expected = `
559 | import { LingoComponent } from "lingo.dev/react/client";
560 | function Component() {
561 |   return <div>
562 |     <LingoComponent
563 |       data-jsx-scope="0/body/0/argument/1"
564 |       $as="div"
565 |       $fileKey="test"
566 |       $entryKey="0/body/0/argument/1"
567 |       $elements={[
568 |         ({ children }) => <p>{children}</p>,
569 |         ({ children }) => <span>{children}</span>
570 |       ]}
571 |       $expressions={[
572 |         items.length + offset,
573 |         items.filter(i => i.active).length > 0
574 |       ]}
575 |     />
576 |   </div>;
577 | }
578 | `.trim();
579 | 
580 |       const result = runMutation(input);
581 |       expect(normalizeCode(result)).toBe(normalizeCode(expected));
582 |     });
583 |   });
584 | });
585 | 
```

--------------------------------------------------------------------------------
/packages/compiler/src/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { createUnplugin } from "unplugin";
  2 | import type { NextConfig } from "next";
  3 | import packageJson from "../package.json";
  4 | import _ from "lodash";
  5 | import dedent from "dedent";
  6 | import { defaultParams } from "./_base";
  7 | import { LCP_DICTIONARY_FILE_NAME } from "./_const";
  8 | import { LCPCache } from "./lib/lcp/cache";
  9 | import { getInvalidLocales } from "./utils/locales";
 10 | import {
 11 |   getGroqKeyFromEnv,
 12 |   getGroqKeyFromRc,
 13 |   getGoogleKeyFromEnv,
 14 |   getGoogleKeyFromRc,
 15 |   getMistralKeyFromEnv,
 16 |   getMistralKeyFromRc,
 17 |   getLingoDotDevKeyFromEnv,
 18 |   getLingoDotDevKeyFromRc,
 19 | } from "./utils/llm-api-key";
 20 | import { isRunningInCIOrDocker } from "./utils/env";
 21 | import { providerDetails } from "./lib/lcp/api/provider-details";
 22 | import { loadDictionary, transformComponent } from "./_loader-utils";
 23 | import trackEvent from "./utils/observability";
 24 | 
 25 | const keyCheckers: Record<
 26 |   string,
 27 |   {
 28 |     checkEnv: () => string | undefined;
 29 |     checkRc: () => string | undefined;
 30 |   }
 31 | > = {
 32 |   groq: {
 33 |     checkEnv: getGroqKeyFromEnv,
 34 |     checkRc: getGroqKeyFromRc,
 35 |   },
 36 |   google: {
 37 |     checkEnv: getGoogleKeyFromEnv,
 38 |     checkRc: getGoogleKeyFromRc,
 39 |   },
 40 |   mistral: {
 41 |     checkEnv: getMistralKeyFromEnv,
 42 |     checkRc: getMistralKeyFromRc,
 43 |   },
 44 |   "lingo.dev": {
 45 |     checkEnv: getLingoDotDevKeyFromEnv,
 46 |     checkRc: getLingoDotDevKeyFromRc,
 47 |   },
 48 | };
 49 | 
 50 | const alreadySentBuildEvent = { value: false };
 51 | 
 52 | function sendBuildEvent(framework: string, config: any, isDev: boolean) {
 53 |   if (alreadySentBuildEvent.value) return;
 54 |   alreadySentBuildEvent.value = true;
 55 |   trackEvent("compiler.build.start", {
 56 |     framework,
 57 |     configuration: config,
 58 |     isDevMode: isDev,
 59 |   });
 60 | }
 61 | 
 62 | const unplugin = createUnplugin<Partial<typeof defaultParams> | undefined>(
 63 |   (_params, _meta) => {
 64 |     console.log("ℹ️  Starting Lingo.dev compiler...");
 65 | 
 66 |     const params = _.defaults(_params, defaultParams);
 67 | 
 68 |     // Validate if not in CI or Docker
 69 |     if (!isRunningInCIOrDocker()) {
 70 |       if (params.models === "lingo.dev") {
 71 |         validateLLMKeyDetails(["lingo.dev"]);
 72 |       } else {
 73 |         const configuredProviders = getConfiguredProviders(params.models);
 74 |         validateLLMKeyDetails(configuredProviders);
 75 | 
 76 |         const invalidLocales = getInvalidLocales(
 77 |           params.models,
 78 |           params.sourceLocale,
 79 |           params.targetLocales,
 80 |         );
 81 |         if (invalidLocales.length > 0) {
 82 |           throw new Error(dedent`
 83 |             ⚠️  Lingo.dev Localization Compiler requires LLM model setup for the following locales: ${invalidLocales.join(
 84 |               ", ",
 85 |             )}.
 86 | 
 87 |             ⭐️ Next steps:
 88 |             1. Refer to documentation for help: https://lingo.dev/compiler
 89 |             2. If you want to use a different LLM, raise an issue in our open-source repo: https://lingo.dev/go/gh
 90 |             3. If you have questions, feature requests, or would like to contribute, join our Discord: https://lingo.dev/go/discord
 91 |           `);
 92 |         }
 93 |       }
 94 |     }
 95 | 
 96 |     LCPCache.ensureDictionaryFile({
 97 |       sourceRoot: params.sourceRoot,
 98 |       lingoDir: params.lingoDir,
 99 |     });
100 | 
101 |     const isDev: boolean =
102 |       "dev" in _meta ? !!_meta.dev : process.env.NODE_ENV !== "production";
103 |     sendBuildEvent("unplugin", params, isDev);
104 | 
105 |     return {
106 |       name: packageJson.name,
107 |       loadInclude: (id) => !!id.match(LCP_DICTIONARY_FILE_NAME),
108 |       async load(id) {
109 |         const dictionary = await loadDictionary({
110 |           resourcePath: id,
111 |           resourceQuery: "",
112 |           params: {
113 |             ...params,
114 |             models: params.models,
115 |             sourceLocale: params.sourceLocale,
116 |             targetLocales: params.targetLocales,
117 |           },
118 |           sourceRoot: params.sourceRoot,
119 |           lingoDir: params.lingoDir,
120 |           isDev,
121 |         });
122 | 
123 |         if (!dictionary) {
124 |           return null;
125 |         }
126 | 
127 |         return {
128 |           code: `export default ${JSON.stringify(dictionary, null, 2)}`,
129 |         };
130 |       },
131 |       transformInclude: (id) => id.endsWith(".tsx") || id.endsWith(".jsx"),
132 |       enforce: "pre",
133 |       transform(code, id) {
134 |         try {
135 |           const result = transformComponent({
136 |             code,
137 |             params,
138 |             resourcePath: id,
139 |             sourceRoot: params.sourceRoot,
140 |           });
141 | 
142 |           return result;
143 |         } catch (error) {
144 |           console.error("⚠️  Lingo.dev compiler failed to localize your app");
145 |           console.error("⚠️  Details:", error);
146 | 
147 |           return code;
148 |         }
149 |       },
150 |     };
151 |   },
152 | );
153 | 
154 | export default {
155 |   /**
156 |    * Initializes Lingo.dev Compiler for Next.js (App Router).
157 |    *
158 |    * @param compilerParams - The compiler parameters.
159 |    *
160 |    * @returns The Next.js configuration.
161 |    *
162 |    * @example Configuration for Next.js's default template
163 |    * ```ts
164 |    * import lingoCompiler from "lingo.dev/compiler";
165 |    * import type { NextConfig } from "next";
166 |    *
167 |    * const nextConfig: NextConfig = {
168 |    *   /* config options here *\/
169 |    * };
170 |    *
171 |    * export default lingoCompiler.next({
172 |    *   sourceRoot: "app",
173 |    *   models: "lingo.dev",
174 |    * })(nextConfig);
175 |    * ```
176 |    */
177 |   next:
178 |     (
179 |       compilerParams?: Partial<typeof defaultParams> & {
180 |         turbopack?: {
181 |           enabled?: boolean | "auto";
182 |           useLegacyTurbo?: boolean;
183 |         };
184 |       },
185 |     ) =>
186 |     (nextConfig: any = {}): NextConfig => {
187 |       const mergedParams = _.merge(
188 |         {},
189 |         defaultParams,
190 |         {
191 |           rsc: true,
192 |           turbopack: {
193 |             enabled: "auto",
194 |             useLegacyTurbo: false,
195 |           },
196 |         },
197 |         compilerParams,
198 |       );
199 | 
200 |       const isDev = process.env.NODE_ENV !== "production";
201 |       sendBuildEvent("Next.js", mergedParams, isDev);
202 | 
203 |       let turbopackEnabled: boolean;
204 |       if (mergedParams.turbopack?.enabled === "auto") {
205 |         turbopackEnabled =
206 |           process.env.TURBOPACK === "1" || process.env.TURBOPACK === "true";
207 |       } else {
208 |         turbopackEnabled = mergedParams.turbopack?.enabled === true;
209 |       }
210 | 
211 |       const supportLegacyTurbo: boolean =
212 |         mergedParams.turbopack?.useLegacyTurbo === true;
213 | 
214 |       const hasWebpackConfig = typeof nextConfig.webpack === "function";
215 |       const hasTurbopackConfig = typeof nextConfig.turbopack === "function";
216 |       if (hasWebpackConfig && turbopackEnabled) {
217 |         console.warn(
218 |           "⚠️  Turbopack is enabled in the Lingo.dev compiler, but you have webpack config. Lingo.dev will still apply turbopack configuration.",
219 |         );
220 |       }
221 |       if (hasTurbopackConfig && !turbopackEnabled) {
222 |         console.warn(
223 |           "⚠️  Turbopack is disabled in the Lingo.dev compiler, but you have turbopack config. Lingo.dev will not apply turbopack configuration.",
224 |         );
225 |       }
226 | 
227 |       // Webpack
228 |       const originalWebpack = nextConfig.webpack;
229 |       nextConfig.webpack = (config: any, options: any) => {
230 |         if (!turbopackEnabled) {
231 |           console.log("Applying Lingo.dev webpack configuration...");
232 |           config.plugins.unshift(unplugin.webpack(mergedParams));
233 |         }
234 | 
235 |         if (typeof originalWebpack === "function") {
236 |           return originalWebpack(config, options);
237 |         }
238 |         return config;
239 |       };
240 | 
241 |       // Turbopack
242 |       if (turbopackEnabled) {
243 |         console.log("Applying Lingo.dev Turbopack configuration...");
244 | 
245 |         // Check if the legacy turbo flag is set
246 |         let turbopackConfigPath = (nextConfig.turbopack ??= {});
247 |         if (supportLegacyTurbo) {
248 |           turbopackConfigPath = (nextConfig.experimental ??= {}).turbo ??= {};
249 |         }
250 | 
251 |         turbopackConfigPath.rules ??= {};
252 |         const rules = turbopackConfigPath.rules;
253 | 
254 |         // Regex for all relevant files for Lingo.dev
255 |         const lingoGlob = `**/*.{ts,tsx,js,jsx}`;
256 | 
257 |         // The .cjs extension is required for Next.js v14
258 |         const lingoLoaderPath = require.resolve("./lingo-turbopack-loader.cjs");
259 | 
260 |         rules[lingoGlob] = {
261 |           loaders: [
262 |             {
263 |               loader: lingoLoaderPath,
264 |               options: mergedParams,
265 |             },
266 |           ],
267 |         };
268 |       }
269 | 
270 |       return nextConfig;
271 |     },
272 |   /**
273 |    * Initializes Lingo.dev Compiler for Vite.
274 |    *
275 |    * @param compilerParams - The compiler parameters.
276 |    *
277 |    * @returns The Vite configuration.
278 |    *
279 |    * @example Configuration for Vite's "react-ts" template
280 |    * ```ts
281 |    * import { defineConfig, type UserConfig } from "vite";
282 |    * import react from "@vitejs/plugin-react";
283 |    * import lingoCompiler from "lingo.dev/compiler";
284 |    *
285 |    * // https://vite.dev/config/
286 |    * const viteConfig: UserConfig = {
287 |    *   plugins: [react()],
288 |    * };
289 |    *
290 |    * export default defineConfig(() =>
291 |    *   lingoCompiler.vite({
292 |    *     models: "lingo.dev",
293 |    *   })(viteConfig)
294 |    * );
295 |    * ```
296 |    *
297 |    * @example Configuration for React Router's default template
298 |    * ```ts
299 |    * import { reactRouter } from "@react-router/dev/vite";
300 |    * import tailwindcss from "@tailwindcss/vite";
301 |    * import lingoCompiler from "lingo.dev/compiler";
302 |    * import { defineConfig, type UserConfig } from "vite";
303 |    * import tsconfigPaths from "vite-tsconfig-paths";
304 |    *
305 |    * const viteConfig: UserConfig = {
306 |    *   plugins: [tailwindcss(), reactRouter(), tsconfigPaths()],
307 |    * };
308 |    *
309 |    * export default defineConfig(() =>
310 |    *   lingoCompiler.vite({
311 |    *     sourceRoot: "app",
312 |    *     models: "lingo.dev",
313 |    *   })(viteConfig)
314 |    * );
315 |    * ```
316 |    */
317 |   vite: (compilerParams?: Partial<typeof defaultParams>) => (config: any) => {
318 |     const mergedParams = _.merge(
319 |       {},
320 |       defaultParams,
321 |       { rsc: false },
322 |       compilerParams,
323 |     );
324 | 
325 |     const isDev = process.env.NODE_ENV !== "production";
326 |     const isReactRouter = config.plugins
327 |       ?.flat()
328 |       ?.some((plugin: any) => plugin.name === "react-router");
329 |     const framework = isReactRouter ? "React Router" : "Vite";
330 |     sendBuildEvent(framework, mergedParams, isDev);
331 |     config.plugins.unshift(unplugin.vite(mergedParams));
332 |     return config;
333 |   },
334 | };
335 | 
336 | /**
337 |  * Extract a list of supported LLM provider IDs from the locale→model mapping.
338 |  * @param models Mapping from locale to "<providerId>:<modelName>" strings.
339 |  */
340 | function getConfiguredProviders(models: Record<string, string>): string[] {
341 |   return _.chain(Object.values(models))
342 |     .map((modelString) => modelString.split(":")[0]) // Extract provider ID
343 |     .filter(Boolean) // Remove empty strings
344 |     .uniq() // Get unique providers
345 |     .filter(
346 |       (providerId) =>
347 |         providerDetails.hasOwnProperty(providerId) &&
348 |         keyCheckers.hasOwnProperty(providerId),
349 |     ) // Only check for known and implemented providers
350 |     .value();
351 | }
352 | 
353 | /**
354 |  * Print helpful information about where the LLM API keys for configured providers
355 |  * were discovered. The compiler looks for the key first in the environment
356 |  * (incl. .env files) and then in the user-wide configuration. Environment always wins.
357 |  * @param configuredProviders List of provider IDs detected in the configuration.
358 |  */
359 | function validateLLMKeyDetails(configuredProviders: string[]): void {
360 |   if (configuredProviders.length === 0) {
361 |     // No LLM providers configured that we can validate keys for.
362 |     return;
363 |   }
364 | 
365 |   const keyStatuses: Record<
366 |     string,
367 |     {
368 |       foundInEnv: boolean;
369 |       foundInRc: boolean;
370 |       details: (typeof providerDetails)[string];
371 |     }
372 |   > = {};
373 |   const missingProviders: string[] = [];
374 |   const foundProviders: string[] = [];
375 | 
376 |   for (const providerId of configuredProviders) {
377 |     const details = providerDetails[providerId];
378 |     const checkers = keyCheckers[providerId];
379 |     if (!details || !checkers) continue; // Should not happen due to filter above
380 | 
381 |     const foundInEnv = !!checkers.checkEnv();
382 |     const foundInRc = !!checkers.checkRc();
383 | 
384 |     keyStatuses[providerId] = { foundInEnv, foundInRc, details };
385 | 
386 |     if (!foundInEnv && !foundInRc) {
387 |       missingProviders.push(providerId);
388 |     } else {
389 |       foundProviders.push(providerId);
390 |     }
391 |   }
392 | 
393 |   if (missingProviders.length > 0) {
394 |     console.log(dedent`
395 |       \n
396 |       💡 Lingo.dev Localization Compiler is configured to use the following LLM provider(s): ${configuredProviders.join(
397 |         ", ",
398 |       )}.
399 | 
400 |       The compiler requires API keys for these providers to work, but the following keys are missing:
401 |     `);
402 | 
403 |     for (const providerId of missingProviders) {
404 |       const status = keyStatuses[providerId];
405 |       if (!status) continue;
406 |       console.log(dedent`
407 |           ⚠️  ${status.details.name} API key is missing. Set ${
408 |             status.details.apiKeyEnvVar
409 |           } environment variable.
410 | 
411 |           👉 You can set the API key in one of the following ways:
412 |           1. User-wide: Run npx lingo.dev@latest config set ${
413 |             status.details.apiKeyConfigKey || "<config-key-not-available>"
414 |           } <your-api-key>
415 |           2. Project-wide: Add ${
416 |             status.details.apiKeyEnvVar
417 |           }=<your-api-key> to .env file in every project that uses Lingo.dev Localization Compiler
418 |           3. Session-wide: Run export ${
419 |             status.details.apiKeyEnvVar
420 |           }=<your-api-key> in your terminal before running the compiler to set the API key for the current session
421 | 
422 |           ⭐️ If you don't yet have a ${
423 |             status.details.name
424 |           } API key, get one for free at ${status.details.getKeyLink}
425 |         `);
426 |     }
427 | 
428 |     const errorMessage = dedent`
429 |       \n
430 |       ⭐️ Also:
431 |       1. If you want to use a different LLM, update your configuration. Refer to documentation for help: https://lingo.dev/compiler
432 |       2. If the model/provider you want to use isn't supported yet, raise an issue in our open-source repo: https://lingo.dev/go/gh
433 |       3. If you have questions, feature requests, or would like to contribute, join our Discord: https://lingo.dev/go/discord
434 |     `;
435 |     console.log(errorMessage);
436 |     throw new Error("Missing required LLM API keys. See details above.");
437 |   } else if (foundProviders.length > 0) {
438 |     console.log(dedent`
439 |         \n
440 |         🔑  LLM API keys detected for configured providers: ${foundProviders.join(
441 |           ", ",
442 |         )}.
443 |       `);
444 |     for (const providerId of foundProviders) {
445 |       const status = keyStatuses[providerId];
446 |       if (!status) continue;
447 |       let sourceMessage = "";
448 |       if (status.foundInEnv && status.foundInRc) {
449 |         sourceMessage = `from both environment variables (${status.details.apiKeyEnvVar}) and your user-wide configuration. The key from the environment will be used because it has higher priority.`;
450 |       } else if (status.foundInEnv) {
451 |         sourceMessage = `from environment variables (${status.details.apiKeyEnvVar}).`;
452 |       } else if (status.foundInRc) {
453 |         sourceMessage = `from your user-wide configuration${
454 |           status.details.apiKeyConfigKey
455 |             ? ` (${status.details.apiKeyConfigKey})`
456 |             : ""
457 |         }.`;
458 |       }
459 |       console.log(dedent`
460 |           • ${status.details.name} API key loaded ${sourceMessage}
461 |         `);
462 |     }
463 |     console.log("✨");
464 |   }
465 | }
466 | 
```

--------------------------------------------------------------------------------
/packages/compiler/src/lib/lcp/server.spec.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, vi, beforeEach } from "vitest";
  2 | import { LCPServer } from "./server";
  3 | import { LCPSchema } from "./schema";
  4 | import { LCPCache } from "./cache";
  5 | import { LCPAPI } from "./api";
  6 | 
  7 | describe("LCPServer", () => {
  8 |   beforeEach(() => {
  9 |     vi.restoreAllMocks();
 10 |     vi.mock("fs");
 11 |     vi.mock("path");
 12 |   });
 13 | 
 14 |   describe("loadDictionaries", () => {
 15 |     it("should load dictionaries for all target locales", async () => {
 16 |       const lcp: LCPSchema = {
 17 |         version: 0.1,
 18 |         files: {},
 19 |       };
 20 |       const loadDictionaryForLocaleSpy = vi.spyOn(
 21 |         LCPServer,
 22 |         "loadDictionaryForLocale",
 23 |       );
 24 |       const dictionaries = await LCPServer.loadDictionaries({
 25 |         models: {
 26 |           "*:*": "groq:mistral-saba-24b",
 27 |         },
 28 |         lcp,
 29 |         sourceLocale: "en",
 30 |         targetLocales: ["fr", "es", "de"],
 31 |         sourceRoot: "src",
 32 |         lingoDir: "lingo",
 33 |       });
 34 | 
 35 |       expect(loadDictionaryForLocaleSpy).toHaveBeenCalledTimes(4);
 36 |       expect(dictionaries).toEqual({
 37 |         fr: {
 38 |           version: 0.1,
 39 |           locale: "fr",
 40 |           files: {},
 41 |         },
 42 |         es: {
 43 |           version: 0.1,
 44 |           locale: "es",
 45 |           files: {},
 46 |         },
 47 |         de: {
 48 |           version: 0.1,
 49 |           locale: "de",
 50 |           files: {},
 51 |         },
 52 |         en: {
 53 |           version: 0.1,
 54 |           locale: "en",
 55 |           files: {},
 56 |         },
 57 |       });
 58 |     });
 59 |   });
 60 | 
 61 |   describe("loadDictionaryForLocale", () => {
 62 |     it("should correctly extract the source dictionary when source and target locales are the same", async () => {
 63 |       // Mock LCPAPI.translate() to ensure it's not called
 64 |       const translateSpy = vi.spyOn(LCPAPI, "translate");
 65 | 
 66 |       const lcp: LCPSchema = {
 67 |         version: 0.1,
 68 |         files: {
 69 |           "app/test.tsx": {
 70 |             scopes: {
 71 |               key1: {
 72 |                 content: "Hello World",
 73 |                 hash: "abcd1234",
 74 |               },
 75 |               key2: {
 76 |                 content: "Button Text",
 77 |                 hash: "efgh5678",
 78 |               },
 79 |             },
 80 |           },
 81 |         },
 82 |       };
 83 | 
 84 |       const result = await LCPServer.loadDictionaryForLocale({
 85 |         lcp,
 86 |         sourceLocale: "en",
 87 |         targetLocale: "en", // Same locale
 88 |         sourceRoot: "src",
 89 |         lingoDir: "lingo",
 90 |       });
 91 | 
 92 |       // Verify the structure
 93 |       expect(result).toEqual({
 94 |         version: 0.1,
 95 |         locale: "en",
 96 |         files: {
 97 |           "app/test.tsx": {
 98 |             entries: {
 99 |               key1: "Hello World",
100 |               key2: "Button Text",
101 |             },
102 |           },
103 |         },
104 |       });
105 | 
106 |       // Ensure LCPAPI.translate() wasn't called since source == target
107 |       expect(translateSpy).not.toHaveBeenCalled();
108 |     });
109 | 
110 |     it("should return empty dictionary when source dictionary is empty", async () => {
111 |       // Mock LCPAPI.translate() to ensure it's not called
112 |       const translateSpy = vi.spyOn(LCPAPI, "translate");
113 | 
114 |       const lcp: LCPSchema = {
115 |         version: 0.1,
116 |         files: {},
117 |       };
118 | 
119 |       const result = await LCPServer.loadDictionaryForLocale({
120 |         lcp,
121 |         sourceLocale: "en",
122 |         targetLocale: "es",
123 |         sourceRoot: "src",
124 |         lingoDir: "lingo",
125 |       });
126 | 
127 |       // Verify the structure
128 |       expect(result).toEqual({
129 |         version: 0.1,
130 |         locale: "es",
131 |         files: {},
132 |       });
133 | 
134 |       // Ensure LCPAPI.translate() wasn't called since source == target
135 |       expect(translateSpy).not.toHaveBeenCalled();
136 |     });
137 | 
138 |     it("should handle overrides in source content", async () => {
139 |       // Mock LCPAPI.translate() to ensure it's not called
140 |       vi.spyOn(LCPAPI, "translate").mockImplementation(() =>
141 |         Promise.resolve({
142 |           version: 0.1,
143 |           locale: "fr",
144 |           files: {
145 |             "app/test.tsx": {
146 |               entries: {
147 |                 key1: "Bonjour le monde",
148 |                 key2: "Texte du bouton",
149 |               },
150 |             },
151 |           },
152 |         }),
153 |       );
154 | 
155 |       const lcp: LCPSchema = {
156 |         version: 0.1,
157 |         files: {
158 |           "app/test.tsx": {
159 |             scopes: {
160 |               key1: {
161 |                 content: "Hello World",
162 |                 hash: "abcd1234",
163 |               },
164 |               key2: {
165 |                 content: "Button Text",
166 |                 hash: "efgh5678",
167 |               },
168 |               key3: {
169 |                 content: "Original",
170 |                 hash: "1234abcd",
171 |                 overrides: {
172 |                   fr: "Remplacé", // French override for 'key3'
173 |                 },
174 |               },
175 |             },
176 |           },
177 |         },
178 |       };
179 | 
180 |       const result = await LCPServer.loadDictionaryForLocale({
181 |         lcp,
182 |         sourceLocale: "en",
183 |         targetLocale: "fr",
184 |         sourceRoot: "src",
185 |         lingoDir: "lingo",
186 |       });
187 | 
188 |       // Check that the overrides were applied
189 |       expect(result.files["app/test.tsx"].entries).toEqual({
190 |         key1: "Bonjour le monde",
191 |         key2: "Texte du bouton",
192 |         key3: "Remplacé",
193 |       });
194 |       expect(result.locale).toBe("fr");
195 |     });
196 | 
197 |     it("should create empty dictionary when no files are provided", async () => {
198 |       const lcp: LCPSchema = {
199 |         version: 0.1,
200 |       };
201 | 
202 |       const result = await LCPServer.loadDictionaryForLocale({
203 |         lcp,
204 |         sourceLocale: "en",
205 |         targetLocale: "en",
206 |         sourceRoot: "src",
207 |         lingoDir: "lingo",
208 |       });
209 | 
210 |       expect(result).toEqual({
211 |         version: 0.1,
212 |         locale: "en",
213 |         files: {},
214 |       });
215 |     });
216 | 
217 |     it("should read dictionary from cache only, not call LCPAPI.translate()", async () => {
218 |       vi.spyOn(LCPCache, "readLocaleDictionary").mockReturnValue({
219 |         version: 0.1,
220 |         locale: "en",
221 |         files: {
222 |           "app/test.tsx": {
223 |             entries: {
224 |               key1: "Hello World",
225 |               key2: "Button Text",
226 |               key3: "New text",
227 |             },
228 |           },
229 |         },
230 |       });
231 |       const translateSpy = vi
232 |         .spyOn(LCPAPI, "translate")
233 |         .mockImplementation(() => {
234 |           throw new Error("Should not translate anything");
235 |         });
236 | 
237 |       const lcp: LCPSchema = {
238 |         version: 0.1,
239 |         files: {
240 |           "app/test.tsx": {
241 |             scopes: {
242 |               key1: {
243 |                 content: "Hello World",
244 |               },
245 |             },
246 |           },
247 |         },
248 |       };
249 | 
250 |       await LCPServer.loadDictionaryForLocale({
251 |         lcp,
252 |         sourceLocale: "en",
253 |         targetLocale: "fr",
254 |         sourceRoot: "src",
255 |         lingoDir: "lingo",
256 |       });
257 | 
258 |       expect(translateSpy).not.toHaveBeenCalled();
259 |       expect(LCPCache.readLocaleDictionary).toHaveBeenCalledWith("fr", {
260 |         lcp,
261 |         sourceLocale: "en",
262 |         lingoDir: "lingo",
263 |         sourceRoot: "src",
264 |       });
265 |     });
266 | 
267 |     it("should write dictionary to cache", async () => {
268 |       vi.spyOn(LCPCache, "writeLocaleDictionary");
269 |       vi.spyOn(LCPAPI, "translate").mockReturnValue({
270 |         version: 0.1,
271 |         locale: "fr",
272 |         files: {
273 |           "app/test.tsx": {
274 |             entries: {
275 |               key1: "Bonjour le monde",
276 |               key2: "Texte du bouton",
277 |             },
278 |           },
279 |         },
280 |       });
281 | 
282 |       const lcp: LCPSchema = {
283 |         version: 0.1,
284 |         files: {
285 |           "app/test.tsx": {
286 |             scopes: {
287 |               key1: {
288 |                 content: "Hello World",
289 |                 hash: "abcd1234",
290 |               },
291 |               key2: {
292 |                 content: "Button Text",
293 |                 hash: "efgh5678",
294 |               },
295 |             },
296 |           },
297 |         },
298 |       };
299 | 
300 |       await LCPServer.loadDictionaryForLocale({
301 |         lcp,
302 |         sourceLocale: "en",
303 |         targetLocale: "fr",
304 |         sourceRoot: "src",
305 |         lingoDir: "lingo",
306 |       });
307 | 
308 |       expect(LCPCache.writeLocaleDictionary).toHaveBeenCalledWith(
309 |         {
310 |           files: {
311 |             "app/test.tsx": {
312 |               entries: {
313 |                 key1: "Bonjour le monde",
314 |                 key2: "Texte du bouton",
315 |               },
316 |             },
317 |           },
318 |           locale: "fr",
319 |           version: 0.1,
320 |         },
321 |         {
322 |           lcp,
323 |           sourceLocale: "en",
324 |           lingoDir: "lingo",
325 |           sourceRoot: "src",
326 |         },
327 |       );
328 |     });
329 | 
330 |     it("should reuse cached keys with matching hash, call LCPAPI.translate() for keys with different hash, fallback to source locale, cache new translations", async () => {
331 |       vi.spyOn(LCPCache, "readLocaleDictionary").mockReturnValue({
332 |         version: 0.1,
333 |         locale: "fr",
334 |         files: {
335 |           "app/test.tsx": {
336 |             entries: {
337 |               key1: "Bonjour le monde",
338 |             },
339 |           },
340 |         },
341 |       });
342 |       const writeCacheSpy = vi.spyOn(LCPCache, "writeLocaleDictionary");
343 |       const translateSpy = vi.spyOn(LCPAPI, "translate").mockResolvedValue({
344 |         version: 0.1,
345 |         locale: "fr",
346 |         files: {
347 |           "app/test.tsx": {
348 |             entries: {
349 |               key2: "Nouveau texte du bouton",
350 |               key3: "", // LLM might return empty string
351 |             },
352 |           },
353 |         },
354 |       });
355 | 
356 |       const lcp: LCPSchema = {
357 |         version: 0.1,
358 |         files: {
359 |           "app/test.tsx": {
360 |             scopes: {
361 |               key1: {
362 |                 content: "Hello World",
363 |                 hash: "abcd1234",
364 |               },
365 |               key2: {
366 |                 content: "Button Text",
367 |                 hash: "new_hash",
368 |               },
369 |               key3: {
370 |                 content: "New text",
371 |                 hash: "ijkl4321",
372 |               },
373 |             },
374 |           },
375 |         },
376 |       };
377 | 
378 |       const models = {
379 |         "*:*": "groq:mistral-saba-24b",
380 |       };
381 | 
382 |       const result = await LCPServer.loadDictionaryForLocale({
383 |         models,
384 |         lcp,
385 |         sourceLocale: "en",
386 |         targetLocale: "fr",
387 |         sourceRoot: "src",
388 |         lingoDir: "lingo",
389 |       });
390 | 
391 |       // Verify that only changed content was sent for translation
392 |       expect(translateSpy).toHaveBeenCalledWith(
393 |         models,
394 |         {
395 |           version: 0.1,
396 |           locale: "en",
397 |           files: {
398 |             "app/test.tsx": {
399 |               entries: {
400 |                 key2: "Button Text",
401 |                 key3: "New text",
402 |               },
403 |             },
404 |           },
405 |         },
406 |         "en",
407 |         "fr",
408 |         undefined,
409 |       );
410 | 
411 |       // Verify final result combines cached and newly translated content
412 |       expect(result).toEqual({
413 |         version: 0.1,
414 |         locale: "fr",
415 |         files: {
416 |           "app/test.tsx": {
417 |             entries: {
418 |               key1: "Bonjour le monde",
419 |               key2: "Nouveau texte du bouton",
420 |               key3: "New text", // LLM returned empty string, but result contains fallback to source locale string
421 |             },
422 |           },
423 |         },
424 |       });
425 | 
426 |       // when LLM returns empty string, we cache empty string (the result contains fallback to source locale string)
427 |       result.files["app/test.tsx"].entries.key3 = "";
428 | 
429 |       // Verify cache is updated with new translations
430 |       expect(writeCacheSpy).toHaveBeenCalledWith(result, {
431 |         lcp,
432 |         sourceLocale: "en",
433 |         lingoDir: "lingo",
434 |         sourceRoot: "src",
435 |       });
436 |     });
437 |   });
438 | 
439 |   describe("_getDictionaryDiff", () => {
440 |     it("should return diff between source and target dictionaries", () => {
441 |       const sourceDictionary = {
442 |         version: 0.1,
443 |         locale: "en",
444 |         files: {
445 |           "app/test.tsx": {
446 |             entries: {
447 |               key1: "Hello World",
448 |               key2: "Button Text",
449 |               key3: "New Text",
450 |               key4: "More text",
451 |             },
452 |           },
453 |         },
454 |       };
455 | 
456 |       const targetDictionary = {
457 |         version: 0.1,
458 |         locale: "es",
459 |         files: {
460 |           "app/test.tsx": {
461 |             entries: {
462 |               key1: "Hola mundo",
463 |               key2: "El texto del botón",
464 |               key3: "", // empty string is valid value
465 |             },
466 |           },
467 |         },
468 |       };
469 | 
470 |       const diff = (LCPServer as any)._getDictionaryDiff(
471 |         sourceDictionary,
472 |         targetDictionary,
473 |       );
474 | 
475 |       expect(diff).toEqual({
476 |         version: 0.1,
477 |         locale: "en",
478 |         files: {
479 |           "app/test.tsx": {
480 |             entries: {
481 |               key4: "More text",
482 |             },
483 |           },
484 |         },
485 |       });
486 |     });
487 |   });
488 | 
489 |   describe("_mergeDictionaries", () => {
490 |     it("should merge dictionaries", () => {
491 |       const sourceDictionary = {
492 |         version: 0.1,
493 |         locale: "es",
494 |         files: {
495 |           "app/test.tsx": {
496 |             entries: {
497 |               key2: "",
498 |               key3: "Nuevo texto",
499 |             },
500 |           },
501 |           "app/test3.tsx": {
502 |             entries: {
503 |               key1: "Como estas?",
504 |               key2: "Yo soy bien",
505 |             },
506 |           },
507 |         },
508 |       };
509 | 
510 |       const targetDictionary = {
511 |         version: 0.1,
512 |         locale: "es",
513 |         files: {
514 |           "app/test.tsx": {
515 |             entries: {
516 |               key1: "Hola mundo",
517 |               key2: "Hola",
518 |             },
519 |           },
520 |           "app/test2.tsx": {
521 |             entries: {
522 |               key1: "Yo soy un programador",
523 |               key2: "",
524 |             },
525 |           },
526 |         },
527 |       };
528 | 
529 |       const merge = (LCPServer as any)._mergeDictionaries(
530 |         sourceDictionary,
531 |         targetDictionary,
532 |       );
533 | 
534 |       expect(merge).toEqual({
535 |         version: 0.1,
536 |         locale: "es",
537 |         files: {
538 |           "app/test.tsx": {
539 |             entries: {
540 |               key1: "Hola mundo",
541 |               key2: "",
542 |               key3: "Nuevo texto",
543 |             },
544 |           },
545 |           "app/test2.tsx": {
546 |             entries: {
547 |               key1: "Yo soy un programador",
548 |               key2: "",
549 |             },
550 |           },
551 |           "app/test3.tsx": {
552 |             entries: {
553 |               key1: "Como estas?",
554 |               key2: "Yo soy bien",
555 |             },
556 |           },
557 |         },
558 |       });
559 |     });
560 | 
561 |     it("should remove empty entries when merging dictionaries", () => {
562 |       const sourceDictionary = {
563 |         version: 0.1,
564 |         locale: "es",
565 |         files: {
566 |           "app/test.tsx": {
567 |             entries: {
568 |               key1: "",
569 |               key2: "El texto del botón",
570 |             },
571 |           },
572 |           "app/test2.tsx": {
573 |             entries: {
574 |               key1: "Yo soy un programador",
575 |               key2: "",
576 |             },
577 |           },
578 |         },
579 |       };
580 | 
581 |       const targetDictionary = {
582 |         version: 0.1,
583 |         locale: "es",
584 |         files: {
585 |           "app/test.tsx": {
586 |             entries: {
587 |               key1: "Hello world",
588 |               key2: "Button Text",
589 |             },
590 |           },
591 |           "app/test2.tsx": {
592 |             entries: {
593 |               key1: "I am a programmer",
594 |               key2: "You are a gardener",
595 |             },
596 |           },
597 |         },
598 |       };
599 | 
600 |       const merge = (LCPServer as any)._mergeDictionaries(
601 |         sourceDictionary,
602 |         targetDictionary,
603 |         true,
604 |       );
605 | 
606 |       expect(merge).toEqual({
607 |         version: 0.1,
608 |         locale: "es",
609 |         files: {
610 |           "app/test.tsx": {
611 |             entries: {
612 |               key1: "Hello world",
613 |               key2: "El texto del botón",
614 |             },
615 |           },
616 |           "app/test2.tsx": {
617 |             entries: {
618 |               key1: "Yo soy un programador",
619 |               key2: "You are a gardener",
620 |             },
621 |           },
622 |         },
623 |       });
624 |     });
625 |   });
626 | });
627 | 
```
Page 13/20FirstPrevNextLast