#
tokens: 45218/50000 3/626 files (page 17/20)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 17 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

--------------------------------------------------------------------------------
/packages/cli/src/cli/cmd/i18n.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import {
  2 |   bucketTypeSchema,
  3 |   I18nConfig,
  4 |   localeCodeSchema,
  5 |   resolveOverriddenLocale,
  6 | } from "@lingo.dev/_spec";
  7 | import { Command } from "interactive-commander";
  8 | import Z from "zod";
  9 | import _ from "lodash";
 10 | import * as path from "path";
 11 | import { getConfig } from "../utils/config";
 12 | import { getSettings } from "../utils/settings";
 13 | import {
 14 |   ConfigError,
 15 |   AuthenticationError,
 16 |   ValidationError,
 17 |   LocalizationError,
 18 |   BucketProcessingError,
 19 |   getCLIErrorType,
 20 |   isLocalizationError,
 21 |   isBucketProcessingError,
 22 |   ErrorDetail,
 23 |   aggregateErrorAnalytics,
 24 |   createPreviousErrorContext,
 25 | } from "../utils/errors";
 26 | import Ora from "ora";
 27 | import createBucketLoader from "../loaders";
 28 | import { createAuthenticator } from "../utils/auth";
 29 | import { getBuckets } from "../utils/buckets";
 30 | import chalk from "chalk";
 31 | import { createTwoFilesPatch } from "diff";
 32 | import inquirer from "inquirer";
 33 | import externalEditor from "external-editor";
 34 | import updateGitignore from "../utils/update-gitignore";
 35 | import createProcessor from "../processor";
 36 | import { withExponentialBackoff } from "../utils/exp-backoff";
 37 | import trackEvent from "../utils/observability";
 38 | import { createDeltaProcessor } from "../utils/delta";
 39 | import { isICUPluralObject } from "../loaders/xcode-xcstrings-icu";
 40 | 
 41 | export default new Command()
 42 |   .command("i18n")
 43 |   .description(
 44 |     "DEPRECATED: Run localization pipeline (prefer `run` command instead)",
 45 |   )
 46 |   .helpOption("-h, --help", "Show help")
 47 |   .option(
 48 |     "--locale <locale>",
 49 |     "Limit processing to the listed target locale codes from i18n.json. Repeat the flag to include multiple locales. Defaults to all configured target locales",
 50 |     (val: string, prev: string[]) => (prev ? [...prev, val] : [val]),
 51 |   )
 52 |   .option(
 53 |     "--bucket <bucket>",
 54 |     "Limit processing to specific bucket types defined in i18n.json (e.g., json, yaml, android). Repeat the flag to include multiple bucket types. Defaults to all buckets",
 55 |     (val: string, prev: string[]) => (prev ? [...prev, val] : [val]),
 56 |   )
 57 |   .option(
 58 |     "--key <key>",
 59 |     "Limit processing to a single translation key by exact match. Filters all buckets and locales to process only this key, useful for testing or debugging specific translations. Example: auth.login.title",
 60 |     (val: string) => encodeURIComponent(val),
 61 |   )
 62 |   .option(
 63 |     "--file [files...]",
 64 |     "Filter processing to only buckets whose file paths contain these substrings. Example: 'components' to process only files in components directories",
 65 |   )
 66 |   .option(
 67 |     "--frozen",
 68 |     "Validate translations are up-to-date without making changes - fails if source files, target files, or lockfile are out of sync. Ideal for CI/CD to ensure translation consistency before deployment",
 69 |   )
 70 |   .option(
 71 |     "--force",
 72 |     "Force re-translation of all keys, bypassing change detection. Useful when you want to regenerate translations with updated AI models or translation settings",
 73 |   )
 74 |   .option(
 75 |     "--verbose",
 76 |     "Print the translation data being processed as formatted JSON for each bucket and locale",
 77 |   )
 78 |   .option(
 79 |     "--interactive",
 80 |     "Review and edit AI-generated translations interactively before applying changes to files",
 81 |   )
 82 |   .option(
 83 |     "--api-key <api-key>",
 84 |     "Override API key from settings or environment variables",
 85 |   )
 86 |   .option(
 87 |     "--debug",
 88 |     "Pause before processing localization so you can attach a debugger",
 89 |   )
 90 |   .option(
 91 |     "--strict",
 92 |     "Stop immediately on first error instead of continuing to process remaining buckets and locales (fail-fast mode)",
 93 |   )
 94 |   .action(async function (options) {
 95 |     updateGitignore();
 96 | 
 97 |     const ora = Ora();
 98 |     let flags: ReturnType<typeof parseFlags>;
 99 | 
100 |     try {
101 |       flags = parseFlags(options);
102 |     } catch (parseError: any) {
103 |       // Handle flag validation errors (like invalid locale codes)
104 |       await trackEvent("unknown", "cmd.i18n.error", {
105 |         errorType: "validation_error",
106 |         errorName: parseError.name || "ValidationError",
107 |         errorMessage: parseError.message || "Invalid command line options",
108 |         errorStack: parseError.stack,
109 |         fatal: true,
110 |         errorCount: 1,
111 |         stage: "flag_validation",
112 |       });
113 |       throw parseError;
114 |     }
115 | 
116 |     if (flags.debug) {
117 |       // wait for user input, use inquirer
118 |       const { debug } = await inquirer.prompt([
119 |         {
120 |           type: "confirm",
121 |           name: "debug",
122 |           message: "Debug mode. Wait for user input before continuing.",
123 |         },
124 |       ]);
125 |     }
126 | 
127 |     let hasErrors = false;
128 |     let authId: string | null = null;
129 |     const errorDetails: ErrorDetail[] = [];
130 |     try {
131 |       ora.start("Loading configuration...");
132 |       const i18nConfig = getConfig();
133 |       const settings = getSettings(flags.apiKey);
134 |       ora.succeed("Configuration loaded");
135 | 
136 |       ora.start("Validating localization configuration...");
137 |       validateParams(i18nConfig, flags);
138 |       ora.succeed("Localization configuration is valid");
139 | 
140 |       ora.start("Connecting to Lingo.dev Localization Engine...");
141 |       const isByokMode = !!i18nConfig?.provider;
142 | 
143 |       if (isByokMode) {
144 |         authId = null;
145 |         ora.succeed("Using external provider (BYOK mode)");
146 |       } else {
147 |         const auth = await validateAuth(settings);
148 |         authId = auth.id;
149 |         ora.succeed(`Authenticated as ${auth.email}`);
150 |       }
151 | 
152 |       await trackEvent(authId, "cmd.i18n.start", {
153 |         i18nConfig,
154 |         flags,
155 |       });
156 | 
157 |       let buckets = getBuckets(i18nConfig!);
158 |       if (flags.bucket?.length) {
159 |         buckets = buckets.filter((bucket: any) =>
160 |           flags.bucket!.includes(bucket.type),
161 |         );
162 |       }
163 |       ora.succeed("Buckets retrieved");
164 | 
165 |       if (flags.file?.length) {
166 |         buckets = buckets
167 |           .map((bucket: any) => {
168 |             const paths = bucket.paths.filter((path: any) =>
169 |               flags.file!.find((file) => path.pathPattern?.includes(file)),
170 |             );
171 |             return { ...bucket, paths };
172 |           })
173 |           .filter((bucket: any) => bucket.paths.length > 0);
174 |         if (buckets.length === 0) {
175 |           ora.fail(
176 |             "No buckets found. All buckets were filtered out by --file option.",
177 |           );
178 |           throw new Error(
179 |             "No buckets found. All buckets were filtered out by --file option.",
180 |           );
181 |         } else {
182 |           ora.info(`\x1b[36mProcessing only filtered buckets:\x1b[0m`);
183 |           buckets.map((bucket: any) => {
184 |             ora.info(`  ${bucket.type}:`);
185 |             bucket.paths.forEach((path: any) => {
186 |               ora.info(`    - ${path.pathPattern}`);
187 |             });
188 |           });
189 |         }
190 |       }
191 | 
192 |       const targetLocales = flags.locale?.length
193 |         ? flags.locale
194 |         : i18nConfig!.locale.targets;
195 | 
196 |       // Ensure the lockfile exists
197 |       ora.start("Setting up localization cache...");
198 |       const checkLockfileProcessor = createDeltaProcessor("");
199 |       const lockfileExists = await checkLockfileProcessor.checkIfLockExists();
200 |       if (!lockfileExists) {
201 |         ora.start("Creating i18n.lock...");
202 |         for (const bucket of buckets) {
203 |           for (const bucketPath of bucket.paths) {
204 |             const sourceLocale = resolveOverriddenLocale(
205 |               i18nConfig!.locale.source,
206 |               bucketPath.delimiter,
207 |             );
208 |             const bucketLoader = createBucketLoader(
209 |               bucket.type,
210 |               bucketPath.pathPattern,
211 |               {
212 |                 defaultLocale: sourceLocale,
213 |                 injectLocale: bucket.injectLocale,
214 |                 formatter: i18nConfig!.formatter,
215 |               },
216 |               bucket.lockedKeys,
217 |               bucket.lockedPatterns,
218 |               bucket.ignoredKeys,
219 |             );
220 |             bucketLoader.setDefaultLocale(sourceLocale);
221 |             await bucketLoader.init();
222 | 
223 |             const sourceData = await bucketLoader.pull(
224 |               i18nConfig!.locale.source,
225 |             );
226 | 
227 |             const deltaProcessor = createDeltaProcessor(bucketPath.pathPattern);
228 |             const checksums = await deltaProcessor.createChecksums(sourceData);
229 |             await deltaProcessor.saveChecksums(checksums);
230 |           }
231 |         }
232 |         ora.succeed("Localization cache initialized");
233 |       } else {
234 |         ora.succeed("Localization cache loaded");
235 |       }
236 | 
237 |       if (flags.frozen) {
238 |         ora.start("Checking for lockfile updates...");
239 |         let requiresUpdate: string | null = null;
240 |         bucketLoop: for (const bucket of buckets) {
241 |           for (const bucketPath of bucket.paths) {
242 |             const sourceLocale = resolveOverriddenLocale(
243 |               i18nConfig!.locale.source,
244 |               bucketPath.delimiter,
245 |             );
246 | 
247 |             const bucketLoader = createBucketLoader(
248 |               bucket.type,
249 |               bucketPath.pathPattern,
250 |               {
251 |                 defaultLocale: sourceLocale,
252 |                 returnUnlocalizedKeys: true,
253 |                 injectLocale: bucket.injectLocale,
254 |               },
255 |               bucket.lockedKeys,
256 |               bucket.lockedPatterns,
257 |               bucket.ignoredKeys,
258 |             );
259 |             bucketLoader.setDefaultLocale(sourceLocale);
260 |             await bucketLoader.init();
261 | 
262 |             const { unlocalizable: sourceUnlocalizable, ...sourceData } =
263 |               await bucketLoader.pull(i18nConfig!.locale.source);
264 |             const deltaProcessor = createDeltaProcessor(bucketPath.pathPattern);
265 |             const sourceChecksums =
266 |               await deltaProcessor.createChecksums(sourceData);
267 |             const savedChecksums = await deltaProcessor.loadChecksums();
268 | 
269 |             // Get updated data by comparing current checksums with saved checksums
270 |             const updatedSourceData = _.pickBy(
271 |               sourceData,
272 |               (value, key) => sourceChecksums[key] !== savedChecksums[key],
273 |             );
274 | 
275 |             // translation was updated in the source file
276 |             if (Object.keys(updatedSourceData).length > 0) {
277 |               requiresUpdate = "updated";
278 |               break bucketLoop;
279 |             }
280 | 
281 |             for (const _targetLocale of targetLocales) {
282 |               const targetLocale = resolveOverriddenLocale(
283 |                 _targetLocale,
284 |                 bucketPath.delimiter,
285 |               );
286 |               const { unlocalizable: targetUnlocalizable, ...targetData } =
287 |                 await bucketLoader.pull(targetLocale);
288 | 
289 |               const missingKeys = _.difference(
290 |                 Object.keys(sourceData),
291 |                 Object.keys(targetData),
292 |               );
293 |               const extraKeys = _.difference(
294 |                 Object.keys(targetData),
295 |                 Object.keys(sourceData),
296 |               );
297 |               const unlocalizableDataDiff = !_.isEqual(
298 |                 sourceUnlocalizable,
299 |                 targetUnlocalizable,
300 |               );
301 | 
302 |               // translation is missing in the target file
303 |               if (missingKeys.length > 0) {
304 |                 requiresUpdate = "missing";
305 |                 break bucketLoop;
306 |               }
307 | 
308 |               // target file has extra translations
309 |               if (extraKeys.length > 0) {
310 |                 requiresUpdate = "extra";
311 |                 break bucketLoop;
312 |               }
313 | 
314 |               // unlocalizable keys do not match
315 |               if (unlocalizableDataDiff) {
316 |                 requiresUpdate = "unlocalizable";
317 |                 break bucketLoop;
318 |               }
319 |             }
320 |           }
321 |         }
322 | 
323 |         if (requiresUpdate) {
324 |           const message = {
325 |             updated: "Source file has been updated.",
326 |             missing: "Target file is missing translations.",
327 |             extra:
328 |               "Target file has extra translations not present in the source file.",
329 |             unlocalizable:
330 |               "Unlocalizable data (such as booleans, dates, URLs, etc.) do not match.",
331 |           }[requiresUpdate];
332 |           ora.fail(
333 |             `Localization data has changed; please update i18n.lock or run without --frozen.`,
334 |           );
335 |           ora.fail(`  Details: ${message}`);
336 |           throw new Error(
337 |             `Localization data has changed; please update i18n.lock or run without --frozen. Details: ${message}`,
338 |           );
339 |         } else {
340 |           ora.succeed("No lockfile updates required.");
341 |         }
342 |       }
343 | 
344 |       // Process each bucket
345 |       for (const bucket of buckets) {
346 |         try {
347 |           console.log();
348 |           ora.info(`Processing bucket: ${bucket.type}`);
349 |           for (const bucketPath of bucket.paths) {
350 |             const bucketOra = Ora({ indent: 2 }).info(
351 |               `Processing path: ${bucketPath.pathPattern}`,
352 |             );
353 | 
354 |             const sourceLocale = resolveOverriddenLocale(
355 |               i18nConfig!.locale.source,
356 |               bucketPath.delimiter,
357 |             );
358 | 
359 |             const bucketLoader = createBucketLoader(
360 |               bucket.type,
361 |               bucketPath.pathPattern,
362 |               {
363 |                 defaultLocale: sourceLocale,
364 |                 injectLocale: bucket.injectLocale,
365 |                 formatter: i18nConfig!.formatter,
366 |               },
367 |               bucket.lockedKeys,
368 |               bucket.lockedPatterns,
369 |               bucket.ignoredKeys,
370 |             );
371 |             bucketLoader.setDefaultLocale(sourceLocale);
372 |             await bucketLoader.init();
373 |             let sourceData = await bucketLoader.pull(sourceLocale);
374 | 
375 |             for (const _targetLocale of targetLocales) {
376 |               const targetLocale = resolveOverriddenLocale(
377 |                 _targetLocale,
378 |                 bucketPath.delimiter,
379 |               );
380 |               try {
381 |                 bucketOra.start(
382 |                   `[${sourceLocale} -> ${targetLocale}] (0%) Localization in progress...`,
383 |                 );
384 | 
385 |                 sourceData = await bucketLoader.pull(sourceLocale);
386 | 
387 |                 const targetData = await bucketLoader.pull(targetLocale);
388 |                 const deltaProcessor = createDeltaProcessor(
389 |                   bucketPath.pathPattern,
390 |                 );
391 |                 const checksums = await deltaProcessor.loadChecksums();
392 |                 const delta = await deltaProcessor.calculateDelta({
393 |                   sourceData,
394 |                   targetData,
395 |                   checksums,
396 |                 });
397 |                 let processableData = _.chain(sourceData)
398 |                   .entries()
399 |                   .filter(
400 |                     ([key, value]) =>
401 |                       delta.added.includes(key) ||
402 |                       delta.updated.includes(key) ||
403 |                       !!flags.force,
404 |                   )
405 |                   .fromPairs()
406 |                   .value();
407 | 
408 |                 if (flags.key) {
409 |                   processableData = _.pickBy(
410 |                     processableData,
411 |                     (_, key) => key === flags.key,
412 |                   );
413 |                 }
414 |                 if (flags.verbose) {
415 |                   bucketOra.info(JSON.stringify(processableData, null, 2));
416 |                 }
417 | 
418 |                 bucketOra.start(
419 |                   `[${sourceLocale} -> ${targetLocale}] [${
420 |                     Object.keys(processableData).length
421 |                   } entries] (0%) AI localization in progress...`,
422 |                 );
423 |                 let processPayload = createProcessor(i18nConfig!.provider, {
424 |                   apiKey: settings.auth.apiKey,
425 |                   apiUrl: settings.auth.apiUrl,
426 |                 });
427 |                 processPayload = withExponentialBackoff(
428 |                   processPayload,
429 |                   3,
430 |                   1000,
431 |                 );
432 | 
433 |                 const processedTargetData = await processPayload(
434 |                   {
435 |                     sourceLocale,
436 |                     sourceData,
437 |                     processableData,
438 |                     targetLocale,
439 |                     targetData,
440 |                   },
441 |                   (progress, sourceChunk, processedChunk) => {
442 |                     bucketOra.text = `[${sourceLocale} -> ${targetLocale}] [${
443 |                       Object.keys(processableData).length
444 |                     } entries] (${progress}%) AI localization in progress...`;
445 |                   },
446 |                 );
447 | 
448 |                 if (flags.verbose) {
449 |                   bucketOra.info(JSON.stringify(processedTargetData, null, 2));
450 |                 }
451 | 
452 |                 let finalTargetData = _.merge(
453 |                   {},
454 |                   sourceData,
455 |                   targetData,
456 |                   processedTargetData,
457 |                 );
458 | 
459 |                 // rename keys
460 |                 finalTargetData = _.chain(finalTargetData)
461 |                   .entries()
462 |                   .map(([key, value]) => {
463 |                     const renaming = delta.renamed.find(
464 |                       ([oldKey, newKey]) => oldKey === key,
465 |                     );
466 |                     if (!renaming) {
467 |                       return [key, value];
468 |                     }
469 |                     return [renaming[1], value];
470 |                   })
471 |                   .fromPairs()
472 |                   .value();
473 | 
474 |                 if (flags.interactive) {
475 |                   bucketOra.stop();
476 |                   const reviewedData = await reviewChanges({
477 |                     pathPattern: bucketPath.pathPattern,
478 |                     targetLocale,
479 |                     currentData: targetData,
480 |                     proposedData: finalTargetData,
481 |                     sourceData,
482 |                     force: flags.force!,
483 |                   });
484 | 
485 |                   finalTargetData = reviewedData;
486 |                   bucketOra.start(
487 |                     `Applying changes to ${bucketPath} (${targetLocale})`,
488 |                   );
489 |                 }
490 | 
491 |                 const finalDiffSize = _.chain(finalTargetData)
492 |                   .omitBy((value, key) => {
493 |                     const targetValue = targetData[key];
494 | 
495 |                     // For ICU plural objects, use deep equality (excluding Symbol)
496 |                     if (
497 |                       isICUPluralObject(value) &&
498 |                       isICUPluralObject(targetValue)
499 |                     ) {
500 |                       return _.isEqual(
501 |                         { icu: value.icu, _meta: value._meta },
502 |                         { icu: targetValue.icu, _meta: targetValue._meta },
503 |                       );
504 |                     }
505 | 
506 |                     // Default strict equality for other values
507 |                     return value === targetValue;
508 |                   })
509 |                   .size()
510 |                   .value();
511 | 
512 |                 // Push to bucket all the time as there might be changes to unlocalizable keys
513 |                 await bucketLoader.push(targetLocale, finalTargetData);
514 | 
515 |                 if (finalDiffSize > 0 || flags.force) {
516 |                   bucketOra.succeed(
517 |                     `[${sourceLocale} -> ${targetLocale}] Localization completed`,
518 |                   );
519 |                 } else {
520 |                   bucketOra.succeed(
521 |                     `[${sourceLocale} -> ${targetLocale}] Localization completed (no changes).`,
522 |                   );
523 |                 }
524 |               } catch (_error: any) {
525 |                 const error = new LocalizationError(
526 |                   `[${sourceLocale} -> ${targetLocale}] Localization failed: ${_error.message}`,
527 |                   {
528 |                     bucket: bucket.type,
529 |                     sourceLocale,
530 |                     targetLocale,
531 |                     pathPattern: bucketPath.pathPattern,
532 |                   },
533 |                 );
534 |                 errorDetails.push({
535 |                   type: "locale_error",
536 |                   bucket: bucket.type,
537 |                   locale: `${sourceLocale} -> ${targetLocale}`,
538 |                   pathPattern: bucketPath.pathPattern,
539 |                   message: _error.message,
540 |                   stack: _error.stack,
541 |                 });
542 |                 if (flags.strict) {
543 |                   throw error;
544 |                 } else {
545 |                   bucketOra.fail(error.message);
546 |                   hasErrors = true;
547 |                 }
548 |               }
549 |             }
550 | 
551 |             const deltaProcessor = createDeltaProcessor(bucketPath.pathPattern);
552 |             const checksums = await deltaProcessor.createChecksums(sourceData);
553 |             if (!flags.locale?.length) {
554 |               await deltaProcessor.saveChecksums(checksums);
555 |             }
556 |           }
557 |         } catch (_error: any) {
558 |           const error = new BucketProcessingError(
559 |             `Failed to process bucket ${bucket.type}: ${_error.message}`,
560 |             bucket.type,
561 |           );
562 |           errorDetails.push({
563 |             type: "bucket_error",
564 |             bucket: bucket.type,
565 |             message: _error.message,
566 |             stack: _error.stack,
567 |           });
568 |           if (flags.strict) {
569 |             throw error;
570 |           } else {
571 |             ora.fail(error.message);
572 |             hasErrors = true;
573 |           }
574 |         }
575 |       }
576 |       console.log();
577 |       if (!hasErrors) {
578 |         ora.succeed("Localization completed.");
579 |         await trackEvent(authId, "cmd.i18n.success", {
580 |           i18nConfig: {
581 |             sourceLocale: i18nConfig!.locale.source,
582 |             targetLocales: i18nConfig!.locale.targets,
583 |             bucketTypes: Object.keys(i18nConfig!.buckets),
584 |           },
585 |           flags,
586 |           bucketCount: buckets.length,
587 |           localeCount: targetLocales.length,
588 |           processedSuccessfully: true,
589 |         });
590 |       } else {
591 |         ora.warn("Localization completed with errors.");
592 |         await trackEvent(authId || "unknown", "cmd.i18n.error", {
593 |           flags,
594 |           ...aggregateErrorAnalytics(
595 |             errorDetails,
596 |             buckets,
597 |             targetLocales,
598 |             i18nConfig!,
599 |           ),
600 |         });
601 |       }
602 |     } catch (error: any) {
603 |       ora.fail(error.message);
604 | 
605 |       // Use robust error type detection
606 |       const errorType = getCLIErrorType(error);
607 | 
608 |       // Extract additional context from typed errors
609 |       let errorContext: any = {};
610 |       if (isLocalizationError(error)) {
611 |         errorContext = {
612 |           bucket: error.bucket,
613 |           sourceLocale: error.sourceLocale,
614 |           targetLocale: error.targetLocale,
615 |           pathPattern: error.pathPattern,
616 |         };
617 |       } else if (isBucketProcessingError(error)) {
618 |         errorContext = {
619 |           bucket: error.bucket,
620 |         };
621 |       }
622 | 
623 |       await trackEvent(authId || "unknown", "cmd.i18n.error", {
624 |         flags,
625 |         errorType,
626 |         errorName: error.name || "Error",
627 |         errorMessage: error.message,
628 |         errorStack: error.stack,
629 |         errorContext,
630 |         fatal: true,
631 |         errorCount: errorDetails.length + 1,
632 |         previousErrors: createPreviousErrorContext(errorDetails),
633 |       });
634 |     }
635 |   });
636 | 
637 | function parseFlags(options: any) {
638 |   return Z.object({
639 |     apiKey: Z.string().optional(),
640 |     locale: Z.array(localeCodeSchema).optional(),
641 |     bucket: Z.array(bucketTypeSchema).optional(),
642 |     force: Z.boolean().optional(),
643 |     frozen: Z.boolean().optional(),
644 |     verbose: Z.boolean().optional(),
645 |     strict: Z.boolean().optional(),
646 |     key: Z.string().optional(),
647 |     file: Z.array(Z.string()).optional(),
648 |     interactive: Z.boolean().default(false),
649 |     debug: Z.boolean().default(false),
650 |   }).parse(options);
651 | }
652 | 
653 | // Export validateAuth for use in other commands
654 | export async function validateAuth(settings: ReturnType<typeof getSettings>) {
655 |   if (!settings.auth.apiKey) {
656 |     throw new AuthenticationError({
657 |       message:
658 |         "Not authenticated. Please run `lingo.dev login` to authenticate.",
659 |       docUrl: "authError",
660 |     });
661 |   }
662 | 
663 |   const authenticator = createAuthenticator({
664 |     apiKey: settings.auth.apiKey,
665 |     apiUrl: settings.auth.apiUrl,
666 |   });
667 |   const user = await authenticator.whoami();
668 |   if (!user) {
669 |     throw new AuthenticationError({
670 |       message: "Invalid API key. Please run `lingo.dev login` to authenticate.",
671 |       docUrl: "authError",
672 |     });
673 |   }
674 | 
675 |   return user;
676 | }
677 | 
678 | function validateParams(
679 |   i18nConfig: I18nConfig | null,
680 |   flags: ReturnType<typeof parseFlags>,
681 | ) {
682 |   if (!i18nConfig) {
683 |     throw new ConfigError({
684 |       message:
685 |         "i18n.json not found. Please run `lingo.dev init` to initialize the project.",
686 |       docUrl: "i18nNotFound",
687 |     });
688 |   } else if (!i18nConfig.buckets || !Object.keys(i18nConfig.buckets).length) {
689 |     throw new ConfigError({
690 |       message:
691 |         "No buckets found in i18n.json. Please add at least one bucket containing i18n content.",
692 |       docUrl: "bucketNotFound",
693 |     });
694 |   } else if (
695 |     flags.locale?.some((locale) => !i18nConfig.locale.targets.includes(locale))
696 |   ) {
697 |     throw new ValidationError({
698 |       message: `One or more specified locales do not exist in i18n.json locale.targets. Please add them to the list and try again.`,
699 |       docUrl: "localeTargetNotFound",
700 |     });
701 |   } else if (
702 |     flags.bucket?.some(
703 |       (bucket) =>
704 |         !i18nConfig.buckets[bucket as keyof typeof i18nConfig.buckets],
705 |     )
706 |   ) {
707 |     throw new ValidationError({
708 |       message: `One or more specified buckets do not exist in i18n.json. Please add them to the list and try again.`,
709 |       docUrl: "bucketNotFound",
710 |     });
711 |   }
712 | }
713 | 
714 | async function reviewChanges(args: {
715 |   pathPattern: string;
716 |   targetLocale: string;
717 |   currentData: Record<string, any>;
718 |   proposedData: Record<string, any>;
719 |   sourceData: Record<string, any>;
720 |   force: boolean;
721 | }): Promise<Record<string, any>> {
722 |   const currentStr = JSON.stringify(args.currentData, null, 2);
723 |   const proposedStr = JSON.stringify(args.proposedData, null, 2);
724 | 
725 |   // Early return if no changes
726 |   if (currentStr === proposedStr && !args.force) {
727 |     console.log(
728 |       `\n${chalk.blue(args.pathPattern)} (${chalk.yellow(
729 |         args.targetLocale,
730 |       )}): ${chalk.gray("No changes to review")}`,
731 |     );
732 |     return args.proposedData;
733 |   }
734 | 
735 |   const patch = createTwoFilesPatch(
736 |     `${args.pathPattern} (current)`,
737 |     `${args.pathPattern} (proposed)`,
738 |     currentStr,
739 |     proposedStr,
740 |     undefined,
741 |     undefined,
742 |     { context: 3 },
743 |   );
744 | 
745 |   // Color the diff output
746 |   const coloredDiff = patch
747 |     .split("\n")
748 |     .map((line) => {
749 |       if (line.startsWith("+")) return chalk.green(line);
750 |       if (line.startsWith("-")) return chalk.red(line);
751 |       if (line.startsWith("@")) return chalk.cyan(line);
752 |       return line;
753 |     })
754 |     .join("\n");
755 | 
756 |   console.log(
757 |     `\nReviewing changes for ${chalk.blue(args.pathPattern)} (${chalk.yellow(
758 |       args.targetLocale,
759 |     )}):`,
760 |   );
761 |   console.log(coloredDiff);
762 | 
763 |   const { action } = await inquirer.prompt([
764 |     {
765 |       type: "list",
766 |       name: "action",
767 |       message: "Choose action:",
768 |       choices: [
769 |         { name: "Approve changes", value: "approve" },
770 |         { name: "Skip changes", value: "skip" },
771 |         { name: "Edit individually", value: "edit" },
772 |       ],
773 |       default: "approve",
774 |     },
775 |   ]);
776 | 
777 |   if (action === "approve") {
778 |     return args.proposedData;
779 |   }
780 | 
781 |   if (action === "skip") {
782 |     return args.currentData;
783 |   }
784 | 
785 |   // If edit was chosen, prompt for each changed value
786 |   const customData = { ...args.currentData };
787 |   const changes = _.reduce(
788 |     args.proposedData,
789 |     (result: string[], value: string, key: string) => {
790 |       if (args.currentData[key] !== value) {
791 |         result.push(key);
792 |       }
793 |       return result;
794 |     },
795 |     [],
796 |   );
797 | 
798 |   for (const key of changes) {
799 |     console.log(`\nEditing value for: ${chalk.cyan(key)}`);
800 |     console.log(chalk.gray("Source text:"), chalk.blue(args.sourceData[key]));
801 |     console.log(
802 |       chalk.gray("Current value:"),
803 |       chalk.red(args.currentData[key] || "(empty)"),
804 |     );
805 |     console.log(
806 |       chalk.gray("Suggested value:"),
807 |       chalk.green(args.proposedData[key]),
808 |     );
809 |     console.log(
810 |       chalk.gray(
811 |         "\nYour editor will open. Edit the text and save to continue.",
812 |       ),
813 |     );
814 |     console.log(chalk.gray("------------"));
815 | 
816 |     try {
817 |       // Prepare the editor content with a header comment and the suggested value
818 |       const editorContent = [
819 |         "# Edit the translation below.",
820 |         "# Lines starting with # will be ignored.",
821 |         "# Save and exit the editor to continue.",
822 |         "#",
823 |         `# Source text (${chalk.blue("English")}):`,
824 |         `# ${args.sourceData[key]}`,
825 |         "#",
826 |         `# Current value (${chalk.red(args.targetLocale)}):`,
827 |         `# ${args.currentData[key] || "(empty)"}`,
828 |         "#",
829 |         args.proposedData[key],
830 |       ].join("\n");
831 | 
832 |       const result = externalEditor.edit(editorContent);
833 | 
834 |       // Clean up the result by removing comments and trimming
835 |       const customValue = result
836 |         .split("\n")
837 |         .filter((line) => !line.startsWith("#"))
838 |         .join("\n")
839 |         .trim();
840 | 
841 |       if (customValue) {
842 |         customData[key] = customValue;
843 |       } else {
844 |         console.log(
845 |           chalk.yellow("Empty value provided, keeping the current value."),
846 |         );
847 |         customData[key] = args.currentData[key] || args.proposedData[key];
848 |       }
849 |     } catch (error) {
850 |       console.log(
851 |         chalk.red("Error while editing, keeping the suggested value."),
852 |       );
853 |       customData[key] = args.proposedData[key];
854 |     }
855 |   }
856 | 
857 |   return customData;
858 | }
859 | 
```

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

```typescript
  1 | import { describe, it, expect, vi, beforeEach } from "vitest";
  2 | import fs from "fs/promises";
  3 | import createAndroidLoader from "./android";
  4 | 
  5 | describe("android loader", () => {
  6 |   const setupMocks = (input: string) => {
  7 |     vi.mock("fs/promises");
  8 |     vi.mocked(fs.readFile).mockResolvedValue(input);
  9 |     vi.mocked(fs.writeFile).mockResolvedValue(undefined);
 10 |   };
 11 | 
 12 |   beforeEach(() => {
 13 |     vi.clearAllMocks();
 14 |   });
 15 | 
 16 |   it("should correctly handle basic string resources", async () => {
 17 |     const input = `
 18 |       <resources>
 19 |         <string name="hello">Hello World</string>
 20 |         <string name="app_name">My App</string>
 21 |       </resources>
 22 |     `.trim();
 23 | 
 24 |     const androidLoader = createAndroidLoader().setDefaultLocale("en");
 25 |     const result = await androidLoader.pull("en", input);
 26 | 
 27 |     expect(result).toEqual({
 28 |       hello: "Hello World",
 29 |       app_name: "My App",
 30 |     });
 31 |   });
 32 | 
 33 |   it("should correctly handle string arrays", async () => {
 34 |     const input = `
 35 |       <resources>
 36 |         <string-array name="planets">
 37 |           <item>Mercury</item>
 38 |           <item>Venus</item>
 39 |           <item>Earth</item>
 40 |           <item>Mars</item>
 41 |         </string-array>
 42 |       </resources>
 43 |     `.trim();
 44 | 
 45 |     const androidLoader = createAndroidLoader().setDefaultLocale("en");
 46 |     const result = await androidLoader.pull("en", input);
 47 | 
 48 |     expect(result).toEqual({
 49 |       planets: ["Mercury", "Venus", "Earth", "Mars"],
 50 |     });
 51 |   });
 52 | 
 53 |   it("should correctly handle plurals with different quantity types", async () => {
 54 |     const input = `
 55 |       <resources>
 56 |         <plurals name="numberOfSongsAvailable">
 57 |           <item quantity="zero">No songs found.</item>
 58 |           <item quantity="one">1 song found.</item>
 59 |           <item quantity="few">%d songs found.</item>
 60 |           <item quantity="many">%d songs found.</item>
 61 |           <item quantity="other">%d songs found.</item>
 62 |         </plurals>
 63 |       </resources>
 64 |     `.trim();
 65 | 
 66 |     const androidLoader = createAndroidLoader().setDefaultLocale("en");
 67 |     const result = await androidLoader.pull("en", input);
 68 | 
 69 |     expect(result).toEqual({
 70 |       numberOfSongsAvailable: {
 71 |         zero: "No songs found.",
 72 |         one: "1 song found.",
 73 |         few: "%d songs found.",
 74 |         many: "%d songs found.",
 75 |         other: "%d songs found.",
 76 |       },
 77 |     });
 78 |   });
 79 | 
 80 |   it("should correctly handle HTML markup in strings", async () => {
 81 |     const input = `
 82 |       <resources>
 83 |         <string name="welcome">Welcome to <b>Android</b>!</string>
 84 |         <string name="formatted">This is <i>italic</i> and this is <b>bold</b></string>
 85 |       </resources>
 86 |     `.trim();
 87 | 
 88 |     const androidLoader = createAndroidLoader().setDefaultLocale("en");
 89 |     const result = await androidLoader.pull("en", input);
 90 | 
 91 |     expect(result).toEqual({
 92 |       welcome: "Welcome to <b>Android</b>!",
 93 |       formatted: "This is <i>italic</i> and this is <b>bold</b>",
 94 |     });
 95 |   });
 96 | 
 97 |   it("should correctly handle format strings", async () => {
 98 |     const input = `
 99 |       <resources>
100 |         <string name="welcome_messages">Hello, %1$s! You have %2$d new messages.</string>
101 |         <string name="complex_format">Value: %1$.2f, Text: %2$s, Number: %3$d</string>
102 |       </resources>
103 |     `.trim();
104 | 
105 |     const androidLoader = createAndroidLoader().setDefaultLocale("en");
106 |     const result = await androidLoader.pull("en", input);
107 | 
108 |     expect(result).toEqual({
109 |       welcome_messages: "Hello, %1$s! You have %2$d new messages.",
110 |       complex_format: "Value: %1$.2f, Text: %2$s, Number: %3$d",
111 |     });
112 |   });
113 | 
114 |   it("should correctly handle single quote escaping", async () => {
115 |     const input = `
116 |       <resources>
117 |         <string name="apostrophe">Don\\'t forget me</string>
118 |         <string name="escaped_quotes">This has \\'single\\' quotes</string>
119 |       </resources>
120 |     `.trim();
121 | 
122 |     const androidLoader = createAndroidLoader().setDefaultLocale("en");
123 |     const result = await androidLoader.pull("en", input);
124 | 
125 |     // Now expect normalized apostrophes in the JS object
126 |     expect(result).toEqual({
127 |       apostrophe: "Don't forget me",
128 |       escaped_quotes: "This has 'single' quotes",
129 |     });
130 | 
131 |     // When pushing back, apostrophes should be escaped again
132 |     const pushed = await androidLoader.push("en", result);
133 |     expect(pushed).toContain("Don\\'t forget me");
134 |     expect(pushed).toContain("This has \\'single\\' quotes");
135 |   });
136 | 
137 |   it("should correctly handle CDATA sections", async () => {
138 |     const input = `
139 |       <resources>
140 |         <string name="html_content"><![CDATA[<html><body><h1>Title</h1><p>Paragraph</p></body></html>]]></string>
141 |       </resources>
142 |     `.trim();
143 | 
144 |     const androidLoader = createAndroidLoader().setDefaultLocale("en");
145 |     const result = await androidLoader.pull("en", input);
146 | 
147 |     expect(result).toEqual({
148 |       html_content: "<html><body><h1>Title</h1><p>Paragraph</p></body></html>",
149 |     });
150 |   });
151 | 
152 |   it("should correctly handle multiple CDATA sections in a single string", async () => {
153 |     const input = `
154 |       <resources>
155 |         <string name="multiple_cdata"><![CDATA[<first>section</first>]]><![CDATA[<second>section</second>]]></string>
156 |       </resources>
157 |     `.trim();
158 | 
159 |     const androidLoader = createAndroidLoader().setDefaultLocale("en");
160 |     const result = await androidLoader.pull("en", input);
161 | 
162 |     expect(result).toEqual({
163 |       multiple_cdata: "<first>section</first><second>section</second>",
164 |     });
165 |   });
166 | 
167 |   it("should correctly handle nested HTML tags with attributes", async () => {
168 |     const input = `
169 |       <resources>
170 |         <string name="complex_html">This is <span style="color:red">red text</span> and <a href="https://example.com">a link</a></string>
171 |       </resources>
172 |     `.trim();
173 | 
174 |     const androidLoader = createAndroidLoader().setDefaultLocale("en");
175 |     const result = await androidLoader.pull("en", input);
176 | 
177 |     expect(result).toEqual({
178 |       complex_html:
179 |         'This is <span style="color:red">red text</span> and <a href="https://example.com">a link</a>',
180 |     });
181 |   });
182 | 
183 |   it("should correctly handle XML entities in strings", async () => {
184 |     const input = `
185 |       <resources>
186 |         <string name="entities">This string contains &lt;brackets&gt; and &amp;ampersands</string>
187 |       </resources>
188 |     `.trim();
189 | 
190 |     const androidLoader = createAndroidLoader().setDefaultLocale("en");
191 |     const result = await androidLoader.pull("en", input);
192 | 
193 |     expect(result).toEqual({
194 |       entities: "This string contains <brackets> and &ampersands",
195 |     });
196 |   });
197 | 
198 |   it("should correctly handle empty strings", async () => {
199 |     const input = `
200 |       <resources>
201 |         <string name="empty"></string>
202 |         <string name="whitespace">   </string>
203 |       </resources>
204 |     `.trim();
205 | 
206 |     const androidLoader = createAndroidLoader().setDefaultLocale("en");
207 |     const result = await androidLoader.pull("en", input);
208 | 
209 |     expect(result).toEqual({
210 |       empty: "",
211 |       whitespace: "   ",
212 |     });
213 |   });
214 | 
215 |   it("should correctly handle very long strings", async () => {
216 |     const longText = "This is a very long string.".repeat(100);
217 |     const input = `
218 |       <resources>
219 |         <string name="long_text">${longText}</string>
220 |       </resources>
221 |     `.trim();
222 | 
223 |     const androidLoader = createAndroidLoader().setDefaultLocale("en");
224 |     const result = await androidLoader.pull("en", input);
225 | 
226 |     expect(result).toEqual({
227 |       long_text: longText,
228 |     });
229 |   });
230 | 
231 |   it("should correctly handle strings with newlines and whitespace", async () => {
232 |     const input = `
233 |       <resources>
234 |         <string name="multiline">Line 1
235 | Line 2
236 |   Line 3 with indent</string>
237 |       </resources>
238 |     `.trim();
239 | 
240 |     const androidLoader = createAndroidLoader().setDefaultLocale("en");
241 |     const result = await androidLoader.pull("en", input);
242 | 
243 |     expect(result).toEqual({
244 |       multiline: "Line 1\nLine 2\n  Line 3 with indent",
245 |     });
246 |   });
247 | 
248 |   it("should correctly handle Unicode characters", async () => {
249 |     const input = `
250 |       <resources>
251 |         <string name="unicode">Unicode: 你好, こんにちは, Привет, مرحبا, 안녕하세요</string>
252 |       </resources>
253 |     `.trim();
254 | 
255 |     const androidLoader = createAndroidLoader().setDefaultLocale("en");
256 |     const result = await androidLoader.pull("en", input);
257 | 
258 |     expect(result).toEqual({
259 |       unicode: "Unicode: 你好, こんにちは, Привет, مرحبا, 안녕하세요",
260 |     });
261 |   });
262 | 
263 |   it("should skip non-translatable strings", async () => {
264 |     const input = `
265 |       <resources>
266 |         <string name="app_name" translatable="false">My App</string>
267 |         <string name="welcome">Welcome</string>
268 |         <string name="version" translatable="false">1.0.0</string>
269 |       </resources>
270 |     `.trim();
271 | 
272 |     const androidLoader = createAndroidLoader().setDefaultLocale("en");
273 |     const result = await androidLoader.pull("en", input);
274 | 
275 |     expect(result).toEqual({
276 |       welcome: "Welcome",
277 |     });
278 |     expect(result.app_name).toBeUndefined();
279 |     expect(result.version).toBeUndefined();
280 |   });
281 | 
282 |   it("should correctly push string resources back to XML", async () => {
283 |     const payload = {
284 |       hello: "Hola",
285 |       welcome: "Bienvenido",
286 |     };
287 | 
288 |     const androidLoader = createAndroidLoader().setDefaultLocale("en");
289 |     await androidLoader.pull(
290 |       "en",
291 |       `
292 |       <resources>
293 |         <string name="hello">Hello</string>
294 |         <string name="welcome">Welcome</string>
295 |       </resources>
296 |     `,
297 |     );
298 | 
299 |     const result = await androidLoader.push("es", payload);
300 | 
301 |     expect(result).toContain('<string name="hello">Hola</string>');
302 |     expect(result).toContain('<string name="welcome">Bienvenido</string>');
303 |   });
304 | 
305 |   it("should correctly push string arrays back to XML", async () => {
306 |     const payload = {
307 |       planets: ["Mercurio", "Venus", "Tierra", "Marte"],
308 |     };
309 | 
310 |     const androidLoader = createAndroidLoader().setDefaultLocale("en");
311 |     await androidLoader.pull(
312 |       "en",
313 |       `
314 |       <resources>
315 |         <string-array name="planets">
316 |           <item>Mercury</item>
317 |           <item>Venus</item>
318 |           <item>Earth</item>
319 |           <item>Mars</item>
320 |         </string-array>
321 |       </resources>
322 |     `,
323 |     );
324 | 
325 |     const result = await androidLoader.push("es", payload);
326 | 
327 |     expect(result).toContain('<string-array name="planets">');
328 |     expect(result).toContain("<item>Mercurio</item>");
329 |     expect(result).toContain("<item>Venus</item>");
330 |     expect(result).toContain("<item>Tierra</item>");
331 |     expect(result).toContain("<item>Marte</item>");
332 |   });
333 | 
334 |   it("should correctly push plurals back to XML", async () => {
335 |     const payload = {
336 |       numberOfSongsAvailable: {
337 |         zero: "No se encontraron canciones.",
338 |         one: "1 canción encontrada.",
339 |         few: "%d canciones encontradas.",
340 |         many: "%d canciones encontradas.",
341 |         other: "%d canciones encontradas.",
342 |       },
343 |     };
344 | 
345 |     const androidLoader = createAndroidLoader().setDefaultLocale("en");
346 |     await androidLoader.pull(
347 |       "en",
348 |       `
349 |       <resources>
350 |         <plurals name="numberOfSongsAvailable">
351 |           <item quantity="zero">No songs found.</item>
352 |           <item quantity="one">1 song found.</item>
353 |           <item quantity="few">%d songs found.</item>
354 |           <item quantity="many">%d songs found.</item>
355 |           <item quantity="other">%d songs found.</item>
356 |         </plurals>
357 |       </resources>
358 |     `,
359 |     );
360 | 
361 |     const result = await androidLoader.push("es", payload);
362 | 
363 |     expect(result).toContain('<plurals name="numberOfSongsAvailable">');
364 |     expect(result).toContain(
365 |       '<item quantity="zero">No se encontraron canciones.</item>',
366 |     );
367 |     expect(result).toContain(
368 |       '<item quantity="one">1 canción encontrada.</item>',
369 |     );
370 |     expect(result).toContain(
371 |       '<item quantity="few">%d canciones encontradas.</item>',
372 |     );
373 |     expect(result).toContain(
374 |       '<item quantity="many">%d canciones encontradas.</item>',
375 |     );
376 |     expect(result).toContain(
377 |       '<item quantity="other">%d canciones encontradas.</item>',
378 |     );
379 |   });
380 | 
381 |   it("should correctly handle mixed resource types", async () => {
382 |     const payload = {
383 |       app_name: "Mi Aplicación",
384 |       planets: ["Mercurio", "Venus", "Tierra", "Marte"],
385 |       numberOfSongsAvailable: {
386 |         zero: "No se encontraron canciones.",
387 |         one: "1 canción encontrada.",
388 |         other: "%d canciones encontradas.",
389 |       },
390 |     };
391 | 
392 |     const androidLoader = createAndroidLoader().setDefaultLocale("en");
393 |     await androidLoader.pull(
394 |       "en",
395 |       `
396 |       <resources>
397 |         <string name="app_name">My App</string>
398 |         <string-array name="planets">
399 |           <item>Mercury</item>
400 |           <item>Venus</item>
401 |           <item>Earth</item>
402 |           <item>Mars</item>
403 |         </string-array>
404 |         <plurals name="numberOfSongsAvailable">
405 |           <item quantity="zero">No songs found.</item>
406 |           <item quantity="one">1 song found.</item>
407 |           <item quantity="other">%d songs found.</item>
408 |         </plurals>
409 |       </resources>
410 |     `,
411 |     );
412 | 
413 |     const result = await androidLoader.push("es", payload);
414 | 
415 |     expect(result).toContain('<string name="app_name">Mi Aplicación</string>');
416 |     expect(result).toContain('<string-array name="planets">');
417 |     expect(result).toContain('<plurals name="numberOfSongsAvailable">');
418 |   });
419 | 
420 |   it("should correctly handle Unicode escape sequences", async () => {
421 |     const input = `
422 |       <resources>
423 |         <string name="unicode_escape">Unicode escape: \\u0041\\u0042\\u0043 and \\u65e5\\u672c\\u8a9e</string>
424 |       </resources>
425 |     `.trim();
426 | 
427 |     const androidLoader = createAndroidLoader().setDefaultLocale("en");
428 |     const result = await androidLoader.pull("en", input);
429 | 
430 |     expect(result).toEqual({
431 |       unicode_escape:
432 |         "Unicode escape: \\u0041\\u0042\\u0043 and \\u65e5\\u672c\\u8a9e",
433 |     });
434 | 
435 |     const pushed = await androidLoader.push("en", result);
436 |     expect(pushed).toContain(
437 |       "Unicode escape: \\u0041\\u0042\\u0043 and \\u65e5\\u672c\\u8a9e",
438 |     );
439 |   });
440 | 
441 |   it("should correctly handle double quote escaping", async () => {
442 |     const input = `
443 |       <resources>
444 |         <string name="double_quotes">He said, \\"Hello World\\"</string>
445 |       </resources>
446 |     `.trim();
447 | 
448 |     const androidLoader = createAndroidLoader().setDefaultLocale("en");
449 |     const result = await androidLoader.pull("en", input);
450 | 
451 |     expect(result).toEqual({
452 |       double_quotes: 'He said, \\"Hello World\\"',
453 |     });
454 | 
455 |     const pushed = await androidLoader.push("en", result);
456 |     expect(pushed).toContain('He said, \\"Hello World\\"');
457 |   });
458 | 
459 |   it("should correctly handle resource references", async () => {
460 |     const input = `
461 |       <resources>
462 |         <string name="welcome_message">Welcome to @string/app_name</string>
463 |         <string name="app_name">My App</string>
464 |       </resources>
465 |     `.trim();
466 | 
467 |     const androidLoader = createAndroidLoader().setDefaultLocale("en");
468 |     const result = await androidLoader.pull("en", input);
469 | 
470 |     expect(result).toEqual({
471 |       welcome_message: "Welcome to @string/app_name",
472 |       app_name: "My App",
473 |     });
474 | 
475 |     const pushed = await androidLoader.push("en", result);
476 |     expect(pushed).toContain(
477 |       '<string name="welcome_message">Welcome to @string/app_name</string>',
478 |     );
479 |   });
480 | 
481 |   it("should correctly handle tools namespace attributes", async () => {
482 |     const input = `
483 |       <resources>
484 |         <string name="debug_only" tools:ignore="MissingTranslation">Debug message</string>
485 |         <string name="normal">Normal message</string>
486 |       </resources>
487 |     `.trim();
488 | 
489 |     const androidLoader = createAndroidLoader().setDefaultLocale("en");
490 |     const result = await androidLoader.pull("en", input);
491 | 
492 |     expect(result).toEqual({
493 |       debug_only: "Debug message",
494 |       normal: "Normal message",
495 |     });
496 |   });
497 | 
498 |   it("should correctly handle whitespace preservation with double quotes", async () => {
499 |     const input = `
500 |       <resources>
501 |         <string name="preserved_whitespace">"   This string preserves    whitespace   "</string>
502 |       </resources>
503 |     `.trim();
504 | 
505 |     const androidLoader = createAndroidLoader().setDefaultLocale("en");
506 |     const result = await androidLoader.pull("en", input);
507 | 
508 |     expect(result).toEqual({
509 |       preserved_whitespace: '"   This string preserves    whitespace   "',
510 |     });
511 | 
512 |     const pushed = await androidLoader.push("en", result);
513 |     expect(pushed).toContain(
514 |       '<string name="preserved_whitespace">"   This string preserves    whitespace   "</string>',
515 |     );
516 |   });
517 | 
518 |   it("should correctly handle special characters that need escaping", async () => {
519 |     const input = `
520 |       <resources>
521 |         <string name="special_chars">Special chars: \\@, \\?, \\#, \\$, \\%</string>
522 |       </resources>
523 |     `.trim();
524 | 
525 |     const androidLoader = createAndroidLoader().setDefaultLocale("en");
526 |     const result = await androidLoader.pull("en", input);
527 | 
528 |     expect(result).toEqual({
529 |       special_chars: "Special chars: \\@, \\?, \\#, \\$, \\%",
530 |     });
531 | 
532 |     const pushed = await androidLoader.push("en", result);
533 |     expect(pushed).toContain("Special chars: \\@, \\?, \\#, \\$, \\%");
534 |   });
535 | 
536 |   it("should correctly handle apostrophes in text", async () => {
537 |     const input = `
538 |       <resources>
539 |         <string name="sign_in_agreement_text_1">J\'accepte les</string>
540 |         <string name="sign_in_agreement_text_2"> et je reconnais la </string>
541 |       </resources>
542 |     `.trim();
543 | 
544 |     const androidLoader = createAndroidLoader().setDefaultLocale("en");
545 |     const result = await androidLoader.pull("en", input);
546 | 
547 |     // During pull, escaped apostrophes should be normalized to simple apostrophes
548 |     expect(result).toEqual({
549 |       sign_in_agreement_text_1: "J'accepte les",
550 |       sign_in_agreement_text_2: " et je reconnais la ",
551 |     });
552 | 
553 |     // When pushing back, apostrophes should be escaped with backslash
554 |     const pushed = await androidLoader.push("en", result);
555 |     expect(pushed).toContain("J\\'accepte les");
556 |     expect(pushed).toContain(" et je reconnais la ");
557 |   });
558 | 
559 |   it("should escape apostrophes even in strings wrapped with double quotes", async () => {
560 |     const input = `
561 |       <resources>
562 |         <string name="quoted_apostrophe">"J'accepte les terms"</string>
563 |       </resources>
564 |     `.trim();
565 | 
566 |     const androidLoader = createAndroidLoader().setDefaultLocale("en");
567 |     const result = await androidLoader.pull("en", input);
568 | 
569 |     // During pull, the double quotes around the content should be preserved
570 |     expect(result).toEqual({
571 |       quoted_apostrophe: '"J\'accepte les terms"',
572 |     });
573 | 
574 |     // When pushing back, apostrophes should be escaped even in double-quoted strings
575 |     const pushed = await androidLoader.push("en", result);
576 |     expect(pushed).toContain('"J\\\'accepte les terms"');
577 |   });
578 | 
579 |   it("should correctly handle strings with apostrophes and avoid double escaping", async () => {
580 |     const input = `
581 |       <resources>
582 |         <string name="welcome_message">Please don't hesitate to contact us</string>
583 |         <item quantity="one">- %d user\'s item</item>
584 |         <item quantity="other">- %d user\'s items</item>
585 |       </resources>
586 |     `.trim();
587 | 
588 |     const androidLoader = createAndroidLoader().setDefaultLocale("en");
589 |     const result = await androidLoader.pull("en", input);
590 | 
591 |     // During pull, escaped apostrophes should be properly handled
592 |     expect(result.welcome_message).toBe("Please don't hesitate to contact us");
593 | 
594 |     // When pushing back, apostrophes should be escaped but not double-escaped
595 |     const pushed = await androidLoader.push("en", {
596 |       welcome_message: "Please don't hesitate to contact us",
597 |       item_count: {
598 |         one: "- %d user's item",
599 |         other: "- %d user's items",
600 |       },
601 |     });
602 | 
603 |     expect(pushed).toContain("Please don\\'t hesitate to contact us");
604 |     expect(pushed).toContain("- %d user\\'s item");
605 |     expect(pushed).not.toContain("- %d user\\\\'s item");
606 |   });
607 | 
608 |   // Tests for Issue Fixes
609 | 
610 |   it("should preserve whitespace in array items during pull and push", async () => {
611 |     const input = `
612 |       <resources>
613 |         <string-array name="mixed_items">
614 |           <item>  Item with spaces  </item>
615 |           <item>    </item>
616 |         </string-array>
617 |       </resources>
618 |     `.trim();
619 | 
620 |     const androidLoader = createAndroidLoader().setDefaultLocale("en");
621 |     const pulled = await androidLoader.pull("en", input);
622 | 
623 |     expect(pulled.mixed_items).toEqual(["  Item with spaces  ", "    "]);
624 | 
625 |     const pushed = await androidLoader.push("en", {
626 |       mixed_items: ["  Elemento con espacios  ", "    "],
627 |     });
628 | 
629 |     expect(pushed).toContain("<item>  Elemento con espacios  </item>");
630 |     expect(pushed).toContain("<item>    </item>");
631 |   });
632 | 
633 |   it("should retain CDATA wrappers for translated strings", async () => {
634 |     const input = `
635 |       <resources>
636 |         <string name="cdata_example"><![CDATA[Special <tag> ]]></string>
637 |       </resources>
638 |     `.trim();
639 | 
640 |     const androidLoader = createAndroidLoader().setDefaultLocale("en");
641 |     await androidLoader.pull("en", input);
642 | 
643 |     const pushed = await androidLoader.push("es", {
644 |       cdata_example: "Especial <tag> ",
645 |     });
646 | 
647 |     expect(pushed).toContain(
648 |       '<string name="cdata_example"><![CDATA[Especial <tag> ]]></string>',
649 |     );
650 |   });
651 | 
652 |   it("should escape apostrophes in CDATA sections", async () => {
653 |     const input = `
654 |       <resources>
655 |         <string name="review_info"><![CDATA[Hosts can't see your review until they've written one. <u>Learn more</u>]]></string>
656 |       </resources>
657 |     `.trim();
658 | 
659 |     const androidLoader = createAndroidLoader().setDefaultLocale("en");
660 |     const pulled = await androidLoader.pull("en", input);
661 | 
662 |     expect(pulled.review_info).toBe(
663 |       "Hosts can't see your review until they've written one. <u>Learn more</u>",
664 |     );
665 | 
666 |     const pushed = await androidLoader.push("fr", {
667 |       review_info:
668 |         "Les hôtes ne peuvent voir votre avis qu'après en avoir écrit un. <u>En savoir plus</u>",
669 |     });
670 | 
671 |     // Apostrophes must be escaped even inside CDATA (Android AAPT requirement)
672 |     expect(pushed).toContain("qu\\'après");
673 |     expect(pushed).toContain("<![CDATA[");
674 |     expect(pushed).toContain("]]>");
675 |     // HTML tags should NOT be escaped inside CDATA
676 |     expect(pushed).toContain("<u>En savoir plus</u>");
677 |     expect(pushed).not.toContain("&lt;u&gt;");
678 |   });
679 | 
680 |   it("should preserve resource ordering after push", async () => {
681 |     const input = `
682 |       <resources>
683 |         <string name="first">First</string>
684 |         <string-array name="colors">
685 |           <item>Red</item>
686 |           <item>Green</item>
687 |         </string-array>
688 |         <plurals name="messages">
689 |           <item quantity="one">%d message</item>
690 |           <item quantity="other">%d messages</item>
691 |         </plurals>
692 |         <bool name="show_tutorial">true</bool>
693 |         <integer name="retry_count">3</integer>
694 |       </resources>
695 |     `.trim();
696 | 
697 |     const androidLoader = createAndroidLoader().setDefaultLocale("en");
698 |     const roundTrip = await androidLoader.pull("en", input);
699 |     const pushed = await androidLoader.push("en", roundTrip);
700 | 
701 |     const order = Array.from(
702 |       pushed.matchAll(
703 |         /<(string|string-array|plurals|bool|integer)\s+name="([^"]+)"/g,
704 |       ),
705 |     ).map(([, , name]) => name);
706 | 
707 |     expect(order).toEqual([
708 |       "first",
709 |       "colors",
710 |       "messages",
711 |       "show_tutorial",
712 |       "retry_count",
713 |     ]);
714 |   });
715 | 
716 |   it("should preserve XML declaration from source file", async () => {
717 |     const input = `<?xml version="1.0" encoding="utf-8"?>
718 | <resources>
719 |     <string name="test">Test</string>
720 | </resources>`;
721 | 
722 |     const androidLoader = createAndroidLoader().setDefaultLocale("en");
723 |     await androidLoader.pull("en", input);
724 | 
725 |     const result = await androidLoader.push("es", { test: "Prueba" });
726 | 
727 |     expect(result).toMatch(/^<\?xml version="1\.0" encoding="utf-8"\?>/);
728 |   });
729 | 
730 |   it('should exclude translatable="false" items from target locale', async () => {
731 |     const input = `
732 |       <resources>
733 |         <string name="app_name">My App</string>
734 |         <string name="api_url" translatable="false">https://api.example.com</string>
735 |         <string name="debug_key" translatable="false">DEBUG_KEY</string>
736 |         <string-array name="colors">
737 |           <item>Red</item>
738 |         </string-array>
739 |         <string-array name="urls" translatable="false">
740 |           <item>https://example.com</item>
741 |         </string-array>
742 |         <plurals name="items">
743 |           <item quantity="one">%d item</item>
744 |           <item quantity="other">%d items</item>
745 |         </plurals>
746 |         <plurals name="bytes" translatable="false">
747 |           <item quantity="one">%d byte</item>
748 |           <item quantity="other">%d bytes</item>
749 |         </plurals>
750 |         <bool name="show_tutorial">true</bool>
751 |         <bool name="is_debug" translatable="false">false</bool>
752 |         <integer name="timeout">30</integer>
753 |         <integer name="version" translatable="false">42</integer>
754 |       </resources>
755 |     `.trim();
756 | 
757 |     const androidLoader = createAndroidLoader().setDefaultLocale("en");
758 |     await androidLoader.pull("en", input);
759 | 
760 |     const result = await androidLoader.push("es", {
761 |       app_name: "Mi Aplicación",
762 |       colors: ["Rojo"],
763 |       items: { one: "%d elemento", other: "%d elementos" },
764 |       show_tutorial: true,
765 |       timeout: 30,
766 |     });
767 | 
768 |     // Check that translatable="false" items are NOT included
769 |     expect(result).not.toContain('name="api_url"');
770 |     expect(result).not.toContain("https://api.example.com");
771 |     expect(result).not.toContain('name="debug_key"');
772 |     expect(result).not.toContain("DEBUG_KEY");
773 |     expect(result).not.toContain('name="urls"');
774 |     expect(result).not.toContain('name="bytes"');
775 |     expect(result).not.toContain('name="is_debug"');
776 |     expect(result).not.toContain('name="version"');
777 | 
778 |     // Check that translatable items are translated
779 |     expect(result).toContain("Mi Aplicación");
780 |     expect(result).toContain("Rojo");
781 |     expect(result).toContain("elemento");
782 |     expect(result).toContain('name="app_name"');
783 |     expect(result).toContain('name="colors"');
784 |     expect(result).toContain('name="items"');
785 |     expect(result).toContain('name="show_tutorial"');
786 |     expect(result).toContain('name="timeout"');
787 |   });
788 | 
789 |   it("should use 4-space indentation by default", async () => {
790 |     const input = `<?xml version="1.0" encoding="utf-8"?>
791 | <resources>
792 |     <string name="test">Test</string>
793 |     <string name="another">Another</string>
794 | </resources>`;
795 | 
796 |     const androidLoader = createAndroidLoader().setDefaultLocale("en");
797 |     await androidLoader.pull("en", input);
798 | 
799 |     const result = await androidLoader.push("es", {
800 |       test: "Prueba",
801 |       another: "Otro",
802 |     });
803 | 
804 |     // Check for 4-space indentation (default)
805 |     // Note: Users should use formatters (Prettier/Biome) for custom indentation
806 |     expect(result).toContain('\n    <string name="test">');
807 |     expect(result).toContain('\n    <string name="another">');
808 |   });
809 | 
810 |   it("should preserve XML declaration encoding from source file", async () => {
811 |     const inputUtf8 = `<?xml version="1.0" encoding="utf-8"?>
812 | <resources>
813 |     <string name="test">Test</string>
814 | </resources>`;
815 | 
816 |     const inputUpperUTF8 = `<?xml version="1.0" encoding="UTF-8"?>
817 | <resources>
818 |     <string name="test">Test</string>
819 | </resources>`;
820 | 
821 |     const inputISO = `<?xml version="1.0" encoding="ISO-8859-1"?>
822 | <resources>
823 |     <string name="test">Test</string>
824 | </resources>`;
825 | 
826 |     const androidLoader = createAndroidLoader().setDefaultLocale("en");
827 | 
828 |     // Test lowercase utf-8
829 |     await androidLoader.pull("en", inputUtf8);
830 |     let result = await androidLoader.push("es", { test: "Prueba" });
831 |     expect(result).toMatch(/^<\?xml version="1\.0" encoding="utf-8"\?>/);
832 | 
833 |     // Test uppercase UTF-8
834 |     await androidLoader.pull("en", inputUpperUTF8);
835 |     result = await androidLoader.push("es", { test: "Prueba" });
836 |     expect(result).toMatch(/^<\?xml version="1\.0" encoding="UTF-8"\?>/);
837 | 
838 |     // Test ISO-8859-1
839 |     await androidLoader.pull("en", inputISO);
840 |     result = await androidLoader.push("es", { test: "Prueba" });
841 |     expect(result).toMatch(/^<\?xml version="1\.0" encoding="ISO-8859-1"\?>/);
842 |   });
843 | 
844 |   it("should preserve XML version from source file", async () => {
845 |     const inputV10 = `<?xml version="1.0" encoding="utf-8"?>
846 | <resources>
847 |     <string name="test">Test</string>
848 | </resources>`;
849 | 
850 |     const inputV11 = `<?xml version="1.1" encoding="utf-8"?>
851 | <resources>
852 |     <string name="test">Test</string>
853 | </resources>`;
854 | 
855 |     const androidLoader = createAndroidLoader().setDefaultLocale("en");
856 | 
857 |     // Test version 1.0
858 |     await androidLoader.pull("en", inputV10);
859 |     let result = await androidLoader.push("es", { test: "Prueba" });
860 |     expect(result).toMatch(/^<\?xml version="1\.0"/);
861 | 
862 |     // Test version 1.1
863 |     await androidLoader.pull("en", inputV11);
864 |     result = await androidLoader.push("es", { test: "Prueba" });
865 |     expect(result).toMatch(/^<\?xml version="1\.1"/);
866 |   });
867 | 
868 |   it("should omit XML declaration when source has none", async () => {
869 |     const inputNoDeclaration = `<resources>
870 |     <string name="test">Test</string>
871 | </resources>`;
872 | 
873 |     const androidLoader = createAndroidLoader().setDefaultLocale("en");
874 |     await androidLoader.pull("en", inputNoDeclaration);
875 | 
876 |     const result = await androidLoader.push("es", { test: "Prueba" });
877 | 
878 |     // Should start immediately with the root element (no declaration)
879 |     expect(result).not.toMatch(/^<\?xml/);
880 |     expect(result.trim().startsWith("<resources>")).toBe(true);
881 |   });
882 | });
883 | 
```

--------------------------------------------------------------------------------
/legacy/cli/CHANGELOG.md:
--------------------------------------------------------------------------------

```markdown
  1 | # replexica
  2 | 
  3 | ## 0.71.0
  4 | 
  5 | ### Minor Changes
  6 | 
  7 | - [#428](https://github.com/lingodotdev/lingo.dev/pull/428) [`5dd7b65`](https://github.com/lingodotdev/lingo.dev/commit/5dd7b6529ce174d8759e80644c3233927b1ecce4) Thanks [@mathio](https://github.com/mathio)! - map old env vars
  8 | 
  9 | ### Patch Changes
 10 | 
 11 | - Updated dependencies [[`cd836e4`](https://github.com/lingodotdev/lingo.dev/commit/cd836e45cf810f495e2c6e1449824dc84794d571), [`5dd7b65`](https://github.com/lingodotdev/lingo.dev/commit/5dd7b6529ce174d8759e80644c3233927b1ecce4)]:
 12 |   - [email protected]
 13 | 
 14 | ## 0.70.1
 15 | 
 16 | ### Patch Changes
 17 | 
 18 | - [`5dee9ee`](https://github.com/lingodotdev/lingo.dev/commit/5dee9ee743fbef489fbe342597a768ebd59e5f67) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add proxies to legacy packages
 19 | 
 20 | - [`63eb57b`](https://github.com/lingodotdev/lingo.dev/commit/63eb57b8f4cc37605be196085fafbbfdab71cce5) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add deprecation message to legacy package jsons
 21 | 
 22 | - [`bbf7760`](https://github.com/lingodotdev/lingo.dev/commit/bbf7760580f1631805d68612053ebcd4601bb02b) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add deprecation warning to the legacy package proxies
 23 | 
 24 | - Updated dependencies [[`b4c7f1e`](https://github.com/lingodotdev/lingo.dev/commit/b4c7f1e86334d229bee62219c26f30d0b523926d)]:
 25 |   - [email protected]
 26 | 
 27 | ## 0.70.0
 28 | 
 29 | ### Minor Changes
 30 | 
 31 | - [`003344f`](https://github.com/lingodotdev/lingo.dev/commit/003344ffcca98a391a298707f18476971c4d4c2b) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add locale delimiter override
 32 | 
 33 | ### Patch Changes
 34 | 
 35 | - Updated dependencies [[`003344f`](https://github.com/lingodotdev/lingo.dev/commit/003344ffcca98a391a298707f18476971c4d4c2b)]:
 36 |   - @replexica/[email protected]
 37 |   - @replexica/[email protected]
 38 | 
 39 | ## 0.69.0
 40 | 
 41 | ### Minor Changes
 42 | 
 43 | - [#411](https://github.com/lingodotdev/lingo.dev/pull/411) [`1b0647d`](https://github.com/lingodotdev/lingo.dev/commit/1b0647d91080f4947ba6227c397fb6232d0d1907) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add structure sync loader to cli
 44 | 
 45 | ### Patch Changes
 46 | 
 47 | - Updated dependencies [[`1b0647d`](https://github.com/lingodotdev/lingo.dev/commit/1b0647d91080f4947ba6227c397fb6232d0d1907)]:
 48 |   - @replexica/[email protected]
 49 | 
 50 | ## 0.68.0
 51 | 
 52 | ### Minor Changes
 53 | 
 54 | - [#408](https://github.com/lingodotdev/lingo.dev/pull/408) [`36fd4af`](https://github.com/lingodotdev/lingo.dev/commit/36fd4af376caf1540dc0a594fd65742c81706aa0) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - disable .po folding
 55 | 
 56 | ### Patch Changes
 57 | 
 58 | - Updated dependencies [[`36fd4af`](https://github.com/lingodotdev/lingo.dev/commit/36fd4af376caf1540dc0a594fd65742c81706aa0)]:
 59 |   - @replexica/[email protected]
 60 | 
 61 | ## 0.67.0
 62 | 
 63 | ### Minor Changes
 64 | 
 65 | - [#405](https://github.com/lingodotdev/lingo.dev/pull/405) [`446cf9c`](https://github.com/lingodotdev/lingo.dev/commit/446cf9c5c933f71a43fd5d80487b1608023cba8e) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - improved .po loader
 66 | 
 67 | - [#404](https://github.com/lingodotdev/lingo.dev/pull/404) [`3edef26`](https://github.com/lingodotdev/lingo.dev/commit/3edef26ef3a4e2d27394c5eeb2bc94b164e037ac) Thanks [@mathio](https://github.com/mathio)! - interactive init comman
 68 | 
 69 | ### Patch Changes
 70 | 
 71 | - Updated dependencies [[`446cf9c`](https://github.com/lingodotdev/lingo.dev/commit/446cf9c5c933f71a43fd5d80487b1608023cba8e), [`3edef26`](https://github.com/lingodotdev/lingo.dev/commit/3edef26ef3a4e2d27394c5eeb2bc94b164e037ac)]:
 72 |   - @replexica/[email protected]
 73 | 
 74 | ## 0.66.2
 75 | 
 76 | ### Patch Changes
 77 | 
 78 | - [#399](https://github.com/lingodotdev/lingo.dev/pull/399) [`01ca7bb`](https://github.com/lingodotdev/lingo.dev/commit/01ca7bb9d28d0de903caf44ec6ede2e2bbbb3ba2) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - feat(cli): enhance .po loader to support plural translations and improve loader composition
 79 | 
 80 | - Updated dependencies [[`01ca7bb`](https://github.com/lingodotdev/lingo.dev/commit/01ca7bb9d28d0de903caf44ec6ede2e2bbbb3ba2)]:
 81 |   - @replexica/[email protected]
 82 | 
 83 | ## 0.66.1
 84 | 
 85 | ### Patch Changes
 86 | 
 87 | - [`aef36b5`](https://github.com/lingodotdev/lingo.dev/commit/aef36b53163efa523f7632786e0f293890f05b23) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - improve .po handling
 88 | 
 89 | - Updated dependencies [[`aef36b5`](https://github.com/lingodotdev/lingo.dev/commit/aef36b53163efa523f7632786e0f293890f05b23)]:
 90 |   - @replexica/[email protected]
 91 | 
 92 | ## 0.66.0
 93 | 
 94 | ### Minor Changes
 95 | 
 96 | - [`e885fcf`](https://github.com/lingodotdev/lingo.dev/commit/e885fcf8731d9f2a250cf44a534c5556a057ca51) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - single quotes escaping
 97 | 
 98 | ### Patch Changes
 99 | 
100 | - Updated dependencies [[`e885fcf`](https://github.com/lingodotdev/lingo.dev/commit/e885fcf8731d9f2a250cf44a534c5556a057ca51)]:
101 |   - @replexica/[email protected]
102 | 
103 | ## 0.65.1
104 | 
105 | ### Patch Changes
106 | 
107 | - [#390](https://github.com/lingodotdev/lingo.dev/pull/390) [`a2ada16`](https://github.com/lingodotdev/lingo.dev/commit/a2ada16ecf4cd559d3486f0e4756d58808194f7e) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add ieee variables support
108 | 
109 | - Updated dependencies [[`a2ada16`](https://github.com/lingodotdev/lingo.dev/commit/a2ada16ecf4cd559d3486f0e4756d58808194f7e)]:
110 |   - @replexica/[email protected]
111 |   - @replexica/[email protected]
112 | 
113 | ## 0.65.0
114 | 
115 | ### Minor Changes
116 | 
117 | - [`bd577f2`](https://github.com/lingodotdev/lingo.dev/commit/bd577f22da52e7e889bb4b419cb5dab9865512f1) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - remove unlocalizable from dato
118 | 
119 | ### Patch Changes
120 | 
121 | - Updated dependencies [[`bd577f2`](https://github.com/lingodotdev/lingo.dev/commit/bd577f22da52e7e889bb4b419cb5dab9865512f1)]:
122 |   - @replexica/[email protected]
123 | 
124 | ## 0.64.0
125 | 
126 | ### Minor Changes
127 | 
128 | - [#387](https://github.com/lingodotdev/lingo.dev/pull/387) [`8db4527`](https://github.com/lingodotdev/lingo.dev/commit/8db4527d9c3501d97f8bb7b414dd61e8a3ee80f6) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add support for blocks / array of blocks / nested blocks
129 | 
130 | ### Patch Changes
131 | 
132 | - Updated dependencies [[`8db4527`](https://github.com/lingodotdev/lingo.dev/commit/8db4527d9c3501d97f8bb7b414dd61e8a3ee80f6)]:
133 |   - @replexica/[email protected]
134 | 
135 | ## 0.63.1
136 | 
137 | ### Patch Changes
138 | 
139 | - [#382](https://github.com/lingodotdev/lingo.dev/pull/382) [`3320c8c`](https://github.com/lingodotdev/lingo.dev/commit/3320c8c6f9df9671e1002b63a00bf877270a6064) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - fix lockfile resetting when --key flag is applied
140 | 
141 | - Updated dependencies [[`3320c8c`](https://github.com/lingodotdev/lingo.dev/commit/3320c8c6f9df9671e1002b63a00bf877270a6064)]:
142 |   - @replexica/[email protected]
143 | 
144 | ## 0.63.0
145 | 
146 | ### Minor Changes
147 | 
148 | - [`db2e800`](https://github.com/lingodotdev/lingo.dev/commit/db2e80013e44b478331b6a97008b3e67bae82a1f) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add --key flag for selective updates
149 | 
150 | ### Patch Changes
151 | 
152 | - Updated dependencies [[`db2e800`](https://github.com/lingodotdev/lingo.dev/commit/db2e80013e44b478331b6a97008b3e67bae82a1f)]:
153 |   - @replexica/[email protected]
154 | 
155 | ## 0.62.0
156 | 
157 | ### Minor Changes
158 | 
159 | - [`302afdf`](https://github.com/lingodotdev/lingo.dev/commit/302afdfd3047b781bd9688921eab3dc84173aa20) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - handle C specifiers in localizable content
160 | 
161 | ### Patch Changes
162 | 
163 | - Updated dependencies [[`302afdf`](https://github.com/lingodotdev/lingo.dev/commit/302afdfd3047b781bd9688921eab3dc84173aa20)]:
164 |   - @replexica/[email protected]
165 | 
166 | ## 0.61.0
167 | 
168 | ### Minor Changes
169 | 
170 | - [`9d38df2`](https://github.com/lingodotdev/lingo.dev/commit/9d38df2fdbe23fdcbb1b7e2e207de650e714e433) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - fixed same-file locale rewrites
171 | 
172 | ### Patch Changes
173 | 
174 | - Updated dependencies [[`9d38df2`](https://github.com/lingodotdev/lingo.dev/commit/9d38df2fdbe23fdcbb1b7e2e207de650e714e433)]:
175 |   - @replexica/[email protected]
176 | 
177 | ## 0.60.1
178 | 
179 | ### Patch Changes
180 | 
181 | - [#372](https://github.com/lingodotdev/lingo.dev/pull/372) [`b9a8350`](https://github.com/lingodotdev/lingo.dev/commit/b9a83502803f4a62fc9a62b4348f853f2baff20d) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - fix single-file results overwriting
182 | 
183 | - [#371](https://github.com/lingodotdev/lingo.dev/pull/371) [`e6521b8`](https://github.com/lingodotdev/lingo.dev/commit/e6521b86637c254c011aba89a3558802c04ab3ca) Thanks [@mathio](https://github.com/mathio)! - support underscore in locale code
184 | 
185 | - Updated dependencies [[`b9a8350`](https://github.com/lingodotdev/lingo.dev/commit/b9a83502803f4a62fc9a62b4348f853f2baff20d), [`e6521b8`](https://github.com/lingodotdev/lingo.dev/commit/e6521b86637c254c011aba89a3558802c04ab3ca)]:
186 |   - @replexica/[email protected]
187 |   - @replexica/[email protected]
188 | 
189 | ## 0.60.0
190 | 
191 | ### Minor Changes
192 | 
193 | - [#356](https://github.com/lingodotdev/lingo.dev/pull/356) [`cff3c4e`](https://github.com/lingodotdev/lingo.dev/commit/cff3c4eb1a40f82a9c4c095e49cfd9fce053b048) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add dato support
194 | 
195 | ### Patch Changes
196 | 
197 | - Updated dependencies [[`cff3c4e`](https://github.com/lingodotdev/lingo.dev/commit/cff3c4eb1a40f82a9c4c095e49cfd9fce053b048)]:
198 |   - @replexica/[email protected]
199 |   - @replexica/[email protected]
200 |   - @replexica/[email protected]
201 | 
202 | ## 0.59.1
203 | 
204 | ### Patch Changes
205 | 
206 | - Updated dependencies []:
207 |   - @replexica/[email protected]
208 |   - @replexica/[email protected]
209 |   - @replexica/[email protected]
210 | 
211 | ## 0.59.0
212 | 
213 | ### Minor Changes
214 | 
215 | - [`63daf00`](https://github.com/lingodotdev/lingo.dev/commit/63daf00e80004775f12c9e1d426cdd2bbf10f5a4) Thanks [@vrcprl](https://github.com/vrcprl)! - noop
216 | 
217 | ### Patch Changes
218 | 
219 | - [`6eb5282`](https://github.com/lingodotdev/lingo.dev/commit/6eb5282063515db93fc76ff3137422862720fa0d) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - noop
220 | 
221 | - Updated dependencies [[`63daf00`](https://github.com/lingodotdev/lingo.dev/commit/63daf00e80004775f12c9e1d426cdd2bbf10f5a4), [`3ab5de6`](https://github.com/lingodotdev/lingo.dev/commit/3ab5de66d8a913297b46095c2e73823124cc8c5b), [`3ab5de6`](https://github.com/lingodotdev/lingo.dev/commit/3ab5de66d8a913297b46095c2e73823124cc8c5b), [`6eb5282`](https://github.com/lingodotdev/lingo.dev/commit/6eb5282063515db93fc76ff3137422862720fa0d)]:
222 |   - @replexica/[email protected]
223 |   - @replexica/[email protected]
224 |   - @replexica/[email protected]
225 | 
226 | ## 0.58.2
227 | 
228 | ### Patch Changes
229 | 
230 | - Updated dependencies []:
231 |   - @replexica/[email protected]
232 |   - @replexica/[email protected]
233 |   - @replexica/[email protected]
234 | 
235 | ## 0.58.1
236 | 
237 | ### Patch Changes
238 | 
239 | - Updated dependencies []:
240 |   - @replexica/[email protected]
241 |   - @replexica/[email protected]
242 | 
243 | ## 0.58.0
244 | 
245 | ### Minor Changes
246 | 
247 | - [`ff0d2d7`](https://github.com/lingodotdev/lingo.dev/commit/ff0d2d7fb12806a7264a72c03e48a8dda3526c23) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add retry with exponential backoff to the cli
248 | 
249 | ### Patch Changes
250 | 
251 | - [`7ff7f8f`](https://github.com/lingodotdev/lingo.dev/commit/7ff7f8fca7318e4dba929194972d20ccf3487e9d) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - display number of entries in localization completion message
252 | 
253 | - Updated dependencies [[`7ff7f8f`](https://github.com/lingodotdev/lingo.dev/commit/7ff7f8fca7318e4dba929194972d20ccf3487e9d), [`ff0d2d7`](https://github.com/lingodotdev/lingo.dev/commit/ff0d2d7fb12806a7264a72c03e48a8dda3526c23)]:
254 |   - @replexica/[email protected]
255 | 
256 | ## 0.57.1
257 | 
258 | ### Patch Changes
259 | 
260 | - Updated dependencies []:
261 |   - @replexica/[email protected]
262 |   - @replexica/[email protected]
263 |   - @replexica/[email protected]
264 | 
265 | ## 0.57.0
266 | 
267 | ### Minor Changes
268 | 
269 | - [`8e2cee4`](https://github.com/lingodotdev/lingo.dev/commit/8e2cee4b282c39fef1e00fa429e03e1c1e489cc5) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add `cleanup` command
270 | 
271 | ### Patch Changes
272 | 
273 | - [`2c5cbcf`](https://github.com/lingodotdev/lingo.dev/commit/2c5cbcfbf6feb28440255cdea0818c8cefa61d91) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - filter out non extistent keys
274 | 
275 | - [`ca10072`](https://github.com/lingodotdev/lingo.dev/commit/ca10072f636d8bd1105ed0f6cc84cf0af5a12402) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - improve progress logging in cli
276 | 
277 | - Updated dependencies [[`2c5cbcf`](https://github.com/lingodotdev/lingo.dev/commit/2c5cbcfbf6feb28440255cdea0818c8cefa61d91), [`8e2cee4`](https://github.com/lingodotdev/lingo.dev/commit/8e2cee4b282c39fef1e00fa429e03e1c1e489cc5), [`ca10072`](https://github.com/lingodotdev/lingo.dev/commit/ca10072f636d8bd1105ed0f6cc84cf0af5a12402)]:
278 |   - @replexica/[email protected]
279 |   - @replexica/[email protected]
280 | 
281 | ## 0.56.3
282 | 
283 | ### Patch Changes
284 | 
285 | - [`b8ad864`](https://github.com/lingodotdev/lingo.dev/commit/b8ad8643347088635eeeb568f1818d71d5226269) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - feat(cli): disable safe mode at localizable chunk level
286 | 
287 | - Updated dependencies [[`b8ad864`](https://github.com/lingodotdev/lingo.dev/commit/b8ad8643347088635eeeb568f1818d71d5226269)]:
288 |   - @replexica/[email protected]
289 | 
290 | ## 0.56.2
291 | 
292 | ### Patch Changes
293 | 
294 | - Updated dependencies []:
295 |   - @replexica/[email protected]
296 |   - @replexica/[email protected]
297 | 
298 | ## 0.56.1
299 | 
300 | ### Patch Changes
301 | 
302 | - Updated dependencies []:
303 |   - @replexica/[email protected]
304 |   - @replexica/[email protected]
305 | 
306 | ## 0.56.0
307 | 
308 | ### Minor Changes
309 | 
310 | - [#298](https://github.com/lingodotdev/lingo.dev/pull/298) [`c03437d`](https://github.com/lingodotdev/lingo.dev/commit/c03437dc9cfd8183e40f74926b4ba7f0874ebf81) Thanks [@partik03](https://github.com/partik03)! - implemented xml loader
311 | 
312 | - [`42d0a5a`](https://github.com/lingodotdev/lingo.dev/commit/42d0a5a7a53e296192a31e8f1d67c126793ea280) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - added .localizeHtml implementation to SDK
313 | 
314 | ### Patch Changes
315 | 
316 | - Updated dependencies [[`c03437d`](https://github.com/lingodotdev/lingo.dev/commit/c03437dc9cfd8183e40f74926b4ba7f0874ebf81), [`a6b22a3`](https://github.com/lingodotdev/lingo.dev/commit/a6b22a3237f574455d8119f914d82b0b247b4151), [`42d0a5a`](https://github.com/lingodotdev/lingo.dev/commit/42d0a5a7a53e296192a31e8f1d67c126793ea280)]:
317 |   - @replexica/[email protected]
318 |   - @replexica/[email protected]
319 |   - @replexica/[email protected]
320 | 
321 | ## 0.55.0
322 | 
323 | ### Minor Changes
324 | 
325 | - [`57e395a`](https://github.com/lingodotdev/lingo.dev/commit/57e395aae8ab100ba470bc7d1104ddfa178249e7) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add `--source` and `--target` flags to show files cmd
326 | 
327 | ### Patch Changes
328 | 
329 | - Updated dependencies [[`57e395a`](https://github.com/lingodotdev/lingo.dev/commit/57e395aae8ab100ba470bc7d1104ddfa178249e7)]:
330 |   - @replexica/[email protected]
331 | 
332 | ## 0.54.0
333 | 
334 | ### Minor Changes
335 | 
336 | - [#301](https://github.com/lingodotdev/lingo.dev/pull/301) [`44b4cca`](https://github.com/lingodotdev/lingo.dev/commit/44b4cca2718bd72d55a938bac458d32a4536508a) Thanks [@partik03](https://github.com/partik03)! - --frozen flag
337 | 
338 | - [`4fc27da`](https://github.com/lingodotdev/lingo.dev/commit/4fc27daae5810f6167726a28d76a874fd8421a5b) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - replexica show files now shows both source and target paths
339 | 
340 | ### Patch Changes
341 | 
342 | - Updated dependencies [[`44b4cca`](https://github.com/lingodotdev/lingo.dev/commit/44b4cca2718bd72d55a938bac458d32a4536508a), [`4fc27da`](https://github.com/lingodotdev/lingo.dev/commit/4fc27daae5810f6167726a28d76a874fd8421a5b)]:
343 |   - @replexica/[email protected]
344 | 
345 | ## 0.53.1
346 | 
347 | ### Patch Changes
348 | 
349 | - [`44b5c5c`](https://github.com/lingodotdev/lingo.dev/commit/44b5c5c498ca8df3bb814764f40057576c28c941) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - downgrade glob to @10, to allow node 18
350 | 
351 | - Updated dependencies [[`44b5c5c`](https://github.com/lingodotdev/lingo.dev/commit/44b5c5c498ca8df3bb814764f40057576c28c941)]:
352 |   - @replexica/[email protected]
353 | 
354 | ## 0.53.0
355 | 
356 | ### Minor Changes
357 | 
358 | - [`072e23e`](https://github.com/lingodotdev/lingo.dev/commit/072e23e58fca0da20bfd01f6a0ae600e6fb760a8) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - hide process summary label when there's zero elements to show
359 | 
360 | ### Patch Changes
361 | 
362 | - Updated dependencies [[`072e23e`](https://github.com/lingodotdev/lingo.dev/commit/072e23e58fca0da20bfd01f6a0ae600e6fb760a8)]:
363 |   - @replexica/[email protected]
364 | 
365 | ## 0.51.2
366 | 
367 | ### Patch Changes
368 | 
369 | - Updated dependencies [[`6bc309c`](https://github.com/lingodotdev/lingo.dev/commit/6bc309c56a8e6a468510109182fd75f8f4e61b8f)]:
370 |   - @replexica/[email protected]
371 | 
372 | ## 0.51.1
373 | 
374 | ### Patch Changes
375 | 
376 | - [`e511b50`](https://github.com/lingodotdev/lingo.dev/commit/e511b5080dba58728e8650c7bf34d810cccdcf4e) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - added node@18 support
377 | 
378 | - Updated dependencies [[`e511b50`](https://github.com/lingodotdev/lingo.dev/commit/e511b5080dba58728e8650c7bf34d810cccdcf4e)]:
379 |   - @replexica/[email protected]
380 | 
381 | ## 0.51.0
382 | 
383 | ### Minor Changes
384 | 
385 | - [#275](https://github.com/lingodotdev/lingo.dev/pull/275) [`091ee35`](https://github.com/lingodotdev/lingo.dev/commit/091ee353081795bf8f61c7d41483bc309c7b62ef) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add support for `.po` format
386 | 
387 | ### Patch Changes
388 | 
389 | - Updated dependencies [[`091ee35`](https://github.com/lingodotdev/lingo.dev/commit/091ee353081795bf8f61c7d41483bc309c7b62ef)]:
390 |   - @replexica/[email protected]
391 |   - @replexica/[email protected]
392 |   - @replexica/[email protected]
393 | 
394 | ## 0.50.0
395 | 
396 | ### Minor Changes
397 | 
398 | - [#268](https://github.com/lingodotdev/lingo.dev/pull/268) [`5e282d7`](https://github.com/lingodotdev/lingo.dev/commit/5e282d7ffa5ca9494aa7046a090bb7c327085a86) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - composable loaders
399 | 
400 | ### Patch Changes
401 | 
402 | - Updated dependencies [[`5e282d7`](https://github.com/lingodotdev/lingo.dev/commit/5e282d7ffa5ca9494aa7046a090bb7c327085a86)]:
403 |   - @replexica/[email protected]
404 |   - @replexica/[email protected]
405 |   - @replexica/[email protected]
406 | 
407 | ## 0.49.1
408 | 
409 | ### Patch Changes
410 | 
411 | - [`64cd6f3`](https://github.com/lingodotdev/lingo.dev/commit/64cd6f3765bb4524e9f78f93ff283e833a6f26a2) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - fixed path patter relativity
412 | 
413 | - Updated dependencies [[`64cd6f3`](https://github.com/lingodotdev/lingo.dev/commit/64cd6f3765bb4524e9f78f93ff283e833a6f26a2)]:
414 |   - @replexica/[email protected]
415 | 
416 | ## 0.49.0
417 | 
418 | ### Minor Changes
419 | 
420 | - [`0071cd6`](https://github.com/lingodotdev/lingo.dev/commit/0071cd66b1c868ad3898fc368390a628c5a67767) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add csv format support
421 | 
422 | ### Patch Changes
423 | 
424 | - [`1cc0796`](https://github.com/lingodotdev/lingo.dev/commit/1cc07961d221e397ad5dd2917bed76cb4f2b1f04) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add path.resolve to text loaders
425 | 
426 | - Updated dependencies [[`0071cd6`](https://github.com/lingodotdev/lingo.dev/commit/0071cd66b1c868ad3898fc368390a628c5a67767), [`1cc0796`](https://github.com/lingodotdev/lingo.dev/commit/1cc07961d221e397ad5dd2917bed76cb4f2b1f04)]:
427 |   - @replexica/[email protected]
428 |   - @replexica/[email protected]
429 |   - @replexica/[email protected]
430 | 
431 | ## 0.48.0
432 | 
433 | ### Minor Changes
434 | 
435 | - [#264](https://github.com/lingodotdev/lingo.dev/pull/264) [`cdef5b7`](https://github.com/lingodotdev/lingo.dev/commit/cdef5b7bfbee4670c6de62cf4b4f3e0315748e25) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - added format specific methods to `@replexica/sdk`
436 | 
437 | ### Patch Changes
438 | 
439 | - [#261](https://github.com/lingodotdev/lingo.dev/pull/261) [`62c464d`](https://github.com/lingodotdev/lingo.dev/commit/62c464d5602909f8f6370dfa5009131a4d6719d0) Thanks [@Nithishvb](https://github.com/Nithishvb)! - This pr introduces a custom error handling base class for the CLI
440 | 
441 | - Updated dependencies [[`cdef5b7`](https://github.com/lingodotdev/lingo.dev/commit/cdef5b7bfbee4670c6de62cf4b4f3e0315748e25), [`62c464d`](https://github.com/lingodotdev/lingo.dev/commit/62c464d5602909f8f6370dfa5009131a4d6719d0)]:
442 |   - @replexica/[email protected]
443 |   - @replexica/[email protected]
444 | 
445 | ## 0.47.1
446 | 
447 | ### Patch Changes
448 | 
449 | - [`2859938`](https://github.com/lingodotdev/lingo.dev/commit/28599388a91bf80cea3813bb4b8999bb4df302c9) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add missing locales
450 | 
451 | - Updated dependencies []:
452 |   - @replexica/[email protected]
453 |   - @replexica/[email protected]
454 |   - @replexica/[email protected]
455 | 
456 | ## 0.47.0
457 | 
458 | ### Minor Changes
459 | 
460 | - [`4dfc8d8`](https://github.com/lingodotdev/lingo.dev/commit/4dfc8d8b301a690875401af5d107a88f1716182a) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - added support for android format
461 | 
462 | - [`ca9e20e`](https://github.com/lingodotdev/lingo.dev/commit/ca9e20eef9047e20d39ccf9dff74d2f6069d4676) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - .strings support
463 | 
464 | - [`2aedf3b`](https://github.com/lingodotdev/lingo.dev/commit/2aedf3bec2d9dffc7b43fc10dea0cab5742d44af) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - added support for .stringsdict
465 | 
466 | - [#245](https://github.com/lingodotdev/lingo.dev/pull/245) [`3fc9da7`](https://github.com/lingodotdev/lingo.dev/commit/3fc9da7e3d2ec58e7f278c79a53eae6d3dfa5896) Thanks [@Nithishvb](https://github.com/Nithishvb)! - prevented overwritting of i18n.json with a default template for unsupported locales
467 | 
468 | - [`626082a`](https://github.com/lingodotdev/lingo.dev/commit/626082a64b88fb3b589acd950afeafe417ce5ddc) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - added Flutter .arb support
469 | 
470 | ### Patch Changes
471 | 
472 | - [`2b5e3ae`](https://github.com/lingodotdev/lingo.dev/commit/2b5e3aea3f0745955266f6edf2ce34830242e503) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - fixed yaml-root-key loader
473 | 
474 | - [`747847a`](https://github.com/lingodotdev/lingo.dev/commit/747847a86720d4c36f15daeb41d13d0aff129ca9) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - fixed .xcstrings plurals
475 | 
476 | - Updated dependencies [[`2b5e3ae`](https://github.com/lingodotdev/lingo.dev/commit/2b5e3aea3f0745955266f6edf2ce34830242e503), [`4dfc8d8`](https://github.com/lingodotdev/lingo.dev/commit/4dfc8d8b301a690875401af5d107a88f1716182a), [`ca9e20e`](https://github.com/lingodotdev/lingo.dev/commit/ca9e20eef9047e20d39ccf9dff74d2f6069d4676), [`2aedf3b`](https://github.com/lingodotdev/lingo.dev/commit/2aedf3bec2d9dffc7b43fc10dea0cab5742d44af), [`747847a`](https://github.com/lingodotdev/lingo.dev/commit/747847a86720d4c36f15daeb41d13d0aff129ca9), [`3fc9da7`](https://github.com/lingodotdev/lingo.dev/commit/3fc9da7e3d2ec58e7f278c79a53eae6d3dfa5896), [`626082a`](https://github.com/lingodotdev/lingo.dev/commit/626082a64b88fb3b589acd950afeafe417ce5ddc)]:
477 |   - @replexica/[email protected]
478 |   - @replexica/[email protected]
479 |   - @replexica/[email protected]
480 | 
481 | ## 0.46.0
482 | 
483 | ### Minor Changes
484 | 
485 | - [`8887ece`](https://github.com/lingodotdev/lingo.dev/commit/8887ece066eccb8da31d42b30a76b005de2219a8) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add node 18 compatibility
486 | 
487 | ### Patch Changes
488 | 
489 | - Updated dependencies [[`8887ece`](https://github.com/lingodotdev/lingo.dev/commit/8887ece066eccb8da31d42b30a76b005de2219a8)]:
490 |   - @replexica/[email protected]
491 | 
492 | ## 0.45.0
493 | 
494 | ### Minor Changes
495 | 
496 | - [`ad78fb2`](https://github.com/lingodotdev/lingo.dev/commit/ad78fb231d4044d09280127ad8d7c7f7141afe1b) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - remove waitlist
497 | 
498 | ### Patch Changes
499 | 
500 | - Updated dependencies [[`ad78fb2`](https://github.com/lingodotdev/lingo.dev/commit/ad78fb231d4044d09280127ad8d7c7f7141afe1b)]:
501 |   - @replexica/[email protected]
502 | 
503 | ## 0.44.3
504 | 
505 | ### Patch Changes
506 | 
507 | - [`1e4cbd9`](https://github.com/lingodotdev/lingo.dev/commit/1e4cbd9670ea330c6938efdda3a965ac1e3e8376) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add support for symlinks in i18n.json
508 | 
509 | - Updated dependencies [[`1e4cbd9`](https://github.com/lingodotdev/lingo.dev/commit/1e4cbd9670ea330c6938efdda3a965ac1e3e8376)]:
510 |   - @replexica/[email protected]
511 | 
512 | ## 0.44.2
513 | 
514 | ### Patch Changes
515 | 
516 | - [#224](https://github.com/lingodotdev/lingo.dev/pull/224) [`2d019f1`](https://github.com/lingodotdev/lingo.dev/commit/2d019f153bd8cc928c2065c9e0260e9de0a6885c) Thanks [@Absterrg0](https://github.com/Absterrg0)! - Added 2 new github issue forms
517 | 
518 | - [#228](https://github.com/lingodotdev/lingo.dev/pull/228) [`38fab73`](https://github.com/lingodotdev/lingo.dev/commit/38fab73377278124dfc85a847326fdc957261c6e) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - avoid stringifying frontmatter dates
519 | 
520 | - Updated dependencies [[`38fab73`](https://github.com/lingodotdev/lingo.dev/commit/38fab73377278124dfc85a847326fdc957261c6e)]:
521 |   - @replexica/[email protected]
522 | 
523 | ## 0.44.1
524 | 
525 | ### Patch Changes
526 | 
527 | - [`4760f61`](https://github.com/lingodotdev/lingo.dev/commit/4760f617ef5cca7bed742e4fac28044721d33fc1) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - update cli messages
528 | 
529 | - Updated dependencies [[`4760f61`](https://github.com/lingodotdev/lingo.dev/commit/4760f617ef5cca7bed742e4fac28044721d33fc1)]:
530 |   - @replexica/[email protected]
531 | 
532 | ## 0.44.0
533 | 
534 | ### Minor Changes
535 | 
536 | - [#220](https://github.com/lingodotdev/lingo.dev/pull/220) [`1b11f8e`](https://github.com/lingodotdev/lingo.dev/commit/1b11f8e710d140045be0c4385bad6348f21f4e5c) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add `replexica show files` command
537 | 
538 | ### Patch Changes
539 | 
540 | - Updated dependencies [[`1b11f8e`](https://github.com/lingodotdev/lingo.dev/commit/1b11f8e710d140045be0c4385bad6348f21f4e5c)]:
541 |   - @replexica/[email protected]
542 | 
543 | ## 0.43.0
544 | 
545 | ### Minor Changes
546 | 
547 | - [`fe09f8b`](https://github.com/lingodotdev/lingo.dev/commit/fe09f8b68b1583ba9be83722beceb1596970809f) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add --api-key to the i18n cmd
548 | 
549 | ### Patch Changes
550 | 
551 | - Updated dependencies [[`fe09f8b`](https://github.com/lingodotdev/lingo.dev/commit/fe09f8b68b1583ba9be83722beceb1596970809f)]:
552 |   - @replexica/[email protected]
553 | 
554 | ## 0.42.0
555 | 
556 | ### Minor Changes
557 | 
558 | - [`7c67fc5`](https://github.com/lingodotdev/lingo.dev/commit/7c67fc5d87d66abbf0a174417b938810a112cc1a) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - migrate to the new markdown parser
559 | 
560 | ### Patch Changes
561 | 
562 | - Updated dependencies [[`7c67fc5`](https://github.com/lingodotdev/lingo.dev/commit/7c67fc5d87d66abbf0a174417b938810a112cc1a)]:
563 |   - @replexica/[email protected]
564 | 
565 | ## 0.41.3
566 | 
567 | ### Patch Changes
568 | 
569 | - [#204](https://github.com/lingodotdev/lingo.dev/pull/204) [`99a4d0a`](https://github.com/lingodotdev/lingo.dev/commit/99a4d0a926d6b6ec0821b47e34f337ca5bb05fca) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add additional exception throws
570 | 
571 | - Updated dependencies [[`99a4d0a`](https://github.com/lingodotdev/lingo.dev/commit/99a4d0a926d6b6ec0821b47e34f337ca5bb05fca)]:
572 |   - @replexica/[email protected]
573 | 
574 | ## 0.41.2
575 | 
576 | ### Patch Changes
577 | 
578 | - [`962ec5e`](https://github.com/lingodotdev/lingo.dev/commit/962ec5e619632d020ff60fb562d3ad7bc8900443) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - avoid rewriting i18n.json when there's no changes
579 | 
580 | - Updated dependencies [[`962ec5e`](https://github.com/lingodotdev/lingo.dev/commit/962ec5e619632d020ff60fb562d3ad7bc8900443)]:
581 |   - @replexica/[email protected]
582 | 
583 | ## 0.41.1
584 | 
585 | ### Patch Changes
586 | 
587 | - [`6fdc5d5`](https://github.com/lingodotdev/lingo.dev/commit/6fdc5d535a077bb0656d37c5edf3423dd32e6412) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Add json repair to json file loader
588 | 
589 | - Updated dependencies [[`6fdc5d5`](https://github.com/lingodotdev/lingo.dev/commit/6fdc5d535a077bb0656d37c5edf3423dd32e6412)]:
590 |   - @replexica/[email protected]
591 | 
592 | ## 0.41.0
593 | 
594 | ### Minor Changes
595 | 
596 | - [#181](https://github.com/lingodotdev/lingo.dev/pull/181) [`1601f70`](https://github.com/lingodotdev/lingo.dev/commit/1601f708bdf0ff1786d3bf9b19265ac5b567f740) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Added support for .properties file
597 | 
598 | ### Patch Changes
599 | 
600 | - Updated dependencies [[`1601f70`](https://github.com/lingodotdev/lingo.dev/commit/1601f708bdf0ff1786d3bf9b19265ac5b567f740)]:
601 |   - @replexica/[email protected]
602 |   - @replexica/[email protected]
603 |   - @replexica/[email protected]
604 | 
605 | ## 0.40.1
606 | 
607 | ### Patch Changes
608 | 
609 | - [`bc5a28c`](https://github.com/lingodotdev/lingo.dev/commit/bc5a28c3c98b619872924b5f913229ac01387524) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Fix spec imports
610 | 
611 | - Updated dependencies [[`bc5a28c`](https://github.com/lingodotdev/lingo.dev/commit/bc5a28c3c98b619872924b5f913229ac01387524)]:
612 |   - @replexica/[email protected]
613 |   - @replexica/[email protected]
614 |   - @replexica/[email protected]
615 | 
616 | ## 0.40.0
617 | 
618 | ### Minor Changes
619 | 
620 | - [#165](https://github.com/lingodotdev/lingo.dev/pull/165) [`5c2ca37`](https://github.com/lingodotdev/lingo.dev/commit/5c2ca37114663eaeb529a027e33949ef3839549b) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Update locale code resolution logic
621 | 
622 | - [#166](https://github.com/lingodotdev/lingo.dev/pull/166) [`78c4ce4`](https://github.com/lingodotdev/lingo.dev/commit/78c4ce479149d3eeb2f67f9283de54eecf3c35ab) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Add lockfile autogeneration
623 | 
624 | ### Patch Changes
625 | 
626 | - Updated dependencies [[`5c2ca37`](https://github.com/lingodotdev/lingo.dev/commit/5c2ca37114663eaeb529a027e33949ef3839549b), [`78c4ce4`](https://github.com/lingodotdev/lingo.dev/commit/78c4ce479149d3eeb2f67f9283de54eecf3c35ab)]:
627 |   - @replexica/[email protected]
628 |   - @replexica/[email protected]
629 |   - @replexica/[email protected]
630 | 
631 | ## 0.39.1
632 | 
633 | ### Patch Changes
634 | 
635 | - [#162](https://github.com/lingodotdev/lingo.dev/pull/162) [`c990101`](https://github.com/lingodotdev/lingo.dev/commit/c990101185aa17b036fa5a21db679fc7781bf551) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Add replexica lockfile command for explicit lockfile generation
636 | 
637 | - Updated dependencies [[`c990101`](https://github.com/lingodotdev/lingo.dev/commit/c990101185aa17b036fa5a21db679fc7781bf551)]:
638 |   - @replexica/[email protected]
639 | 
640 | ## 0.39.0
641 | 
642 | ### Minor Changes
643 | 
644 | - [`6870fc7`](https://github.com/lingodotdev/lingo.dev/commit/6870fc758dae9d1adb641576befbd8cda61cd5ea) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Fix version number bumping in 1.2 config autoupgrade
645 | 
646 | ### Patch Changes
647 | 
648 | - Updated dependencies [[`6870fc7`](https://github.com/lingodotdev/lingo.dev/commit/6870fc758dae9d1adb641576befbd8cda61cd5ea)]:
649 |   - @replexica/[email protected]
650 |   - @replexica/[email protected]
651 |   - @replexica/[email protected]
652 | 
653 | ## 0.38.0
654 | 
655 | ### Minor Changes
656 | 
657 | - [`d6e6d5c`](https://github.com/lingodotdev/lingo.dev/commit/d6e6d5c24b266de3769e95545f74632e7d75c697) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Add support for multisource localization to the CLI
658 | 
659 | ### Patch Changes
660 | 
661 | - Updated dependencies [[`d6e6d5c`](https://github.com/lingodotdev/lingo.dev/commit/d6e6d5c24b266de3769e95545f74632e7d75c697)]:
662 |   - @replexica/[email protected]
663 |   - @replexica/[email protected]
664 |   - @replexica/[email protected]
665 | 
666 | ## 0.37.0
667 | 
668 | ### Minor Changes
669 | 
670 | - [#158](https://github.com/lingodotdev/lingo.dev/pull/158) [`73c9250`](https://github.com/lingodotdev/lingo.dev/commit/73c925084989ccea120cae1617ec87776c88e83e) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Configuration spec v1.1: Improved bucket config structure, to support exclusion patterns
671 | 
672 | ### Patch Changes
673 | 
674 | - Updated dependencies [[`73c9250`](https://github.com/lingodotdev/lingo.dev/commit/73c925084989ccea120cae1617ec87776c88e83e)]:
675 |   - @replexica/[email protected]
676 |   - @replexica/[email protected]
677 |   - @replexica/[email protected]
678 | 
679 | ## 0.36.2
680 | 
681 | ### Patch Changes
682 | 
683 | - [#156](https://github.com/lingodotdev/lingo.dev/pull/156) [`f59380f`](https://github.com/lingodotdev/lingo.dev/commit/f59380f85c98fae4dfb938f842bdf39fe795ddcd) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Preserve order of keys in JSONs
684 | 
685 | - Updated dependencies [[`f59380f`](https://github.com/lingodotdev/lingo.dev/commit/f59380f85c98fae4dfb938f842bdf39fe795ddcd)]:
686 |   - @replexica/[email protected]
687 | 
688 | ## 0.36.1
689 | 
690 | ### Patch Changes
691 | 
692 | - [`5ad1879`](https://github.com/lingodotdev/lingo.dev/commit/5ad18797f22bc06fe38769120c27bd7c4642fe2d) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Add ascii art
693 | 
694 | - Updated dependencies [[`5ad1879`](https://github.com/lingodotdev/lingo.dev/commit/5ad18797f22bc06fe38769120c27bd7c4642fe2d)]:
695 |   - @replexica/[email protected]
696 | 
697 | ## 0.36.0
698 | 
699 | ### Minor Changes
700 | 
701 | - [#148](https://github.com/lingodotdev/lingo.dev/pull/148) [`fca3bd9`](https://github.com/lingodotdev/lingo.dev/commit/fca3bd984e5bef20a4a9921d7562980a3401f131) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Add basic glob pattern support for multi-file buckets
702 | 
703 | ### Patch Changes
704 | 
705 | - Updated dependencies [[`fca3bd9`](https://github.com/lingodotdev/lingo.dev/commit/fca3bd984e5bef20a4a9921d7562980a3401f131)]:
706 |   - @replexica/[email protected]
707 | 
708 | ## 0.35.0
709 | 
710 | ### Minor Changes
711 | 
712 | - [`d293f05`](https://github.com/lingodotdev/lingo.dev/commit/d293f059e1bd9131d6d41ceffc713efa8d6fa598) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - New feature: remove unused keys, whenever a key gets deleted in the source file (thanks @quentin-decre!)
713 | 
714 | ### Patch Changes
715 | 
716 | - Updated dependencies [[`d293f05`](https://github.com/lingodotdev/lingo.dev/commit/d293f059e1bd9131d6d41ceffc713efa8d6fa598)]:
717 |   - @replexica/[email protected]
718 | 
719 | ## 0.34.0
720 | 
721 | ### Minor Changes
722 | 
723 | - [#142](https://github.com/lingodotdev/lingo.dev/pull/142) [`d9b0e51`](https://github.com/lingodotdev/lingo.dev/commit/d9b0e512196329cc781a4d33346f8ca0f3a81e7e) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Extract API calling into SDK package
724 | 
725 | ### Patch Changes
726 | 
727 | - Updated dependencies [[`d9b0e51`](https://github.com/lingodotdev/lingo.dev/commit/d9b0e512196329cc781a4d33346f8ca0f3a81e7e)]:
728 |   - @replexica/[email protected]
729 | 
730 | ## 0.33.0
731 | 
732 | ### Minor Changes
733 | 
734 | - [#138](https://github.com/lingodotdev/lingo.dev/pull/138) [`8948266`](https://github.com/lingodotdev/lingo.dev/commit/8948266b0f026da9f656c916bedcedc72e5aedba) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Added JSON flat/unflat for more granular control over lockfile caching and performance
735 | 
736 | ### Patch Changes
737 | 
738 | - Updated dependencies [[`8948266`](https://github.com/lingodotdev/lingo.dev/commit/8948266b0f026da9f656c916bedcedc72e5aedba)]:
739 |   - @replexica/[email protected]
740 | 
741 | ## 0.32.0
742 | 
743 | ### Minor Changes
744 | 
745 | - [`dab6f68`](https://github.com/lingodotdev/lingo.dev/commit/dab6f68b4e564f4f1a757431b5a590f87e30aeca) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Add frontmatter support
746 | 
747 | ### Patch Changes
748 | 
749 | - Updated dependencies [[`dab6f68`](https://github.com/lingodotdev/lingo.dev/commit/dab6f68b4e564f4f1a757431b5a590f87e30aeca)]:
750 |   - @replexica/[email protected]
751 | 
752 | ## 0.31.1
753 | 
754 | ### Patch Changes
755 | 
756 | - [`387b6b7`](https://github.com/lingodotdev/lingo.dev/commit/387b6b74c1718503f50f18991b0337ee87cb53f8) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Fixed extra newline added to markdown results
757 | 
758 | - Updated dependencies [[`387b6b7`](https://github.com/lingodotdev/lingo.dev/commit/387b6b74c1718503f50f18991b0337ee87cb53f8)]:
759 |   - @replexica/[email protected]
760 | 
761 | ## 0.31.0
762 | 
763 | ### Minor Changes
764 | 
765 | - [`8c8e7dd`](https://github.com/lingodotdev/lingo.dev/commit/8c8e7dd4d35669d484240d643427612ecdaf73eb) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Added new locales
766 | 
767 | ### Patch Changes
768 | 
769 | - Updated dependencies [[`8c8e7dd`](https://github.com/lingodotdev/lingo.dev/commit/8c8e7dd4d35669d484240d643427612ecdaf73eb)]:
770 |   - @replexica/[email protected]
771 |   - @replexica/[email protected]
772 |   - @replexica/[email protected]
773 | 
774 | ## 0.30.0
775 | 
776 | ### Minor Changes
777 | 
778 | - [`bd2029d`](https://github.com/lingodotdev/lingo.dev/commit/bd2029d5c1241f7355ea08621dbeb7e04b7f5b5c) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Updated markdown processor algo
779 | 
780 | ### Patch Changes
781 | 
782 | - Updated dependencies [[`bd2029d`](https://github.com/lingodotdev/lingo.dev/commit/bd2029d5c1241f7355ea08621dbeb7e04b7f5b5c)]:
783 |   - @replexica/[email protected]
784 | 
785 | ## 0.29.0
786 | 
787 | ### Minor Changes
788 | 
789 | - [`7d83cfc`](https://github.com/lingodotdev/lingo.dev/commit/7d83cfc79921346a47ccef43accee454ba80c83c) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Added retry mechanism to i18n engine calls
790 | 
791 | ### Patch Changes
792 | 
793 | - Updated dependencies [[`7d83cfc`](https://github.com/lingodotdev/lingo.dev/commit/7d83cfc79921346a47ccef43accee454ba80c83c)]:
794 |   - @replexica/[email protected]
795 | 
796 | ## 0.24.0
797 | 
798 | ### Minor Changes
799 | 
800 | - [`37167d6`](https://github.com/lingodotdev/lingo.dev/commit/37167d6d29d747b0dd35e26e5b6f0978f0e156d9) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Added -v, --version flag to print out CLI version
801 | 
802 | ### Patch Changes
803 | 
804 | - Updated dependencies [[`37167d6`](https://github.com/lingodotdev/lingo.dev/commit/37167d6d29d747b0dd35e26e5b6f0978f0e156d9)]:
805 |   - @replexica/[email protected]
806 | 
807 | ## 0.23.7
808 | 
809 | ### Patch Changes
810 | 
811 | - Updated dependencies [[`c0be1a2`](https://github.com/lingodotdev/lingo.dev/commit/c0be1a29e3069ef2c8bdc4e4f52d2fb17abdb1f5), [`a083a55`](https://github.com/lingodotdev/lingo.dev/commit/a083a551cbe755c87a78ad14673f5dbac6d86832)]:
812 |   - @replexica/[email protected]
813 |   - @replexica/[email protected]
814 |   - @replexica/[email protected]
815 | 
816 | ## 0.23.6
817 | 
818 | ### Patch Changes
819 | 
820 | - [`eee21e1`](https://github.com/lingodotdev/lingo.dev/commit/eee21e1913e86f18938f1d6fd0dffaf6c17fb33c) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Improved markdown performance, added support for VERY large markdown content files.
821 | 
822 | - Updated dependencies [[`eee21e1`](https://github.com/lingodotdev/lingo.dev/commit/eee21e1913e86f18938f1d6fd0dffaf6c17fb33c)]:
823 |   - @replexica/[email protected]
824 | 
825 | ## 0.23.5
826 | 
827 | ### Patch Changes
828 | 
829 | - Updated dependencies [[`ca1dd58`](https://github.com/lingodotdev/lingo.dev/commit/ca1dd58008e31c8aa88ab14362f6506d6efb970a)]:
830 |   - @replexica/[email protected]
831 | 
832 | ## 0.23.4
833 | 
834 | ### Patch Changes
835 | 
836 | - Updated dependencies [[`3c7a30c`](https://github.com/lingodotdev/lingo.dev/commit/3c7a30c6be91fb27c00681c998452d7bf1beca0e)]:
837 |   - @replexica/[email protected]
838 | 
839 | ## 0.23.3
840 | 
841 | ### Patch Changes
842 | 
843 | - Updated dependencies [[`fbce978`](https://github.com/lingodotdev/lingo.dev/commit/fbce97846eabf00fb1c903b82e7d556480de5d23), [`10252ce`](https://github.com/lingodotdev/lingo.dev/commit/10252ceaa2685cc23f4dbeb6ac985cc2148853e2)]:
844 |   - @replexica/[email protected]
845 |   - @replexica/[email protected]
846 |   - @replexica/[email protected]
847 | 
848 | ## 0.23.2
849 | 
850 | ### Patch Changes
851 | 
852 | - Updated dependencies [[`27bb7fd`](https://github.com/lingodotdev/lingo.dev/commit/27bb7fd7e644e37c59e2cce9b453122097f6362c)]:
853 |   - @replexica/[email protected]
854 | 
855 | ## 0.23.1
856 | 
857 | ### Patch Changes
858 | 
859 | - [`088de18`](https://github.com/lingodotdev/lingo.dev/commit/088de18a53f45fa8df5833fe81ed96a2ed231299) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Fix @replexica/config reference
860 | 
861 | - Updated dependencies [[`088de18`](https://github.com/lingodotdev/lingo.dev/commit/088de18a53f45fa8df5833fe81ed96a2ed231299)]:
862 |   - @replexica/[email protected]
863 |   - @replexica/[email protected]
864 |   - @replexica/[email protected]
865 | 
866 | ## 0.23.0
867 | 
868 | ### Minor Changes
869 | 
870 | - [#99](https://github.com/lingodotdev/lingo.dev/pull/99) [`4e94058`](https://github.com/lingodotdev/lingo.dev/commit/4e940582ea8ebe5a058b76fb33420729f7bfdcef) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Added support for i18n lockfiles to improve AI localization performance.
871 | 
872 | ### Patch Changes
873 | 
874 | - Updated dependencies [[`4e94058`](https://github.com/lingodotdev/lingo.dev/commit/4e940582ea8ebe5a058b76fb33420729f7bfdcef)]:
875 |   - @replexica/[email protected]
876 |   - @replexica/[email protected]
877 |   - @replexica/[email protected]
878 | 
```
Page 17/20FirstPrevNextLast