This is page 13 of 16. Use http://codebase.md/lingodotdev/lingo.dev?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/loaders/mdx2/code-placeholder.spec.ts:
--------------------------------------------------------------------------------
```typescript
import { describe, it, expect } from "vitest";
import createMdxCodePlaceholderLoader from "./code-placeholder";
import dedent from "dedent";
import { md5 } from "../../utils/md5";
const PLACEHOLDER_REGEX = /---CODE-PLACEHOLDER-[0-9a-f]+---/g;
const sampleContent = dedent`
Paragraph with some code:
\`\`\`js
console.log("foo");
\`\`\`
`;
describe("MDX Code Placeholder Loader", () => {
const loader = createMdxCodePlaceholderLoader();
loader.setDefaultLocale("en");
it("should replace fenced code with placeholder on pull", async () => {
const result = await loader.pull("en", sampleContent);
const hash = md5('```js\nconsole.log("foo");\n```');
const expected = `Paragraph with some code:\n\n---CODE-PLACEHOLDER-${hash}---`;
expect(result.trim()).toBe(expected);
});
it("should restore fenced code from placeholder on push", async () => {
const pulled = await loader.pull("en", sampleContent);
const translated = pulled.replace("Paragraph", "Párrafo");
const output = await loader.push("es", translated);
const expected = dedent`
Párrafo with some code:
\`\`\`js
console.log("foo");
\`\`\`
`;
expect(output.trim()).toBe(expected.trim());
});
describe("round-trip scenarios", () => {
it("round-trips a fenced block with language tag", async () => {
const md = dedent`
Example:
\`\`\`js
console.log()
\`\`\`
`;
const pulled = await loader.pull("en", md);
const pushed = await loader.push("es", pulled);
expect(pushed).toBe(md);
});
it("round-trips a fenced block without language tag", async () => {
const md = dedent`
Intro:
\`\`\`
generic code
\`\`\`
`;
const pulled = await loader.pull("en", md);
const pushed = await loader.push("es", pulled);
expect(pushed).toBe(md);
});
it("round-trips a meta-tagged fenced block", async () => {
const md = dedent`
Meta:
\`\`\`js {1,2} title="Sample"
line1
line2
\`\`\`
`;
const pulled = await loader.pull("en", md);
const pushed = await loader.push("es", pulled);
expect(pushed).toBe(md);
});
it("round-trips a fenced block inside a blockquote", async () => {
const md = dedent`
> Quote start
> \`\`\`ts
> let x = 42;
> \`\`\`
> Quote end
`;
const pulled = await loader.pull("en", md);
const pushed = await loader.push("es", pulled);
expect(pushed).toBe(md);
});
it("round-trips multiple separated fenced blocks", async () => {
const md = dedent`
A:
\`\`\`js
1
\`\`\`
B:
\`\`\`js
2
\`\`\`
`;
const pulled = await loader.pull("en", md);
const pushed = await loader.push("es", pulled);
expect(pushed).toBe(md);
});
it("round-trips adjacent fenced blocks", async () => {
const md = dedent`
\`\`\`
a()
\`\`\`
\`\`\`
b()
\`\`\`
`;
const expected = dedent`
\`\`\`
a()
\`\`\`
\`\`\`
b()
\`\`\`
`;
const pulled = await loader.pull("en", md);
const pushed = await loader.push("es", pulled);
expect(pushed).toBe(expected);
});
it("round-trips an indented fenced block", async () => {
const md = dedent`
Outer:
\`\`\`py
pass
\`\`\`
`;
const pulled = await loader.pull("en", md);
const pushed = await loader.push("es", pulled);
expect(pushed).toBe(md);
});
it("round-trips a fenced block after a heading", async () => {
const md = dedent`
# Title
\`\`\`bash
echo hi
\`\`\`
`;
const pulled = await loader.pull("en", md);
const pushed = await loader.push("es", pulled);
expect(pushed).toBe(md);
});
it("round-trips a fenced block inside a list item", async () => {
const md = `
- item:
\`\`\`js
io()
\`\`\`
`.trim();
const pulled = await loader.pull("en", md);
const pushed = await loader.push("es", pulled);
expect(pushed).toBe(md);
});
it("round-trips a fenced block inside JSX component", async () => {
const md = dedent`
<Component>
\`\`\`js
x
\`\`\`
</Component>
`;
const pulled = await loader.pull("en", md);
const pushed = await loader.push("es", pulled);
expect(pushed).toBe(md);
});
it("round-trips a fenced block inside JSX component - adds new lines", async () => {
const md = dedent`
<Component>
\`\`\`js
x
\`\`\`
</Component>
`;
const pulled = await loader.pull("en", md);
const pushed = await loader.push("es", pulled);
expect(pushed).toBe(
dedent`
<Component>
\`\`\`js
x
\`\`\`
</Component>
`,
);
});
it("round-trips a large JSON fenced block", async () => {
const md = dedent`
\`\`\`shell
{ "key": [1,2,3] }
\`\`\`
`;
const pulled = await loader.pull("en", md);
const pushed = await loader.push("es", pulled);
expect(pushed).toBe(md);
});
it("handles identical code snippets correctly", async () => {
const md = dedent`
First paragraph:
\`\`\`shell
echo "hello world"
\`\`\`
Second paragraph:
\`\`\`shell
echo "hello world"
\`\`\`
`;
const pulled = await loader.pull("en", md);
const pushed = await loader.push("es", pulled);
expect(pushed).toBe(
dedent`
First paragraph:
\`\`\`shell
echo "hello world"
\`\`\`
Second paragraph:
\`\`\`shell
echo "hello world"
\`\`\`
`,
);
});
it("handles fenced code blocks inside quotes correctly", async () => {
const md = dedent`
> Code snippet inside quote:
>
> \`\`\`shell
> npx -y mucho@latest install
> \`\`\`
`;
const pulled = await loader.pull("en", md);
const pushed = await loader.push("es", pulled);
expect(pushed).toBe(md);
});
it("round-trips an image block with surrounding blank lines unchanged", async () => {
const md = dedent`
Text above.

Text below.
`;
const pulled = await loader.pull("en", md);
const pushed = await loader.push("es", pulled);
expect(pushed).toBe(md);
});
it("round-trips and adds blank lines around an image block when missing", async () => {
const md = dedent`
Text above.

Text below.
`;
const expected = dedent`
Text above.

Text below.
`;
const pulled = await loader.pull("en", md);
const pushed = await loader.push("es", pulled);
expect(pushed).toBe(expected);
});
it("keeps image inside blockquote as-is", async () => {
const md = dedent`
> 
`;
const pulled = await loader.pull("en", md);
const pushed = await loader.push("es", pulled);
expect(pushed).toBe(md);
});
it("leaves incomplete fences untouched", async () => {
const md = "```js\nno close";
const pulled = await loader.pull("en", md);
expect(pulled).toBe(md);
const pushed = await loader.push("es", pulled);
expect(pushed).toBe(md);
});
// Edge cases for image spacing
it("adds blank line after image when only before exists", async () => {
const md = dedent`
Before.

After.
`;
const expected = dedent`
Before.

After.
`;
const pulled = await loader.pull("en", md);
const pushed = await loader.push("es", pulled);
expect(pushed).toBe(expected);
});
it("adds blank line before image when only after exists", async () => {
const md = dedent`
Before.

After.
`;
const expected = dedent`
Before.

After.
`;
const pulled = await loader.pull("en", md);
const pushed = await loader.push("es", pulled);
expect(pushed).toBe(expected);
});
it("inserts spacing between consecutive images", async () => {
const md = dedent`


`;
const expected = dedent`


`;
const pulled = await loader.pull("en", md);
const pushed = await loader.push("es", pulled);
expect(pushed).toBe(expected);
});
it("handles image inside JSX component - adds blank lines", async () => {
const md = dedent`
<Wrapper>

</Wrapper>
`;
const expected = dedent`
<Wrapper>

</Wrapper>
`;
const pulled = await loader.pull("en", md);
const pushed = await loader.push("es", pulled);
expect(pushed).toBe(expected);
});
});
describe("inline code placeholder", () => {
it("should replace inline code with placeholder on pull", async () => {
const md = "This is some `inline()` code.";
const pulled = await loader.pull("en", md);
const hash = md5("`inline()`");
const expected = `This is some ---INLINE-CODE-PLACEHOLDER-${hash}--- code.`;
expect(pulled).toBe(expected);
});
it("should restore inline code from placeholder on push", async () => {
const md = "Some `code` here.";
const pulled = await loader.pull("en", md);
const translated = pulled.replace("Some", "Algún");
const pushed = await loader.push("es", translated);
expect(pushed).toBe("Algún `code` here.");
});
it("round-trips multiple inline code snippets", async () => {
const md = "Use `a` and `b` and `c`.";
const pulled = await loader.pull("en", md);
const pushed = await loader.push("es", pulled);
expect(pushed).toBe(md);
});
it("handles identical inline snippets correctly", async () => {
const md = "Repeat `x` and `x` again.";
const pulled = await loader.pull("en", md);
const pushed = await loader.push("es", pulled);
expect(pushed).toBe(md);
});
it("retains custom inline code in target locale when it differs from source", async () => {
const enMd = "Use `foo` function.";
const ruMd = "Используйте `бар` функцию.";
// Pull English source to establish originalInput in loader state
await loader.pull("en", enMd);
// Pull Russian content (with its own inline code value)
const ruPulled = await loader.pull("ru", ruMd);
// Simulate translator editing surrounding text but keeping placeholder intact
const ruTranslated = ruPulled.replace("Используйте", "Примените");
// Push back to Russian locale and ensure inline code is preserved
const ruPushed = await loader.push("ru", ruTranslated);
expect(ruPushed).toBe("Примените `бар` функцию.");
});
});
describe("Image URLs with Parentheses", () => {
it("should handle image URLs with parentheses", async () => {
const md = dedent`
Text above.
parentheses.jpg)
Text below.
`;
const pulled = await loader.pull("en", md);
const pushed = await loader.push("es", pulled);
expect(pushed).toBe(md);
});
it("should handle image URLs with nested parentheses", async () => {
const md = dedent`
Text above.
parentheses).jpg)
Text below.
`;
const pulled = await loader.pull("en", md);
const pushed = await loader.push("es", pulled);
expect(pushed).toBe(md);
});
it("should handle image URLs with parentheses in blockquotes", async () => {
const md = dedent`
> blockquote.jpg)
`;
const pulled = await loader.pull("en", md);
const pushed = await loader.push("es", pulled);
expect(pushed).toBe(md);
});
it("should handle image URLs with parentheses in JSX components", async () => {
const md = dedent`
<Component>
component.jpg)
</Component>
`;
const expected = dedent`
<Component>
component.jpg)
</Component>
`;
const pulled = await loader.pull("en", md);
const pushed = await loader.push("es", pulled);
expect(pushed).toBe(expected);
});
});
describe("placeholder replacement bugs", () => {
it("should handle special $ characters in code content correctly", async () => {
const loader = createMdxCodePlaceholderLoader();
loader.setDefaultLocale("en");
// Code containing special $ characters that have special meaning in replaceAll
const content = dedent`
Text before.
\`\`\`js
const price = "$100";
const template = "$\`text\`";
const special = "$&$'$\`";
\`\`\`
Text after.
`;
// Pull and then push the same content
const pulled = await loader.pull("en", content);
const translated = pulled.replace("Text before", "Texto antes");
const pushed = await loader.push("en", translated);
// Should not contain any placeholders
expect(pushed).not.toMatch(/---CODE-PLACEHOLDER-[0-9a-f]+---/);
// Should preserve all special $ characters exactly as they were
expect(pushed).toContain('const price = "$100";');
expect(pushed).toContain('const template = "$`text`";');
expect(pushed).toContain('const special = "$&$\'$`";');
expect(pushed).toContain("Texto antes");
});
it("should handle inline code with $ characters correctly", async () => {
const loader = createMdxCodePlaceholderLoader();
loader.setDefaultLocale("en");
const content = "Use `$price` and `$&` and `$\`` in your code.";
// Pull and then push the same content
const pulled = await loader.pull("en", content);
const translated = pulled.replace("Use", "Utilize");
const pushed = await loader.push("en", translated);
// Should not contain any placeholders
expect(pushed).not.toMatch(/---INLINE-CODE-PLACEHOLDER-[0-9a-f]+---/);
// Should preserve all special $ characters
expect(pushed).toContain("`$price`");
expect(pushed).toContain("`$&`");
expect(pushed).toContain("`$\``");
expect(pushed).toContain("Utilize");
});
it("should not leave placeholders when content matches", async () => {
const loader = createMdxCodePlaceholderLoader();
loader.setDefaultLocale("en");
const content = "Use the `getData()` function.";
// Pull and then push the same content - should work correctly
const pulled = await loader.pull("en", content);
const translated = pulled.replace("Use", "Utilize");
const pushed = await loader.push("en", translated);
// Should not contain any placeholders
expect(pushed).not.toMatch(/---INLINE-CODE-PLACEHOLDER-[0-9a-f]+---/);
expect(pushed).not.toMatch(/---CODE-PLACEHOLDER-[0-9a-f]+---/);
expect(pushed).toContain("`getData()`");
expect(pushed).toContain("Utilize");
});
it("should replace all placeholders including those from different sources", async () => {
const loader = createMdxCodePlaceholderLoader();
loader.setDefaultLocale("en");
// Simulate the exact scenario from the user's bug report
const englishContent = "Use the `getData()` function.";
const arabicContent = "استخدم `الحصول_على_البيانات()` الدالة.";
// First pull English (required as default locale)
await loader.pull("en", englishContent);
// Pull Arabic content to create placeholders
const arabicPulled = await loader.pull("ar", arabicContent);
// Simulate translation: translator changes text but keeps placeholder
const arabicTranslated = arabicPulled.replace("استخدم", "قم بتطبيق");
// Push back - this should now work correctly with the fix
const pushedResult = await loader.push("ar", arabicTranslated);
// The fix: ALL placeholders should be replaced, including Arabic ones
expect(pushedResult).not.toMatch(
/---INLINE-CODE-PLACEHOLDER-[0-9a-f]+---/,
);
expect(pushedResult).not.toMatch(/---CODE-PLACEHOLDER-[0-9a-f]+---/);
// The Arabic inline code should be preserved and translated text should be there
expect(pushedResult).toContain("`الحصول_على_البيانات()`");
expect(pushedResult).toContain("قم بتطبيق");
});
it("should replace placeholders even when pullInput state is overwritten", async () => {
const loader = createMdxCodePlaceholderLoader();
loader.setDefaultLocale("en");
const englishContent = "Use the `getData()` function.";
const arabicContent = "استخدم `الحصول_على_البيانات()` الدالة.";
// First pull English (required as default locale)
await loader.pull("en", englishContent);
// Pull Arabic content to create placeholders
const arabicPulled = await loader.pull("ar", arabicContent);
// Simulate translation: translator changes text but keeps placeholder
const arabicTranslated = arabicPulled.replace("استخدم", "قم بتطبيق");
// Now pull English again, overwriting pullInput state
// This simulates the real-world scenario where the loader state gets out of sync
await loader.pull("en", englishContent);
// Push the Arabic translation - should work despite state being overwritten
const pushedResult = await loader.push("ar", arabicTranslated);
// All placeholders should be replaced, even when not in current pullInput
expect(pushedResult).not.toMatch(
/---INLINE-CODE-PLACEHOLDER-[0-9a-f]+---/,
);
expect(pushedResult).not.toMatch(/---CODE-PLACEHOLDER-[0-9a-f]+---/);
expect(pushedResult).toContain("`الحصول_على_البيانات()`");
expect(pushedResult).toContain("قم بتطبيق");
});
});
describe("raw code outside fences", () => {
it("should handle raw JavaScript code outside fences", async () => {
const loader = createMdxCodePlaceholderLoader();
loader.setDefaultLocale("en");
// Test case matching user's file structure - raw JS between JSX components
const md = dedent`
</Tabs>
// Attach to button click
document.getElementById('executeBtn')?.addEventListener('click', executeClientSideWorkflow);
<Callout type="warning">
Content here
</Callout>
`;
const pulled = await loader.pull("en", md);
const pushed = await loader.push("en", pulled);
// Should round-trip correctly
expect(pushed).toBe(md);
});
it("should handle mixed code blocks and raw code", async () => {
const loader = createMdxCodePlaceholderLoader();
loader.setDefaultLocale("en");
const md = dedent`
Here's a code block:
\`\`\`typescript
const x = 1;
\`\`\`
Now some raw code outside:
// This is outside
const y = 2;
And another block:
\`\`\`javascript
const z = 3;
\`\`\`
`;
const pulled = await loader.pull("en", md);
const pushed = await loader.push("en", pulled);
// Should preserve raw code outside fences
expect(pushed).toContain("// This is outside");
expect(pushed).toContain("const y = 2;");
});
it("should handle code blocks with extra blank lines added by translation", async () => {
const loader = createMdxCodePlaceholderLoader();
loader.setDefaultLocale("en");
// English source - no extra blank lines
const enMd = dedent`
<Tab value="npm">
\`\`\`bash
npm install
\`\`\`
</Tab>
`;
// Pull English to establish placeholders
const enPulled = await loader.pull("en", enMd);
// German translation with extra blank lines (simulating AI translation behavior)
const deMd = dedent`
<Tab value="npm">
\`\`\`bash
npm install
\`\`\`
</Tab>
`;
// Pull German version
const dePulled = await loader.pull("de", deMd);
// Push back - should restore code blocks correctly
const dePushed = await loader.push("de", dePulled);
// The code block should be present and not replaced with placeholder
expect(dePushed).toContain("```bash");
expect(dePushed).toContain("npm install");
expect(dePushed).not.toMatch(/---CODE-PLACEHOLDER-/);
});
it("should preserve double newlines around placeholders for section splitting", async () => {
const loader = createMdxCodePlaceholderLoader();
loader.setDefaultLocale("en");
// Test that placeholders maintain double newlines so section-split works correctly
const md = dedent`
Text before.
\`\`\`typescript
code1
\`\`\`
Text between.
\`\`\`javascript
code2
\`\`\`
Text after.
`;
const pulled = await loader.pull("en", md);
// Verify placeholders are surrounded by double newlines for proper section splitting
const placeholders = pulled.match(/---CODE-PLACEHOLDER-[a-f0-9]+---/g);
expect(placeholders).toHaveLength(2);
// Check that each placeholder has double newlines around it
for (const placeholder of placeholders!) {
// Should have \n\n before (except at start) and \n\n after (except at end)
const placeholderIndex = pulled.indexOf(placeholder);
// Check for double newline after (unless at end)
const afterPlaceholder = pulled.substring(
placeholderIndex + placeholder.length,
placeholderIndex + placeholder.length + 2,
);
if (placeholderIndex + placeholder.length < pulled.length - 2) {
expect(afterPlaceholder).toBe("\n\n");
}
}
// Ensure we can split on \n\n and get separate sections
const sections = pulled.split("\n\n").filter(Boolean);
expect(sections.length).toBeGreaterThanOrEqual(5); // Text + placeholder + text + placeholder + text
});
});
});
describe("adjacent code blocks bug", () => {
it("should handle closing fence followed immediately by opening fence", async () => {
const loader = createMdxCodePlaceholderLoader();
loader.setDefaultLocale("en");
// This reproduces the actual bug from the user's file
const md = dedent`
\`\`\`typescript
function example() {
return true;
}
\`\`\`
\`\`\`typescript
import { Something } from 'somewhere';
\`\`\`
`;
const pulled = await loader.pull("en", md);
console.log("PULLED CONTENT:");
console.log(pulled);
console.log("---");
// The bug: placeholder is concatenated with "typescript" from next block
const bugPattern = /---CODE-PLACEHOLDER-[a-f0-9]+---typescript/;
expect(pulled).not.toMatch(bugPattern);
// Should have proper separation
expect(pulled).toMatch(
/---CODE-PLACEHOLDER-[a-f0-9]+---\n\n---CODE-PLACEHOLDER-[a-f0-9]+---/,
);
});
});
describe("$ special character handling in replacement functions", () => {
it("should preserve $ characters in ensureTrailingFenceNewline", async () => {
const loader = createMdxCodePlaceholderLoader();
loader.setDefaultLocale("en");
// Tests fix for lines 38, 68: replaceAll(match, () => replacement)
// Code block with $ that would trigger special replacement behavior if not using function replacer
const content = dedent`
Some text
\`\`\`js
console.log('Current period cost: $' + amount);
const template = \`Price: $\${price}\`;
\`\`\`
More text
`;
const pulled = await loader.pull("en", content);
const pushed = await loader.push("en", pulled);
// All $ characters should be preserved exactly
expect(pushed).toContain("console.log('Current period cost: $' + amount);");
expect(pushed).toContain("const template = `Price: $");
});
it("should preserve $ characters in ensureSurroundingImageNewlines", async () => {
const loader = createMdxCodePlaceholderLoader();
loader.setDefaultLocale("en");
// Tests fix for line 38: replaceAll(match, () => replacement) in image handling
// Image with $ in URL and alt text that would break with string replacer
const content = dedent`
Here is an image:

End of text
`;
const pulled = await loader.pull("en", content);
const pushed = await loader.push("en", pulled);
// All $ characters in URL and alt text should be preserved
expect(pushed).toContain("![Price: $100]");
expect(pushed).toContain("price=$500¤cy=$USD");
});
});
```
--------------------------------------------------------------------------------
/packages/cli/src/cli/cmd/i18n.ts:
--------------------------------------------------------------------------------
```typescript
import {
bucketTypeSchema,
I18nConfig,
localeCodeSchema,
resolveOverriddenLocale,
} from "@lingo.dev/_spec";
import { Command } from "interactive-commander";
import Z from "zod";
import _ from "lodash";
import * as path from "path";
import { getConfig } from "../utils/config";
import { getSettings } from "../utils/settings";
import {
ConfigError,
AuthenticationError,
ValidationError,
LocalizationError,
BucketProcessingError,
getCLIErrorType,
isLocalizationError,
isBucketProcessingError,
ErrorDetail,
aggregateErrorAnalytics,
createPreviousErrorContext,
} from "../utils/errors";
import Ora from "ora";
import createBucketLoader from "../loaders";
import { createAuthenticator } from "../utils/auth";
import { getBuckets } from "../utils/buckets";
import chalk from "chalk";
import { createTwoFilesPatch } from "diff";
import inquirer from "inquirer";
import externalEditor from "external-editor";
import updateGitignore from "../utils/update-gitignore";
import createProcessor from "../processor";
import { withExponentialBackoff } from "../utils/exp-backoff";
import trackEvent from "../utils/observability";
import { createDeltaProcessor } from "../utils/delta";
import { isICUPluralObject } from "../loaders/xcode-xcstrings-icu";
export default new Command()
.command("i18n")
.description(
"DEPRECATED: Run localization pipeline (prefer `run` command instead)",
)
.helpOption("-h, --help", "Show help")
.option(
"--locale <locale>",
"Limit processing to the listed target locale codes from i18n.json. Repeat the flag to include multiple locales. Defaults to all configured target locales",
(val: string, prev: string[]) => (prev ? [...prev, val] : [val]),
)
.option(
"--bucket <bucket>",
"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",
(val: string, prev: string[]) => (prev ? [...prev, val] : [val]),
)
.option(
"--key <key>",
"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",
(val: string) => encodeURIComponent(val),
)
.option(
"--file [files...]",
"Filter processing to only buckets whose file paths contain these substrings. Example: 'components' to process only files in components directories",
)
.option(
"--frozen",
"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",
)
.option(
"--force",
"Force re-translation of all keys, bypassing change detection. Useful when you want to regenerate translations with updated AI models or translation settings",
)
.option(
"--verbose",
"Print the translation data being processed as formatted JSON for each bucket and locale",
)
.option(
"--interactive",
"Review and edit AI-generated translations interactively before applying changes to files",
)
.option(
"--api-key <api-key>",
"Override API key from settings or environment variables",
)
.option(
"--debug",
"Pause before processing localization so you can attach a debugger",
)
.option(
"--strict",
"Stop immediately on first error instead of continuing to process remaining buckets and locales (fail-fast mode)",
)
.action(async function (options) {
updateGitignore();
const ora = Ora();
let flags: ReturnType<typeof parseFlags>;
try {
flags = parseFlags(options);
} catch (parseError: any) {
// Handle flag validation errors (like invalid locale codes)
await trackEvent("unknown", "cmd.i18n.error", {
errorType: "validation_error",
errorName: parseError.name || "ValidationError",
errorMessage: parseError.message || "Invalid command line options",
errorStack: parseError.stack,
fatal: true,
errorCount: 1,
stage: "flag_validation",
});
throw parseError;
}
if (flags.debug) {
// wait for user input, use inquirer
const { debug } = await inquirer.prompt([
{
type: "confirm",
name: "debug",
message: "Debug mode. Wait for user input before continuing.",
},
]);
}
let hasErrors = false;
let authId: string | null = null;
const errorDetails: ErrorDetail[] = [];
try {
ora.start("Loading configuration...");
const i18nConfig = getConfig();
const settings = getSettings(flags.apiKey);
ora.succeed("Configuration loaded");
ora.start("Validating localization configuration...");
validateParams(i18nConfig, flags);
ora.succeed("Localization configuration is valid");
ora.start("Connecting to Lingo.dev Localization Engine...");
const isByokMode = !!i18nConfig?.provider;
if (isByokMode) {
authId = null;
ora.succeed("Using external provider (BYOK mode)");
} else {
const auth = await validateAuth(settings);
authId = auth.id;
ora.succeed(`Authenticated as ${auth.email}`);
}
await trackEvent(authId, "cmd.i18n.start", {
i18nConfig,
flags,
});
let buckets = getBuckets(i18nConfig!);
if (flags.bucket?.length) {
buckets = buckets.filter((bucket: any) =>
flags.bucket!.includes(bucket.type),
);
}
ora.succeed("Buckets retrieved");
if (flags.file?.length) {
buckets = buckets
.map((bucket: any) => {
const paths = bucket.paths.filter((path: any) =>
flags.file!.find((file) => path.pathPattern?.includes(file)),
);
return { ...bucket, paths };
})
.filter((bucket: any) => bucket.paths.length > 0);
if (buckets.length === 0) {
ora.fail(
"No buckets found. All buckets were filtered out by --file option.",
);
throw new Error(
"No buckets found. All buckets were filtered out by --file option.",
);
} else {
ora.info(`\x1b[36mProcessing only filtered buckets:\x1b[0m`);
buckets.map((bucket: any) => {
ora.info(` ${bucket.type}:`);
bucket.paths.forEach((path: any) => {
ora.info(` - ${path.pathPattern}`);
});
});
}
}
const targetLocales = flags.locale?.length
? flags.locale
: i18nConfig!.locale.targets;
// Ensure the lockfile exists
ora.start("Setting up localization cache...");
const checkLockfileProcessor = createDeltaProcessor("");
const lockfileExists = await checkLockfileProcessor.checkIfLockExists();
if (!lockfileExists) {
ora.start("Creating i18n.lock...");
for (const bucket of buckets) {
for (const bucketPath of bucket.paths) {
const sourceLocale = resolveOverriddenLocale(
i18nConfig!.locale.source,
bucketPath.delimiter,
);
const bucketLoader = createBucketLoader(
bucket.type,
bucketPath.pathPattern,
{
defaultLocale: sourceLocale,
injectLocale: bucket.injectLocale,
formatter: i18nConfig!.formatter,
},
bucket.lockedKeys,
bucket.lockedPatterns,
bucket.ignoredKeys,
);
bucketLoader.setDefaultLocale(sourceLocale);
await bucketLoader.init();
const sourceData = await bucketLoader.pull(
i18nConfig!.locale.source,
);
const deltaProcessor = createDeltaProcessor(bucketPath.pathPattern);
const checksums = await deltaProcessor.createChecksums(sourceData);
await deltaProcessor.saveChecksums(checksums);
}
}
ora.succeed("Localization cache initialized");
} else {
ora.succeed("Localization cache loaded");
}
if (flags.frozen) {
ora.start("Checking for lockfile updates...");
let requiresUpdate: string | null = null;
bucketLoop: for (const bucket of buckets) {
for (const bucketPath of bucket.paths) {
const sourceLocale = resolveOverriddenLocale(
i18nConfig!.locale.source,
bucketPath.delimiter,
);
const bucketLoader = createBucketLoader(
bucket.type,
bucketPath.pathPattern,
{
defaultLocale: sourceLocale,
returnUnlocalizedKeys: true,
injectLocale: bucket.injectLocale,
},
bucket.lockedKeys,
bucket.lockedPatterns,
bucket.ignoredKeys,
);
bucketLoader.setDefaultLocale(sourceLocale);
await bucketLoader.init();
const { unlocalizable: sourceUnlocalizable, ...sourceData } =
await bucketLoader.pull(i18nConfig!.locale.source);
const deltaProcessor = createDeltaProcessor(bucketPath.pathPattern);
const sourceChecksums =
await deltaProcessor.createChecksums(sourceData);
const savedChecksums = await deltaProcessor.loadChecksums();
// Get updated data by comparing current checksums with saved checksums
const updatedSourceData = _.pickBy(
sourceData,
(value, key) => sourceChecksums[key] !== savedChecksums[key],
);
// translation was updated in the source file
if (Object.keys(updatedSourceData).length > 0) {
requiresUpdate = "updated";
break bucketLoop;
}
for (const _targetLocale of targetLocales) {
const targetLocale = resolveOverriddenLocale(
_targetLocale,
bucketPath.delimiter,
);
const { unlocalizable: targetUnlocalizable, ...targetData } =
await bucketLoader.pull(targetLocale);
const missingKeys = _.difference(
Object.keys(sourceData),
Object.keys(targetData),
);
const extraKeys = _.difference(
Object.keys(targetData),
Object.keys(sourceData),
);
const unlocalizableDataDiff = !_.isEqual(
sourceUnlocalizable,
targetUnlocalizable,
);
// translation is missing in the target file
if (missingKeys.length > 0) {
requiresUpdate = "missing";
break bucketLoop;
}
// target file has extra translations
if (extraKeys.length > 0) {
requiresUpdate = "extra";
break bucketLoop;
}
// unlocalizable keys do not match
if (unlocalizableDataDiff) {
requiresUpdate = "unlocalizable";
break bucketLoop;
}
}
}
}
if (requiresUpdate) {
const message = {
updated: "Source file has been updated.",
missing: "Target file is missing translations.",
extra:
"Target file has extra translations not present in the source file.",
unlocalizable:
"Unlocalizable data (such as booleans, dates, URLs, etc.) do not match.",
}[requiresUpdate];
ora.fail(
`Localization data has changed; please update i18n.lock or run without --frozen.`,
);
ora.fail(` Details: ${message}`);
throw new Error(
`Localization data has changed; please update i18n.lock or run without --frozen. Details: ${message}`,
);
} else {
ora.succeed("No lockfile updates required.");
}
}
// Process each bucket
for (const bucket of buckets) {
try {
console.log();
ora.info(`Processing bucket: ${bucket.type}`);
for (const bucketPath of bucket.paths) {
const bucketOra = Ora({ indent: 2 }).info(
`Processing path: ${bucketPath.pathPattern}`,
);
const sourceLocale = resolveOverriddenLocale(
i18nConfig!.locale.source,
bucketPath.delimiter,
);
const bucketLoader = createBucketLoader(
bucket.type,
bucketPath.pathPattern,
{
defaultLocale: sourceLocale,
injectLocale: bucket.injectLocale,
formatter: i18nConfig!.formatter,
},
bucket.lockedKeys,
bucket.lockedPatterns,
bucket.ignoredKeys,
);
bucketLoader.setDefaultLocale(sourceLocale);
await bucketLoader.init();
let sourceData = await bucketLoader.pull(sourceLocale);
for (const _targetLocale of targetLocales) {
const targetLocale = resolveOverriddenLocale(
_targetLocale,
bucketPath.delimiter,
);
try {
bucketOra.start(
`[${sourceLocale} -> ${targetLocale}] (0%) Localization in progress...`,
);
sourceData = await bucketLoader.pull(sourceLocale);
const targetData = await bucketLoader.pull(targetLocale);
const deltaProcessor = createDeltaProcessor(
bucketPath.pathPattern,
);
const checksums = await deltaProcessor.loadChecksums();
const delta = await deltaProcessor.calculateDelta({
sourceData,
targetData,
checksums,
});
let processableData = _.chain(sourceData)
.entries()
.filter(
([key, value]) =>
delta.added.includes(key) ||
delta.updated.includes(key) ||
!!flags.force,
)
.fromPairs()
.value();
if (flags.key) {
processableData = _.pickBy(
processableData,
(_, key) => key === flags.key,
);
}
if (flags.verbose) {
bucketOra.info(JSON.stringify(processableData, null, 2));
}
bucketOra.start(
`[${sourceLocale} -> ${targetLocale}] [${
Object.keys(processableData).length
} entries] (0%) AI localization in progress...`,
);
let processPayload = createProcessor(i18nConfig!.provider, {
apiKey: settings.auth.apiKey,
apiUrl: settings.auth.apiUrl,
});
processPayload = withExponentialBackoff(
processPayload,
3,
1000,
);
const processedTargetData = await processPayload(
{
sourceLocale,
sourceData,
processableData,
targetLocale,
targetData,
},
(progress, sourceChunk, processedChunk) => {
bucketOra.text = `[${sourceLocale} -> ${targetLocale}] [${
Object.keys(processableData).length
} entries] (${progress}%) AI localization in progress...`;
},
);
if (flags.verbose) {
bucketOra.info(JSON.stringify(processedTargetData, null, 2));
}
let finalTargetData = _.merge(
{},
sourceData,
targetData,
processedTargetData,
);
// rename keys
finalTargetData = _.chain(finalTargetData)
.entries()
.map(([key, value]) => {
const renaming = delta.renamed.find(
([oldKey, newKey]) => oldKey === key,
);
if (!renaming) {
return [key, value];
}
return [renaming[1], value];
})
.fromPairs()
.value();
if (flags.interactive) {
bucketOra.stop();
const reviewedData = await reviewChanges({
pathPattern: bucketPath.pathPattern,
targetLocale,
currentData: targetData,
proposedData: finalTargetData,
sourceData,
force: flags.force!,
});
finalTargetData = reviewedData;
bucketOra.start(
`Applying changes to ${bucketPath} (${targetLocale})`,
);
}
const finalDiffSize = _.chain(finalTargetData)
.omitBy((value, key) => {
const targetValue = targetData[key];
// For ICU plural objects, use deep equality (excluding Symbol)
if (
isICUPluralObject(value) &&
isICUPluralObject(targetValue)
) {
return _.isEqual(
{ icu: value.icu, _meta: value._meta },
{ icu: targetValue.icu, _meta: targetValue._meta },
);
}
// Default strict equality for other values
return value === targetValue;
})
.size()
.value();
// Push to bucket all the time as there might be changes to unlocalizable keys
await bucketLoader.push(targetLocale, finalTargetData);
if (finalDiffSize > 0 || flags.force) {
bucketOra.succeed(
`[${sourceLocale} -> ${targetLocale}] Localization completed`,
);
} else {
bucketOra.succeed(
`[${sourceLocale} -> ${targetLocale}] Localization completed (no changes).`,
);
}
} catch (_error: any) {
const error = new LocalizationError(
`[${sourceLocale} -> ${targetLocale}] Localization failed: ${_error.message}`,
{
bucket: bucket.type,
sourceLocale,
targetLocale,
pathPattern: bucketPath.pathPattern,
},
);
errorDetails.push({
type: "locale_error",
bucket: bucket.type,
locale: `${sourceLocale} -> ${targetLocale}`,
pathPattern: bucketPath.pathPattern,
message: _error.message,
stack: _error.stack,
});
if (flags.strict) {
throw error;
} else {
bucketOra.fail(error.message);
hasErrors = true;
}
}
}
const deltaProcessor = createDeltaProcessor(bucketPath.pathPattern);
const checksums = await deltaProcessor.createChecksums(sourceData);
if (!flags.locale?.length) {
await deltaProcessor.saveChecksums(checksums);
}
}
} catch (_error: any) {
const error = new BucketProcessingError(
`Failed to process bucket ${bucket.type}: ${_error.message}`,
bucket.type,
);
errorDetails.push({
type: "bucket_error",
bucket: bucket.type,
message: _error.message,
stack: _error.stack,
});
if (flags.strict) {
throw error;
} else {
ora.fail(error.message);
hasErrors = true;
}
}
}
console.log();
if (!hasErrors) {
ora.succeed("Localization completed.");
await trackEvent(authId, "cmd.i18n.success", {
i18nConfig: {
sourceLocale: i18nConfig!.locale.source,
targetLocales: i18nConfig!.locale.targets,
bucketTypes: Object.keys(i18nConfig!.buckets),
},
flags,
bucketCount: buckets.length,
localeCount: targetLocales.length,
processedSuccessfully: true,
});
} else {
ora.warn("Localization completed with errors.");
await trackEvent(authId || "unknown", "cmd.i18n.error", {
flags,
...aggregateErrorAnalytics(
errorDetails,
buckets,
targetLocales,
i18nConfig!,
),
});
}
} catch (error: any) {
ora.fail(error.message);
// Use robust error type detection
const errorType = getCLIErrorType(error);
// Extract additional context from typed errors
let errorContext: any = {};
if (isLocalizationError(error)) {
errorContext = {
bucket: error.bucket,
sourceLocale: error.sourceLocale,
targetLocale: error.targetLocale,
pathPattern: error.pathPattern,
};
} else if (isBucketProcessingError(error)) {
errorContext = {
bucket: error.bucket,
};
}
await trackEvent(authId || "unknown", "cmd.i18n.error", {
flags,
errorType,
errorName: error.name || "Error",
errorMessage: error.message,
errorStack: error.stack,
errorContext,
fatal: true,
errorCount: errorDetails.length + 1,
previousErrors: createPreviousErrorContext(errorDetails),
});
}
});
function parseFlags(options: any) {
return Z.object({
apiKey: Z.string().optional(),
locale: Z.array(localeCodeSchema).optional(),
bucket: Z.array(bucketTypeSchema).optional(),
force: Z.boolean().optional(),
frozen: Z.boolean().optional(),
verbose: Z.boolean().optional(),
strict: Z.boolean().optional(),
key: Z.string().optional(),
file: Z.array(Z.string()).optional(),
interactive: Z.boolean().default(false),
debug: Z.boolean().default(false),
}).parse(options);
}
// Export validateAuth for use in other commands
export async function validateAuth(settings: ReturnType<typeof getSettings>) {
if (!settings.auth.apiKey) {
throw new AuthenticationError({
message:
"Not authenticated. Please run `lingo.dev login` to authenticate.",
docUrl: "authError",
});
}
const authenticator = createAuthenticator({
apiKey: settings.auth.apiKey,
apiUrl: settings.auth.apiUrl,
});
const user = await authenticator.whoami();
if (!user) {
throw new AuthenticationError({
message: "Invalid API key. Please run `lingo.dev login` to authenticate.",
docUrl: "authError",
});
}
return user;
}
function validateParams(
i18nConfig: I18nConfig | null,
flags: ReturnType<typeof parseFlags>,
) {
if (!i18nConfig) {
throw new ConfigError({
message:
"i18n.json not found. Please run `lingo.dev init` to initialize the project.",
docUrl: "i18nNotFound",
});
} else if (!i18nConfig.buckets || !Object.keys(i18nConfig.buckets).length) {
throw new ConfigError({
message:
"No buckets found in i18n.json. Please add at least one bucket containing i18n content.",
docUrl: "bucketNotFound",
});
} else if (
flags.locale?.some((locale) => !i18nConfig.locale.targets.includes(locale))
) {
throw new ValidationError({
message: `One or more specified locales do not exist in i18n.json locale.targets. Please add them to the list and try again.`,
docUrl: "localeTargetNotFound",
});
} else if (
flags.bucket?.some(
(bucket) =>
!i18nConfig.buckets[bucket as keyof typeof i18nConfig.buckets],
)
) {
throw new ValidationError({
message: `One or more specified buckets do not exist in i18n.json. Please add them to the list and try again.`,
docUrl: "bucketNotFound",
});
}
}
async function reviewChanges(args: {
pathPattern: string;
targetLocale: string;
currentData: Record<string, any>;
proposedData: Record<string, any>;
sourceData: Record<string, any>;
force: boolean;
}): Promise<Record<string, any>> {
const currentStr = JSON.stringify(args.currentData, null, 2);
const proposedStr = JSON.stringify(args.proposedData, null, 2);
// Early return if no changes
if (currentStr === proposedStr && !args.force) {
console.log(
`\n${chalk.blue(args.pathPattern)} (${chalk.yellow(
args.targetLocale,
)}): ${chalk.gray("No changes to review")}`,
);
return args.proposedData;
}
const patch = createTwoFilesPatch(
`${args.pathPattern} (current)`,
`${args.pathPattern} (proposed)`,
currentStr,
proposedStr,
undefined,
undefined,
{ context: 3 },
);
// Color the diff output
const coloredDiff = patch
.split("\n")
.map((line) => {
if (line.startsWith("+")) return chalk.green(line);
if (line.startsWith("-")) return chalk.red(line);
if (line.startsWith("@")) return chalk.cyan(line);
return line;
})
.join("\n");
console.log(
`\nReviewing changes for ${chalk.blue(args.pathPattern)} (${chalk.yellow(
args.targetLocale,
)}):`,
);
console.log(coloredDiff);
const { action } = await inquirer.prompt([
{
type: "list",
name: "action",
message: "Choose action:",
choices: [
{ name: "Approve changes", value: "approve" },
{ name: "Skip changes", value: "skip" },
{ name: "Edit individually", value: "edit" },
],
default: "approve",
},
]);
if (action === "approve") {
return args.proposedData;
}
if (action === "skip") {
return args.currentData;
}
// If edit was chosen, prompt for each changed value
const customData = { ...args.currentData };
const changes = _.reduce(
args.proposedData,
(result: string[], value: string, key: string) => {
if (args.currentData[key] !== value) {
result.push(key);
}
return result;
},
[],
);
for (const key of changes) {
console.log(`\nEditing value for: ${chalk.cyan(key)}`);
console.log(chalk.gray("Source text:"), chalk.blue(args.sourceData[key]));
console.log(
chalk.gray("Current value:"),
chalk.red(args.currentData[key] || "(empty)"),
);
console.log(
chalk.gray("Suggested value:"),
chalk.green(args.proposedData[key]),
);
console.log(
chalk.gray(
"\nYour editor will open. Edit the text and save to continue.",
),
);
console.log(chalk.gray("------------"));
try {
// Prepare the editor content with a header comment and the suggested value
const editorContent = [
"# Edit the translation below.",
"# Lines starting with # will be ignored.",
"# Save and exit the editor to continue.",
"#",
`# Source text (${chalk.blue("English")}):`,
`# ${args.sourceData[key]}`,
"#",
`# Current value (${chalk.red(args.targetLocale)}):`,
`# ${args.currentData[key] || "(empty)"}`,
"#",
args.proposedData[key],
].join("\n");
const result = externalEditor.edit(editorContent);
// Clean up the result by removing comments and trimming
const customValue = result
.split("\n")
.filter((line) => !line.startsWith("#"))
.join("\n")
.trim();
if (customValue) {
customData[key] = customValue;
} else {
console.log(
chalk.yellow("Empty value provided, keeping the current value."),
);
customData[key] = args.currentData[key] || args.proposedData[key];
}
} catch (error) {
console.log(
chalk.red("Error while editing, keeping the suggested value."),
);
customData[key] = args.proposedData[key];
}
}
return customData;
}
```
--------------------------------------------------------------------------------
/packages/cli/src/cli/loaders/android.spec.ts:
--------------------------------------------------------------------------------
```typescript
import { describe, it, expect, vi, beforeEach } from "vitest";
import fs from "fs/promises";
import createAndroidLoader from "./android";
describe("android loader", () => {
const setupMocks = (input: string) => {
vi.mock("fs/promises");
vi.mocked(fs.readFile).mockResolvedValue(input);
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
};
beforeEach(() => {
vi.clearAllMocks();
});
it("should correctly handle basic string resources", async () => {
const input = `
<resources>
<string name="hello">Hello World</string>
<string name="app_name">My App</string>
</resources>
`.trim();
const androidLoader = createAndroidLoader().setDefaultLocale("en");
const result = await androidLoader.pull("en", input);
expect(result).toEqual({
hello: "Hello World",
app_name: "My App",
});
});
it("should correctly handle string arrays", async () => {
const input = `
<resources>
<string-array name="planets">
<item>Mercury</item>
<item>Venus</item>
<item>Earth</item>
<item>Mars</item>
</string-array>
</resources>
`.trim();
const androidLoader = createAndroidLoader().setDefaultLocale("en");
const result = await androidLoader.pull("en", input);
expect(result).toEqual({
planets: ["Mercury", "Venus", "Earth", "Mars"],
});
});
it("should correctly handle plurals with different quantity types", async () => {
const input = `
<resources>
<plurals name="numberOfSongsAvailable">
<item quantity="zero">No songs found.</item>
<item quantity="one">1 song found.</item>
<item quantity="few">%d songs found.</item>
<item quantity="many">%d songs found.</item>
<item quantity="other">%d songs found.</item>
</plurals>
</resources>
`.trim();
const androidLoader = createAndroidLoader().setDefaultLocale("en");
const result = await androidLoader.pull("en", input);
expect(result).toEqual({
numberOfSongsAvailable: {
zero: "No songs found.",
one: "1 song found.",
few: "%d songs found.",
many: "%d songs found.",
other: "%d songs found.",
},
});
});
it("should correctly handle HTML markup in strings", async () => {
const input = `
<resources>
<string name="welcome">Welcome to <b>Android</b>!</string>
<string name="formatted">This is <i>italic</i> and this is <b>bold</b></string>
</resources>
`.trim();
const androidLoader = createAndroidLoader().setDefaultLocale("en");
const result = await androidLoader.pull("en", input);
expect(result).toEqual({
welcome: "Welcome to <b>Android</b>!",
formatted: "This is <i>italic</i> and this is <b>bold</b>",
});
});
it("should correctly handle format strings", async () => {
const input = `
<resources>
<string name="welcome_messages">Hello, %1$s! You have %2$d new messages.</string>
<string name="complex_format">Value: %1$.2f, Text: %2$s, Number: %3$d</string>
</resources>
`.trim();
const androidLoader = createAndroidLoader().setDefaultLocale("en");
const result = await androidLoader.pull("en", input);
expect(result).toEqual({
welcome_messages: "Hello, %1$s! You have %2$d new messages.",
complex_format: "Value: %1$.2f, Text: %2$s, Number: %3$d",
});
});
it("should correctly handle single quote escaping", async () => {
const input = `
<resources>
<string name="apostrophe">Don\\'t forget me</string>
<string name="escaped_quotes">This has \\'single\\' quotes</string>
</resources>
`.trim();
const androidLoader = createAndroidLoader().setDefaultLocale("en");
const result = await androidLoader.pull("en", input);
// Now expect normalized apostrophes in the JS object
expect(result).toEqual({
apostrophe: "Don't forget me",
escaped_quotes: "This has 'single' quotes",
});
// When pushing back, apostrophes should be escaped again
const pushed = await androidLoader.push("en", result);
expect(pushed).toContain("Don\\'t forget me");
expect(pushed).toContain("This has \\'single\\' quotes");
});
it("should correctly handle CDATA sections", async () => {
const input = `
<resources>
<string name="html_content"><![CDATA[<html><body><h1>Title</h1><p>Paragraph</p></body></html>]]></string>
</resources>
`.trim();
const androidLoader = createAndroidLoader().setDefaultLocale("en");
const result = await androidLoader.pull("en", input);
expect(result).toEqual({
html_content: "<html><body><h1>Title</h1><p>Paragraph</p></body></html>",
});
});
it("should correctly handle multiple CDATA sections in a single string", async () => {
const input = `
<resources>
<string name="multiple_cdata"><![CDATA[<first>section</first>]]><![CDATA[<second>section</second>]]></string>
</resources>
`.trim();
const androidLoader = createAndroidLoader().setDefaultLocale("en");
const result = await androidLoader.pull("en", input);
expect(result).toEqual({
multiple_cdata: "<first>section</first><second>section</second>",
});
});
it("should correctly handle nested HTML tags with attributes", async () => {
const input = `
<resources>
<string name="complex_html">This is <span style="color:red">red text</span> and <a href="https://example.com">a link</a></string>
</resources>
`.trim();
const androidLoader = createAndroidLoader().setDefaultLocale("en");
const result = await androidLoader.pull("en", input);
expect(result).toEqual({
complex_html:
'This is <span style="color:red">red text</span> and <a href="https://example.com">a link</a>',
});
});
it("should correctly handle XML entities in strings", async () => {
const input = `
<resources>
<string name="entities">This string contains <brackets> and &ampersands</string>
</resources>
`.trim();
const androidLoader = createAndroidLoader().setDefaultLocale("en");
const result = await androidLoader.pull("en", input);
expect(result).toEqual({
entities: "This string contains <brackets> and &ersands",
});
});
it("should correctly handle empty strings", async () => {
const input = `
<resources>
<string name="empty"></string>
<string name="whitespace"> </string>
</resources>
`.trim();
const androidLoader = createAndroidLoader().setDefaultLocale("en");
const result = await androidLoader.pull("en", input);
expect(result).toEqual({
empty: "",
whitespace: " ",
});
});
it("should correctly handle very long strings", async () => {
const longText = "This is a very long string.".repeat(100);
const input = `
<resources>
<string name="long_text">${longText}</string>
</resources>
`.trim();
const androidLoader = createAndroidLoader().setDefaultLocale("en");
const result = await androidLoader.pull("en", input);
expect(result).toEqual({
long_text: longText,
});
});
it("should correctly handle strings with newlines and whitespace", async () => {
const input = `
<resources>
<string name="multiline">Line 1
Line 2
Line 3 with indent</string>
</resources>
`.trim();
const androidLoader = createAndroidLoader().setDefaultLocale("en");
const result = await androidLoader.pull("en", input);
expect(result).toEqual({
multiline: "Line 1\nLine 2\n Line 3 with indent",
});
});
it("should correctly handle Unicode characters", async () => {
const input = `
<resources>
<string name="unicode">Unicode: 你好, こんにちは, Привет, مرحبا, 안녕하세요</string>
</resources>
`.trim();
const androidLoader = createAndroidLoader().setDefaultLocale("en");
const result = await androidLoader.pull("en", input);
expect(result).toEqual({
unicode: "Unicode: 你好, こんにちは, Привет, مرحبا, 안녕하세요",
});
});
it("should skip non-translatable strings", async () => {
const input = `
<resources>
<string name="app_name" translatable="false">My App</string>
<string name="welcome">Welcome</string>
<string name="version" translatable="false">1.0.0</string>
</resources>
`.trim();
const androidLoader = createAndroidLoader().setDefaultLocale("en");
const result = await androidLoader.pull("en", input);
expect(result).toEqual({
welcome: "Welcome",
});
expect(result.app_name).toBeUndefined();
expect(result.version).toBeUndefined();
});
it("should correctly push string resources back to XML", async () => {
const payload = {
hello: "Hola",
welcome: "Bienvenido",
};
const androidLoader = createAndroidLoader().setDefaultLocale("en");
await androidLoader.pull(
"en",
`
<resources>
<string name="hello">Hello</string>
<string name="welcome">Welcome</string>
</resources>
`,
);
const result = await androidLoader.push("es", payload);
expect(result).toContain('<string name="hello">Hola</string>');
expect(result).toContain('<string name="welcome">Bienvenido</string>');
});
it("should correctly push string arrays back to XML", async () => {
const payload = {
planets: ["Mercurio", "Venus", "Tierra", "Marte"],
};
const androidLoader = createAndroidLoader().setDefaultLocale("en");
await androidLoader.pull(
"en",
`
<resources>
<string-array name="planets">
<item>Mercury</item>
<item>Venus</item>
<item>Earth</item>
<item>Mars</item>
</string-array>
</resources>
`,
);
const result = await androidLoader.push("es", payload);
expect(result).toContain('<string-array name="planets">');
expect(result).toContain("<item>Mercurio</item>");
expect(result).toContain("<item>Venus</item>");
expect(result).toContain("<item>Tierra</item>");
expect(result).toContain("<item>Marte</item>");
});
it("should correctly push plurals back to XML", async () => {
const payload = {
numberOfSongsAvailable: {
zero: "No se encontraron canciones.",
one: "1 canción encontrada.",
few: "%d canciones encontradas.",
many: "%d canciones encontradas.",
other: "%d canciones encontradas.",
},
};
const androidLoader = createAndroidLoader().setDefaultLocale("en");
await androidLoader.pull(
"en",
`
<resources>
<plurals name="numberOfSongsAvailable">
<item quantity="zero">No songs found.</item>
<item quantity="one">1 song found.</item>
<item quantity="few">%d songs found.</item>
<item quantity="many">%d songs found.</item>
<item quantity="other">%d songs found.</item>
</plurals>
</resources>
`,
);
const result = await androidLoader.push("es", payload);
expect(result).toContain('<plurals name="numberOfSongsAvailable">');
expect(result).toContain(
'<item quantity="zero">No se encontraron canciones.</item>',
);
expect(result).toContain(
'<item quantity="one">1 canción encontrada.</item>',
);
expect(result).toContain(
'<item quantity="few">%d canciones encontradas.</item>',
);
expect(result).toContain(
'<item quantity="many">%d canciones encontradas.</item>',
);
expect(result).toContain(
'<item quantity="other">%d canciones encontradas.</item>',
);
});
it("should correctly handle mixed resource types", async () => {
const payload = {
app_name: "Mi Aplicación",
planets: ["Mercurio", "Venus", "Tierra", "Marte"],
numberOfSongsAvailable: {
zero: "No se encontraron canciones.",
one: "1 canción encontrada.",
other: "%d canciones encontradas.",
},
};
const androidLoader = createAndroidLoader().setDefaultLocale("en");
await androidLoader.pull(
"en",
`
<resources>
<string name="app_name">My App</string>
<string-array name="planets">
<item>Mercury</item>
<item>Venus</item>
<item>Earth</item>
<item>Mars</item>
</string-array>
<plurals name="numberOfSongsAvailable">
<item quantity="zero">No songs found.</item>
<item quantity="one">1 song found.</item>
<item quantity="other">%d songs found.</item>
</plurals>
</resources>
`,
);
const result = await androidLoader.push("es", payload);
expect(result).toContain('<string name="app_name">Mi Aplicación</string>');
expect(result).toContain('<string-array name="planets">');
expect(result).toContain('<plurals name="numberOfSongsAvailable">');
});
it("should correctly handle Unicode escape sequences", async () => {
const input = `
<resources>
<string name="unicode_escape">Unicode escape: \\u0041\\u0042\\u0043 and \\u65e5\\u672c\\u8a9e</string>
</resources>
`.trim();
const androidLoader = createAndroidLoader().setDefaultLocale("en");
const result = await androidLoader.pull("en", input);
expect(result).toEqual({
unicode_escape:
"Unicode escape: \\u0041\\u0042\\u0043 and \\u65e5\\u672c\\u8a9e",
});
const pushed = await androidLoader.push("en", result);
expect(pushed).toContain(
"Unicode escape: \\u0041\\u0042\\u0043 and \\u65e5\\u672c\\u8a9e",
);
});
it("should correctly handle double quote escaping", async () => {
const input = `
<resources>
<string name="double_quotes">He said, \\"Hello World\\"</string>
</resources>
`.trim();
const androidLoader = createAndroidLoader().setDefaultLocale("en");
const result = await androidLoader.pull("en", input);
expect(result).toEqual({
double_quotes: 'He said, \\"Hello World\\"',
});
const pushed = await androidLoader.push("en", result);
expect(pushed).toContain('He said, \\"Hello World\\"');
});
it("should correctly handle resource references", async () => {
const input = `
<resources>
<string name="welcome_message">Welcome to @string/app_name</string>
<string name="app_name">My App</string>
</resources>
`.trim();
const androidLoader = createAndroidLoader().setDefaultLocale("en");
const result = await androidLoader.pull("en", input);
expect(result).toEqual({
welcome_message: "Welcome to @string/app_name",
app_name: "My App",
});
const pushed = await androidLoader.push("en", result);
expect(pushed).toContain(
'<string name="welcome_message">Welcome to @string/app_name</string>',
);
});
it("should correctly handle tools namespace attributes", async () => {
const input = `
<resources>
<string name="debug_only" tools:ignore="MissingTranslation">Debug message</string>
<string name="normal">Normal message</string>
</resources>
`.trim();
const androidLoader = createAndroidLoader().setDefaultLocale("en");
const result = await androidLoader.pull("en", input);
expect(result).toEqual({
debug_only: "Debug message",
normal: "Normal message",
});
});
it("should correctly handle whitespace preservation with double quotes", async () => {
const input = `
<resources>
<string name="preserved_whitespace">" This string preserves whitespace "</string>
</resources>
`.trim();
const androidLoader = createAndroidLoader().setDefaultLocale("en");
const result = await androidLoader.pull("en", input);
expect(result).toEqual({
preserved_whitespace: '" This string preserves whitespace "',
});
const pushed = await androidLoader.push("en", result);
expect(pushed).toContain(
'<string name="preserved_whitespace">" This string preserves whitespace "</string>',
);
});
it("should correctly handle special characters that need escaping", async () => {
const input = `
<resources>
<string name="special_chars">Special chars: \\@, \\?, \\#, \\$, \\%</string>
</resources>
`.trim();
const androidLoader = createAndroidLoader().setDefaultLocale("en");
const result = await androidLoader.pull("en", input);
expect(result).toEqual({
special_chars: "Special chars: \\@, \\?, \\#, \\$, \\%",
});
const pushed = await androidLoader.push("en", result);
expect(pushed).toContain("Special chars: \\@, \\?, \\#, \\$, \\%");
});
it("should correctly handle apostrophes in text", async () => {
const input = `
<resources>
<string name="sign_in_agreement_text_1">J\'accepte les</string>
<string name="sign_in_agreement_text_2"> et je reconnais la </string>
</resources>
`.trim();
const androidLoader = createAndroidLoader().setDefaultLocale("en");
const result = await androidLoader.pull("en", input);
// During pull, escaped apostrophes should be normalized to simple apostrophes
expect(result).toEqual({
sign_in_agreement_text_1: "J'accepte les",
sign_in_agreement_text_2: " et je reconnais la ",
});
// When pushing back, apostrophes should be escaped with backslash
const pushed = await androidLoader.push("en", result);
expect(pushed).toContain("J\\'accepte les");
expect(pushed).toContain(" et je reconnais la ");
});
it("should escape apostrophes even in strings wrapped with double quotes", async () => {
const input = `
<resources>
<string name="quoted_apostrophe">"J'accepte les terms"</string>
</resources>
`.trim();
const androidLoader = createAndroidLoader().setDefaultLocale("en");
const result = await androidLoader.pull("en", input);
// During pull, the double quotes around the content should be preserved
expect(result).toEqual({
quoted_apostrophe: '"J\'accepte les terms"',
});
// When pushing back, apostrophes should be escaped even in double-quoted strings
const pushed = await androidLoader.push("en", result);
expect(pushed).toContain('"J\\\'accepte les terms"');
});
it("should correctly handle strings with apostrophes and avoid double escaping", async () => {
const input = `
<resources>
<string name="welcome_message">Please don't hesitate to contact us</string>
<item quantity="one">- %d user\'s item</item>
<item quantity="other">- %d user\'s items</item>
</resources>
`.trim();
const androidLoader = createAndroidLoader().setDefaultLocale("en");
const result = await androidLoader.pull("en", input);
// During pull, escaped apostrophes should be properly handled
expect(result.welcome_message).toBe("Please don't hesitate to contact us");
// When pushing back, apostrophes should be escaped but not double-escaped
const pushed = await androidLoader.push("en", {
welcome_message: "Please don't hesitate to contact us",
item_count: {
one: "- %d user's item",
other: "- %d user's items",
},
});
expect(pushed).toContain("Please don\\'t hesitate to contact us");
expect(pushed).toContain("- %d user\\'s item");
expect(pushed).not.toContain("- %d user\\\\'s item");
});
// Tests for Issue Fixes
it("should preserve whitespace in array items during pull and push", async () => {
const input = `
<resources>
<string-array name="mixed_items">
<item> Item with spaces </item>
<item> </item>
</string-array>
</resources>
`.trim();
const androidLoader = createAndroidLoader().setDefaultLocale("en");
const pulled = await androidLoader.pull("en", input);
expect(pulled.mixed_items).toEqual([" Item with spaces ", " "]);
const pushed = await androidLoader.push("en", {
mixed_items: [" Elemento con espacios ", " "],
});
expect(pushed).toContain("<item> Elemento con espacios </item>");
expect(pushed).toContain("<item> </item>");
});
it("should retain CDATA wrappers for translated strings", async () => {
const input = `
<resources>
<string name="cdata_example"><![CDATA[Special <tag> ]]></string>
</resources>
`.trim();
const androidLoader = createAndroidLoader().setDefaultLocale("en");
await androidLoader.pull("en", input);
const pushed = await androidLoader.push("es", {
cdata_example: "Especial <tag> ",
});
expect(pushed).toContain(
'<string name="cdata_example"><![CDATA[Especial <tag> ]]></string>',
);
});
it("should escape apostrophes in CDATA sections", async () => {
const input = `
<resources>
<string name="review_info"><![CDATA[Hosts can't see your review until they've written one. <u>Learn more</u>]]></string>
</resources>
`.trim();
const androidLoader = createAndroidLoader().setDefaultLocale("en");
const pulled = await androidLoader.pull("en", input);
expect(pulled.review_info).toBe(
"Hosts can't see your review until they've written one. <u>Learn more</u>",
);
const pushed = await androidLoader.push("fr", {
review_info:
"Les hôtes ne peuvent voir votre avis qu'après en avoir écrit un. <u>En savoir plus</u>",
});
// Apostrophes must be escaped even inside CDATA (Android AAPT requirement)
expect(pushed).toContain("qu\\'après");
expect(pushed).toContain("<![CDATA[");
expect(pushed).toContain("]]>");
// HTML tags should NOT be escaped inside CDATA
expect(pushed).toContain("<u>En savoir plus</u>");
expect(pushed).not.toContain("<u>");
});
it("should preserve resource ordering after push", async () => {
const input = `
<resources>
<string name="first">First</string>
<string-array name="colors">
<item>Red</item>
<item>Green</item>
</string-array>
<plurals name="messages">
<item quantity="one">%d message</item>
<item quantity="other">%d messages</item>
</plurals>
<bool name="show_tutorial">true</bool>
<integer name="retry_count">3</integer>
</resources>
`.trim();
const androidLoader = createAndroidLoader().setDefaultLocale("en");
const roundTrip = await androidLoader.pull("en", input);
const pushed = await androidLoader.push("en", roundTrip);
const order = Array.from(
pushed.matchAll(
/<(string|string-array|plurals|bool|integer)\s+name="([^"]+)"/g,
),
).map(([, , name]) => name);
expect(order).toEqual([
"first",
"colors",
"messages",
"show_tutorial",
"retry_count",
]);
});
it("should preserve XML declaration from source file", async () => {
const input = `<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="test">Test</string>
</resources>`;
const androidLoader = createAndroidLoader().setDefaultLocale("en");
await androidLoader.pull("en", input);
const result = await androidLoader.push("es", { test: "Prueba" });
expect(result).toMatch(/^<\?xml version="1\.0" encoding="utf-8"\?>/);
});
it('should exclude translatable="false" items from target locale', async () => {
const input = `
<resources>
<string name="app_name">My App</string>
<string name="api_url" translatable="false">https://api.example.com</string>
<string name="debug_key" translatable="false">DEBUG_KEY</string>
<string-array name="colors">
<item>Red</item>
</string-array>
<string-array name="urls" translatable="false">
<item>https://example.com</item>
</string-array>
<plurals name="items">
<item quantity="one">%d item</item>
<item quantity="other">%d items</item>
</plurals>
<plurals name="bytes" translatable="false">
<item quantity="one">%d byte</item>
<item quantity="other">%d bytes</item>
</plurals>
<bool name="show_tutorial">true</bool>
<bool name="is_debug" translatable="false">false</bool>
<integer name="timeout">30</integer>
<integer name="version" translatable="false">42</integer>
</resources>
`.trim();
const androidLoader = createAndroidLoader().setDefaultLocale("en");
await androidLoader.pull("en", input);
const result = await androidLoader.push("es", {
app_name: "Mi Aplicación",
colors: ["Rojo"],
items: { one: "%d elemento", other: "%d elementos" },
show_tutorial: true,
timeout: 30,
});
// Check that translatable="false" items are NOT included
expect(result).not.toContain('name="api_url"');
expect(result).not.toContain("https://api.example.com");
expect(result).not.toContain('name="debug_key"');
expect(result).not.toContain("DEBUG_KEY");
expect(result).not.toContain('name="urls"');
expect(result).not.toContain('name="bytes"');
expect(result).not.toContain('name="is_debug"');
expect(result).not.toContain('name="version"');
// Check that translatable items are translated
expect(result).toContain("Mi Aplicación");
expect(result).toContain("Rojo");
expect(result).toContain("elemento");
expect(result).toContain('name="app_name"');
expect(result).toContain('name="colors"');
expect(result).toContain('name="items"');
expect(result).toContain('name="show_tutorial"');
expect(result).toContain('name="timeout"');
});
it("should use 4-space indentation by default", async () => {
const input = `<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="test">Test</string>
<string name="another">Another</string>
</resources>`;
const androidLoader = createAndroidLoader().setDefaultLocale("en");
await androidLoader.pull("en", input);
const result = await androidLoader.push("es", {
test: "Prueba",
another: "Otro",
});
// Check for 4-space indentation (default)
// Note: Users should use formatters (Prettier/Biome) for custom indentation
expect(result).toContain('\n <string name="test">');
expect(result).toContain('\n <string name="another">');
});
it("should preserve XML declaration encoding from source file", async () => {
const inputUtf8 = `<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="test">Test</string>
</resources>`;
const inputUpperUTF8 = `<?xml version="1.0" encoding="UTF-8"?>
<resources>
<string name="test">Test</string>
</resources>`;
const inputISO = `<?xml version="1.0" encoding="ISO-8859-1"?>
<resources>
<string name="test">Test</string>
</resources>`;
const androidLoader = createAndroidLoader().setDefaultLocale("en");
// Test lowercase utf-8
await androidLoader.pull("en", inputUtf8);
let result = await androidLoader.push("es", { test: "Prueba" });
expect(result).toMatch(/^<\?xml version="1\.0" encoding="utf-8"\?>/);
// Test uppercase UTF-8
await androidLoader.pull("en", inputUpperUTF8);
result = await androidLoader.push("es", { test: "Prueba" });
expect(result).toMatch(/^<\?xml version="1\.0" encoding="UTF-8"\?>/);
// Test ISO-8859-1
await androidLoader.pull("en", inputISO);
result = await androidLoader.push("es", { test: "Prueba" });
expect(result).toMatch(/^<\?xml version="1\.0" encoding="ISO-8859-1"\?>/);
});
it("should preserve XML version from source file", async () => {
const inputV10 = `<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="test">Test</string>
</resources>`;
const inputV11 = `<?xml version="1.1" encoding="utf-8"?>
<resources>
<string name="test">Test</string>
</resources>`;
const androidLoader = createAndroidLoader().setDefaultLocale("en");
// Test version 1.0
await androidLoader.pull("en", inputV10);
let result = await androidLoader.push("es", { test: "Prueba" });
expect(result).toMatch(/^<\?xml version="1\.0"/);
// Test version 1.1
await androidLoader.pull("en", inputV11);
result = await androidLoader.push("es", { test: "Prueba" });
expect(result).toMatch(/^<\?xml version="1\.1"/);
});
it("should omit XML declaration when source has none", async () => {
const inputNoDeclaration = `<resources>
<string name="test">Test</string>
</resources>`;
const androidLoader = createAndroidLoader().setDefaultLocale("en");
await androidLoader.pull("en", inputNoDeclaration);
const result = await androidLoader.push("es", { test: "Prueba" });
// Should start immediately with the root element (no declaration)
expect(result).not.toMatch(/^<\?xml/);
expect(result.trim().startsWith("<resources>")).toBe(true);
});
});
```
--------------------------------------------------------------------------------
/legacy/cli/CHANGELOG.md:
--------------------------------------------------------------------------------
```markdown
# replexica
## 0.71.0
### Minor Changes
- [#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
### Patch Changes
- Updated dependencies [[`cd836e4`](https://github.com/lingodotdev/lingo.dev/commit/cd836e45cf810f495e2c6e1449824dc84794d571), [`5dd7b65`](https://github.com/lingodotdev/lingo.dev/commit/5dd7b6529ce174d8759e80644c3233927b1ecce4)]:
- [email protected]
## 0.70.1
### Patch Changes
- [`5dee9ee`](https://github.com/lingodotdev/lingo.dev/commit/5dee9ee743fbef489fbe342597a768ebd59e5f67) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add proxies to legacy packages
- [`63eb57b`](https://github.com/lingodotdev/lingo.dev/commit/63eb57b8f4cc37605be196085fafbbfdab71cce5) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add deprecation message to legacy package jsons
- [`bbf7760`](https://github.com/lingodotdev/lingo.dev/commit/bbf7760580f1631805d68612053ebcd4601bb02b) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add deprecation warning to the legacy package proxies
- Updated dependencies [[`b4c7f1e`](https://github.com/lingodotdev/lingo.dev/commit/b4c7f1e86334d229bee62219c26f30d0b523926d)]:
- [email protected]
## 0.70.0
### Minor Changes
- [`003344f`](https://github.com/lingodotdev/lingo.dev/commit/003344ffcca98a391a298707f18476971c4d4c2b) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add locale delimiter override
### Patch Changes
- Updated dependencies [[`003344f`](https://github.com/lingodotdev/lingo.dev/commit/003344ffcca98a391a298707f18476971c4d4c2b)]:
- @replexica/[email protected]
- @replexica/[email protected]
## 0.69.0
### Minor Changes
- [#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
### Patch Changes
- Updated dependencies [[`1b0647d`](https://github.com/lingodotdev/lingo.dev/commit/1b0647d91080f4947ba6227c397fb6232d0d1907)]:
- @replexica/[email protected]
## 0.68.0
### Minor Changes
- [#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
### Patch Changes
- Updated dependencies [[`36fd4af`](https://github.com/lingodotdev/lingo.dev/commit/36fd4af376caf1540dc0a594fd65742c81706aa0)]:
- @replexica/[email protected]
## 0.67.0
### Minor Changes
- [#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
- [#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
### Patch Changes
- Updated dependencies [[`446cf9c`](https://github.com/lingodotdev/lingo.dev/commit/446cf9c5c933f71a43fd5d80487b1608023cba8e), [`3edef26`](https://github.com/lingodotdev/lingo.dev/commit/3edef26ef3a4e2d27394c5eeb2bc94b164e037ac)]:
- @replexica/[email protected]
## 0.66.2
### Patch Changes
- [#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
- Updated dependencies [[`01ca7bb`](https://github.com/lingodotdev/lingo.dev/commit/01ca7bb9d28d0de903caf44ec6ede2e2bbbb3ba2)]:
- @replexica/[email protected]
## 0.66.1
### Patch Changes
- [`aef36b5`](https://github.com/lingodotdev/lingo.dev/commit/aef36b53163efa523f7632786e0f293890f05b23) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - improve .po handling
- Updated dependencies [[`aef36b5`](https://github.com/lingodotdev/lingo.dev/commit/aef36b53163efa523f7632786e0f293890f05b23)]:
- @replexica/[email protected]
## 0.66.0
### Minor Changes
- [`e885fcf`](https://github.com/lingodotdev/lingo.dev/commit/e885fcf8731d9f2a250cf44a534c5556a057ca51) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - single quotes escaping
### Patch Changes
- Updated dependencies [[`e885fcf`](https://github.com/lingodotdev/lingo.dev/commit/e885fcf8731d9f2a250cf44a534c5556a057ca51)]:
- @replexica/[email protected]
## 0.65.1
### Patch Changes
- [#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
- Updated dependencies [[`a2ada16`](https://github.com/lingodotdev/lingo.dev/commit/a2ada16ecf4cd559d3486f0e4756d58808194f7e)]:
- @replexica/[email protected]
- @replexica/[email protected]
## 0.65.0
### Minor Changes
- [`bd577f2`](https://github.com/lingodotdev/lingo.dev/commit/bd577f22da52e7e889bb4b419cb5dab9865512f1) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - remove unlocalizable from dato
### Patch Changes
- Updated dependencies [[`bd577f2`](https://github.com/lingodotdev/lingo.dev/commit/bd577f22da52e7e889bb4b419cb5dab9865512f1)]:
- @replexica/[email protected]
## 0.64.0
### Minor Changes
- [#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
### Patch Changes
- Updated dependencies [[`8db4527`](https://github.com/lingodotdev/lingo.dev/commit/8db4527d9c3501d97f8bb7b414dd61e8a3ee80f6)]:
- @replexica/[email protected]
## 0.63.1
### Patch Changes
- [#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
- Updated dependencies [[`3320c8c`](https://github.com/lingodotdev/lingo.dev/commit/3320c8c6f9df9671e1002b63a00bf877270a6064)]:
- @replexica/[email protected]
## 0.63.0
### Minor Changes
- [`db2e800`](https://github.com/lingodotdev/lingo.dev/commit/db2e80013e44b478331b6a97008b3e67bae82a1f) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add --key flag for selective updates
### Patch Changes
- Updated dependencies [[`db2e800`](https://github.com/lingodotdev/lingo.dev/commit/db2e80013e44b478331b6a97008b3e67bae82a1f)]:
- @replexica/[email protected]
## 0.62.0
### Minor Changes
- [`302afdf`](https://github.com/lingodotdev/lingo.dev/commit/302afdfd3047b781bd9688921eab3dc84173aa20) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - handle C specifiers in localizable content
### Patch Changes
- Updated dependencies [[`302afdf`](https://github.com/lingodotdev/lingo.dev/commit/302afdfd3047b781bd9688921eab3dc84173aa20)]:
- @replexica/[email protected]
## 0.61.0
### Minor Changes
- [`9d38df2`](https://github.com/lingodotdev/lingo.dev/commit/9d38df2fdbe23fdcbb1b7e2e207de650e714e433) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - fixed same-file locale rewrites
### Patch Changes
- Updated dependencies [[`9d38df2`](https://github.com/lingodotdev/lingo.dev/commit/9d38df2fdbe23fdcbb1b7e2e207de650e714e433)]:
- @replexica/[email protected]
## 0.60.1
### Patch Changes
- [#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
- [#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
- Updated dependencies [[`b9a8350`](https://github.com/lingodotdev/lingo.dev/commit/b9a83502803f4a62fc9a62b4348f853f2baff20d), [`e6521b8`](https://github.com/lingodotdev/lingo.dev/commit/e6521b86637c254c011aba89a3558802c04ab3ca)]:
- @replexica/[email protected]
- @replexica/[email protected]
## 0.60.0
### Minor Changes
- [#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
### Patch Changes
- Updated dependencies [[`cff3c4e`](https://github.com/lingodotdev/lingo.dev/commit/cff3c4eb1a40f82a9c4c095e49cfd9fce053b048)]:
- @replexica/[email protected]
- @replexica/[email protected]
- @replexica/[email protected]
## 0.59.1
### Patch Changes
- Updated dependencies []:
- @replexica/[email protected]
- @replexica/[email protected]
- @replexica/[email protected]
## 0.59.0
### Minor Changes
- [`63daf00`](https://github.com/lingodotdev/lingo.dev/commit/63daf00e80004775f12c9e1d426cdd2bbf10f5a4) Thanks [@vrcprl](https://github.com/vrcprl)! - noop
### Patch Changes
- [`6eb5282`](https://github.com/lingodotdev/lingo.dev/commit/6eb5282063515db93fc76ff3137422862720fa0d) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - noop
- 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)]:
- @replexica/[email protected]
- @replexica/[email protected]
- @replexica/[email protected]
## 0.58.2
### Patch Changes
- Updated dependencies []:
- @replexica/[email protected]
- @replexica/[email protected]
- @replexica/[email protected]
## 0.58.1
### Patch Changes
- Updated dependencies []:
- @replexica/[email protected]
- @replexica/[email protected]
## 0.58.0
### Minor Changes
- [`ff0d2d7`](https://github.com/lingodotdev/lingo.dev/commit/ff0d2d7fb12806a7264a72c03e48a8dda3526c23) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add retry with exponential backoff to the cli
### Patch Changes
- [`7ff7f8f`](https://github.com/lingodotdev/lingo.dev/commit/7ff7f8fca7318e4dba929194972d20ccf3487e9d) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - display number of entries in localization completion message
- Updated dependencies [[`7ff7f8f`](https://github.com/lingodotdev/lingo.dev/commit/7ff7f8fca7318e4dba929194972d20ccf3487e9d), [`ff0d2d7`](https://github.com/lingodotdev/lingo.dev/commit/ff0d2d7fb12806a7264a72c03e48a8dda3526c23)]:
- @replexica/[email protected]
## 0.57.1
### Patch Changes
- Updated dependencies []:
- @replexica/[email protected]
- @replexica/[email protected]
- @replexica/[email protected]
## 0.57.0
### Minor Changes
- [`8e2cee4`](https://github.com/lingodotdev/lingo.dev/commit/8e2cee4b282c39fef1e00fa429e03e1c1e489cc5) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add `cleanup` command
### Patch Changes
- [`2c5cbcf`](https://github.com/lingodotdev/lingo.dev/commit/2c5cbcfbf6feb28440255cdea0818c8cefa61d91) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - filter out non extistent keys
- [`ca10072`](https://github.com/lingodotdev/lingo.dev/commit/ca10072f636d8bd1105ed0f6cc84cf0af5a12402) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - improve progress logging in cli
- 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)]:
- @replexica/[email protected]
- @replexica/[email protected]
## 0.56.3
### Patch Changes
- [`b8ad864`](https://github.com/lingodotdev/lingo.dev/commit/b8ad8643347088635eeeb568f1818d71d5226269) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - feat(cli): disable safe mode at localizable chunk level
- Updated dependencies [[`b8ad864`](https://github.com/lingodotdev/lingo.dev/commit/b8ad8643347088635eeeb568f1818d71d5226269)]:
- @replexica/[email protected]
## 0.56.2
### Patch Changes
- Updated dependencies []:
- @replexica/[email protected]
- @replexica/[email protected]
## 0.56.1
### Patch Changes
- Updated dependencies []:
- @replexica/[email protected]
- @replexica/[email protected]
## 0.56.0
### Minor Changes
- [#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
- [`42d0a5a`](https://github.com/lingodotdev/lingo.dev/commit/42d0a5a7a53e296192a31e8f1d67c126793ea280) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - added .localizeHtml implementation to SDK
### Patch Changes
- 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)]:
- @replexica/[email protected]
- @replexica/[email protected]
- @replexica/[email protected]
## 0.55.0
### Minor Changes
- [`57e395a`](https://github.com/lingodotdev/lingo.dev/commit/57e395aae8ab100ba470bc7d1104ddfa178249e7) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add `--source` and `--target` flags to show files cmd
### Patch Changes
- Updated dependencies [[`57e395a`](https://github.com/lingodotdev/lingo.dev/commit/57e395aae8ab100ba470bc7d1104ddfa178249e7)]:
- @replexica/[email protected]
## 0.54.0
### Minor Changes
- [#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
- [`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
### Patch Changes
- Updated dependencies [[`44b4cca`](https://github.com/lingodotdev/lingo.dev/commit/44b4cca2718bd72d55a938bac458d32a4536508a), [`4fc27da`](https://github.com/lingodotdev/lingo.dev/commit/4fc27daae5810f6167726a28d76a874fd8421a5b)]:
- @replexica/[email protected]
## 0.53.1
### Patch Changes
- [`44b5c5c`](https://github.com/lingodotdev/lingo.dev/commit/44b5c5c498ca8df3bb814764f40057576c28c941) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - downgrade glob to @10, to allow node 18
- Updated dependencies [[`44b5c5c`](https://github.com/lingodotdev/lingo.dev/commit/44b5c5c498ca8df3bb814764f40057576c28c941)]:
- @replexica/[email protected]
## 0.53.0
### Minor Changes
- [`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
### Patch Changes
- Updated dependencies [[`072e23e`](https://github.com/lingodotdev/lingo.dev/commit/072e23e58fca0da20bfd01f6a0ae600e6fb760a8)]:
- @replexica/[email protected]
## 0.51.2
### Patch Changes
- Updated dependencies [[`6bc309c`](https://github.com/lingodotdev/lingo.dev/commit/6bc309c56a8e6a468510109182fd75f8f4e61b8f)]:
- @replexica/[email protected]
## 0.51.1
### Patch Changes
- [`e511b50`](https://github.com/lingodotdev/lingo.dev/commit/e511b5080dba58728e8650c7bf34d810cccdcf4e) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - added node@18 support
- Updated dependencies [[`e511b50`](https://github.com/lingodotdev/lingo.dev/commit/e511b5080dba58728e8650c7bf34d810cccdcf4e)]:
- @replexica/[email protected]
## 0.51.0
### Minor Changes
- [#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
### Patch Changes
- Updated dependencies [[`091ee35`](https://github.com/lingodotdev/lingo.dev/commit/091ee353081795bf8f61c7d41483bc309c7b62ef)]:
- @replexica/[email protected]
- @replexica/[email protected]
- @replexica/[email protected]
## 0.50.0
### Minor Changes
- [#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
### Patch Changes
- Updated dependencies [[`5e282d7`](https://github.com/lingodotdev/lingo.dev/commit/5e282d7ffa5ca9494aa7046a090bb7c327085a86)]:
- @replexica/[email protected]
- @replexica/[email protected]
- @replexica/[email protected]
## 0.49.1
### Patch Changes
- [`64cd6f3`](https://github.com/lingodotdev/lingo.dev/commit/64cd6f3765bb4524e9f78f93ff283e833a6f26a2) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - fixed path patter relativity
- Updated dependencies [[`64cd6f3`](https://github.com/lingodotdev/lingo.dev/commit/64cd6f3765bb4524e9f78f93ff283e833a6f26a2)]:
- @replexica/[email protected]
## 0.49.0
### Minor Changes
- [`0071cd6`](https://github.com/lingodotdev/lingo.dev/commit/0071cd66b1c868ad3898fc368390a628c5a67767) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add csv format support
### Patch Changes
- [`1cc0796`](https://github.com/lingodotdev/lingo.dev/commit/1cc07961d221e397ad5dd2917bed76cb4f2b1f04) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add path.resolve to text loaders
- Updated dependencies [[`0071cd6`](https://github.com/lingodotdev/lingo.dev/commit/0071cd66b1c868ad3898fc368390a628c5a67767), [`1cc0796`](https://github.com/lingodotdev/lingo.dev/commit/1cc07961d221e397ad5dd2917bed76cb4f2b1f04)]:
- @replexica/[email protected]
- @replexica/[email protected]
- @replexica/[email protected]
## 0.48.0
### Minor Changes
- [#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`
### Patch Changes
- [#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
- Updated dependencies [[`cdef5b7`](https://github.com/lingodotdev/lingo.dev/commit/cdef5b7bfbee4670c6de62cf4b4f3e0315748e25), [`62c464d`](https://github.com/lingodotdev/lingo.dev/commit/62c464d5602909f8f6370dfa5009131a4d6719d0)]:
- @replexica/[email protected]
- @replexica/[email protected]
## 0.47.1
### Patch Changes
- [`2859938`](https://github.com/lingodotdev/lingo.dev/commit/28599388a91bf80cea3813bb4b8999bb4df302c9) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add missing locales
- Updated dependencies []:
- @replexica/[email protected]
- @replexica/[email protected]
- @replexica/[email protected]
## 0.47.0
### Minor Changes
- [`4dfc8d8`](https://github.com/lingodotdev/lingo.dev/commit/4dfc8d8b301a690875401af5d107a88f1716182a) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - added support for android format
- [`ca9e20e`](https://github.com/lingodotdev/lingo.dev/commit/ca9e20eef9047e20d39ccf9dff74d2f6069d4676) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - .strings support
- [`2aedf3b`](https://github.com/lingodotdev/lingo.dev/commit/2aedf3bec2d9dffc7b43fc10dea0cab5742d44af) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - added support for .stringsdict
- [#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
- [`626082a`](https://github.com/lingodotdev/lingo.dev/commit/626082a64b88fb3b589acd950afeafe417ce5ddc) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - added Flutter .arb support
### Patch Changes
- [`2b5e3ae`](https://github.com/lingodotdev/lingo.dev/commit/2b5e3aea3f0745955266f6edf2ce34830242e503) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - fixed yaml-root-key loader
- [`747847a`](https://github.com/lingodotdev/lingo.dev/commit/747847a86720d4c36f15daeb41d13d0aff129ca9) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - fixed .xcstrings plurals
- 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)]:
- @replexica/[email protected]
- @replexica/[email protected]
- @replexica/[email protected]
## 0.46.0
### Minor Changes
- [`8887ece`](https://github.com/lingodotdev/lingo.dev/commit/8887ece066eccb8da31d42b30a76b005de2219a8) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add node 18 compatibility
### Patch Changes
- Updated dependencies [[`8887ece`](https://github.com/lingodotdev/lingo.dev/commit/8887ece066eccb8da31d42b30a76b005de2219a8)]:
- @replexica/[email protected]
## 0.45.0
### Minor Changes
- [`ad78fb2`](https://github.com/lingodotdev/lingo.dev/commit/ad78fb231d4044d09280127ad8d7c7f7141afe1b) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - remove waitlist
### Patch Changes
- Updated dependencies [[`ad78fb2`](https://github.com/lingodotdev/lingo.dev/commit/ad78fb231d4044d09280127ad8d7c7f7141afe1b)]:
- @replexica/[email protected]
## 0.44.3
### Patch Changes
- [`1e4cbd9`](https://github.com/lingodotdev/lingo.dev/commit/1e4cbd9670ea330c6938efdda3a965ac1e3e8376) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add support for symlinks in i18n.json
- Updated dependencies [[`1e4cbd9`](https://github.com/lingodotdev/lingo.dev/commit/1e4cbd9670ea330c6938efdda3a965ac1e3e8376)]:
- @replexica/[email protected]
## 0.44.2
### Patch Changes
- [#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
- [#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
- Updated dependencies [[`38fab73`](https://github.com/lingodotdev/lingo.dev/commit/38fab73377278124dfc85a847326fdc957261c6e)]:
- @replexica/[email protected]
## 0.44.1
### Patch Changes
- [`4760f61`](https://github.com/lingodotdev/lingo.dev/commit/4760f617ef5cca7bed742e4fac28044721d33fc1) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - update cli messages
- Updated dependencies [[`4760f61`](https://github.com/lingodotdev/lingo.dev/commit/4760f617ef5cca7bed742e4fac28044721d33fc1)]:
- @replexica/[email protected]
## 0.44.0
### Minor Changes
- [#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
### Patch Changes
- Updated dependencies [[`1b11f8e`](https://github.com/lingodotdev/lingo.dev/commit/1b11f8e710d140045be0c4385bad6348f21f4e5c)]:
- @replexica/[email protected]
## 0.43.0
### Minor Changes
- [`fe09f8b`](https://github.com/lingodotdev/lingo.dev/commit/fe09f8b68b1583ba9be83722beceb1596970809f) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add --api-key to the i18n cmd
### Patch Changes
- Updated dependencies [[`fe09f8b`](https://github.com/lingodotdev/lingo.dev/commit/fe09f8b68b1583ba9be83722beceb1596970809f)]:
- @replexica/[email protected]
## 0.42.0
### Minor Changes
- [`7c67fc5`](https://github.com/lingodotdev/lingo.dev/commit/7c67fc5d87d66abbf0a174417b938810a112cc1a) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - migrate to the new markdown parser
### Patch Changes
- Updated dependencies [[`7c67fc5`](https://github.com/lingodotdev/lingo.dev/commit/7c67fc5d87d66abbf0a174417b938810a112cc1a)]:
- @replexica/[email protected]
## 0.41.3
### Patch Changes
- [#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
- Updated dependencies [[`99a4d0a`](https://github.com/lingodotdev/lingo.dev/commit/99a4d0a926d6b6ec0821b47e34f337ca5bb05fca)]:
- @replexica/[email protected]
## 0.41.2
### Patch Changes
- [`962ec5e`](https://github.com/lingodotdev/lingo.dev/commit/962ec5e619632d020ff60fb562d3ad7bc8900443) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - avoid rewriting i18n.json when there's no changes
- Updated dependencies [[`962ec5e`](https://github.com/lingodotdev/lingo.dev/commit/962ec5e619632d020ff60fb562d3ad7bc8900443)]:
- @replexica/[email protected]
## 0.41.1
### Patch Changes
- [`6fdc5d5`](https://github.com/lingodotdev/lingo.dev/commit/6fdc5d535a077bb0656d37c5edf3423dd32e6412) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Add json repair to json file loader
- Updated dependencies [[`6fdc5d5`](https://github.com/lingodotdev/lingo.dev/commit/6fdc5d535a077bb0656d37c5edf3423dd32e6412)]:
- @replexica/[email protected]
## 0.41.0
### Minor Changes
- [#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
### Patch Changes
- Updated dependencies [[`1601f70`](https://github.com/lingodotdev/lingo.dev/commit/1601f708bdf0ff1786d3bf9b19265ac5b567f740)]:
- @replexica/[email protected]
- @replexica/[email protected]
- @replexica/[email protected]
## 0.40.1
### Patch Changes
- [`bc5a28c`](https://github.com/lingodotdev/lingo.dev/commit/bc5a28c3c98b619872924b5f913229ac01387524) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Fix spec imports
- Updated dependencies [[`bc5a28c`](https://github.com/lingodotdev/lingo.dev/commit/bc5a28c3c98b619872924b5f913229ac01387524)]:
- @replexica/[email protected]
- @replexica/[email protected]
- @replexica/[email protected]
## 0.40.0
### Minor Changes
- [#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
- [#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
### Patch Changes
- Updated dependencies [[`5c2ca37`](https://github.com/lingodotdev/lingo.dev/commit/5c2ca37114663eaeb529a027e33949ef3839549b), [`78c4ce4`](https://github.com/lingodotdev/lingo.dev/commit/78c4ce479149d3eeb2f67f9283de54eecf3c35ab)]:
- @replexica/[email protected]
- @replexica/[email protected]
- @replexica/[email protected]
## 0.39.1
### Patch Changes
- [#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
- Updated dependencies [[`c990101`](https://github.com/lingodotdev/lingo.dev/commit/c990101185aa17b036fa5a21db679fc7781bf551)]:
- @replexica/[email protected]
## 0.39.0
### Minor Changes
- [`6870fc7`](https://github.com/lingodotdev/lingo.dev/commit/6870fc758dae9d1adb641576befbd8cda61cd5ea) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Fix version number bumping in 1.2 config autoupgrade
### Patch Changes
- Updated dependencies [[`6870fc7`](https://github.com/lingodotdev/lingo.dev/commit/6870fc758dae9d1adb641576befbd8cda61cd5ea)]:
- @replexica/[email protected]
- @replexica/[email protected]
- @replexica/[email protected]
## 0.38.0
### Minor Changes
- [`d6e6d5c`](https://github.com/lingodotdev/lingo.dev/commit/d6e6d5c24b266de3769e95545f74632e7d75c697) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Add support for multisource localization to the CLI
### Patch Changes
- Updated dependencies [[`d6e6d5c`](https://github.com/lingodotdev/lingo.dev/commit/d6e6d5c24b266de3769e95545f74632e7d75c697)]:
- @replexica/[email protected]
- @replexica/[email protected]
- @replexica/[email protected]
## 0.37.0
### Minor Changes
- [#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
### Patch Changes
- Updated dependencies [[`73c9250`](https://github.com/lingodotdev/lingo.dev/commit/73c925084989ccea120cae1617ec87776c88e83e)]:
- @replexica/[email protected]
- @replexica/[email protected]
- @replexica/[email protected]
## 0.36.2
### Patch Changes
- [#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
- Updated dependencies [[`f59380f`](https://github.com/lingodotdev/lingo.dev/commit/f59380f85c98fae4dfb938f842bdf39fe795ddcd)]:
- @replexica/[email protected]
## 0.36.1
### Patch Changes
- [`5ad1879`](https://github.com/lingodotdev/lingo.dev/commit/5ad18797f22bc06fe38769120c27bd7c4642fe2d) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Add ascii art
- Updated dependencies [[`5ad1879`](https://github.com/lingodotdev/lingo.dev/commit/5ad18797f22bc06fe38769120c27bd7c4642fe2d)]:
- @replexica/[email protected]
## 0.36.0
### Minor Changes
- [#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
### Patch Changes
- Updated dependencies [[`fca3bd9`](https://github.com/lingodotdev/lingo.dev/commit/fca3bd984e5bef20a4a9921d7562980a3401f131)]:
- @replexica/[email protected]
## 0.35.0
### Minor Changes
- [`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!)
### Patch Changes
- Updated dependencies [[`d293f05`](https://github.com/lingodotdev/lingo.dev/commit/d293f059e1bd9131d6d41ceffc713efa8d6fa598)]:
- @replexica/[email protected]
## 0.34.0
### Minor Changes
- [#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
### Patch Changes
- Updated dependencies [[`d9b0e51`](https://github.com/lingodotdev/lingo.dev/commit/d9b0e512196329cc781a4d33346f8ca0f3a81e7e)]:
- @replexica/[email protected]
## 0.33.0
### Minor Changes
- [#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
### Patch Changes
- Updated dependencies [[`8948266`](https://github.com/lingodotdev/lingo.dev/commit/8948266b0f026da9f656c916bedcedc72e5aedba)]:
- @replexica/[email protected]
## 0.32.0
### Minor Changes
- [`dab6f68`](https://github.com/lingodotdev/lingo.dev/commit/dab6f68b4e564f4f1a757431b5a590f87e30aeca) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Add frontmatter support
### Patch Changes
- Updated dependencies [[`dab6f68`](https://github.com/lingodotdev/lingo.dev/commit/dab6f68b4e564f4f1a757431b5a590f87e30aeca)]:
- @replexica/[email protected]
## 0.31.1
### Patch Changes
- [`387b6b7`](https://github.com/lingodotdev/lingo.dev/commit/387b6b74c1718503f50f18991b0337ee87cb53f8) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Fixed extra newline added to markdown results
- Updated dependencies [[`387b6b7`](https://github.com/lingodotdev/lingo.dev/commit/387b6b74c1718503f50f18991b0337ee87cb53f8)]:
- @replexica/[email protected]
## 0.31.0
### Minor Changes
- [`8c8e7dd`](https://github.com/lingodotdev/lingo.dev/commit/8c8e7dd4d35669d484240d643427612ecdaf73eb) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Added new locales
### Patch Changes
- Updated dependencies [[`8c8e7dd`](https://github.com/lingodotdev/lingo.dev/commit/8c8e7dd4d35669d484240d643427612ecdaf73eb)]:
- @replexica/[email protected]
- @replexica/[email protected]
- @replexica/[email protected]
## 0.30.0
### Minor Changes
- [`bd2029d`](https://github.com/lingodotdev/lingo.dev/commit/bd2029d5c1241f7355ea08621dbeb7e04b7f5b5c) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Updated markdown processor algo
### Patch Changes
- Updated dependencies [[`bd2029d`](https://github.com/lingodotdev/lingo.dev/commit/bd2029d5c1241f7355ea08621dbeb7e04b7f5b5c)]:
- @replexica/[email protected]
## 0.29.0
### Minor Changes
- [`7d83cfc`](https://github.com/lingodotdev/lingo.dev/commit/7d83cfc79921346a47ccef43accee454ba80c83c) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Added retry mechanism to i18n engine calls
### Patch Changes
- Updated dependencies [[`7d83cfc`](https://github.com/lingodotdev/lingo.dev/commit/7d83cfc79921346a47ccef43accee454ba80c83c)]:
- @replexica/[email protected]
## 0.24.0
### Minor Changes
- [`37167d6`](https://github.com/lingodotdev/lingo.dev/commit/37167d6d29d747b0dd35e26e5b6f0978f0e156d9) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Added -v, --version flag to print out CLI version
### Patch Changes
- Updated dependencies [[`37167d6`](https://github.com/lingodotdev/lingo.dev/commit/37167d6d29d747b0dd35e26e5b6f0978f0e156d9)]:
- @replexica/[email protected]
## 0.23.7
### Patch Changes
- Updated dependencies [[`c0be1a2`](https://github.com/lingodotdev/lingo.dev/commit/c0be1a29e3069ef2c8bdc4e4f52d2fb17abdb1f5), [`a083a55`](https://github.com/lingodotdev/lingo.dev/commit/a083a551cbe755c87a78ad14673f5dbac6d86832)]:
- @replexica/[email protected]
- @replexica/[email protected]
- @replexica/[email protected]
## 0.23.6
### Patch Changes
- [`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.
- Updated dependencies [[`eee21e1`](https://github.com/lingodotdev/lingo.dev/commit/eee21e1913e86f18938f1d6fd0dffaf6c17fb33c)]:
- @replexica/[email protected]
## 0.23.5
### Patch Changes
- Updated dependencies [[`ca1dd58`](https://github.com/lingodotdev/lingo.dev/commit/ca1dd58008e31c8aa88ab14362f6506d6efb970a)]:
- @replexica/[email protected]
## 0.23.4
### Patch Changes
- Updated dependencies [[`3c7a30c`](https://github.com/lingodotdev/lingo.dev/commit/3c7a30c6be91fb27c00681c998452d7bf1beca0e)]:
- @replexica/[email protected]
## 0.23.3
### Patch Changes
- Updated dependencies [[`fbce978`](https://github.com/lingodotdev/lingo.dev/commit/fbce97846eabf00fb1c903b82e7d556480de5d23), [`10252ce`](https://github.com/lingodotdev/lingo.dev/commit/10252ceaa2685cc23f4dbeb6ac985cc2148853e2)]:
- @replexica/[email protected]
- @replexica/[email protected]
- @replexica/[email protected]
## 0.23.2
### Patch Changes
- Updated dependencies [[`27bb7fd`](https://github.com/lingodotdev/lingo.dev/commit/27bb7fd7e644e37c59e2cce9b453122097f6362c)]:
- @replexica/[email protected]
## 0.23.1
### Patch Changes
- [`088de18`](https://github.com/lingodotdev/lingo.dev/commit/088de18a53f45fa8df5833fe81ed96a2ed231299) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Fix @replexica/config reference
- Updated dependencies [[`088de18`](https://github.com/lingodotdev/lingo.dev/commit/088de18a53f45fa8df5833fe81ed96a2ed231299)]:
- @replexica/[email protected]
- @replexica/[email protected]
- @replexica/[email protected]
## 0.23.0
### Minor Changes
- [#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.
### Patch Changes
- Updated dependencies [[`4e94058`](https://github.com/lingodotdev/lingo.dev/commit/4e940582ea8ebe5a058b76fb33420729f7bfdcef)]:
- @replexica/[email protected]
- @replexica/[email protected]
- @replexica/[email protected]
```