#
tokens: 48919/50000 7/626 files (page 14/20)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 14 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/generate-cli-docs.ts:
--------------------------------------------------------------------------------

```typescript
  1 | #!/usr/bin/env node
  2 | 
  3 | import type { Argument, Command, Option } from "commander";
  4 | import { existsSync } from "fs";
  5 | import { mkdir, writeFile } from "fs/promises";
  6 | import type {
  7 |   Content,
  8 |   Heading,
  9 |   List,
 10 |   ListItem,
 11 |   Paragraph,
 12 |   PhrasingContent,
 13 |   Root,
 14 | } from "mdast";
 15 | import { dirname, join, resolve } from "path";
 16 | import remarkStringify from "remark-stringify";
 17 | import { unified } from "unified";
 18 | import { pathToFileURL } from "url";
 19 | import { createOrUpdateGitHubComment, getRepoRoot } from "./utils";
 20 | import { format as prettierFormat, resolveConfig } from "prettier";
 21 | 
 22 | type CommandWithInternals = Command & {
 23 |   _hidden?: boolean;
 24 |   _helpCommand?: Command;
 25 | };
 26 | 
 27 | const FRONTMATTER_DELIMITER = "---";
 28 | 
 29 | async function getProgram(repoRoot: string): Promise<Command> {
 30 |   const filePath = resolve(
 31 |     repoRoot,
 32 |     "packages",
 33 |     "cli",
 34 |     "src",
 35 |     "cli",
 36 |     "index.ts",
 37 |   );
 38 | 
 39 |   if (!existsSync(filePath)) {
 40 |     throw new Error(`CLI source file not found at ${filePath}`);
 41 |   }
 42 | 
 43 |   const cliModule = (await import(pathToFileURL(filePath).href)) as {
 44 |     default: Command;
 45 |   };
 46 | 
 47 |   return cliModule.default;
 48 | }
 49 | 
 50 | function slugifyCommandName(name: string): string {
 51 |   const slug = name
 52 |     .trim()
 53 |     .toLowerCase()
 54 |     .replace(/[^a-z0-9]+/g, "-")
 55 |     .replace(/^-+|-+$/g, "");
 56 | 
 57 |   return slug.length > 0 ? slug : "command";
 58 | }
 59 | 
 60 | function formatYamlValue(value: string): string {
 61 |   const escaped = value.replace(/"/g, '\\"');
 62 |   return `"${escaped}"`;
 63 | }
 64 | 
 65 | function createHeading(
 66 |   depth: number,
 67 |   content: string | PhrasingContent[],
 68 | ): Heading {
 69 |   const children = Array.isArray(content)
 70 |     ? content
 71 |     : [{ type: "text", value: content }];
 72 | 
 73 |   return {
 74 |     type: "heading",
 75 |     depth: Math.min(Math.max(depth, 1), 6),
 76 |     children,
 77 |   };
 78 | }
 79 | 
 80 | function createInlineCode(value: string): PhrasingContent {
 81 |   return { type: "inlineCode", value };
 82 | }
 83 | 
 84 | function createParagraph(text: string): Paragraph {
 85 |   return {
 86 |     type: "paragraph",
 87 |     children: createTextNodes(text),
 88 |   };
 89 | }
 90 | 
 91 | function createTextNodes(text: string): PhrasingContent[] {
 92 |   if (!text) {
 93 |     return [];
 94 |   }
 95 | 
 96 |   const nodes: PhrasingContent[] = [];
 97 |   const parts = text.split(/(`[^`]*`)/g);
 98 | 
 99 |   parts.forEach((part) => {
100 |     if (!part) {
101 |       return;
102 |     }
103 | 
104 |     if (part.startsWith("`") && part.endsWith("`")) {
105 |       nodes.push(createInlineCode(part.slice(1, -1)));
106 |     } else {
107 |       nodes.push(...createBracketAwareTextNodes(part));
108 |     }
109 |   });
110 | 
111 |   return nodes;
112 | }
113 | 
114 | function createBracketAwareTextNodes(text: string): PhrasingContent[] {
115 |   const nodes: PhrasingContent[] = [];
116 |   const bracketPattern = /\[[^\]]+\]/g;
117 |   let lastIndex = 0;
118 | 
119 |   for (const match of text.matchAll(bracketPattern)) {
120 |     const [value] = match;
121 |     const start = match.index ?? 0;
122 | 
123 |     if (start > lastIndex) {
124 |       nodes.push({ type: "text", value: text.slice(lastIndex, start) });
125 |     }
126 | 
127 |     nodes.push(createInlineCode(value));
128 |     lastIndex = start + value.length;
129 |   }
130 | 
131 |   if (lastIndex < text.length) {
132 |     nodes.push({ type: "text", value: text.slice(lastIndex) });
133 |   }
134 | 
135 |   if (nodes.length === 0) {
136 |     nodes.push({ type: "text", value: text });
137 |   }
138 | 
139 |   return nodes;
140 | }
141 | 
142 | function createList(items: ListItem[]): List {
143 |   return {
144 |     type: "list",
145 |     ordered: false,
146 |     spread: false,
147 |     children: items,
148 |   };
149 | }
150 | 
151 | function createListItem(children: PhrasingContent[]): ListItem {
152 |   return {
153 |     type: "listItem",
154 |     spread: false,
155 |     children: [
156 |       {
157 |         type: "paragraph",
158 |         children,
159 |       },
160 |     ],
161 |   };
162 | }
163 | 
164 | function formatArgumentLabel(arg: Argument): string {
165 |   const name = arg.name();
166 |   const suffix = arg.variadic ? "..." : "";
167 |   return arg.required ? `<${name}${suffix}>` : `[${name}${suffix}]`;
168 | }
169 | 
170 | function formatValue(value: unknown): string {
171 |   if (value === undefined) {
172 |     return "";
173 |   }
174 | 
175 |   if (value === null) {
176 |     return "null";
177 |   }
178 | 
179 |   if (typeof value === "string") {
180 |     return value;
181 |   }
182 | 
183 |   if (typeof value === "number" || typeof value === "bigint") {
184 |     return value.toString();
185 |   }
186 | 
187 |   if (typeof value === "boolean") {
188 |     return value ? "true" : "false";
189 |   }
190 | 
191 |   if (Array.isArray(value)) {
192 |     if (value.length === 0) {
193 |       return "[]";
194 |     }
195 |     return value.map((item) => formatValue(item)).join(", ");
196 |   }
197 | 
198 |   return JSON.stringify(value);
199 | }
200 | 
201 | function getCommandPath(
202 |   rootName: string,
203 |   ancestors: string[],
204 |   command: Command,
205 | ): string {
206 |   return [rootName, ...ancestors, command.name()].filter(Boolean).join(" ");
207 | }
208 | 
209 | function isHiddenCommand(command: Command): boolean {
210 |   return Boolean((command as CommandWithInternals)._hidden);
211 | }
212 | 
213 | function isHelpCommand(parent: Command, command: Command): boolean {
214 |   const helpCmd = (parent as CommandWithInternals)._helpCommand;
215 |   return helpCmd === command;
216 | }
217 | 
218 | function partitionOptions(options: Option[]): {
219 |   flags: Option[];
220 |   valueOptions: Option[];
221 | } {
222 |   const flags: Option[] = [];
223 |   const valueOptions: Option[] = [];
224 | 
225 |   options.forEach((option) => {
226 |     if (option.hidden) {
227 |       return;
228 |     }
229 | 
230 |     if (option.required || option.optional) {
231 |       valueOptions.push(option);
232 |     } else {
233 |       flags.push(option);
234 |     }
235 |   });
236 | 
237 |   return { flags, valueOptions };
238 | }
239 | 
240 | function buildUsage(command: Command): string {
241 |   return command.createHelp().commandUsage(command).trim();
242 | }
243 | 
244 | function formatOptionSignature(option: Option): string {
245 |   return option.flags.replace(/\s+/g, " ").trim();
246 | }
247 | 
248 | function extractOptionPlaceholder(option: Option): string {
249 |   const match = option.flags.match(/(<[^>]+>|\[[^\]]+\])/);
250 |   return match ? match[0] : "";
251 | }
252 | 
253 | function buildOptionUsage(commandPath: string, option: Option): string {
254 |   const preferred =
255 |     option.long || option.short || formatOptionSignature(option);
256 |   const placeholder = extractOptionPlaceholder(option);
257 |   const usage = [commandPath, preferred, placeholder]
258 |     .filter(Boolean)
259 |     .join(" ")
260 |     .replace(/\s+/g, " ")
261 |     .trim();
262 | 
263 |   return usage;
264 | }
265 | 
266 | function buildOptionDetails(option: Option): string[] {
267 |   const details: string[] = [];
268 | 
269 |   if (option.mandatory) {
270 |     details.push("Must be specified.");
271 |   }
272 | 
273 |   if (option.required) {
274 |     details.push("Requires a value.");
275 |   } else if (option.optional) {
276 |     details.push("Accepts an optional value.");
277 |   }
278 | 
279 |   if (option.defaultValueDescription) {
280 |     details.push(`Default: ${option.defaultValueDescription}.`);
281 |   } else if (option.defaultValue !== undefined) {
282 |     details.push(`Default: ${formatValue(option.defaultValue)}.`);
283 |   }
284 | 
285 |   if (option.argChoices && option.argChoices.length > 0) {
286 |     details.push(`Allowed values: ${option.argChoices.join(", ")}.`);
287 |   }
288 | 
289 |   if (option.envVar) {
290 |     details.push(`Environment variable: ${option.envVar}.`);
291 |   }
292 | 
293 |   if (option.presetArg !== undefined) {
294 |     details.push(`Preset value: ${formatValue(option.presetArg)}.`);
295 |   }
296 | 
297 |   return details;
298 | }
299 | 
300 | type BuildOptionEntriesArgs = {
301 |   options: Option[];
302 |   commandPath: string;
303 |   depth: number;
304 | };
305 | 
306 | function buildOptionEntries({
307 |   options,
308 |   commandPath,
309 |   depth,
310 | }: BuildOptionEntriesArgs): Content[] {
311 |   const nodes: Content[] = [];
312 |   const headingDepth = Math.min(depth + 1, 6);
313 | 
314 |   options.forEach((option) => {
315 |     const signature = formatOptionSignature(option);
316 |     nodes.push(createHeading(headingDepth, [createInlineCode(signature)]));
317 | 
318 |     nodes.push({
319 |       type: "code",
320 |       lang: "bash",
321 |       value: buildOptionUsage(commandPath, option),
322 |     });
323 | 
324 |     if (option.description) {
325 |       nodes.push(createParagraph(option.description));
326 |     }
327 | 
328 |     const details = buildOptionDetails(option);
329 |     if (details.length > 0) {
330 |       nodes.push(createParagraph(details.join(" ")));
331 |     }
332 |   });
333 | 
334 |   return nodes;
335 | }
336 | 
337 | function buildArgumentListItems(args: readonly Argument[]): ListItem[] {
338 |   return args.map((arg) => {
339 |     const children: PhrasingContent[] = [
340 |       createInlineCode(formatArgumentLabel(arg)),
341 |     ];
342 | 
343 |     if (arg.description) {
344 |       children.push({ type: "text", value: ` — ${arg.description}` });
345 |     }
346 | 
347 |     const details: string[] = [];
348 | 
349 |     if (arg.defaultValueDescription) {
350 |       details.push(`default: ${arg.defaultValueDescription}`);
351 |     } else if (arg.defaultValue !== undefined) {
352 |       details.push(`default: ${formatValue(arg.defaultValue)}`);
353 |     }
354 | 
355 |     if (arg.argChoices && arg.argChoices.length > 0) {
356 |       details.push(`choices: ${arg.argChoices.join(", ")}`);
357 |     }
358 | 
359 |     if (!arg.required) {
360 |       details.push("optional");
361 |     }
362 | 
363 |     if (details.length > 0) {
364 |       children.push({
365 |         type: "text",
366 |         value: ` (${details.join("; ")})`,
367 |       });
368 |     }
369 | 
370 |     return createListItem(children);
371 |   });
372 | }
373 | 
374 | type BuildCommandSectionOptions = {
375 |   command: Command;
376 |   rootName: string;
377 |   ancestors: string[];
378 |   depth: number;
379 |   useRootIntro: boolean;
380 | };
381 | 
382 | function buildCommandSection({
383 |   command,
384 |   rootName,
385 |   ancestors,
386 |   depth,
387 |   useRootIntro,
388 | }: BuildCommandSectionOptions): Content[] {
389 |   const nodes: Content[] = [];
390 |   const commandPath = getCommandPath(rootName, ancestors, command);
391 |   const isRootCommand = ancestors.length === 0;
392 |   const shouldUseIntro = isRootCommand && useRootIntro;
393 |   const headingContent = shouldUseIntro
394 |     ? "Introduction"
395 |     : [createInlineCode(commandPath)];
396 | 
397 |   nodes.push(createHeading(depth, headingContent));
398 | 
399 |   const description = command.description();
400 |   if (description) {
401 |     nodes.push(createParagraph(description));
402 |   }
403 | 
404 |   const usage = buildUsage(command);
405 |   if (usage) {
406 |     const sectionDepth = shouldUseIntro ? depth : Math.min(depth + 1, 6);
407 |     nodes.push(createHeading(sectionDepth, "Usage"));
408 |     nodes.push({
409 |       type: "paragraph",
410 |       children: [createInlineCode(usage)],
411 |     });
412 |   }
413 | 
414 |   const aliases = command.aliases();
415 |   if (aliases.length > 0) {
416 |     const sectionDepth = shouldUseIntro ? depth : Math.min(depth + 1, 6);
417 |     nodes.push(createHeading(sectionDepth, "Aliases"));
418 |     nodes.push(
419 |       createList(
420 |         aliases.map((alias) => createListItem([createInlineCode(alias)])),
421 |       ),
422 |     );
423 |   }
424 | 
425 |   const args = command.registeredArguments ?? [];
426 |   if (args.length > 0) {
427 |     const sectionDepth = shouldUseIntro ? depth : Math.min(depth + 1, 6);
428 |     nodes.push(createHeading(sectionDepth, "Arguments"));
429 |     nodes.push(createList(buildArgumentListItems(args)));
430 |   }
431 | 
432 |   const visibleOptions = command.options.filter((option) => !option.hidden);
433 |   if (visibleOptions.length > 0) {
434 |     const { flags, valueOptions } = partitionOptions(visibleOptions);
435 |     const sectionDepth = shouldUseIntro ? depth : Math.min(depth + 1, 6);
436 | 
437 |     if (valueOptions.length > 0) {
438 |       nodes.push(createHeading(sectionDepth, "Options"));
439 |       nodes.push(
440 |         ...buildOptionEntries({
441 |           options: valueOptions,
442 |           commandPath,
443 |           depth: sectionDepth,
444 |         }),
445 |       );
446 |     }
447 | 
448 |     if (flags.length > 0) {
449 |       nodes.push(createHeading(sectionDepth, "Flags"));
450 |       nodes.push(
451 |         ...buildOptionEntries({
452 |           options: flags,
453 |           commandPath,
454 |           depth: sectionDepth,
455 |         }),
456 |       );
457 |     }
458 |   }
459 | 
460 |   const subcommands = command.commands.filter(
461 |     (sub) =>
462 |       !isHiddenCommand(sub) &&
463 |       !isHelpCommand(command, sub) &&
464 |       sub.parent === command,
465 |   );
466 | 
467 |   if (subcommands.length > 0) {
468 |     const sectionDepth = shouldUseIntro ? depth : Math.min(depth + 1, 6);
469 |     nodes.push(createHeading(sectionDepth, "Subcommands"));
470 | 
471 |     subcommands.forEach((sub) => {
472 |       nodes.push(
473 |         ...buildCommandSection({
474 |           command: sub,
475 |           rootName,
476 |           ancestors: [...ancestors, command.name()],
477 |           depth: Math.min(sectionDepth + 1, 6),
478 |           useRootIntro,
479 |         }),
480 |       );
481 |     });
482 |   }
483 | 
484 |   return nodes;
485 | }
486 | 
487 | function toMarkdown(root: Root): string {
488 |   return unified().use(remarkStringify).stringify(root).trimEnd();
489 | }
490 | 
491 | async function formatWithPrettier(
492 |   content: string,
493 |   filePath: string,
494 | ): Promise<string> {
495 |   const config = await resolveConfig(filePath);
496 |   return prettierFormat(content, {
497 |     ...(config ?? {}),
498 |     filepath: filePath,
499 |   });
500 | }
501 | 
502 | type CommandDoc = {
503 |   fileName: string;
504 |   markdown: string;
505 |   mdx: string;
506 |   commandPath: string;
507 | };
508 | 
509 | type BuildCommandDocOptions = {
510 |   useRootIntro?: boolean;
511 | };
512 | 
513 | function buildCommandDoc(
514 |   command: Command,
515 |   rootName: string,
516 |   options?: BuildCommandDocOptions,
517 | ): CommandDoc {
518 |   const useRootIntro = options?.useRootIntro ?? true;
519 |   const commandPath = getCommandPath(rootName, [], command);
520 |   const title = commandPath;
521 |   const subtitle = `CLI reference docs for ${command.name()} command`;
522 |   const root: Root = {
523 |     type: "root",
524 |     children: buildCommandSection({
525 |       command,
526 |       rootName,
527 |       ancestors: [],
528 |       depth: 2,
529 |       useRootIntro,
530 |     }),
531 |   };
532 | 
533 |   const markdown = toMarkdown(root);
534 |   const frontmatter = [
535 |     FRONTMATTER_DELIMITER,
536 |     `title: ${formatYamlValue(title)}`,
537 |     `subtitle: ${formatYamlValue(subtitle)}`,
538 |     FRONTMATTER_DELIMITER,
539 |     "",
540 |   ].join("\n");
541 | 
542 |   const mdx = `${frontmatter}${markdown}\n`;
543 |   const fileName = `${slugifyCommandName(command.name())}.mdx`;
544 | 
545 |   return { fileName, markdown, mdx, commandPath };
546 | }
547 | 
548 | function buildIndexDoc(commands: Command[], rootName: string): CommandDoc {
549 |   const root: Root = {
550 |     type: "root",
551 |     children: [
552 |       createHeading(2, "Introduction"),
553 |       createParagraph(
554 |         `This page aggregates CLI reference docs for ${rootName} commands.`,
555 |       ),
556 |     ],
557 |   };
558 | 
559 |   commands.forEach((command) => {
560 |     root.children.push(
561 |       ...buildCommandSection({
562 |         command,
563 |         rootName,
564 |         ancestors: [],
565 |         depth: 2,
566 |         useRootIntro: false,
567 |       }),
568 |     );
569 |   });
570 | 
571 |   const markdown = toMarkdown(root);
572 |   const frontmatter = [
573 |     FRONTMATTER_DELIMITER,
574 |     `title: ${formatYamlValue(`${rootName} CLI reference`)}`,
575 |     "seo:",
576 |     "  noindex: true",
577 |     FRONTMATTER_DELIMITER,
578 |     "",
579 |   ].join("\n");
580 | 
581 |   const mdx = `${frontmatter}${markdown}\n`;
582 | 
583 |   return {
584 |     fileName: "index.mdx",
585 |     markdown,
586 |     mdx,
587 |     commandPath: `${rootName} (index)`,
588 |   };
589 | }
590 | 
591 | async function main(): Promise<void> {
592 |   const repoRoot = getRepoRoot();
593 |   const cli = await getProgram(repoRoot);
594 | 
595 |   const outputArg = process.argv[2];
596 | 
597 |   if (!outputArg) {
598 |     throw new Error(
599 |       "Output directory is required. Usage: generate-cli-docs <output-directory>",
600 |     );
601 |   }
602 | 
603 |   const outputDir = resolve(process.cwd(), outputArg);
604 |   await mkdir(outputDir, { recursive: true });
605 | 
606 |   const topLevelCommands = cli.commands.filter(
607 |     (command) => command.parent === cli && !isHiddenCommand(command),
608 |   );
609 | 
610 |   if (topLevelCommands.length === 0) {
611 |     console.warn("No top-level commands found. Nothing to document.");
612 |     return;
613 |   }
614 | 
615 |   const docs = topLevelCommands.map((command) =>
616 |     buildCommandDoc(command, cli.name()),
617 |   );
618 |   const indexDoc = buildIndexDoc(topLevelCommands, cli.name());
619 | 
620 |   for (const doc of [...docs, indexDoc]) {
621 |     const filePath = join(outputDir, doc.fileName);
622 |     await mkdir(dirname(filePath), { recursive: true });
623 |     const formatted = await formatWithPrettier(doc.mdx, filePath);
624 |     await writeFile(filePath, formatted, "utf8");
625 |     console.log(`✅ Saved ${doc.commandPath} docs to ${filePath}`);
626 |   }
627 | 
628 |   if (process.env.GITHUB_ACTIONS) {
629 |     const commentMarker = "<!-- generate-cli-docs -->";
630 |     const combinedMarkdown = docs
631 |       .map((doc) => doc.markdown)
632 |       .join("\n\n---\n\n");
633 | 
634 |     const commentBody = [
635 |       commentMarker,
636 |       "",
637 |       "Your PR updates Lingo.dev CLI behavior. Please review the regenerated reference docs below.",
638 |       "",
639 |       combinedMarkdown,
640 |     ].join("\n");
641 | 
642 |     await createOrUpdateGitHubComment({
643 |       commentMarker,
644 |       body: commentBody,
645 |     });
646 |   }
647 | }
648 | 
649 | main().catch((err) => {
650 |   console.error(err);
651 |   process.exit(1);
652 | });
653 | 
```

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

```typescript
  1 | /**
  2 |  * ICU MessageFormat conversion utilities for xcstrings pluralization
  3 |  *
  4 |  * This module handles converting between xcstrings plural format and ICU MessageFormat,
  5 |  * preserving format specifier precision and supporting multiple variables.
  6 |  */
  7 | 
  8 | /**
  9 |  * Type guard marker to distinguish ICU objects from user data
 10 |  * Using a symbol ensures no collision with user data
 11 |  */
 12 | const ICU_TYPE_MARKER = Symbol.for("@lingo.dev/icu-plural-object");
 13 | 
 14 | export interface PluralWithMetadata {
 15 |   icu: string;
 16 |   _meta?: {
 17 |     variables: {
 18 |       [varName: string]: {
 19 |         format: string;
 20 |         role: "plural" | "other";
 21 |       };
 22 |     };
 23 |   };
 24 |   // Type marker for robust detection
 25 |   [ICU_TYPE_MARKER]?: true;
 26 | }
 27 | 
 28 | /**
 29 |  * CLDR plural categories as defined by Unicode
 30 |  * https://unicode-org.github.io/cldr-staging/charts/latest/supplemental/language_plural_rules.html
 31 |  */
 32 | const CLDR_PLURAL_CATEGORIES = new Set([
 33 |   "zero",
 34 |   "one",
 35 |   "two",
 36 |   "few",
 37 |   "many",
 38 |   "other",
 39 | ]);
 40 | 
 41 | /**
 42 |  * Type guard to check if a value is a valid ICU object with metadata
 43 |  * This is more robust than simple key checking
 44 |  */
 45 | export function isICUPluralObject(value: any): value is PluralWithMetadata {
 46 |   if (!value || typeof value !== "object" || Array.isArray(value)) {
 47 |     return false;
 48 |   }
 49 | 
 50 |   // Check for type marker (most reliable)
 51 |   if (ICU_TYPE_MARKER in value) {
 52 |     return true;
 53 |   }
 54 | 
 55 |   // Fallback: validate structure thoroughly
 56 |   if (!("icu" in value) || typeof value.icu !== "string") {
 57 |     return false;
 58 |   }
 59 | 
 60 |   // Must match ICU plural format pattern
 61 |   const icuPluralPattern = /^\{[\w]+,\s*plural,\s*.+\}$/;
 62 |   if (!icuPluralPattern.test(value.icu)) {
 63 |     return false;
 64 |   }
 65 | 
 66 |   // If _meta exists, validate its structure
 67 |   if (value._meta !== undefined) {
 68 |     if (
 69 |       typeof value._meta !== "object" ||
 70 |       !value._meta.variables ||
 71 |       typeof value._meta.variables !== "object"
 72 |     ) {
 73 |       return false;
 74 |     }
 75 | 
 76 |     // Validate each variable entry
 77 |     for (const [varName, varMeta] of Object.entries(value._meta.variables)) {
 78 |       if (
 79 |         !varMeta ||
 80 |         typeof varMeta !== "object" ||
 81 |         typeof (varMeta as any).format !== "string" ||
 82 |         ((varMeta as any).role !== "plural" &&
 83 |           (varMeta as any).role !== "other")
 84 |       ) {
 85 |         return false;
 86 |       }
 87 |     }
 88 |   }
 89 | 
 90 |   return true;
 91 | }
 92 | 
 93 | /**
 94 |  * Type guard to check if an object is a valid plural forms object
 95 |  * Ensures ALL keys are CLDR categories to avoid false positives
 96 |  */
 97 | export function isPluralFormsObject(
 98 |   value: any,
 99 | ): value is Record<string, string> {
100 |   if (!value || typeof value !== "object" || Array.isArray(value)) {
101 |     return false;
102 |   }
103 | 
104 |   const keys = Object.keys(value);
105 | 
106 |   // Must have at least one key
107 |   if (keys.length === 0) {
108 |     return false;
109 |   }
110 | 
111 |   // Check if ALL keys are CLDR plural categories
112 |   const allKeysAreCldr = keys.every((key) => CLDR_PLURAL_CATEGORIES.has(key));
113 | 
114 |   if (!allKeysAreCldr) {
115 |     return false;
116 |   }
117 | 
118 |   // Check if all values are strings
119 |   const allValuesAreStrings = keys.every(
120 |     (key) => typeof value[key] === "string",
121 |   );
122 | 
123 |   if (!allValuesAreStrings) {
124 |     return false;
125 |   }
126 | 
127 |   // Must have at least "other" form (required in all locales)
128 |   if (!("other" in value)) {
129 |     return false;
130 |   }
131 | 
132 |   return true;
133 | }
134 | 
135 | /**
136 |  * Get required CLDR plural categories for a locale
137 |  *
138 |  * @throws {Error} If locale is invalid and cannot be resolved
139 |  */
140 | function getRequiredPluralCategories(locale: string): string[] {
141 |   try {
142 |     const pluralRules = new Intl.PluralRules(locale);
143 |     const categories = pluralRules.resolvedOptions().pluralCategories;
144 | 
145 |     if (!categories || categories.length === 0) {
146 |       throw new Error(`No plural categories found for locale: ${locale}`);
147 |     }
148 | 
149 |     return categories;
150 |   } catch (error) {
151 |     // Log warning but use safe fallback
152 |     console.warn(
153 |       `[xcode-xcstrings-icu] Failed to resolve plural categories for locale "${locale}". ` +
154 |         `Using fallback ["one", "other"]. Error: ${error instanceof Error ? error.message : String(error)}`,
155 |     );
156 |     return ["one", "other"];
157 |   }
158 | }
159 | 
160 | /**
161 |  * Map CLDR category names to their numeric values for exact match conversion
162 |  */
163 | const CLDR_CATEGORY_TO_NUMBER: Record<string, number> = {
164 |   zero: 0,
165 |   one: 1,
166 |   two: 2,
167 | };
168 | 
169 | /**
170 |  * Map numeric values back to CLDR category names
171 |  */
172 | const NUMBER_TO_CLDR_CATEGORY: Record<number, string> = {
173 |   0: "zero",
174 |   1: "one",
175 |   2: "two",
176 | };
177 | 
178 | /**
179 |  * Convert xcstrings plural forms to ICU MessageFormat with metadata
180 |  *
181 |  * @param pluralForms - Record of plural forms (e.g., { one: "1 item", other: "%d items" })
182 |  * @param sourceLocale - Source language locale (e.g., "en", "ru") to determine required vs optional forms
183 |  * @returns ICU string with metadata for format preservation
184 |  *
185 |  * @example
186 |  * xcstringsToPluralWithMeta({ one: "1 mile", other: "%.1f miles" }, "en")
187 |  * // Returns:
188 |  * // {
189 |  * //   icu: "{count, plural, one {1 mile} other {# miles}}",
190 |  * //   _meta: { variables: { count: { format: "%.1f", role: "plural" } } }
191 |  * // }
192 |  *
193 |  * @example
194 |  * xcstringsToPluralWithMeta({ zero: "No items", one: "1 item", other: "%d items" }, "en")
195 |  * // Returns:
196 |  * // {
197 |  * //   icu: "{count, plural, =0 {No items} one {1 item} other {# items}}",
198 |  * //   _meta: { variables: { count: { format: "%d", role: "plural" } } }
199 |  * // }
200 |  */
201 | export function xcstringsToPluralWithMeta(
202 |   pluralForms: Record<string, string>,
203 |   sourceLocale: string = "en",
204 | ): PluralWithMetadata {
205 |   if (!pluralForms || Object.keys(pluralForms).length === 0) {
206 |     throw new Error("pluralForms cannot be empty");
207 |   }
208 | 
209 |   // Get required CLDR categories for this locale
210 |   const requiredCategories = getRequiredPluralCategories(sourceLocale);
211 | 
212 |   const variables: Record<
213 |     string,
214 |     { format: string; role: "plural" | "other" }
215 |   > = {};
216 | 
217 |   // Regex to match format specifiers:
218 |   // %[position$][flags][width][.precision][length]specifier
219 |   // Examples: %d, %lld, %.2f, %@, %1$@, %2$lld
220 |   const formatRegex =
221 |     /(%(?:(\d+)\$)?(?:[+-])?(?:\d+)?(?:\.(\d+))?([lhqLzjt]*)([diuoxXfFeEgGaAcspn@]))/g;
222 | 
223 |   // Analyze ALL forms to find the one with most variables (typically "other")
224 |   let maxMatches: RegExpMatchArray[] = [];
225 |   let maxMatchText = "";
226 |   for (const [form, text] of Object.entries(pluralForms)) {
227 |     // Skip if text is not a string
228 |     if (typeof text !== "string") {
229 |       console.warn(
230 |         `Warning: Plural form "${form}" has non-string value:`,
231 |         text,
232 |       );
233 |       continue;
234 |     }
235 |     const matches = [...text.matchAll(formatRegex)];
236 |     if (matches.length > maxMatches.length) {
237 |       maxMatches = matches;
238 |       maxMatchText = text;
239 |     }
240 |   }
241 | 
242 |   let lastNumericIndex = -1;
243 | 
244 |   // Find which variable is the plural one (heuristic: last numeric format)
245 |   maxMatches.forEach((match, idx) => {
246 |     const specifier = match[5];
247 |     // Numeric specifiers that could be plural counts
248 |     if (/[diuoxXfFeE]/.test(specifier)) {
249 |       lastNumericIndex = idx;
250 |     }
251 |   });
252 | 
253 |   // Build variable metadata
254 |   let nonPluralCounter = 0;
255 |   maxMatches.forEach((match, idx) => {
256 |     const fullFormat = match[1]; // e.g., "%.2f", "%lld", "%@"
257 |     const position = match[2]; // e.g., "1" from "%1$@"
258 |     const precision = match[3]; // e.g., "2" from "%.2f"
259 |     const lengthMod = match[4]; // e.g., "ll" from "%lld"
260 |     const specifier = match[5]; // e.g., "f", "d", "@"
261 | 
262 |     const isPluralVar = idx === lastNumericIndex;
263 |     const varName = isPluralVar ? "count" : `var${nonPluralCounter++}`;
264 | 
265 |     variables[varName] = {
266 |       format: fullFormat,
267 |       role: isPluralVar ? "plural" : "other",
268 |     };
269 |   });
270 | 
271 |   // Build ICU string for each plural form
272 |   const variableKeys = Object.keys(variables);
273 |   const icuForms = Object.entries(pluralForms)
274 |     .filter(([form, text]) => {
275 |       // Skip non-string values
276 |       if (typeof text !== "string") {
277 |         return false;
278 |       }
279 |       return true;
280 |     })
281 |     .map(([form, text]) => {
282 |       let processed = text as string;
283 |       let vIdx = 0;
284 | 
285 |       // Replace format specifiers with ICU equivalents
286 |       processed = processed.replace(formatRegex, () => {
287 |         if (vIdx >= variableKeys.length) {
288 |           // Shouldn't happen, but fallback
289 |           vIdx++;
290 |           return "#";
291 |         }
292 | 
293 |         const varName = variableKeys[vIdx];
294 |         const varMeta = variables[varName];
295 |         vIdx++;
296 | 
297 |         if (varMeta.role === "plural") {
298 |           // Plural variable uses # in ICU
299 |           return "#";
300 |         } else {
301 |           // Non-plural variables use {varName}
302 |           return `{${varName}}`;
303 |         }
304 |       });
305 | 
306 |       // Determine if this form is required or optional
307 |       const isRequired = requiredCategories.includes(form);
308 |       const formKey =
309 |         !isRequired && form in CLDR_CATEGORY_TO_NUMBER
310 |           ? `=${CLDR_CATEGORY_TO_NUMBER[form]}` // Convert optional forms to exact matches
311 |           : form; // Keep required forms as CLDR keywords
312 | 
313 |       return `${formKey} {${processed}}`;
314 |     })
315 |     .join(" ");
316 | 
317 |   // Find plural variable name
318 |   const pluralVarName =
319 |     Object.keys(variables).find((name) => variables[name].role === "plural") ||
320 |     "count";
321 | 
322 |   const icu = `{${pluralVarName}, plural, ${icuForms}}`;
323 | 
324 |   const result: PluralWithMetadata = {
325 |     icu,
326 |     _meta: Object.keys(variables).length > 0 ? { variables } : undefined,
327 |     [ICU_TYPE_MARKER]: true, // Add type marker for robust detection
328 |   };
329 | 
330 |   return result;
331 | }
332 | 
333 | /**
334 |  * Convert ICU MessageFormat with metadata back to xcstrings plural forms
335 |  *
336 |  * Uses metadata to restore original format specifiers with full precision.
337 |  *
338 |  * @param data - ICU string with metadata
339 |  * @returns Record of plural forms suitable for xcstrings
340 |  *
341 |  * @example
342 |  * pluralWithMetaToXcstrings({
343 |  *   icu: "{count, plural, one {# километр} other {# километров}}",
344 |  *   _meta: { variables: { count: { format: "%.1f", role: "plural" } } }
345 |  * })
346 |  * // Returns: { one: "%.1f километр", other: "%.1f километров" }
347 |  */
348 | export function pluralWithMetaToXcstrings(
349 |   data: PluralWithMetadata,
350 | ): Record<string, string> {
351 |   if (!data.icu) {
352 |     throw new Error("ICU string is required");
353 |   }
354 | 
355 |   // Parse ICU MessageFormat string
356 |   const ast = parseICU(data.icu);
357 | 
358 |   if (!ast || ast.length === 0) {
359 |     throw new Error("Invalid ICU format");
360 |   }
361 | 
362 |   // Find the plural node
363 |   const pluralNode = ast.find((node) => node.type === "plural");
364 | 
365 |   if (!pluralNode) {
366 |     throw new Error("No plural found in ICU format");
367 |   }
368 | 
369 |   const forms: Record<string, string> = {};
370 | 
371 |   // Convert each plural form back to xcstrings format
372 |   for (const [form, option] of Object.entries(pluralNode.options)) {
373 |     let text = "";
374 | 
375 |     const optionValue = (option as any).value;
376 |     for (const element of optionValue) {
377 |       if (element.type === "literal") {
378 |         // Plain text
379 |         text += element.value;
380 |       } else if (element.type === "pound") {
381 |         // # → look up plural variable format in metadata
382 |         const pluralVar = Object.entries(data._meta?.variables || {}).find(
383 |           ([_, meta]) => meta.role === "plural",
384 |         );
385 | 
386 |         text += pluralVar?.[1].format || "%lld";
387 |       } else if (element.type === "argument") {
388 |         // {varName} → look up variable format by name
389 |         const varName = element.value;
390 |         const varMeta = data._meta?.variables?.[varName];
391 | 
392 |         text += varMeta?.format || "%@";
393 |       }
394 |     }
395 | 
396 |     // Convert exact matches (=0, =1) back to CLDR category names
397 |     let xcstringsFormName = form;
398 |     if (form.startsWith("=")) {
399 |       const numValue = parseInt(form.substring(1), 10);
400 |       xcstringsFormName = NUMBER_TO_CLDR_CATEGORY[numValue] || form;
401 |     }
402 | 
403 |     forms[xcstringsFormName] = text;
404 |   }
405 | 
406 |   return forms;
407 | }
408 | 
409 | /**
410 |  * Simple ICU MessageFormat parser
411 |  *
412 |  * This is a lightweight parser for our specific use case.
413 |  * For production, consider using @formatjs/icu-messageformat-parser
414 |  */
415 | function parseICU(icu: string): any[] {
416 |   // Remove outer braces and split by "plural,"
417 |   const match = icu.match(/\{(\w+),\s*plural,\s*(.+)\}$/);
418 | 
419 |   if (!match) {
420 |     throw new Error("Invalid ICU plural format");
421 |   }
422 | 
423 |   const varName = match[1];
424 |   const formsText = match[2];
425 | 
426 |   // Parse plural forms manually to handle nested braces
427 |   const options: Record<string, any> = {};
428 | 
429 |   let i = 0;
430 |   while (i < formsText.length) {
431 |     // Skip whitespace
432 |     while (i < formsText.length && /\s/.test(formsText[i])) {
433 |       i++;
434 |     }
435 | 
436 |     if (i >= formsText.length) break;
437 | 
438 |     // Read form name (e.g., "one", "other", "few", "=0", "=1")
439 |     let formName = "";
440 | 
441 |     // Check for exact match syntax (=0, =1, etc.)
442 |     if (formsText[i] === "=") {
443 |       formName += formsText[i];
444 |       i++;
445 |       // Read the number
446 |       while (i < formsText.length && /\d/.test(formsText[i])) {
447 |         formName += formsText[i];
448 |         i++;
449 |       }
450 |     } else {
451 |       // Read word form name
452 |       while (i < formsText.length && /\w/.test(formsText[i])) {
453 |         formName += formsText[i];
454 |         i++;
455 |       }
456 |     }
457 | 
458 |     if (!formName) break;
459 | 
460 |     // Skip whitespace and find opening brace
461 |     while (i < formsText.length && /\s/.test(formsText[i])) {
462 |       i++;
463 |     }
464 | 
465 |     if (i >= formsText.length || formsText[i] !== "{") {
466 |       throw new Error(`Expected '{' after form name '${formName}'`);
467 |     }
468 | 
469 |     // Find matching closing brace
470 |     i++; // skip opening brace
471 |     let braceCount = 1;
472 |     let formText = "";
473 | 
474 |     while (i < formsText.length && braceCount > 0) {
475 |       if (formsText[i] === "{") {
476 |         braceCount++;
477 |         formText += formsText[i];
478 |       } else if (formsText[i] === "}") {
479 |         braceCount--;
480 |         if (braceCount > 0) {
481 |           formText += formsText[i];
482 |         }
483 |       } else {
484 |         formText += formsText[i];
485 |       }
486 |       i++;
487 |     }
488 | 
489 |     if (braceCount !== 0) {
490 |       // Provide detailed error with context
491 |       const preview = formsText.substring(
492 |         Math.max(0, i - 50),
493 |         Math.min(formsText.length, i + 50),
494 |       );
495 |       throw new Error(
496 |         `Unclosed brace for form '${formName}' in ICU MessageFormat.\n` +
497 |           `Expected ${braceCount} more closing brace(s).\n` +
498 |           `Context: ...${preview}...\n` +
499 |           `Full ICU: {${varName}, plural, ${formsText}}`,
500 |       );
501 |     }
502 | 
503 |     // Parse the form text to extract elements
504 |     const elements = parseFormText(formText);
505 | 
506 |     options[formName] = {
507 |       value: elements,
508 |     };
509 |   }
510 | 
511 |   return [
512 |     {
513 |       type: "plural",
514 |       value: varName,
515 |       options,
516 |     },
517 |   ];
518 | }
519 | 
520 | /**
521 |  * Parse form text into elements (literals, pounds, arguments)
522 |  */
523 | function parseFormText(text: string): any[] {
524 |   const elements: any[] = [];
525 |   let currentText = "";
526 |   let i = 0;
527 | 
528 |   while (i < text.length) {
529 |     if (text[i] === "#") {
530 |       // Add accumulated text as literal
531 |       if (currentText) {
532 |         elements.push({ type: "literal", value: currentText });
533 |         currentText = "";
534 |       }
535 |       // Add pound element
536 |       elements.push({ type: "pound" });
537 |       i++;
538 |     } else if (text[i] === "{") {
539 |       // Variable reference - need to handle nested braces
540 |       // Add accumulated text as literal
541 |       if (currentText) {
542 |         elements.push({ type: "literal", value: currentText });
543 |         currentText = "";
544 |       }
545 | 
546 |       // Find matching closing brace (handle nesting)
547 |       let braceCount = 1;
548 |       let j = i + 1;
549 |       while (j < text.length && braceCount > 0) {
550 |         if (text[j] === "{") {
551 |           braceCount++;
552 |         } else if (text[j] === "}") {
553 |           braceCount--;
554 |         }
555 |         j++;
556 |       }
557 | 
558 |       if (braceCount !== 0) {
559 |         throw new Error("Unclosed variable reference");
560 |       }
561 | 
562 |       // j is now positioned after the closing brace
563 |       const varName = text.slice(i + 1, j - 1);
564 |       elements.push({ type: "argument", value: varName });
565 | 
566 |       i = j;
567 |     } else {
568 |       currentText += text[i];
569 |       i++;
570 |     }
571 |   }
572 | 
573 |   // Add remaining text
574 |   if (currentText) {
575 |     elements.push({ type: "literal", value: currentText });
576 |   }
577 | 
578 |   return elements;
579 | }
580 | 
```

--------------------------------------------------------------------------------
/packages/compiler/CHANGELOG.md:
--------------------------------------------------------------------------------

```markdown
  1 | # @lingo.dev/\_compiler
  2 | 
  3 | ## 0.7.15
  4 | 
  5 | ### Patch Changes
  6 | 
  7 | - [#1231](https://github.com/lingodotdev/lingo.dev/pull/1231) [`44a928b`](https://github.com/lingodotdev/lingo.dev/commit/44a928b473802cd07bec64f94a273ee1b845a0d0) Thanks [@davidturnbull](https://github.com/davidturnbull)! - Compiler now throws errors instead of abruptly exiting the process, allowing parent applications to handle errors gracefully
  8 | 
  9 | ## 0.7.14
 10 | 
 11 | ### Patch Changes
 12 | 
 13 | - Updated dependencies [[`b45347c`](https://github.com/lingodotdev/lingo.dev/commit/b45347c38572ee371b2bc494261b7e3e90c4aed1)]:
 14 |   - @lingo.dev/[email protected]
 15 |   - @lingo.dev/[email protected]
 16 | 
 17 | ## 0.7.13
 18 | 
 19 | ### Patch Changes
 20 | 
 21 | - [#1222](https://github.com/lingodotdev/lingo.dev/pull/1222) [`38139c8`](https://github.com/lingodotdev/lingo.dev/commit/38139c81a85001739cece60873c0c6ad711327a4) Thanks [@vrcprl](https://github.com/vrcprl)! - fix regex replacement
 22 | 
 23 | ## 0.7.12
 24 | 
 25 | ### Patch Changes
 26 | 
 27 | - Updated dependencies [[`82f5e7c`](https://github.com/lingodotdev/lingo.dev/commit/82f5e7cdde9a2a15b4c2a7fcb8c67ed64eab596b), [`e858174`](https://github.com/lingodotdev/lingo.dev/commit/e858174fd5165e0ea3e3f25fa1fc3edb292bc58f)]:
 28 |   - @lingo.dev/[email protected]
 29 |   - @lingo.dev/[email protected]
 30 | 
 31 | ## 0.7.11
 32 | 
 33 | ### Patch Changes
 34 | 
 35 | - Updated dependencies [[`1fa218c`](https://github.com/lingodotdev/lingo.dev/commit/1fa218c13bf90df6d175fb18264f59c1a10b967c)]:
 36 |   - @lingo.dev/[email protected]
 37 |   - @lingo.dev/[email protected]
 38 | 
 39 | ## 0.7.10
 40 | 
 41 | ### Patch Changes
 42 | 
 43 | - Updated dependencies [[`bbc71b9`](https://github.com/lingodotdev/lingo.dev/commit/bbc71b9948ccc289c9669d8b0c276c9596f6a5e7)]:
 44 |   - @lingo.dev/[email protected]
 45 |   - @lingo.dev/[email protected]
 46 | 
 47 | ## 0.7.9
 48 | 
 49 | ### Patch Changes
 50 | 
 51 | - Updated dependencies [[`6579d70`](https://github.com/lingodotdev/lingo.dev/commit/6579d70bc670c2fdc06c09842d931b07e134151c)]:
 52 |   - @lingo.dev/[email protected]
 53 |   - @lingo.dev/[email protected]
 54 | 
 55 | ## 0.7.8
 56 | 
 57 | ### Patch Changes
 58 | 
 59 | - Updated dependencies [[`a35032e`](https://github.com/lingodotdev/lingo.dev/commit/a35032e7e7a188d1f5e774576352068124526e24)]:
 60 |   - @lingo.dev/[email protected]
 61 |   - @lingo.dev/[email protected]
 62 | 
 63 | ## 0.7.7
 64 | 
 65 | ### Patch Changes
 66 | 
 67 | - [#1130](https://github.com/lingodotdev/lingo.dev/pull/1130) [`bc7b08e`](https://github.com/lingodotdev/lingo.dev/commit/bc7b08ef1245d1af0c68813cb18193d4f14bc7e0) Thanks [@mathio](https://github.com/mathio)! - dictionary path calculation
 68 | 
 69 | ## 0.7.6
 70 | 
 71 | ### Patch Changes
 72 | 
 73 | - [#1121](https://github.com/lingodotdev/lingo.dev/pull/1121) [`b6071e4`](https://github.com/lingodotdev/lingo.dev/commit/b6071e4f19dd1823f4f2ce54ba5495538a94d4fd) Thanks [@mathio](https://github.com/mathio)! - compiler: prevent duplicate props
 74 | 
 75 | ## 0.7.5
 76 | 
 77 | ### Patch Changes
 78 | 
 79 | - [#1118](https://github.com/lingodotdev/lingo.dev/pull/1118) [`410825c`](https://github.com/lingodotdev/lingo.dev/commit/410825c8bf0029d8ee458514d6f203a7397c8f22) Thanks [@mathio](https://github.com/mathio)! - support Turbopack in Next.js v14 by Compiler
 80 | 
 81 | - [#1116](https://github.com/lingodotdev/lingo.dev/pull/1116) [`bc419ae`](https://github.com/lingodotdev/lingo.dev/commit/bc419aeeb4211d80d3c0ddd65deeab62ad68fea8) Thanks [@devin-ai-integration](https://github.com/apps/devin-ai-integration)! - fix: move vitest from dependencies to devDependencies
 82 | 
 83 | ## 0.7.4
 84 | 
 85 | ### Patch Changes
 86 | 
 87 | - [#1072](https://github.com/lingodotdev/lingo.dev/pull/1072) [`3cb1ebe`](https://github.com/lingodotdev/lingo.dev/commit/3cb1ebec5441882678ab30a7d1b532bc2fc397b6) Thanks [@The-Best-Codes](https://github.com/The-Best-Codes)! - Fixed compiler handling of namespace imports (import \* as React from "react") and default imports.
 88 | 
 89 | ## 0.7.3
 90 | 
 91 | ### Patch Changes
 92 | 
 93 | - Updated dependencies [[`6af91a0`](https://github.com/lingodotdev/lingo.dev/commit/6af91a083d16f85051fb49a4034789abe784017e), [`6af91a0`](https://github.com/lingodotdev/lingo.dev/commit/6af91a083d16f85051fb49a4034789abe784017e)]:
 94 |   - @lingo.dev/[email protected]
 95 |   - @lingo.dev/[email protected]
 96 | 
 97 | ## 0.7.2
 98 | 
 99 | ### Patch Changes
100 | 
101 | - Updated dependencies [[`85dfc10`](https://github.com/lingodotdev/lingo.dev/commit/85dfc10961b116e31b2bb478f42013756ca49974)]:
102 |   - @lingo.dev/[email protected]
103 | 
104 | ## 0.7.1
105 | 
106 | ### Patch Changes
107 | 
108 | - [#1040](https://github.com/lingodotdev/lingo.dev/pull/1040) [`f897a7d`](https://github.com/lingodotdev/lingo.dev/commit/f897a7d0a3f7a236fb64f19bce9a8d00626d09ca) Thanks [@The-Best-Codes](https://github.com/The-Best-Codes)! - Fixed the compiler to handle type-only react imports.
109 | 
110 | ## 0.7.0
111 | 
112 | ### Minor Changes
113 | 
114 | - [#997](https://github.com/lingodotdev/lingo.dev/pull/997) [`bd9538a`](https://github.com/lingodotdev/lingo.dev/commit/bd9538ac6eba0ffc91ffc1fef5db6366c13e9e06) Thanks [@VAIBHAVSING](https://github.com/VAIBHAVSING)! - ### Whitespace Normalization Fix
115 | 
116 |   - Improved `normalizeJsxWhitespace` logic to preserve leading spaces inside JSX elements while removing unnecessary formatting whitespace and extra lines.
117 |   - Ensured explicit whitespace (e.g., `{" "}`) is handled correctly without introducing double spaces.
118 |   - Added targeted tests (`jsx-content-whitespace.spec.ts`) to verify whitespace handling.
119 |   - Cleaned up unnecessary debug/test files created during development.
120 | 
121 | ## 0.6.3
122 | 
123 | ### Patch Changes
124 | 
125 | - Updated dependencies [[`afbb978`](https://github.com/lingodotdev/lingo.dev/commit/afbb978fec83d574f2c43b7d68457e435fca9b57)]:
126 |   - @lingo.dev/[email protected]
127 |   - @lingo.dev/[email protected]
128 | 
129 | ## 0.6.2
130 | 
131 | ### Patch Changes
132 | 
133 | - [#1023](https://github.com/lingodotdev/lingo.dev/pull/1023) [`9266fd0`](https://github.com/lingodotdev/lingo.dev/commit/9266fd0bcddf4b07ca51d2609af92a9473106f9d) Thanks [@devin-ai-integration](https://github.com/apps/devin-ai-integration)! - Update Zod dependency to version 3.25.76
134 | 
135 | - Updated dependencies [[`9266fd0`](https://github.com/lingodotdev/lingo.dev/commit/9266fd0bcddf4b07ca51d2609af92a9473106f9d)]:
136 |   - @lingo.dev/[email protected]
137 |   - @lingo.dev/[email protected]
138 | 
139 | ## 0.6.1
140 | 
141 | ### Patch Changes
142 | 
143 | - [#1021](https://github.com/lingodotdev/lingo.dev/pull/1021) [`6baa1a7`](https://github.com/lingodotdev/lingo.dev/commit/6baa1a7e88dbfac3783d1d49695595077fd8d209) Thanks [@mathio](https://github.com/mathio)! - add lingo.dev provider details
144 | 
145 | ## 0.6.0
146 | 
147 | ### Minor Changes
148 | 
149 | - [#1010](https://github.com/lingodotdev/lingo.dev/pull/1010) [`864c305`](https://github.com/lingodotdev/lingo.dev/commit/864c30586510e6b69739c20fa42efdf45d8881ed) Thanks [@davidturnbull](https://github.com/davidturnbull)! - improve type safety of compiler params
150 | 
151 | ### Patch Changes
152 | 
153 | - Updated dependencies [[`cb2aa0f`](https://github.com/lingodotdev/lingo.dev/commit/cb2aa0f505d6b7dbc435b526e8a6f62265d1f453)]:
154 |   - @lingo.dev/[email protected]
155 | 
156 | ## 0.5.5
157 | 
158 | ### Patch Changes
159 | 
160 | - [#1011](https://github.com/lingodotdev/lingo.dev/pull/1011) [`bfcb424`](https://github.com/lingodotdev/lingo.dev/commit/bfcb424eb4479d0d3b767e062d30f02c5bcaeb14) Thanks [@mathio](https://github.com/mathio)! - replace elements with dot in name
161 | 
162 | ## 0.5.4
163 | 
164 | ### Patch Changes
165 | 
166 | - [#1002](https://github.com/lingodotdev/lingo.dev/pull/1002) [`2b297ba`](https://github.com/lingodotdev/lingo.dev/commit/2b297babe76f9799c5154d9421fecd1ebbe1bb72) Thanks [@mathio](https://github.com/mathio)! - support custom prompts in compiler
167 | 
168 | ## 0.5.3
169 | 
170 | ### Patch Changes
171 | 
172 | - Updated dependencies []:
173 |   - @lingo.dev/[email protected]
174 | 
175 | ## 0.5.2
176 | 
177 | ### Patch Changes
178 | 
179 | - Updated dependencies []:
180 |   - @lingo.dev/[email protected]
181 | 
182 | ## 0.5.1
183 | 
184 | ### Patch Changes
185 | 
186 | - [#972](https://github.com/lingodotdev/lingo.dev/pull/972) [`b249484`](https://github.com/lingodotdev/lingo.dev/commit/b249484d6f0060e29cd5b50b3d8ce68b857ccad5) Thanks [@mathio](https://github.com/mathio)! - support components with dot in name
187 | 
188 | ## 0.5.0
189 | 
190 | ### Minor Changes
191 | 
192 | - [#958](https://github.com/lingodotdev/lingo.dev/pull/958) [`84fd214`](https://github.com/lingodotdev/lingo.dev/commit/84fd214a21766e7683c5d645fcb8c4c0162eb0b6) Thanks [@chrissiwaffler](https://github.com/chrissiwaffler)! - feat: add Mistral AI as a supported LLM provider
193 | 
194 |   - Added Mistral AI provider support across the entire lingo.dev ecosystem
195 |   - Users can now use Mistral models for localization by setting MISTRAL_API_KEY
196 |   - Supports all Mistral models available through the @ai-sdk/mistral package
197 |   - Configuration via environment variable or user-wide config: `npx lingo.dev@latest config set llm.mistralApiKey <key>`
198 | 
199 | ### Patch Changes
200 | 
201 | - Updated dependencies []:
202 |   - @lingo.dev/[email protected]
203 | 
204 | ## 0.4.1
205 | 
206 | ### Patch Changes
207 | 
208 | - Updated dependencies []:
209 |   - @lingo.dev/[email protected]
210 | 
211 | ## 0.4.0
212 | 
213 | ### Minor Changes
214 | 
215 | - [#932](https://github.com/lingodotdev/lingo.dev/pull/932) [`1bba8ee`](https://github.com/lingodotdev/lingo.dev/commit/1bba8eed6272ae166ceb9b92963404bfe90a4aaa) Thanks [@The-Best-Codes](https://github.com/The-Best-Codes)! - Add support for Next.js Turbopack with the Lingo.dev compiler.
216 | 
217 | ## 0.3.5
218 | 
219 | ### Patch Changes
220 | 
221 | - [#947](https://github.com/lingodotdev/lingo.dev/pull/947) [`d80285a`](https://github.com/lingodotdev/lingo.dev/commit/d80285a9b12bd85425564cb00e558812fd0aee40) Thanks [@mathio](https://github.com/mathio)! - remove local variable cache
222 | 
223 | ## 0.3.4
224 | 
225 | ### Patch Changes
226 | 
227 | - [#937](https://github.com/lingodotdev/lingo.dev/pull/937) [`4e5983d`](https://github.com/lingodotdev/lingo.dev/commit/4e5983d7e59ebf9eb529c4b7c1c87689432ac873) Thanks [@devin-ai-integration](https://github.com/apps/devin-ai-integration)! - Update documentation URLs from docs.lingo.dev to lingo.dev/cli and lingo.dev/compiler
228 | 
229 | - Updated dependencies [[`4e5983d`](https://github.com/lingodotdev/lingo.dev/commit/4e5983d7e59ebf9eb529c4b7c1c87689432ac873)]:
230 |   - @lingo.dev/[email protected]
231 | 
232 | ## 0.3.3
233 | 
234 | ### Patch Changes
235 | 
236 | - [`76cbd9b`](https://github.com/lingodotdev/lingo.dev/commit/76cbd9b2f2e1217421ad1f671bed5b3d64b43333) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - dictionary merging
237 | 
238 | ## 0.3.2
239 | 
240 | ### Patch Changes
241 | 
242 | - [`01f253d`](https://github.com/lingodotdev/lingo.dev/commit/01f253dd9759b518f400dff03ab51b460b9b8997) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - fix dictionary merging
243 | 
244 | ## 0.3.1
245 | 
246 | ### Patch Changes
247 | 
248 | - [`8e97256`](https://github.com/lingodotdev/lingo.dev/commit/8e97256ca4e78dd09a967539ca9dec359bd558ef) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - fix dictionary merging
249 | 
250 | ## 0.3.0
251 | 
252 | ### Minor Changes
253 | 
254 | - [#913](https://github.com/lingodotdev/lingo.dev/pull/913) [`1b9b113`](https://github.com/lingodotdev/lingo.dev/commit/1b9b11301978e8caa2555832d027ff93216aa6e1) Thanks [@The-Best-Codes](https://github.com/The-Best-Codes)! - Add support for Ollama as a CLI and Compiler provider.
255 | 
256 | - [#922](https://github.com/lingodotdev/lingo.dev/pull/922) [`0329a9c`](https://github.com/lingodotdev/lingo.dev/commit/0329a9cdb5e5a63fcecab4efcd7cce22f155a0e9) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add openrouter ais support for compiler
257 | 
258 | ### Patch Changes
259 | 
260 | - [#925](https://github.com/lingodotdev/lingo.dev/pull/925) [`215af19`](https://github.com/lingodotdev/lingo.dev/commit/215af1944667cce66e9c5966f4fb627186687b74) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - improved compiler concurrency, caching, added lingo.dev engine to the compiler, and updated demo apps
261 | 
262 | - Updated dependencies []:
263 |   - @lingo.dev/[email protected]
264 | 
265 | ## 0.2.4
266 | 
267 | ### Patch Changes
268 | 
269 | - [#919](https://github.com/lingodotdev/lingo.dev/pull/919) [`3b6574f`](https://github.com/lingodotdev/lingo.dev/commit/3b6574f0499f3f4d3c48f66ba2b828d2c1c0ceb0) Thanks [@mathio](https://github.com/mathio)! - update package import names
270 | 
271 | ## 0.2.3
272 | 
273 | ### Patch Changes
274 | 
275 | - [#911](https://github.com/lingodotdev/lingo.dev/pull/911) [`d7e74c6`](https://github.com/lingodotdev/lingo.dev/commit/d7e74c6cc724da8ae759ba8d8fdb1a64867d505c) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - fix hyphens in locale names
276 | 
277 | ## 0.2.2
278 | 
279 | ### Patch Changes
280 | 
281 | - [#905](https://github.com/lingodotdev/lingo.dev/pull/905) [`1a235a1`](https://github.com/lingodotdev/lingo.dev/commit/1a235a17455fb2631f7426283aa8431209999758) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - remove @/ path mapping in compiler
282 | 
283 | ## 0.2.1
284 | 
285 | ### Patch Changes
286 | 
287 | - [#900](https://github.com/lingodotdev/lingo.dev/pull/900) [`fead8e0`](https://github.com/lingodotdev/lingo.dev/commit/fead8e08dc2b2869a093cb25a04f6e0aa78cf6b7) Thanks [@mathio](https://github.com/mathio)! - load API key from env var and env files
288 | 
289 | ## 0.2.0
290 | 
291 | ### Minor Changes
292 | 
293 | - [#897](https://github.com/lingodotdev/lingo.dev/pull/897) [`a5da697`](https://github.com/lingodotdev/lingo.dev/commit/a5da697f7efd46de31d17b202d06eb5f655ed9b9) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Add support for other providers in the compiler and implement Google AI as a provider.
294 | 
295 | ## 0.1.13
296 | 
297 | ### Patch Changes
298 | 
299 | - [#890](https://github.com/lingodotdev/lingo.dev/pull/890) [`145fb74`](https://github.com/lingodotdev/lingo.dev/commit/145fb74c09b42c8810f351be5a641b1366881ae1) Thanks [@mathio](https://github.com/mathio)! - do not parse LingoProvider component
300 | 
301 | - [#889](https://github.com/lingodotdev/lingo.dev/pull/889) [`0c45acc`](https://github.com/lingodotdev/lingo.dev/commit/0c45accfc45e63f597758c47033bc58d2f6059b5) Thanks [@mathio](https://github.com/mathio)! - update Groq API error handling
302 | 
303 | ## 0.1.12
304 | 
305 | ### Patch Changes
306 | 
307 | - [#887](https://github.com/lingodotdev/lingo.dev/pull/887) [`511a2ec`](https://github.com/lingodotdev/lingo.dev/commit/511a2ecd68a9c5e2800035d5c6a6b5b31b2dc80f) Thanks [@mathio](https://github.com/mathio)! - handle when lingo dir is deleted
308 | 
309 | ## 0.1.11
310 | 
311 | ### Patch Changes
312 | 
313 | - [#883](https://github.com/lingodotdev/lingo.dev/pull/883) [`7191444`](https://github.com/lingodotdev/lingo.dev/commit/7191444f67864ea5b5a91a9be759b2445bf186d3) Thanks [@mathio](https://github.com/mathio)! - client-side loading state
314 | 
315 | ## 0.1.10
316 | 
317 | ### Patch Changes
318 | 
319 | - [#876](https://github.com/lingodotdev/lingo.dev/pull/876) [`152e96a`](https://github.com/lingodotdev/lingo.dev/commit/152e96a46b98dd25d558ff0e7e20b18b954d375a) Thanks [@vrcprl](https://github.com/vrcprl)! - fix for triggering reload on Windows
320 | 
321 | ## 0.1.9
322 | 
323 | ### Patch Changes
324 | 
325 | - [#866](https://github.com/lingodotdev/lingo.dev/pull/866) [`77461a7`](https://github.com/lingodotdev/lingo.dev/commit/77461a7872eec3ea188b3ca6c6f7ce1fd13fdfbb) Thanks [@vrcprl](https://github.com/vrcprl)! - normalize paths in dictionaries
326 | 
327 | ## 0.1.8
328 | 
329 | ### Patch Changes
330 | 
331 | - [#861](https://github.com/lingodotdev/lingo.dev/pull/861) [`1bccb7e`](https://github.com/lingodotdev/lingo.dev/commit/1bccb7ed51ac1f13ea79e618bbee551d5529efdc) Thanks [@vrcprl](https://github.com/vrcprl)! - support filePath on Windows
332 | 
333 | ## 0.1.7
334 | 
335 | ### Patch Changes
336 | 
337 | - [`5b68641`](https://github.com/lingodotdev/lingo.dev/commit/5b686414f363f8ee4b79fd4e804a434db5cfcb36) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - feat: unshift the plugins
338 | 
339 | ## 0.1.6
340 | 
341 | ### Patch Changes
342 | 
343 | - [`7a5898b`](https://github.com/lingodotdev/lingo.dev/commit/7a5898b12dcd0015a5e57236bf65172cedb8a6ee) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - merge dictionaries
344 | 
345 | ## 0.1.5
346 | 
347 | ### Patch Changes
348 | 
349 | - [`7013b53`](https://github.com/lingodotdev/lingo.dev/commit/7013b5300d6c2c26f39da62b5ad2c7cf11158c74) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - value.trim() issue
350 | 
351 | ## 0.1.4
352 | 
353 | ### Patch Changes
354 | 
355 | - [#853](https://github.com/lingodotdev/lingo.dev/pull/853) [`cb7d5e2`](https://github.com/lingodotdev/lingo.dev/commit/cb7d5e213282c00af658159472183a763f84ca3d) Thanks [@vrcprl](https://github.com/vrcprl)! - Fix groq api key retrieval from .env
356 | 
357 | ## 0.1.3
358 | 
359 | ### Patch Changes
360 | 
361 | - [`f42cff8`](https://github.com/lingodotdev/lingo.dev/commit/f42cff8355b1ff7bba1445bd04d11ee4672903c2) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - flat reexports
362 | 
363 | ## 0.1.2
364 | 
365 | ### Patch Changes
366 | 
367 | - [`920e3f5`](https://github.com/lingodotdev/lingo.dev/commit/920e3f5c3ca1fd51b0919db13a4787cfd616de54) Thanks [@mathio](https://github.com/mathio)! - remove cloneDeep for optimization
368 | 
369 | ## 0.1.1
370 | 
371 | ### Patch Changes
372 | 
373 | - [`caef325`](https://github.com/lingodotdev/lingo.dev/commit/caef3253bc99fa7bf7a0b40e5604c3590dcb4958) Thanks [@mathio](https://github.com/mathio)! - release fix
374 | 
375 | ## 0.1.0
376 | 
377 | ### Minor Changes
378 | 
379 | - [`e980e84`](https://github.com/lingodotdev/lingo.dev/commit/e980e84178439ad70417d38b425acf9148cfc4b6) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - added the compiler
380 | 
```

--------------------------------------------------------------------------------
/packages/react/src/core/component.spec.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect } from "vitest";
  2 | import { render } from "@testing-library/react";
  3 | import { LingoComponent } from "./component";
  4 | 
  5 | describe("LingoComponent", () => {
  6 |   const dictionary = {
  7 |     files: {
  8 |       messages: {
  9 |         entries: {
 10 |           greeting: "Hello {user.profile.name} you have {count} messages",
 11 |           welcome:
 12 |             "Welcome <element:a>incredible <element:span>fantastic <element:em>wonderful <element:strong>amazing</element:strong></element:em></element:span> user</element:a> <element:Icons.Rocket></element:Icons.Rocket>",
 13 |           complex:
 14 |             "<element:a>Hello {user.profile.name}, welcome to <element:span>wonderful <element:strong><element:em>{placeholder}</element:em> nested</element:strong></element:span> world</element:a> of the <element:u>universe number {count}</element:u>",
 15 |         },
 16 |       },
 17 |     },
 18 |   };
 19 | 
 20 |   it("replaces variables in text", () => {
 21 |     const { container } = render(
 22 |       <LingoComponent
 23 |         $dictionary={dictionary}
 24 |         $as="div"
 25 |         $fileKey="messages"
 26 |         $entryKey="greeting"
 27 |         $variables={{ "user.profile.name": "John", count: 69 }}
 28 |       />,
 29 |     );
 30 |     expect(container.textContent).toBe("Hello John you have 69 messages");
 31 |   });
 32 | 
 33 |   it("replaces variables with JSX", () => {
 34 |     const { container } = render(
 35 |       <LingoComponent
 36 |         $dictionary={dictionary}
 37 |         $as="div"
 38 |         $fileKey="messages"
 39 |         $entryKey="greeting"
 40 |         $variables={{
 41 |           "user.profile.name": <strong>John</strong>,
 42 |           count: <em>69</em>,
 43 |         }}
 44 |       />,
 45 |     );
 46 |     expect(container.innerHTML).toBe(
 47 |       "<div>Hello <strong>John</strong> you have <em>69</em> messages</div>",
 48 |     );
 49 |   });
 50 | 
 51 |   it("replaces element placeholders", () => {
 52 |     const Icons = {
 53 |       Rocket: () => <span>🚀</span>,
 54 |     };
 55 | 
 56 |     const { container } = render(
 57 |       <LingoComponent
 58 |         $dictionary={dictionary}
 59 |         $as="div"
 60 |         $fileKey="messages"
 61 |         $entryKey="welcome"
 62 |         $elements={[
 63 |           ({ children }: any) => <a href="#">{children}</a>,
 64 |           ({ children }: any) => <span>{children}</span>,
 65 |           ({ children }: any) => <em>{children}</em>,
 66 |           ({ children }: any) => <strong className="red">{children}</strong>,
 67 |           ({ children }: any) => <Icons.Rocket />,
 68 |         ]}
 69 |       />,
 70 |     );
 71 |     expect(container.innerHTML).toBe(
 72 |       '<div>Welcome <a href="#">incredible <span>fantastic <em>wonderful <strong class="red">amazing</strong></em></span> user</a> <span>🚀</span></div>',
 73 |     );
 74 |   });
 75 | 
 76 |   it("handles both variables and elements", () => {
 77 |     const { container } = render(
 78 |       <LingoComponent
 79 |         $dictionary={dictionary}
 80 |         $as="div"
 81 |         $fileKey="messages"
 82 |         $entryKey="complex"
 83 |         $variables={{
 84 |           "user.profile.name": "John",
 85 |           count: 42,
 86 |           placeholder: "very",
 87 |         }}
 88 |         $elements={[
 89 |           ({ children }: any) => <a>{children}</a>,
 90 |           ({ children }: any) => <span>{children}</span>,
 91 |           ({ children }: any) => <strong>{children}</strong>,
 92 |           ({ children }: any) => <em>{children}</em>,
 93 |           ({ children }: any) => <u>{children}</u>,
 94 |         ]}
 95 |       />,
 96 |     );
 97 |     expect(container.innerHTML).toBe(
 98 |       "<div><a>Hello John, welcome to <span>wonderful <strong><em>very</em> nested</strong></span> world</a> of the <u>universe number 42</u></div>",
 99 |     );
100 |   });
101 | 
102 |   it("falls back to entryKey if value not found", () => {
103 |     const { container } = render(
104 |       <LingoComponent
105 |         $dictionary={dictionary}
106 |         $as="div"
107 |         $fileKey="messages"
108 |         $entryKey="nonexistent"
109 |         $variables={{}}
110 |         $elements={[]}
111 |       />,
112 |     );
113 |     expect(container.textContent).toBe("nonexistent");
114 |   });
115 | 
116 |   describe("function replacement", () => {
117 |     const getName = () => "John";
118 |     const getCount = () => 42;
119 |     const formatName = () => "John Doe";
120 |     const getUnread = () => 3;
121 |     const fnDictionary = {
122 |       files: {
123 |         messages: {
124 |           entries: {
125 |             simple:
126 |               "Hello <function:getName/>, you have <function:getCount/> items",
127 |             chained: "Hello <function:user.details.profile.name/>",
128 |             mixed:
129 |               "Welcome <function:formatName/>, you have {count} items and <function:getUnread/> unread",
130 |             nested:
131 |               "<element:strong>User <function:getName/></element:strong> has <element:em><function:getCount/></element:em>",
132 |           },
133 |         },
134 |       },
135 |     };
136 | 
137 |     it("replaces function calls in text", () => {
138 |       const { container } = render(
139 |         <LingoComponent
140 |           $dictionary={fnDictionary}
141 |           $as="div"
142 |           $fileKey="messages"
143 |           $entryKey="simple"
144 |           $functions={{
145 |             getName: [getName()],
146 |             getCount: [getCount()],
147 |           }}
148 |         />,
149 |       );
150 |       expect(container.textContent).toBe("Hello John, you have 42 items");
151 |     });
152 | 
153 |     it("handles mixed variables and functions", () => {
154 |       const { container } = render(
155 |         <LingoComponent
156 |           $dictionary={fnDictionary}
157 |           $as="div"
158 |           $fileKey="messages"
159 |           $entryKey="mixed"
160 |           $variables={{
161 |             count: 5,
162 |           }}
163 |           $functions={{
164 |             formatName: [formatName()],
165 |             getUnread: [getUnread()],
166 |           }}
167 |         />,
168 |       );
169 |       expect(container.textContent).toBe(
170 |         "Welcome John Doe, you have 5 items and 3 unread",
171 |       );
172 |     });
173 | 
174 |     it("handles functions with nested elements", () => {
175 |       const { container } = render(
176 |         <LingoComponent
177 |           $dictionary={fnDictionary}
178 |           $as="div"
179 |           $fileKey="messages"
180 |           $entryKey="nested"
181 |           $functions={{
182 |             getName: [getName()],
183 |             getCount: [getCount()],
184 |           }}
185 |           $elements={[
186 |             ({ children }: any) => <strong>{children}</strong>,
187 |             ({ children }: any) => <em>{children}</em>,
188 |           ]}
189 |         />,
190 |       );
191 |       expect(container.innerHTML).toBe(
192 |         "<div><strong>User John</strong> has <em>42</em></div>",
193 |       );
194 |     });
195 | 
196 |     it("handles function with chained names", () => {
197 |       const { container } = render(
198 |         <LingoComponent
199 |           $dictionary={fnDictionary}
200 |           $as="div"
201 |           $fileKey="messages"
202 |           $entryKey="chained"
203 |           $functions={{
204 |             "user.details.profile.name": [getName()],
205 |           }}
206 |         />,
207 |       );
208 |       expect(container.textContent).toBe("Hello John");
209 |     });
210 | 
211 |     it("preserves function placeholder if function not provided", () => {
212 |       const { container } = render(
213 |         <LingoComponent
214 |           $dictionary={fnDictionary}
215 |           $as="div"
216 |           $fileKey="messages"
217 |           $entryKey="simple"
218 |           $functions={{
219 |             getName: [getName()],
220 |             // fn1:getCount not provided
221 |           }}
222 |         />,
223 |       );
224 |       expect(container.textContent).toBe(
225 |         "Hello John, you have <function:getCount/> items",
226 |       );
227 |     });
228 | 
229 |     it("replaces function calls with JSX", () => {
230 |       const { container } = render(
231 |         <LingoComponent
232 |           $dictionary={fnDictionary}
233 |           $as="div"
234 |           $fileKey="messages"
235 |           $entryKey="simple"
236 |           $functions={{
237 |             getName: [<strong>John</strong>],
238 |             getCount: [<em>42</em>],
239 |           }}
240 |         />,
241 |       );
242 |       expect(container.innerHTML).toBe(
243 |         "<div>Hello <strong>John</strong>, you have <em>42</em> items</div>",
244 |       );
245 |     });
246 |   });
247 | 
248 |   describe("expression replacement", () => {
249 |     const exprDictionary = {
250 |       files: {
251 |         messages: {
252 |           entries: {
253 |             simple: "Result: <expression/>",
254 |             multiple: "First: <expression/>, Second: <expression/>",
255 |             mixed:
256 |               "Count: <expression/>, User: {user.name}, Items: <expression/>",
257 |             nested:
258 |               "<element:strong>Value: <expression/></element:strong> and <element:em>Total: <expression/></element:em>",
259 |           },
260 |         },
261 |       },
262 |     };
263 | 
264 |     it("replaces simple expressions", () => {
265 |       const { container } = render(
266 |         <LingoComponent
267 |           $dictionary={exprDictionary}
268 |           $as="div"
269 |           $fileKey="messages"
270 |           $entryKey="simple"
271 |           $expressions={[42]}
272 |         />,
273 |       );
274 |       expect(container.textContent).toBe("Result: 42");
275 |     });
276 | 
277 |     it("handles multiple expressions", () => {
278 |       const { container } = render(
279 |         <LingoComponent
280 |           $dictionary={exprDictionary}
281 |           $as="div"
282 |           $fileKey="messages"
283 |           $entryKey="multiple"
284 |           $expressions={[42 * 2, "hello".toUpperCase()]}
285 |         />,
286 |       );
287 |       expect(container.textContent).toBe("First: 84, Second: HELLO");
288 |     });
289 | 
290 |     it("handles mixed variables and expressions", () => {
291 |       const { container } = render(
292 |         <LingoComponent
293 |           $dictionary={exprDictionary}
294 |           $as="div"
295 |           $fileKey="messages"
296 |           $entryKey="mixed"
297 |           $variables={{
298 |             "user.name": "John",
299 |           }}
300 |           $expressions={[42 + 1, [1, 2, 3].length]}
301 |         />,
302 |       );
303 |       expect(container.textContent).toBe("Count: 43, User: John, Items: 3");
304 |     });
305 | 
306 |     it("handles expressions with nested elements", () => {
307 |       const { container } = render(
308 |         <LingoComponent
309 |           $dictionary={exprDictionary}
310 |           $as="div"
311 |           $fileKey="messages"
312 |           $entryKey="nested"
313 |           $expressions={[42 * 2, [1, 2, 3].reduce((a, b) => a + b, 0)]}
314 |           $elements={[
315 |             ({ children }: any) => <strong>{children}</strong>,
316 |             ({ children }: any) => <em>{children}</em>,
317 |           ]}
318 |         />,
319 |       );
320 |       expect(container.innerHTML).toBe(
321 |         "<div><strong>Value: 84</strong> and <em>Total: 6</em></div>",
322 |       );
323 |     });
324 | 
325 |     it("preserves expression placeholder if not provided", () => {
326 |       const { container } = render(
327 |         <LingoComponent
328 |           $dictionary={exprDictionary}
329 |           $as="div"
330 |           $fileKey="messages"
331 |           $entryKey="multiple"
332 |           $expressions={[
333 |             42,
334 |             // second expression not provided
335 |           ]}
336 |         />,
337 |       );
338 |       expect(container.textContent).toBe("First: 42, Second: <expression/>");
339 |     });
340 | 
341 |     it("replaces expressions with JSX", () => {
342 |       const { container } = render(
343 |         <LingoComponent
344 |           $dictionary={exprDictionary}
345 |           $as="div"
346 |           $fileKey="messages"
347 |           $entryKey="multiple"
348 |           $expressions={[<strong>foo</strong>, <code>bar</code>]}
349 |         />,
350 |       );
351 |       expect(container.innerHTML).toBe(
352 |         "<div>First: <strong>foo</strong>, Second: <code>bar</code></div>",
353 |       );
354 |     });
355 |   });
356 | 
357 |   describe("array mutation prevention (shift() bug fix)", () => {
358 |     const mutationDictionary = {
359 |       files: {
360 |         test: {
361 |           entries: {
362 |             elements:
363 |               "First <element:0>text</element:0> and <element:1>more</element:1>",
364 |             functions: "Call <function:fn1/> then <function:fn2/>",
365 |             expressions: "Value <expression/> and <expression/>",
366 |             mixed:
367 |               "Element <element:0>content</element:0> with <function:fn1/> and <expression/>",
368 |           },
369 |         },
370 |       },
371 |     };
372 | 
373 |     it("does not mutate elements array during processing", () => {
374 |       const elements = [
375 |         ({ children }: any) => <span>{children}</span>,
376 |         ({ children }: any) => <strong>{children}</strong>,
377 |       ];
378 |       const originalElements = [...elements];
379 | 
380 |       render(
381 |         <LingoComponent
382 |           $dictionary={mutationDictionary}
383 |           $as="div"
384 |           $fileKey="test"
385 |           $entryKey="elements"
386 |           $elements={elements}
387 |         />,
388 |       );
389 | 
390 |       expect(elements).toEqual(originalElements);
391 |       expect(elements.length).toBe(2);
392 |     });
393 | 
394 |     it("does not mutate functions arrays during processing", () => {
395 |       const functions = {
396 |         fn1: ["result1", "result2"],
397 |         fn2: ["result3", "result4"],
398 |       };
399 |       const originalFunctions = {
400 |         fn1: [...functions.fn1],
401 |         fn2: [...functions.fn2],
402 |       };
403 | 
404 |       render(
405 |         <LingoComponent
406 |           $dictionary={mutationDictionary}
407 |           $as="div"
408 |           $fileKey="test"
409 |           $entryKey="functions"
410 |           $functions={functions}
411 |         />,
412 |       );
413 | 
414 |       expect(functions.fn1).toEqual(originalFunctions.fn1);
415 |       expect(functions.fn2).toEqual(originalFunctions.fn2);
416 |       expect(functions.fn1.length).toBe(2);
417 |       expect(functions.fn2.length).toBe(2);
418 |     });
419 | 
420 |     it("does not mutate expressions array during processing", () => {
421 |       const expressions = ["value1", "value2"];
422 |       const originalExpressions = [...expressions];
423 | 
424 |       render(
425 |         <LingoComponent
426 |           $dictionary={mutationDictionary}
427 |           $as="div"
428 |           $fileKey="test"
429 |           $entryKey="expressions"
430 |           $expressions={expressions}
431 |         />,
432 |       );
433 | 
434 |       expect(expressions).toEqual(originalExpressions);
435 |       expect(expressions.length).toBe(2);
436 |     });
437 | 
438 |     it("produces consistent output across multiple renders", () => {
439 |       const elements = [
440 |         ({ children }: any) => <span>{children}</span>,
441 |         ({ children }: any) => <strong>{children}</strong>,
442 |       ];
443 |       const functions = { fn1: ["result1"] };
444 |       const expressions = ["value1"];
445 | 
446 |       const { container: container1 } = render(
447 |         <LingoComponent
448 |           $dictionary={mutationDictionary}
449 |           $as="div"
450 |           $fileKey="test"
451 |           $entryKey="mixed"
452 |           $elements={elements}
453 |           $functions={functions}
454 |           $expressions={expressions}
455 |         />,
456 |       );
457 | 
458 |       const { container: container2 } = render(
459 |         <LingoComponent
460 |           $dictionary={mutationDictionary}
461 |           $as="div"
462 |           $fileKey="test"
463 |           $entryKey="mixed"
464 |           $elements={elements}
465 |           $functions={functions}
466 |           $expressions={expressions}
467 |         />,
468 |       );
469 | 
470 |       expect(container1.innerHTML).toBe(container2.innerHTML);
471 |     });
472 | 
473 |     it("handles shared arrays across multiple component instances", () => {
474 |       const sharedElements = [
475 |         ({ children }: any) => <span>{children}</span>,
476 |         ({ children }: any) => <strong>{children}</strong>,
477 |       ];
478 | 
479 |       const { container: container1 } = render(
480 |         <div>
481 |           <LingoComponent
482 |             $dictionary={mutationDictionary}
483 |             $as="div"
484 |             $fileKey="test"
485 |             $entryKey="elements"
486 |             $elements={sharedElements}
487 |           />
488 |         </div>,
489 |       );
490 | 
491 |       const { container: container2 } = render(
492 |         <div>
493 |           <LingoComponent
494 |             $dictionary={mutationDictionary}
495 |             $as="div"
496 |             $fileKey="test"
497 |             $entryKey="elements"
498 |             $elements={sharedElements}
499 |           />
500 |         </div>,
501 |       );
502 | 
503 |       expect(container1.innerHTML).toBe(container2.innerHTML);
504 |       expect(sharedElements.length).toBe(2);
505 |     });
506 | 
507 |     it("extracts inner content when elements array is exhausted", () => {
508 |       const { container } = render(
509 |         <LingoComponent
510 |           $dictionary={mutationDictionary}
511 |           $as="div"
512 |           $fileKey="test"
513 |           $entryKey="elements"
514 |           $elements={[({ children }: any) => <span>{children}</span>]}
515 |         />,
516 |       );
517 | 
518 |       expect(container.textContent).toBe("First text and more");
519 |       expect(container.innerHTML).toBe(
520 |         "<div>First <span>text</span> and more</div>",
521 |       );
522 |     });
523 | 
524 |     it("handles completely empty elements array gracefully", () => {
525 |       const { container } = render(
526 |         <LingoComponent
527 |           $dictionary={mutationDictionary}
528 |           $as="div"
529 |           $fileKey="test"
530 |           $entryKey="elements"
531 |           $elements={[]}
532 |         />,
533 |       );
534 | 
535 |       expect(container.textContent).toBe("First text and more");
536 |       expect(container.innerHTML).toBe("<div>First text and more</div>");
537 |     });
538 | 
539 |     it("maintains function index tracking per function name", () => {
540 |       const multiCallDictionary = {
541 |         files: {
542 |           test: {
543 |             entries: {
544 |               multiCall:
545 |                 "First <function:fn1/>, second <function:fn1/>, third <function:fn2/>",
546 |             },
547 |           },
548 |         },
549 |       };
550 | 
551 |       const { container } = render(
552 |         <LingoComponent
553 |           $dictionary={multiCallDictionary}
554 |           $as="div"
555 |           $fileKey="test"
556 |           $entryKey="multiCall"
557 |           $functions={{
558 |             fn1: ["A", "B"],
559 |             fn2: ["C"],
560 |           }}
561 |         />,
562 |       );
563 | 
564 |       expect(container.textContent).toBe("First A, second B, third C");
565 |     });
566 |   });
567 | });
568 | 
```

--------------------------------------------------------------------------------
/packages/cli/src/cli/loaders/xliff.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { ILoader } from "./_types";
  2 | import { createLoader } from "./_utils";
  3 | import { JSDOM } from "jsdom";
  4 | 
  5 | /**
  6 |  * Creates a comprehensive XLIFF loader supporting versions 1.2 and 2.0
  7 |  * with deterministic key generation and structure preservation
  8 |  */
  9 | export default function createXliffLoader(): ILoader<
 10 |   string,
 11 |   Record<string, string>
 12 | > {
 13 |   return createLoader({
 14 |     async pull(locale, input, _ctx, originalLocale) {
 15 |       const trimmedInput = (input ?? "").trim();
 16 | 
 17 |       if (!trimmedInput) {
 18 |         return createEmptyResult(originalLocale, locale);
 19 |       }
 20 | 
 21 |       try {
 22 |         const dom = new JSDOM(trimmedInput, { contentType: "text/xml" });
 23 |         const document = dom.window.document;
 24 | 
 25 |         // Check for parsing errors
 26 |         const parserError = document.querySelector("parsererror");
 27 |         if (parserError) {
 28 |           throw new Error(`XML parsing failed: ${parserError.textContent}`);
 29 |         }
 30 | 
 31 |         const xliffElement = document.documentElement;
 32 |         if (!xliffElement || xliffElement.tagName !== "xliff") {
 33 |           throw new Error("Invalid XLIFF: missing root <xliff> element");
 34 |         }
 35 | 
 36 |         const version = xliffElement.getAttribute("version") || "1.2";
 37 |         const isV2 = version === "2.0";
 38 | 
 39 |         if (isV2) {
 40 |           return pullV2(xliffElement, locale, originalLocale);
 41 |         } else {
 42 |           return pullV1(xliffElement, locale, originalLocale);
 43 |         }
 44 |       } catch (error: any) {
 45 |         throw new Error(`Failed to parse XLIFF file: ${error.message}`);
 46 |       }
 47 |     },
 48 | 
 49 |     async push(locale, translations, originalInput, originalLocale, pullInput) {
 50 |       if (!originalInput) {
 51 |         // Create new file from scratch
 52 |         return pushNewFile(locale, translations, originalLocale);
 53 |       }
 54 | 
 55 |       try {
 56 |         const dom = new JSDOM(originalInput, { contentType: "text/xml" });
 57 |         const document = dom.window.document;
 58 |         const xliffElement = document.documentElement;
 59 |         const version = xliffElement.getAttribute("version") || "1.2";
 60 |         const isV2 = version === "2.0";
 61 | 
 62 |         if (isV2) {
 63 |           return pushV2(
 64 |             dom,
 65 |             xliffElement,
 66 |             locale,
 67 |             translations,
 68 |             originalLocale,
 69 |             originalInput,
 70 |           );
 71 |         } else {
 72 |           return pushV1(
 73 |             dom,
 74 |             xliffElement,
 75 |             locale,
 76 |             translations,
 77 |             originalLocale,
 78 |             originalInput,
 79 |           );
 80 |         }
 81 |       } catch (error: any) {
 82 |         throw new Error(`Failed to update XLIFF file: ${error.message}`);
 83 |       }
 84 |     },
 85 |   });
 86 | }
 87 | 
 88 | /* -------------------------------------------------------------------------- */
 89 | /*                            Version 1.2 Support                            */
 90 | /* -------------------------------------------------------------------------- */
 91 | 
 92 | function pullV1(
 93 |   xliffElement: Element,
 94 |   locale: string,
 95 |   originalLocale: string,
 96 | ): Record<string, string> {
 97 |   const result: Record<string, string> = {};
 98 |   const fileElement = xliffElement.querySelector("file");
 99 | 
100 |   if (!fileElement) {
101 |     return result;
102 |   }
103 | 
104 |   const sourceLanguage =
105 |     fileElement.getAttribute("source-language") || originalLocale;
106 |   const isSourceLocale = sourceLanguage === locale;
107 |   const bodyElement = fileElement.querySelector("body");
108 | 
109 |   if (!bodyElement) {
110 |     return result;
111 |   }
112 | 
113 |   const transUnits = bodyElement.querySelectorAll("trans-unit");
114 |   const seenKeys = new Set<string>();
115 | 
116 |   transUnits.forEach((unit) => {
117 |     let key = getTransUnitKey(unit as Element);
118 |     if (!key) return;
119 | 
120 |     // Handle duplicates deterministically
121 |     if (seenKeys.has(key)) {
122 |       const id = (unit as Element).getAttribute("id")?.trim();
123 |       if (id) {
124 |         key = `${key}#${id}`;
125 |       } else {
126 |         let counter = 1;
127 |         let newKey = `${key}__${counter}`;
128 |         while (seenKeys.has(newKey)) {
129 |           counter++;
130 |           newKey = `${key}__${counter}`;
131 |         }
132 |         key = newKey;
133 |       }
134 |     }
135 |     seenKeys.add(key);
136 | 
137 |     const elementName = isSourceLocale ? "source" : "target";
138 |     const textElement = (unit as Element).querySelector(elementName);
139 | 
140 |     if (textElement) {
141 |       result[key] = extractTextContent(textElement);
142 |     } else if (isSourceLocale) {
143 |       result[key] = key; // fallback for source
144 |     } else {
145 |       result[key] = ""; // empty for missing target
146 |     }
147 |   });
148 | 
149 |   return result;
150 | }
151 | 
152 | function pushV1(
153 |   dom: JSDOM,
154 |   xliffElement: Element,
155 |   locale: string,
156 |   translations: Record<string, string>,
157 |   originalLocale: string,
158 |   originalInput?: string,
159 | ): string {
160 |   const document = dom.window.document;
161 |   const fileElement = xliffElement.querySelector("file");
162 | 
163 |   if (!fileElement) {
164 |     throw new Error("Invalid XLIFF 1.2: missing <file> element");
165 |   }
166 | 
167 |   // Update language attributes
168 |   const sourceLanguage =
169 |     fileElement.getAttribute("source-language") || originalLocale;
170 |   const isSourceLocale = sourceLanguage === locale;
171 | 
172 |   if (!isSourceLocale) {
173 |     fileElement.setAttribute("target-language", locale);
174 |   }
175 | 
176 |   let bodyElement = fileElement.querySelector("body");
177 |   if (!bodyElement) {
178 |     bodyElement = document.createElement("body");
179 |     fileElement.appendChild(bodyElement);
180 |   }
181 | 
182 |   // Build current index
183 |   const existingUnits = new Map<string, Element>();
184 |   const seenKeys = new Set<string>();
185 | 
186 |   bodyElement.querySelectorAll("trans-unit").forEach((unit) => {
187 |     let key = getTransUnitKey(unit as Element);
188 |     if (!key) return;
189 | 
190 |     if (seenKeys.has(key)) {
191 |       const id = (unit as Element).getAttribute("id")?.trim();
192 |       if (id) {
193 |         key = `${key}#${id}`;
194 |       } else {
195 |         let counter = 1;
196 |         let newKey = `${key}__${counter}`;
197 |         while (seenKeys.has(newKey)) {
198 |           counter++;
199 |           newKey = `${key}__${counter}`;
200 |         }
201 |         key = newKey;
202 |       }
203 |     }
204 |     seenKeys.add(key);
205 |     existingUnits.set(key, unit as Element);
206 |   });
207 | 
208 |   // Update/create translation units
209 |   Object.entries(translations).forEach(([key, value]) => {
210 |     let unit = existingUnits.get(key);
211 | 
212 |     if (!unit) {
213 |       unit = document.createElement("trans-unit");
214 |       unit.setAttribute("id", key);
215 |       unit.setAttribute("resname", key);
216 |       unit.setAttribute("restype", "string");
217 |       unit.setAttribute("datatype", "plaintext");
218 | 
219 |       const sourceElement = document.createElement("source");
220 |       setTextContent(sourceElement, isSourceLocale ? value : key);
221 |       unit.appendChild(sourceElement);
222 | 
223 |       if (!isSourceLocale) {
224 |         const targetElement = document.createElement("target");
225 |         targetElement.setAttribute("state", value ? "translated" : "new");
226 |         setTextContent(targetElement, value);
227 |         unit.appendChild(targetElement);
228 |       }
229 | 
230 |       bodyElement.appendChild(unit);
231 |       existingUnits.set(key, unit);
232 |     } else {
233 |       updateTransUnitV1(unit, key, value, isSourceLocale);
234 |     }
235 |   });
236 | 
237 |   // Remove orphaned units
238 |   const translationKeys = new Set(Object.keys(translations));
239 |   existingUnits.forEach((unit, key) => {
240 |     if (!translationKeys.has(key)) {
241 |       unit.parentNode?.removeChild(unit);
242 |     }
243 |   });
244 | 
245 |   return serializeWithDeclaration(
246 |     dom,
247 |     extractXmlDeclaration(originalInput || ""),
248 |   );
249 | }
250 | 
251 | function updateTransUnitV1(
252 |   unit: Element,
253 |   key: string,
254 |   value: string,
255 |   isSourceLocale: boolean,
256 | ): void {
257 |   const document = unit.ownerDocument!;
258 | 
259 |   if (isSourceLocale) {
260 |     let sourceElement = unit.querySelector("source");
261 |     if (!sourceElement) {
262 |       sourceElement = document.createElement("source");
263 |       unit.appendChild(sourceElement);
264 |     }
265 |     setTextContent(sourceElement, value);
266 |   } else {
267 |     let targetElement = unit.querySelector("target");
268 |     if (!targetElement) {
269 |       targetElement = document.createElement("target");
270 |       unit.appendChild(targetElement);
271 |     }
272 | 
273 |     setTextContent(targetElement, value);
274 |     targetElement.setAttribute("state", value.trim() ? "translated" : "new");
275 |   }
276 | }
277 | 
278 | /* -------------------------------------------------------------------------- */
279 | /*                            Version 2.0 Support                            */
280 | /* -------------------------------------------------------------------------- */
281 | 
282 | function pullV2(
283 |   xliffElement: Element,
284 |   locale: string,
285 |   originalLocale: string,
286 | ): Record<string, string> {
287 |   const result: Record<string, string> = {};
288 | 
289 |   // Add source language metadata
290 |   const srcLang = xliffElement.getAttribute("srcLang") || originalLocale;
291 |   result.sourceLanguage = srcLang;
292 | 
293 |   const fileElements = xliffElement.querySelectorAll("file");
294 | 
295 |   fileElements.forEach((fileElement) => {
296 |     const fileId = fileElement.getAttribute("id");
297 |     if (!fileId) return;
298 | 
299 |     traverseUnitsV2(fileElement, fileId, "", result);
300 |   });
301 | 
302 |   return result;
303 | }
304 | 
305 | function traverseUnitsV2(
306 |   container: Element,
307 |   fileId: string,
308 |   currentPath: string,
309 |   result: Record<string, string>,
310 | ): void {
311 |   Array.from(container.children).forEach((child) => {
312 |     const tagName = child.tagName;
313 | 
314 |     if (tagName === "unit") {
315 |       const unitId = child.getAttribute("id")?.trim();
316 |       if (!unitId) return;
317 | 
318 |       const key = `resources/${fileId}/${currentPath}${unitId}/source`;
319 |       const segment = child.querySelector("segment");
320 |       const source = segment?.querySelector("source");
321 | 
322 |       if (source) {
323 |         result[key] = extractTextContent(source);
324 |       } else {
325 |         result[key] = unitId; // fallback
326 |       }
327 |     } else if (tagName === "group") {
328 |       const groupId = child.getAttribute("id")?.trim();
329 |       const newPath = groupId
330 |         ? `${currentPath}${groupId}/groupUnits/`
331 |         : currentPath;
332 |       traverseUnitsV2(child, fileId, newPath, result);
333 |     }
334 |   });
335 | }
336 | 
337 | function pushV2(
338 |   dom: JSDOM,
339 |   xliffElement: Element,
340 |   locale: string,
341 |   translations: Record<string, string>,
342 |   originalLocale: string,
343 |   originalInput?: string,
344 | ): string {
345 |   const document = dom.window.document;
346 | 
347 |   // Handle sourceLanguage metadata
348 |   if (translations.sourceLanguage) {
349 |     xliffElement.setAttribute("srcLang", translations.sourceLanguage);
350 |     delete translations.sourceLanguage; // Don't process as regular translation
351 |   }
352 | 
353 |   // Build index of existing units
354 |   const existingUnits = new Map<string, Element>();
355 |   const fileElements = xliffElement.querySelectorAll("file");
356 | 
357 |   fileElements.forEach((fileElement) => {
358 |     const fileId = fileElement.getAttribute("id");
359 |     if (!fileId) return;
360 | 
361 |     indexUnitsV2(fileElement, fileId, "", existingUnits);
362 |   });
363 | 
364 |   // Update existing units
365 |   Object.entries(translations).forEach(([key, value]) => {
366 |     const unit = existingUnits.get(key);
367 |     if (unit) {
368 |       updateUnitV2(unit, value);
369 |     } else {
370 |       // For new units, we'd need to create the structure
371 |       // This is complex in V2 due to the hierarchical nature
372 |       console.warn(`Cannot create new unit for key: ${key} in XLIFF 2.0`);
373 |     }
374 |   });
375 | 
376 |   return serializeWithDeclaration(
377 |     dom,
378 |     extractXmlDeclaration(originalInput || ""),
379 |   );
380 | }
381 | 
382 | function indexUnitsV2(
383 |   container: Element,
384 |   fileId: string,
385 |   currentPath: string,
386 |   index: Map<string, Element>,
387 | ): void {
388 |   Array.from(container.children).forEach((child) => {
389 |     const tagName = child.tagName;
390 | 
391 |     if (tagName === "unit") {
392 |       const unitId = child.getAttribute("id")?.trim();
393 |       if (!unitId) return;
394 | 
395 |       const key = `resources/${fileId}/${currentPath}${unitId}/source`;
396 |       index.set(key, child);
397 |     } else if (tagName === "group") {
398 |       const groupId = child.getAttribute("id")?.trim();
399 |       const newPath = groupId
400 |         ? `${currentPath}${groupId}/groupUnits/`
401 |         : currentPath;
402 |       indexUnitsV2(child, fileId, newPath, index);
403 |     }
404 |   });
405 | }
406 | 
407 | function updateUnitV2(unit: Element, value: string): void {
408 |   const document = unit.ownerDocument!;
409 | 
410 |   let segment = unit.querySelector("segment");
411 |   if (!segment) {
412 |     segment = document.createElement("segment");
413 |     unit.appendChild(segment);
414 |   }
415 | 
416 |   let source = segment.querySelector("source");
417 |   if (!source) {
418 |     source = document.createElement("source");
419 |     segment.appendChild(source);
420 |   }
421 | 
422 |   setTextContent(source, value);
423 | }
424 | 
425 | /* -------------------------------------------------------------------------- */
426 | /*                              Utilities                                     */
427 | /* -------------------------------------------------------------------------- */
428 | 
429 | function getTransUnitKey(transUnit: Element): string {
430 |   const resname = transUnit.getAttribute("resname")?.trim();
431 |   if (resname) return resname;
432 | 
433 |   const id = transUnit.getAttribute("id")?.trim();
434 |   if (id) return id;
435 | 
436 |   const sourceElement = transUnit.querySelector("source");
437 |   if (sourceElement) {
438 |     const sourceText = extractTextContent(sourceElement).trim();
439 |     if (sourceText) return sourceText;
440 |   }
441 | 
442 |   return "";
443 | }
444 | 
445 | function extractTextContent(element: Element): string {
446 |   // Handle CDATA sections
447 |   const cdataNode = Array.from(element.childNodes).find(
448 |     (node) => node.nodeType === element.CDATA_SECTION_NODE,
449 |   );
450 | 
451 |   if (cdataNode) {
452 |     return cdataNode.nodeValue || "";
453 |   }
454 | 
455 |   return element.textContent || "";
456 | }
457 | 
458 | function setTextContent(element: Element, content: string): void {
459 |   const document = element.ownerDocument!;
460 | 
461 |   // Clear existing content
462 |   while (element.firstChild) {
463 |     element.removeChild(element.firstChild);
464 |   }
465 | 
466 |   // Use CDATA if content contains XML-sensitive characters
467 |   if (/[<>&"']/.test(content)) {
468 |     const cdataSection = document.createCDATASection(content);
469 |     element.appendChild(cdataSection);
470 |   } else {
471 |     element.textContent = content;
472 |   }
473 | }
474 | 
475 | function extractXmlDeclaration(xmlContent: string): string {
476 |   const match = xmlContent.match(/^<\?xml[^>]*\?>/);
477 |   return match ? match[0] : "";
478 | }
479 | 
480 | function serializeWithDeclaration(dom: JSDOM, declaration: string): string {
481 |   let serialized = dom.serialize();
482 | 
483 |   // Add proper indentation for readability
484 |   serialized = formatXml(serialized);
485 | 
486 |   if (declaration) {
487 |     serialized = `${declaration}\n${serialized}`;
488 |   }
489 | 
490 |   return serialized;
491 | }
492 | 
493 | function formatXml(xml: string): string {
494 |   // Parse and reformat XML with proper indentation using JSDOM
495 |   const dom = new JSDOM(xml, { contentType: "text/xml" });
496 |   const doc = dom.window.document;
497 | 
498 |   function formatElement(element: Element, depth: number = 0): string {
499 |     const indent = "  ".repeat(depth);
500 |     const tagName = element.tagName;
501 |     const attributes = Array.from(element.attributes)
502 |       .map((attr) => `${attr.name}="${attr.value}"`)
503 |       .join(" ");
504 | 
505 |     const openTag = attributes ? `<${tagName} ${attributes}>` : `<${tagName}>`;
506 | 
507 |     // Check for CDATA sections first
508 |     const cdataNode = Array.from(element.childNodes).find(
509 |       (node) => node.nodeType === element.CDATA_SECTION_NODE,
510 |     );
511 | 
512 |     if (cdataNode) {
513 |       return `${indent}${openTag}<![CDATA[${cdataNode.nodeValue}]]></${tagName}>`;
514 |     }
515 | 
516 |     // Check if element has only text content
517 |     const textContent = element.textContent?.trim() || "";
518 |     const hasOnlyText =
519 |       element.childNodes.length === 1 && element.childNodes[0].nodeType === 3;
520 | 
521 |     if (hasOnlyText && textContent) {
522 |       return `${indent}${openTag}${textContent}</${tagName}>`;
523 |     }
524 | 
525 |     // Element has child elements
526 |     const children = Array.from(element.children);
527 |     if (children.length === 0) {
528 |       return `${indent}${openTag}</${tagName}>`;
529 |     }
530 | 
531 |     let result = `${indent}${openTag}\n`;
532 |     for (const child of children) {
533 |       result += formatElement(child, depth + 1) + "\n";
534 |     }
535 |     result += `${indent}</${tagName}>`;
536 | 
537 |     return result;
538 |   }
539 | 
540 |   return formatElement(doc.documentElement);
541 | }
542 | 
543 | function createEmptyResult(
544 |   originalLocale: string,
545 |   locale: string,
546 | ): Record<string, string> {
547 |   return {};
548 | }
549 | 
550 | function pushNewFile(
551 |   locale: string,
552 |   translations: Record<string, string>,
553 |   originalLocale: string,
554 | ): string {
555 |   const skeleton = `<?xml version="1.0" encoding="utf-8"?>
556 | <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
557 |   <file original="" source-language="${originalLocale}" target-language="${locale}" datatype="plaintext">
558 |     <header></header>
559 |     <body></body>
560 |   </file>
561 | </xliff>`;
562 | 
563 |   const dom = new JSDOM(skeleton, { contentType: "text/xml" });
564 |   const document = dom.window.document;
565 |   const bodyElement = document.querySelector("body")!;
566 | 
567 |   Object.entries(translations).forEach(([key, value]) => {
568 |     const unit = document.createElement("trans-unit");
569 |     unit.setAttribute("id", key);
570 |     unit.setAttribute("resname", key);
571 |     unit.setAttribute("restype", "string");
572 |     unit.setAttribute("datatype", "plaintext");
573 | 
574 |     const sourceElement = document.createElement("source");
575 |     setTextContent(sourceElement, key);
576 |     unit.appendChild(sourceElement);
577 | 
578 |     const targetElement = document.createElement("target");
579 |     targetElement.setAttribute("state", value ? "translated" : "new");
580 |     setTextContent(targetElement, value);
581 |     unit.appendChild(targetElement);
582 | 
583 |     bodyElement.appendChild(unit);
584 |   });
585 | 
586 |   return serializeWithDeclaration(
587 |     dom,
588 |     '<?xml version="1.0" encoding="utf-8"?>',
589 |   );
590 | }
591 | 
```

--------------------------------------------------------------------------------
/packages/cli/src/cli/loaders/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import Z from "zod";
  2 | import jsdom from "jsdom";
  3 | import { bucketTypeSchema } from "@lingo.dev/_spec";
  4 | import { composeLoaders } from "./_utils";
  5 | import createJsonLoader from "./json";
  6 | import createJson5Loader from "./json5";
  7 | import createJsoncLoader from "./jsonc";
  8 | import createFlatLoader from "./flat";
  9 | import createTextFileLoader from "./text-file";
 10 | import createYamlLoader from "./yaml";
 11 | import createRootKeyLoader from "./root-key";
 12 | import createFlutterLoader from "./flutter";
 13 | import { ILoader } from "./_types";
 14 | import createAndroidLoader from "./android";
 15 | import createCsvLoader from "./csv";
 16 | import createHtmlLoader from "./html";
 17 | import createMarkdownLoader from "./markdown";
 18 | import createMarkdocLoader from "./markdoc";
 19 | import createPropertiesLoader from "./properties";
 20 | import createXcodeStringsLoader from "./xcode-strings";
 21 | import createXcodeStringsdictLoader from "./xcode-stringsdict";
 22 | import createXcodeXcstringsLoader from "./xcode-xcstrings";
 23 | import createXcodeXcstringsV2Loader from "./xcode-xcstrings-v2-loader";
 24 | import { isICUPluralObject } from "./xcode-xcstrings-icu";
 25 | import createUnlocalizableLoader from "./unlocalizable";
 26 | import { createFormatterLoader, FormatterType } from "./formatters";
 27 | import createPoLoader from "./po";
 28 | import createXliffLoader from "./xliff";
 29 | import createXmlLoader from "./xml";
 30 | import createSrtLoader from "./srt";
 31 | import createDatoLoader from "./dato";
 32 | import createVttLoader from "./vtt";
 33 | import createVariableLoader from "./variable";
 34 | import createSyncLoader from "./sync";
 35 | import createPlutilJsonTextLoader from "./plutil-json-loader";
 36 | import createPhpLoader from "./php";
 37 | import createVueJsonLoader from "./vue-json";
 38 | import createTypescriptLoader from "./typescript";
 39 | import createInjectLocaleLoader from "./inject-locale";
 40 | import createLockedKeysLoader from "./locked-keys";
 41 | import createMdxFrontmatterSplitLoader from "./mdx2/frontmatter-split";
 42 | import createMdxCodePlaceholderLoader from "./mdx2/code-placeholder";
 43 | import createLocalizableMdxDocumentLoader from "./mdx2/localizable-document";
 44 | import createMdxSectionsSplit2Loader from "./mdx2/sections-split-2";
 45 | import createLockedPatternsLoader from "./locked-patterns";
 46 | import createIgnoredKeysLoader from "./ignored-keys";
 47 | import createEjsLoader from "./ejs";
 48 | import createEnsureKeyOrderLoader from "./ensure-key-order";
 49 | import createTxtLoader from "./txt";
 50 | import createJsonKeysLoader from "./json-dictionary";
 51 | 
 52 | type BucketLoaderOptions = {
 53 |   returnUnlocalizedKeys?: boolean;
 54 |   defaultLocale: string;
 55 |   injectLocale?: string[];
 56 |   targetLocale?: string;
 57 |   formatter?: FormatterType;
 58 | };
 59 | 
 60 | export default function createBucketLoader(
 61 |   bucketType: Z.infer<typeof bucketTypeSchema>,
 62 |   bucketPathPattern: string,
 63 |   options: BucketLoaderOptions,
 64 |   lockedKeys?: string[],
 65 |   lockedPatterns?: string[],
 66 |   ignoredKeys?: string[],
 67 | ): ILoader<void, Record<string, any>> {
 68 |   switch (bucketType) {
 69 |     default:
 70 |       throw new Error(`Unsupported bucket type: ${bucketType}`);
 71 |     case "android":
 72 |       return composeLoaders(
 73 |         createTextFileLoader(bucketPathPattern),
 74 |         createLockedPatternsLoader(lockedPatterns),
 75 |         createAndroidLoader(),
 76 |         createEnsureKeyOrderLoader(),
 77 |         createFlatLoader(),
 78 |         createLockedKeysLoader(lockedKeys || []),
 79 |         createIgnoredKeysLoader(ignoredKeys || []),
 80 |         createSyncLoader(),
 81 |         createUnlocalizableLoader(options.returnUnlocalizedKeys),
 82 |       );
 83 |     case "csv":
 84 |       return composeLoaders(
 85 |         createTextFileLoader(bucketPathPattern),
 86 |         createLockedPatternsLoader(lockedPatterns),
 87 |         createCsvLoader(),
 88 |         createEnsureKeyOrderLoader(),
 89 |         createFlatLoader(),
 90 |         createLockedKeysLoader(lockedKeys || []),
 91 |         createIgnoredKeysLoader(ignoredKeys || []),
 92 |         createSyncLoader(),
 93 |         createUnlocalizableLoader(options.returnUnlocalizedKeys),
 94 |       );
 95 |     case "html":
 96 |       return composeLoaders(
 97 |         createTextFileLoader(bucketPathPattern),
 98 |         createFormatterLoader(options.formatter, "html", bucketPathPattern),
 99 |         createLockedPatternsLoader(lockedPatterns),
100 |         createHtmlLoader(),
101 |         createLockedKeysLoader(lockedKeys || []),
102 |         createIgnoredKeysLoader(ignoredKeys || []),
103 |         createSyncLoader(),
104 |         createUnlocalizableLoader(options.returnUnlocalizedKeys),
105 |       );
106 |     case "ejs":
107 |       return composeLoaders(
108 |         createTextFileLoader(bucketPathPattern),
109 |         createLockedPatternsLoader(lockedPatterns),
110 |         createEjsLoader(),
111 |         createLockedKeysLoader(lockedKeys || []),
112 |         createIgnoredKeysLoader(ignoredKeys || []),
113 |         createSyncLoader(),
114 |         createUnlocalizableLoader(options.returnUnlocalizedKeys),
115 |       );
116 |     case "json":
117 |       return composeLoaders(
118 |         createTextFileLoader(bucketPathPattern),
119 |         createFormatterLoader(options.formatter, "json", bucketPathPattern),
120 |         createLockedPatternsLoader(lockedPatterns),
121 |         createJsonLoader(),
122 |         createEnsureKeyOrderLoader(),
123 |         createFlatLoader(),
124 |         createInjectLocaleLoader(options.injectLocale),
125 |         createLockedKeysLoader(lockedKeys || []),
126 |         createIgnoredKeysLoader(ignoredKeys || []),
127 |         createSyncLoader(),
128 |         createUnlocalizableLoader(options.returnUnlocalizedKeys),
129 |       );
130 |     case "json5":
131 |       return composeLoaders(
132 |         createTextFileLoader(bucketPathPattern),
133 |         createLockedPatternsLoader(lockedPatterns),
134 |         createJson5Loader(),
135 |         createEnsureKeyOrderLoader(),
136 |         createFlatLoader(),
137 |         createInjectLocaleLoader(options.injectLocale),
138 |         createLockedKeysLoader(lockedKeys || []),
139 |         createIgnoredKeysLoader(ignoredKeys || []),
140 |         createSyncLoader(),
141 |         createUnlocalizableLoader(options.returnUnlocalizedKeys),
142 |       );
143 |     case "jsonc":
144 |       return composeLoaders(
145 |         createTextFileLoader(bucketPathPattern),
146 |         createLockedPatternsLoader(lockedPatterns),
147 |         createJsoncLoader(),
148 |         createEnsureKeyOrderLoader(),
149 |         createFlatLoader(),
150 |         createInjectLocaleLoader(options.injectLocale),
151 |         createLockedKeysLoader(lockedKeys || []),
152 |         createIgnoredKeysLoader(ignoredKeys || []),
153 |         createSyncLoader(),
154 |         createUnlocalizableLoader(options.returnUnlocalizedKeys),
155 |       );
156 |     case "markdown":
157 |       return composeLoaders(
158 |         createTextFileLoader(bucketPathPattern),
159 |         createFormatterLoader(options.formatter, "markdown", bucketPathPattern),
160 |         createLockedPatternsLoader(lockedPatterns),
161 |         createMarkdownLoader(),
162 |         createLockedKeysLoader(lockedKeys || []),
163 |         createIgnoredKeysLoader(ignoredKeys || []),
164 |         createSyncLoader(),
165 |         createUnlocalizableLoader(options.returnUnlocalizedKeys),
166 |       );
167 |     case "markdoc":
168 |       return composeLoaders(
169 |         createTextFileLoader(bucketPathPattern),
170 |         createLockedPatternsLoader(lockedPatterns),
171 |         createMarkdocLoader(),
172 |         createFlatLoader(),
173 |         createEnsureKeyOrderLoader(),
174 |         createLockedKeysLoader(lockedKeys || []),
175 |         createIgnoredKeysLoader(ignoredKeys || []),
176 |         createSyncLoader(),
177 |         createUnlocalizableLoader(options.returnUnlocalizedKeys),
178 |       );
179 |     case "mdx":
180 |       return composeLoaders(
181 |         createTextFileLoader(bucketPathPattern),
182 |         createFormatterLoader(options.formatter, "mdx", bucketPathPattern),
183 |         createMdxCodePlaceholderLoader(),
184 |         createLockedPatternsLoader(lockedPatterns),
185 |         createMdxFrontmatterSplitLoader(),
186 |         createMdxSectionsSplit2Loader(),
187 |         createLocalizableMdxDocumentLoader(),
188 |         createFlatLoader(),
189 |         createEnsureKeyOrderLoader(),
190 |         createLockedKeysLoader(lockedKeys || []),
191 |         createIgnoredKeysLoader(ignoredKeys || []),
192 |         createSyncLoader(),
193 |         createUnlocalizableLoader(options.returnUnlocalizedKeys),
194 |       );
195 |     case "po":
196 |       return composeLoaders(
197 |         createTextFileLoader(bucketPathPattern),
198 |         createLockedPatternsLoader(lockedPatterns),
199 |         createPoLoader(),
200 |         createFlatLoader(),
201 |         createEnsureKeyOrderLoader(),
202 |         createLockedKeysLoader(lockedKeys || []),
203 |         createIgnoredKeysLoader(ignoredKeys || []),
204 |         createSyncLoader(),
205 |         createVariableLoader({ type: "python" }),
206 |         createUnlocalizableLoader(options.returnUnlocalizedKeys),
207 |       );
208 |     case "properties":
209 |       return composeLoaders(
210 |         createTextFileLoader(bucketPathPattern),
211 |         createLockedPatternsLoader(lockedPatterns),
212 |         createPropertiesLoader(),
213 |         createLockedKeysLoader(lockedKeys || []),
214 |         createIgnoredKeysLoader(ignoredKeys || []),
215 |         createSyncLoader(),
216 |         createUnlocalizableLoader(options.returnUnlocalizedKeys),
217 |       );
218 |     case "xcode-strings":
219 |       return composeLoaders(
220 |         createTextFileLoader(bucketPathPattern),
221 |         createLockedPatternsLoader(lockedPatterns),
222 |         createXcodeStringsLoader(),
223 |         createLockedKeysLoader(lockedKeys || []),
224 |         createIgnoredKeysLoader(ignoredKeys || []),
225 |         createSyncLoader(),
226 |         createUnlocalizableLoader(options.returnUnlocalizedKeys),
227 |       );
228 |     case "xcode-stringsdict":
229 |       return composeLoaders(
230 |         createTextFileLoader(bucketPathPattern),
231 |         createLockedPatternsLoader(lockedPatterns),
232 |         createXcodeStringsdictLoader(),
233 |         createFlatLoader(),
234 |         createEnsureKeyOrderLoader(),
235 |         createLockedKeysLoader(lockedKeys || []),
236 |         createIgnoredKeysLoader(ignoredKeys || []),
237 |         createSyncLoader(),
238 |         createUnlocalizableLoader(options.returnUnlocalizedKeys),
239 |       );
240 |     case "xcode-xcstrings":
241 |       return composeLoaders(
242 |         createTextFileLoader(bucketPathPattern),
243 |         createPlutilJsonTextLoader(),
244 |         createLockedPatternsLoader(lockedPatterns),
245 |         createJsonLoader(),
246 |         createXcodeXcstringsLoader(options.defaultLocale),
247 |         createFlatLoader(),
248 |         createEnsureKeyOrderLoader(),
249 |         createLockedKeysLoader(lockedKeys || []),
250 |         createIgnoredKeysLoader(ignoredKeys || []),
251 |         createSyncLoader(),
252 |         createVariableLoader({ type: "ieee" }),
253 |         createUnlocalizableLoader(options.returnUnlocalizedKeys),
254 |       );
255 |     case "xcode-xcstrings-v2":
256 |       return composeLoaders(
257 |         createTextFileLoader(bucketPathPattern),
258 |         createPlutilJsonTextLoader(),
259 |         createLockedPatternsLoader(lockedPatterns),
260 |         createJsonLoader(),
261 |         createXcodeXcstringsLoader(options.defaultLocale),
262 |         createXcodeXcstringsV2Loader(options.defaultLocale),
263 |         createFlatLoader({ shouldPreserveObject: isICUPluralObject }),
264 |         createEnsureKeyOrderLoader(),
265 |         createLockedKeysLoader(lockedKeys || []),
266 |         createIgnoredKeysLoader(ignoredKeys || []),
267 |         createSyncLoader(),
268 |         createVariableLoader({ type: "ieee" }),
269 |         createUnlocalizableLoader(options.returnUnlocalizedKeys),
270 |       );
271 |     case "yaml":
272 |       return composeLoaders(
273 |         createTextFileLoader(bucketPathPattern),
274 |         createFormatterLoader(options.formatter, "yaml", bucketPathPattern),
275 |         createLockedPatternsLoader(lockedPatterns),
276 |         createYamlLoader(),
277 |         createFlatLoader(),
278 |         createEnsureKeyOrderLoader(),
279 |         createLockedKeysLoader(lockedKeys || []),
280 |         createIgnoredKeysLoader(ignoredKeys || []),
281 |         createSyncLoader(),
282 |         createUnlocalizableLoader(options.returnUnlocalizedKeys),
283 |       );
284 |     case "yaml-root-key":
285 |       return composeLoaders(
286 |         createTextFileLoader(bucketPathPattern),
287 |         createFormatterLoader(options.formatter, "yaml", bucketPathPattern),
288 |         createLockedPatternsLoader(lockedPatterns),
289 |         createYamlLoader(),
290 |         createRootKeyLoader(true),
291 |         createFlatLoader(),
292 |         createEnsureKeyOrderLoader(),
293 |         createLockedKeysLoader(lockedKeys || []),
294 |         createIgnoredKeysLoader(ignoredKeys || []),
295 |         createSyncLoader(),
296 |         createUnlocalizableLoader(options.returnUnlocalizedKeys),
297 |       );
298 |     case "flutter":
299 |       return composeLoaders(
300 |         createTextFileLoader(bucketPathPattern),
301 |         createFormatterLoader(options.formatter, "json", bucketPathPattern),
302 |         createLockedPatternsLoader(lockedPatterns),
303 |         createJsonLoader(),
304 |         createEnsureKeyOrderLoader(),
305 |         createFlutterLoader(),
306 |         createFlatLoader(),
307 |         createLockedKeysLoader(lockedKeys || []),
308 |         createIgnoredKeysLoader(ignoredKeys || []),
309 |         createSyncLoader(),
310 |         createUnlocalizableLoader(options.returnUnlocalizedKeys),
311 |       );
312 |     case "xliff":
313 |       return composeLoaders(
314 |         createTextFileLoader(bucketPathPattern),
315 |         createLockedPatternsLoader(lockedPatterns),
316 |         createXliffLoader(),
317 |         createFlatLoader(),
318 |         createEnsureKeyOrderLoader(),
319 |         createLockedKeysLoader(lockedKeys || []),
320 |         createIgnoredKeysLoader(ignoredKeys || []),
321 |         createSyncLoader(),
322 |         createUnlocalizableLoader(options.returnUnlocalizedKeys),
323 |       );
324 |     case "xml":
325 |       return composeLoaders(
326 |         createTextFileLoader(bucketPathPattern),
327 |         createLockedPatternsLoader(lockedPatterns),
328 |         createXmlLoader(),
329 |         createFlatLoader(),
330 |         createEnsureKeyOrderLoader(),
331 |         createLockedKeysLoader(lockedKeys || []),
332 |         createIgnoredKeysLoader(ignoredKeys || []),
333 |         createSyncLoader(),
334 |         createUnlocalizableLoader(options.returnUnlocalizedKeys),
335 |       );
336 |     case "srt":
337 |       return composeLoaders(
338 |         createTextFileLoader(bucketPathPattern),
339 |         createLockedPatternsLoader(lockedPatterns),
340 |         createSrtLoader(),
341 |         createLockedKeysLoader(lockedKeys || []),
342 |         createIgnoredKeysLoader(ignoredKeys || []),
343 |         createSyncLoader(),
344 |         createUnlocalizableLoader(options.returnUnlocalizedKeys),
345 |       );
346 |     case "dato":
347 |       return composeLoaders(
348 |         createDatoLoader(bucketPathPattern),
349 |         createSyncLoader(),
350 |         createFlatLoader(),
351 |         createEnsureKeyOrderLoader(),
352 |         createLockedKeysLoader(lockedKeys || []),
353 |         createIgnoredKeysLoader(ignoredKeys || []),
354 |         createUnlocalizableLoader(options.returnUnlocalizedKeys),
355 |       );
356 |     case "vtt":
357 |       return composeLoaders(
358 |         createTextFileLoader(bucketPathPattern),
359 |         createLockedPatternsLoader(lockedPatterns),
360 |         createVttLoader(),
361 |         createLockedKeysLoader(lockedKeys || []),
362 |         createIgnoredKeysLoader(ignoredKeys || []),
363 |         createSyncLoader(),
364 |         createUnlocalizableLoader(options.returnUnlocalizedKeys),
365 |       );
366 |     case "php":
367 |       return composeLoaders(
368 |         createTextFileLoader(bucketPathPattern),
369 |         createLockedPatternsLoader(lockedPatterns),
370 |         createPhpLoader(),
371 |         createSyncLoader(),
372 |         createFlatLoader(),
373 |         createEnsureKeyOrderLoader(),
374 |         createLockedKeysLoader(lockedKeys || []),
375 |         createIgnoredKeysLoader(ignoredKeys || []),
376 |         createUnlocalizableLoader(options.returnUnlocalizedKeys),
377 |       );
378 |     case "vue-json":
379 |       return composeLoaders(
380 |         createTextFileLoader(bucketPathPattern),
381 |         createLockedPatternsLoader(lockedPatterns),
382 |         createVueJsonLoader(),
383 |         createSyncLoader(),
384 |         createFlatLoader(),
385 |         createEnsureKeyOrderLoader(),
386 |         createLockedKeysLoader(lockedKeys || []),
387 |         createIgnoredKeysLoader(ignoredKeys || []),
388 |         createUnlocalizableLoader(options.returnUnlocalizedKeys),
389 |       );
390 |     case "typescript":
391 |       return composeLoaders(
392 |         createTextFileLoader(bucketPathPattern),
393 |         createFormatterLoader(
394 |           options.formatter,
395 |           "typescript",
396 |           bucketPathPattern,
397 |         ),
398 |         createLockedPatternsLoader(lockedPatterns),
399 |         createTypescriptLoader(),
400 |         createFlatLoader(),
401 |         createEnsureKeyOrderLoader(),
402 |         createSyncLoader(),
403 |         createLockedKeysLoader(lockedKeys || []),
404 |         createIgnoredKeysLoader(ignoredKeys || []),
405 |         createUnlocalizableLoader(options.returnUnlocalizedKeys),
406 |       );
407 |     case "txt":
408 |       return composeLoaders(
409 |         createTextFileLoader(bucketPathPattern),
410 |         createLockedPatternsLoader(lockedPatterns),
411 |         createTxtLoader(),
412 |         createLockedKeysLoader(lockedKeys || []),
413 |         createIgnoredKeysLoader(ignoredKeys || []),
414 |         createSyncLoader(),
415 |         createUnlocalizableLoader(options.returnUnlocalizedKeys),
416 |       );
417 |     case "json-dictionary":
418 |       return composeLoaders(
419 |         createTextFileLoader(bucketPathPattern),
420 |         createFormatterLoader(options.formatter, "json", bucketPathPattern),
421 |         createLockedPatternsLoader(lockedPatterns),
422 |         createJsonLoader(),
423 |         createJsonKeysLoader(),
424 |         createEnsureKeyOrderLoader(),
425 |         createFlatLoader(),
426 |         createInjectLocaleLoader(options.injectLocale),
427 |         createLockedKeysLoader(lockedKeys || []),
428 |         createIgnoredKeysLoader(ignoredKeys || []),
429 |         createSyncLoader(),
430 |         createUnlocalizableLoader(options.returnUnlocalizedKeys),
431 |       );
432 |   }
433 | }
434 | 
```

--------------------------------------------------------------------------------
/packages/cli/src/cli/loaders/markdoc.spec.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect } from "vitest";
  2 | import createMarkdocLoader from "./markdoc";
  3 | 
  4 | describe("markdoc loader", () => {
  5 |   describe("block-level tag", () => {
  6 |     it("should extract text content from block-level tag", async () => {
  7 |       const loader = createMarkdocLoader();
  8 |       loader.setDefaultLocale("en");
  9 | 
 10 |       const input = `{% foo %}
 11 | This is content inside of a block-level tag
 12 | {% /foo %}`;
 13 | 
 14 |       const output = await loader.pull("en", input);
 15 | 
 16 |       // Should extract the text content with semantic keys
 17 |       const contents = Object.values(output);
 18 | 
 19 |       expect(contents).toContain("This is content inside of a block-level tag");
 20 |     });
 21 | 
 22 |     it("should preserve tag structure on push", async () => {
 23 |       const loader = createMarkdocLoader();
 24 |       loader.setDefaultLocale("en");
 25 | 
 26 |       const input = `{% foo %}
 27 | This is content inside of a block-level tag
 28 | {% /foo %}`;
 29 | 
 30 |       const pulled = await loader.pull("en", input);
 31 |       const pushed = await loader.push("en", pulled);
 32 | 
 33 |       expect(pushed.trim()).toBe(input.trim());
 34 |     });
 35 | 
 36 |     it("should apply translations on push", async () => {
 37 |       const loader = createMarkdocLoader();
 38 |       loader.setDefaultLocale("en");
 39 | 
 40 |       const input = `{% example %}
 41 | This paragraph is nested within a Markdoc tag.
 42 | {% /example %}`;
 43 | 
 44 |       const pulled = await loader.pull("en", input);
 45 | 
 46 |       // Modify the content using semantic keys
 47 |       const translated = { ...pulled };
 48 |       const contentKey = Object.keys(translated).find(
 49 |         (k) =>
 50 |           translated[k] === "This paragraph is nested within a Markdoc tag.",
 51 |       );
 52 |       if (contentKey) {
 53 |         translated[contentKey] =
 54 |           "Este párrafo está anidado dentro de una etiqueta Markdoc.";
 55 |       }
 56 | 
 57 |       const pushed = await loader.push("es", translated);
 58 | 
 59 |       expect(pushed).toContain(
 60 |         "Este párrafo está anidado dentro de una etiqueta Markdoc.",
 61 |       );
 62 |       expect(pushed).toContain("{% example %}");
 63 |       expect(pushed).toContain("{% /example %}");
 64 |     });
 65 |   });
 66 | 
 67 |   describe("self-closing tag", () => {
 68 |     it("should handle self-closing tag with no content", async () => {
 69 |       const loader = createMarkdocLoader();
 70 |       loader.setDefaultLocale("en");
 71 | 
 72 |       const input = `{% example /%}`;
 73 | 
 74 |       const output = await loader.pull("en", input);
 75 | 
 76 |       // Should have the tag but no text content
 77 |       expect(output).toBeDefined();
 78 |     });
 79 | 
 80 |     it("should preserve self-closing tag on push", async () => {
 81 |       const loader = createMarkdocLoader();
 82 |       loader.setDefaultLocale("en");
 83 | 
 84 |       const input = `{% example /%}`;
 85 | 
 86 |       const pulled = await loader.pull("en", input);
 87 |       const pushed = await loader.push("en", pulled);
 88 | 
 89 |       expect(pushed.trim()).toBe(input.trim());
 90 |     });
 91 |   });
 92 | 
 93 |   describe("inline tag", () => {
 94 |     it("should extract text from inline tag", async () => {
 95 |       const loader = createMarkdocLoader();
 96 |       loader.setDefaultLocale("en");
 97 | 
 98 |       const input = `This is a paragraph {% foo %}that contains a tag{% /foo %}`;
 99 | 
100 |       const output = await loader.pull("en", input);
101 | 
102 |       // Should extract both text segments
103 |       const contents = Object.values(output);
104 | 
105 |       expect(contents).toContain("This is a paragraph ");
106 |       expect(contents).toContain("that contains a tag");
107 |     });
108 | 
109 |     it("should preserve inline tag structure on push", async () => {
110 |       const loader = createMarkdocLoader();
111 |       loader.setDefaultLocale("en");
112 | 
113 |       const input = `This is a paragraph {% foo %}that contains a tag{% /foo %}`;
114 | 
115 |       const pulled = await loader.pull("en", input);
116 |       const pushed = await loader.push("en", pulled);
117 | 
118 |       expect(pushed.trim()).toBe(input.trim());
119 |     });
120 | 
121 |     it("should apply translations to inline tag content", async () => {
122 |       const loader = createMarkdocLoader();
123 |       loader.setDefaultLocale("en");
124 | 
125 |       const input = `This is a paragraph {% foo %}that contains a tag{% /foo %}`;
126 | 
127 |       const pulled = await loader.pull("en", input);
128 | 
129 |       // Translate both text segments
130 |       const translated = { ...pulled };
131 |       Object.keys(translated).forEach((key) => {
132 |         if (translated[key] === "This is a paragraph ") {
133 |           translated[key] = "Este es un párrafo ";
134 |         } else if (translated[key] === "that contains a tag") {
135 |           translated[key] = "que contiene una etiqueta";
136 |         }
137 |       });
138 | 
139 |       const pushed = await loader.push("es", translated);
140 | 
141 |       expect(pushed).toContain("Este es un párrafo");
142 |       expect(pushed).toContain("que contiene una etiqueta");
143 |       expect(pushed).toContain("{% foo %}");
144 |       expect(pushed).toContain("{% /foo %}");
145 |     });
146 |   });
147 | 
148 |   describe("inline tag only content", () => {
149 |     it("should handle inline tag as sole paragraph content", async () => {
150 |       const loader = createMarkdocLoader();
151 |       loader.setDefaultLocale("en");
152 | 
153 |       const input = `{% foo %}This is content inside of an inline tag{% /foo %}`;
154 | 
155 |       const output = await loader.pull("en", input);
156 | 
157 |       const contents = Object.values(output);
158 | 
159 |       expect(contents).toContain("This is content inside of an inline tag");
160 |     });
161 | 
162 |     it("should preserve inline-only tag structure on push", async () => {
163 |       const loader = createMarkdocLoader();
164 |       loader.setDefaultLocale("en");
165 | 
166 |       const input = `{% foo %}This is content inside of an inline tag{% /foo %}`;
167 | 
168 |       const pulled = await loader.pull("en", input);
169 |       const pushed = await loader.push("en", pulled);
170 | 
171 |       expect(pushed.trim()).toBe(input.trim());
172 |     });
173 |   });
174 | 
175 |   describe("mixed content", () => {
176 |     it("should handle document with multiple tags and text", async () => {
177 |       const loader = createMarkdocLoader();
178 |       loader.setDefaultLocale("en");
179 | 
180 |       const input = `# Heading
181 | 
182 | This is a paragraph.
183 | 
184 | {% note %}
185 | Important information here.
186 | {% /note %}
187 | 
188 | Another paragraph with {% inline %}inline content{% /inline %}.
189 | 
190 | {% self-closing /%}`;
191 | 
192 |       const output = await loader.pull("en", input);
193 |       const pushed = await loader.push("en", output);
194 | 
195 |       // Verify structure is preserved
196 |       expect(pushed).toContain("# Heading");
197 |       expect(pushed).toContain("{% note %}");
198 |       expect(pushed).toContain("{% /note %}");
199 |       expect(pushed).toContain("{% inline %}");
200 |       expect(pushed).toContain("{% /inline %}");
201 |       expect(pushed).toContain("{% self-closing /%}");
202 |     });
203 |   });
204 | 
205 |   describe("nested tags", () => {
206 |     it("should handle nested tags", async () => {
207 |       const loader = createMarkdocLoader();
208 |       loader.setDefaultLocale("en");
209 | 
210 |       const input = `{% outer %}
211 | Outer content
212 | {% inner %}
213 | Inner content
214 | {% /inner %}
215 | More outer content
216 | {% /outer %}`;
217 | 
218 |       const output = await loader.pull("en", input);
219 |       const pushed = await loader.push("en", output);
220 | 
221 |       expect(pushed).toContain("{% outer %}");
222 |       expect(pushed).toContain("{% inner %}");
223 |       expect(pushed).toContain("{% /inner %}");
224 |       expect(pushed).toContain("{% /outer %}");
225 |     });
226 |   });
227 | 
228 |   describe("interpolation", () => {
229 |     it("should preserve variable interpolation", async () => {
230 |       const loader = createMarkdocLoader();
231 |       loader.setDefaultLocale("en");
232 | 
233 |       const input = `Hello {% $username %}`;
234 | 
235 |       const output = await loader.pull("en", input);
236 |       const pushed = await loader.push("en", output);
237 | 
238 |       expect(pushed.trim()).toBe(input.trim());
239 |     });
240 | 
241 |     it("should preserve function interpolation", async () => {
242 |       const loader = createMarkdocLoader();
243 |       loader.setDefaultLocale("en");
244 | 
245 |       const input = `Result: {% calculateValue() %}`;
246 | 
247 |       const output = await loader.pull("en", input);
248 |       const pushed = await loader.push("en", output);
249 | 
250 |       expect(pushed.trim()).toBe(input.trim());
251 |     });
252 | 
253 |     it("should preserve interpolation in middle of text", async () => {
254 |       const loader = createMarkdocLoader();
255 |       loader.setDefaultLocale("en");
256 | 
257 |       const input = `This is {% $var %} some text.`;
258 | 
259 |       const output = await loader.pull("en", input);
260 |       const pushed = await loader.push("en", output);
261 | 
262 |       expect(pushed.trim()).toBe(input.trim());
263 |     });
264 | 
265 |     it("should translate text around interpolation", async () => {
266 |       const loader = createMarkdocLoader();
267 |       loader.setDefaultLocale("en");
268 | 
269 |       const input = `Hello {% $username %}, welcome!`;
270 | 
271 |       const output = await loader.pull("en", input);
272 | 
273 |       // Should extract text segments but not interpolation
274 |       const textContents = Object.values(output).filter(
275 |         (v) => typeof v === "string",
276 |       );
277 | 
278 |       expect(textContents).toContain("Hello ");
279 |       expect(textContents).toContain(", welcome!");
280 | 
281 |       // Translate the text segments
282 |       const translated = { ...output };
283 |       Object.keys(translated).forEach((key) => {
284 |         if (translated[key] === "Hello ") {
285 |           translated[key] = "Hola ";
286 |         } else if (translated[key] === ", welcome!") {
287 |           translated[key] = ", ¡bienvenido!";
288 |         }
289 |       });
290 | 
291 |       const pushed = await loader.push("es", translated);
292 | 
293 |       expect(pushed).toContain("Hola");
294 |       expect(pushed).toContain("¡bienvenido!");
295 |       expect(pushed).toContain("{% $username %}");
296 |     });
297 | 
298 |     it("should handle interpolation in tags", async () => {
299 |       const loader = createMarkdocLoader();
300 |       loader.setDefaultLocale("en");
301 | 
302 |       const input = `{% callout %}
303 | The value is {% $value %} today.
304 | {% /callout %}`;
305 | 
306 |       const output = await loader.pull("en", input);
307 |       const pushed = await loader.push("en", output);
308 | 
309 |       expect(pushed).toContain("{% callout %}");
310 |       expect(pushed).toContain("{% $value %}");
311 |       expect(pushed).toContain("{% /callout %}");
312 |     });
313 |   });
314 | 
315 |   describe("annotations", () => {
316 |     it("should preserve annotations with shorthand class attribute", async () => {
317 |       const loader = createMarkdocLoader();
318 |       loader.setDefaultLocale("en");
319 | 
320 |       const input = `# Heading {% .example %}`;
321 | 
322 |       const output = await loader.pull("en", input);
323 |       const pushed = await loader.push("en", output);
324 | 
325 |       expect(pushed).toContain("# Heading");
326 |       expect(pushed).toContain("{% .example %}");
327 |     });
328 | 
329 |     it("should preserve annotations with shorthand id attribute", async () => {
330 |       const loader = createMarkdocLoader();
331 |       loader.setDefaultLocale("en");
332 | 
333 |       const input = `# Heading {% #main-title %}`;
334 | 
335 |       const output = await loader.pull("en", input);
336 |       const pushed = await loader.push("en", output);
337 | 
338 |       expect(pushed).toContain("# Heading");
339 |       expect(pushed).toContain("{% #main-title %}");
340 |     });
341 | 
342 |     it("should preserve annotations with multiple shorthand attributes", async () => {
343 |       const loader = createMarkdocLoader();
344 |       loader.setDefaultLocale("en");
345 | 
346 |       const input = `# Heading {% #foo .bar .baz %}`;
347 | 
348 |       const output = await loader.pull("en", input);
349 |       const pushed = await loader.push("en", output);
350 | 
351 |       expect(pushed).toContain("# Heading");
352 |       expect(pushed).toContain("{% #foo .bar .baz %}");
353 |     });
354 | 
355 |     it("should translate heading text with annotations", async () => {
356 |       const loader = createMarkdocLoader();
357 |       loader.setDefaultLocale("en");
358 | 
359 |       const input = `# Welcome {% .hero-title %}`;
360 | 
361 |       const output = await loader.pull("en", input);
362 | 
363 |       // Find and translate the heading text (note: has trailing space)
364 |       const translated = { ...output };
365 |       Object.keys(translated).forEach((key) => {
366 |         if (translated[key] === "Welcome ") {
367 |           translated[key] = "Bienvenido ";
368 |         }
369 |       });
370 | 
371 |       const pushed = await loader.push("es", translated);
372 | 
373 |       expect(pushed).toContain("Bienvenido");
374 |       expect(pushed).toContain("{% .hero-title %}");
375 |     });
376 |   });
377 | 
378 |   describe("tag attributes", () => {
379 |     it("should preserve tags with full attributes", async () => {
380 |       const loader = createMarkdocLoader();
381 |       loader.setDefaultLocale("en");
382 | 
383 |       const input = `{% callout type="note" %}
384 | This is important information.
385 | {% /callout %}`;
386 | 
387 |       const output = await loader.pull("en", input);
388 |       const pushed = await loader.push("en", output);
389 | 
390 |       expect(pushed).toContain('{% callout type="note" %}');
391 |       expect(pushed).toContain("{% /callout %}");
392 |     });
393 | 
394 |     it("should preserve tags with multiple attributes", async () => {
395 |       const loader = createMarkdocLoader();
396 |       loader.setDefaultLocale("en");
397 | 
398 |       const input = `{% image src="logo.png" alt="Company Logo" width="200" /%}`;
399 | 
400 |       const output = await loader.pull("en", input);
401 |       const pushed = await loader.push("en", output);
402 | 
403 |       expect(pushed.trim()).toBe(input.trim());
404 |     });
405 | 
406 |     it("should preserve tags with array attributes", async () => {
407 |       const loader = createMarkdocLoader();
408 |       loader.setDefaultLocale("en");
409 | 
410 |       const input = `{% chart data=[1, 2, 3] /%}`;
411 | 
412 |       const output = await loader.pull("en", input);
413 |       const pushed = await loader.push("en", output);
414 | 
415 |       expect(pushed.trim()).toBe(input.trim());
416 |     });
417 | 
418 |     it("should translate content in tags with attributes", async () => {
419 |       const loader = createMarkdocLoader();
420 |       loader.setDefaultLocale("en");
421 | 
422 |       const input = `{% callout type="warning" %}
423 | Please read carefully.
424 | {% /callout %}`;
425 | 
426 |       const output = await loader.pull("en", input);
427 | 
428 |       // Translate the content
429 |       const translated = { ...output };
430 |       Object.keys(translated).forEach((key) => {
431 |         if (translated[key] === "Please read carefully.") {
432 |           translated[key] = "Por favor lea con atención.";
433 |         }
434 |       });
435 | 
436 |       const pushed = await loader.push("es", translated);
437 | 
438 |       expect(pushed).toContain("Por favor lea con atención.");
439 |       expect(pushed).toContain('{% callout type="warning" %}');
440 |       expect(pushed).toContain("{% /callout %}");
441 |     });
442 |   });
443 | 
444 |   describe("primary attributes", () => {
445 |     it("should preserve tags with primary attribute", async () => {
446 |       const loader = createMarkdocLoader();
447 |       loader.setDefaultLocale("en");
448 | 
449 |       const input = `{% if $showContent %}
450 | Content is visible.
451 | {% /if %}`;
452 | 
453 |       const output = await loader.pull("en", input);
454 |       const pushed = await loader.push("en", output);
455 | 
456 |       expect(pushed).toContain("{% if $showContent %}");
457 |       expect(pushed).toContain("{% /if %}");
458 |     });
459 | 
460 |     it("should translate content in tags with primary attribute", async () => {
461 |       const loader = createMarkdocLoader();
462 |       loader.setDefaultLocale("en");
463 | 
464 |       const input = `{% if $showContent %}
465 | Content is visible.
466 | {% /if %}`;
467 | 
468 |       const output = await loader.pull("en", input);
469 | 
470 |       // Translate the content
471 |       const translated = { ...output };
472 |       Object.keys(translated).forEach((key) => {
473 |         if (translated[key] === "Content is visible.") {
474 |           translated[key] = "El contenido es visible.";
475 |         }
476 |       });
477 | 
478 |       const pushed = await loader.push("es", translated);
479 | 
480 |       expect(pushed).toContain("El contenido es visible.");
481 |       expect(pushed).toContain("{% if $showContent %}");
482 |     });
483 |   });
484 | 
485 |   describe("frontmatter", () => {
486 |     it("should extract frontmatter attributes", async () => {
487 |       const loader = createMarkdocLoader();
488 |       loader.setDefaultLocale("en");
489 | 
490 |       const input = `---
491 | title: My Document
492 | description: A sample document
493 | author: John Doe
494 | ---
495 | 
496 | # Heading
497 | 
498 | Content here.`;
499 | 
500 |       const output = await loader.pull("en", input);
501 | 
502 |       expect(output["fm-attr-title"]).toBe("My Document");
503 |       expect(output["fm-attr-description"]).toBe("A sample document");
504 |       expect(output["fm-attr-author"]).toBe("John Doe");
505 |     });
506 | 
507 |     it("should preserve frontmatter on push", async () => {
508 |       const loader = createMarkdocLoader();
509 |       loader.setDefaultLocale("en");
510 | 
511 |       const input = `---
512 | title: My Document
513 | description: A sample document
514 | ---
515 | 
516 | # Heading
517 | 
518 | Content here.`;
519 | 
520 |       const pulled = await loader.pull("en", input);
521 |       const pushed = await loader.push("en", pulled);
522 | 
523 |       expect(pushed).toContain("title: My Document");
524 |       expect(pushed).toContain("description: A sample document");
525 |       expect(pushed).toContain("# Heading");
526 |       expect(pushed).toContain("Content here.");
527 |     });
528 | 
529 |     it("should translate frontmatter attributes", async () => {
530 |       const loader = createMarkdocLoader();
531 |       loader.setDefaultLocale("en");
532 | 
533 |       const input = `---
534 | title: Welcome
535 | description: This is a guide
536 | ---
537 | 
538 | # Content
539 | 
540 | Some text.`;
541 | 
542 |       const pulled = await loader.pull("en", input);
543 | 
544 |       // Translate frontmatter
545 |       const translated = { ...pulled };
546 |       translated["fm-attr-title"] = "Bienvenido";
547 |       translated["fm-attr-description"] = "Esta es una guía";
548 | 
549 |       const pushed = await loader.push("es", translated);
550 | 
551 |       expect(pushed).toContain("title: Bienvenido");
552 |       expect(pushed).toContain("description: Esta es una guía");
553 |     });
554 | 
555 |     it("should handle documents without frontmatter", async () => {
556 |       const loader = createMarkdocLoader();
557 |       loader.setDefaultLocale("en");
558 | 
559 |       const input = `# Heading
560 | 
561 | Content without frontmatter.`;
562 | 
563 |       const output = await loader.pull("en", input);
564 |       const pushed = await loader.push("en", output);
565 | 
566 |       expect(pushed).not.toContain("---");
567 |       expect(pushed).toContain("# Heading");
568 |       expect(pushed).toContain("Content without frontmatter.");
569 |     });
570 |   });
571 | });
572 | 
```
Page 14/20FirstPrevNextLast