This is page 17 of 20. Use http://codebase.md/lingodotdev/lingo.dev?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .changeset
│ ├── config.json
│ └── README.md
├── .claude
│ ├── agents
│ │ └── code-architect-reviewer.md
│ └── commands
│ ├── analyze-bucket-type.md
│ └── create-bucket-docs.md
├── .editorconfig
├── .github
│ ├── dependabot.yml
│ └── workflows
│ ├── docker.yml
│ ├── lingodotdev.yml
│ ├── pr-check.yml
│ ├── pr-lint.yml
│ └── release.yml
├── .gitignore
├── .husky
│ └── commit-msg
├── .npmrc
├── .prettierignore
├── .prettierrc
├── .vscode
│ ├── extensions.json
│ ├── launch.json
│ └── settings.json
├── action.yml
├── CLAUDE.md
├── CODE_OF_CONDUCT.md
├── commitlint.config.js
├── composer.json
├── content
│ ├── banner.compiler.png
│ ├── banner.dark.png
│ └── banner.launch.png
├── CONTRIBUTING.md
├── DEBUGGING.md
├── demo
│ ├── adonisjs
│ │ ├── .editorconfig
│ │ ├── .env.example
│ │ ├── .gitignore
│ │ ├── ace.js
│ │ ├── adonisrc.ts
│ │ ├── app
│ │ │ ├── exceptions
│ │ │ │ └── handler.ts
│ │ │ └── middleware
│ │ │ └── container_bindings_middleware.ts
│ │ ├── bin
│ │ │ ├── console.ts
│ │ │ ├── server.ts
│ │ │ └── test.ts
│ │ ├── CHANGELOG.md
│ │ ├── config
│ │ │ ├── app.ts
│ │ │ ├── bodyparser.ts
│ │ │ ├── cors.ts
│ │ │ ├── hash.ts
│ │ │ ├── inertia.ts
│ │ │ ├── logger.ts
│ │ │ ├── session.ts
│ │ │ ├── shield.ts
│ │ │ ├── static.ts
│ │ │ └── vite.ts
│ │ ├── eslint.config.js
│ │ ├── inertia
│ │ │ ├── app
│ │ │ │ ├── app.tsx
│ │ │ │ └── ssr.tsx
│ │ │ ├── css
│ │ │ │ └── app.css
│ │ │ ├── lingo
│ │ │ │ ├── dictionary.js
│ │ │ │ └── meta.json
│ │ │ ├── pages
│ │ │ │ ├── errors
│ │ │ │ │ ├── not_found.tsx
│ │ │ │ │ └── server_error.tsx
│ │ │ │ └── home.tsx
│ │ │ └── tsconfig.json
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── resources
│ │ │ └── views
│ │ │ └── inertia_layout.edge
│ │ ├── start
│ │ │ ├── env.ts
│ │ │ ├── kernel.ts
│ │ │ └── routes.ts
│ │ ├── tests
│ │ │ └── bootstrap.ts
│ │ ├── tsconfig.json
│ │ └── vite.config.ts
│ ├── next-app
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── eslint.config.mjs
│ │ ├── next.config.ts
│ │ ├── package.json
│ │ ├── postcss.config.mjs
│ │ ├── public
│ │ │ ├── file.svg
│ │ │ ├── globe.svg
│ │ │ ├── next.svg
│ │ │ ├── vercel.svg
│ │ │ └── window.svg
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── app
│ │ │ │ ├── client-component.tsx
│ │ │ │ ├── favicon.ico
│ │ │ │ ├── globals.css
│ │ │ │ ├── layout.tsx
│ │ │ │ ├── lingo-dot-dev.tsx
│ │ │ │ ├── page.tsx
│ │ │ │ └── test
│ │ │ │ └── page.tsx
│ │ │ ├── components
│ │ │ │ ├── hero-actions.tsx
│ │ │ │ ├── hero-subtitle.tsx
│ │ │ │ ├── hero-title.tsx
│ │ │ │ └── index.ts
│ │ │ └── lingo
│ │ │ ├── dictionary.js
│ │ │ └── meta.json
│ │ └── tsconfig.json
│ ├── react-router-app
│ │ ├── .dockerignore
│ │ ├── .gitignore
│ │ ├── app
│ │ │ ├── app.css
│ │ │ ├── lingo
│ │ │ │ ├── dictionary.js
│ │ │ │ └── meta.json
│ │ │ ├── root.tsx
│ │ │ ├── routes
│ │ │ │ ├── home.tsx
│ │ │ │ └── test.tsx
│ │ │ ├── routes.ts
│ │ │ └── welcome
│ │ │ ├── lingo-dot-dev.tsx
│ │ │ ├── logo-dark.svg
│ │ │ ├── logo-light.svg
│ │ │ └── welcome.tsx
│ │ ├── Dockerfile
│ │ ├── package.json
│ │ ├── public
│ │ │ └── favicon.ico
│ │ ├── react-router.config.ts
│ │ ├── README.md
│ │ ├── tsconfig.json
│ │ └── vite.config.ts
│ └── vite-project
│ ├── .gitignore
│ ├── CHANGELOG.md
│ ├── eslint.config.js
│ ├── index.html
│ ├── package.json
│ ├── public
│ │ └── vite.svg
│ ├── README.md
│ ├── src
│ │ ├── App.css
│ │ ├── App.tsx
│ │ ├── assets
│ │ │ └── react.svg
│ │ ├── components
│ │ │ └── test.tsx
│ │ ├── index.css
│ │ ├── lingo
│ │ │ ├── dictionary.js
│ │ │ └── meta.json
│ │ ├── lingo-dot-dev.tsx
│ │ ├── main.tsx
│ │ └── vite-env.d.ts
│ ├── tsconfig.app.json
│ ├── tsconfig.json
│ ├── tsconfig.node.json
│ └── vite.config.ts
├── Dockerfile
├── i18n.json
├── i18n.lock
├── integrations
│ └── directus
│ ├── .gitignore
│ ├── CHANGELOG.md
│ ├── docker-compose.yml
│ ├── Dockerfile
│ ├── package.json
│ ├── README.md
│ ├── src
│ │ ├── api.ts
│ │ ├── app.ts
│ │ └── index.spec.ts
│ ├── tsconfig.json
│ ├── tsconfig.test.json
│ └── tsup.config.ts
├── ISSUE_TEMPLATE.md
├── legacy
│ ├── cli
│ │ ├── bin
│ │ │ └── cli.mjs
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ └── readme.md
│ └── sdk
│ ├── CHANGELOG.md
│ ├── index.d.ts
│ ├── index.js
│ ├── package.json
│ └── README.md
├── LICENSE.md
├── mcp.md
├── package.json
├── packages
│ ├── cli
│ │ ├── assets
│ │ │ ├── failure.mp3
│ │ │ └── success.mp3
│ │ ├── bin
│ │ │ └── cli.mjs
│ │ ├── CHANGELOG.md
│ │ ├── demo
│ │ │ ├── android
│ │ │ │ ├── en
│ │ │ │ │ └── example.xml
│ │ │ │ ├── es
│ │ │ │ │ └── example.xml
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── csv
│ │ │ │ ├── example.csv
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── demo.spec.ts
│ │ │ ├── ejs
│ │ │ │ ├── en
│ │ │ │ │ └── example.ejs
│ │ │ │ ├── es
│ │ │ │ │ └── example.ejs
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── flutter
│ │ │ │ ├── en
│ │ │ │ │ └── example.arb
│ │ │ │ ├── es
│ │ │ │ │ └── example.arb
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── html
│ │ │ │ ├── en
│ │ │ │ │ └── example.html
│ │ │ │ ├── es
│ │ │ │ │ └── example.html
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── json
│ │ │ │ ├── en
│ │ │ │ │ └── example.json
│ │ │ │ ├── es
│ │ │ │ │ └── example.json
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── json-dictionary
│ │ │ │ ├── example.json
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── json5
│ │ │ │ ├── en
│ │ │ │ │ └── example.json5
│ │ │ │ ├── es
│ │ │ │ │ └── example.json5
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── jsonc
│ │ │ │ ├── en
│ │ │ │ │ └── example.jsonc
│ │ │ │ ├── es
│ │ │ │ │ └── example.jsonc
│ │ │ │ ├── i18n.json
│ │ │ │ ├── i18n.lock
│ │ │ │ └── ru
│ │ │ │ └── example.jsonc
│ │ │ ├── markdoc
│ │ │ │ ├── en
│ │ │ │ │ └── example.markdoc
│ │ │ │ ├── es
│ │ │ │ │ └── example.markdoc
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── markdown
│ │ │ │ ├── en
│ │ │ │ │ └── example.md
│ │ │ │ ├── es
│ │ │ │ │ └── example.md
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── mdx
│ │ │ │ ├── en
│ │ │ │ │ └── example.mdx
│ │ │ │ ├── es
│ │ │ │ │ └── example.mdx
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── php
│ │ │ │ ├── en
│ │ │ │ │ └── example.php
│ │ │ │ ├── es
│ │ │ │ │ └── example.php
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── po
│ │ │ │ ├── en
│ │ │ │ │ └── example.po
│ │ │ │ ├── es
│ │ │ │ │ └── example.po
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── properties
│ │ │ │ ├── en
│ │ │ │ │ └── example.properties
│ │ │ │ ├── es
│ │ │ │ │ └── example.properties
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── run_i18n.sh
│ │ │ ├── srt
│ │ │ │ ├── en
│ │ │ │ │ └── example.srt
│ │ │ │ ├── es
│ │ │ │ │ └── example.srt
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── txt
│ │ │ │ ├── en
│ │ │ │ │ └── example.txt
│ │ │ │ ├── es
│ │ │ │ │ └── example.txt
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── typescript
│ │ │ │ ├── en
│ │ │ │ │ └── example.ts
│ │ │ │ ├── es
│ │ │ │ │ └── example.ts
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── vtt
│ │ │ │ ├── en
│ │ │ │ │ └── example.vtt
│ │ │ │ ├── es
│ │ │ │ │ └── example.vtt
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── vue-json
│ │ │ │ ├── example.vue
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── xcode-strings
│ │ │ │ ├── en
│ │ │ │ │ └── example.strings
│ │ │ │ ├── es
│ │ │ │ │ └── example.strings
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── xcode-stringsdict
│ │ │ │ ├── en
│ │ │ │ │ └── example.stringsdict
│ │ │ │ ├── es
│ │ │ │ │ └── example.stringsdict
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── xcode-xcstrings
│ │ │ │ ├── example.xcstrings
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── xcode-xcstrings-v2
│ │ │ │ ├── complex-example.xcstrings
│ │ │ │ ├── example.xcstrings
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── xliff
│ │ │ │ ├── en
│ │ │ │ │ ├── example-v1.2.xliff
│ │ │ │ │ └── example-v2.xliff
│ │ │ │ ├── es
│ │ │ │ │ ├── example-v1.2.xliff
│ │ │ │ │ ├── example-v2.xliff
│ │ │ │ │ └── example.xliff
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── xml
│ │ │ │ ├── en
│ │ │ │ │ └── example.xml
│ │ │ │ ├── es
│ │ │ │ │ └── example.xml
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── yaml
│ │ │ │ ├── en
│ │ │ │ │ └── example.yml
│ │ │ │ ├── es
│ │ │ │ │ └── example.yml
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ └── yaml-root-key
│ │ │ ├── en
│ │ │ │ └── example.yml
│ │ │ ├── es
│ │ │ │ └── example.yml
│ │ │ ├── i18n.json
│ │ │ └── i18n.lock
│ │ ├── i18n.json
│ │ ├── i18n.lock
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── cli
│ │ │ │ ├── cmd
│ │ │ │ │ ├── auth.ts
│ │ │ │ │ ├── ci
│ │ │ │ │ │ ├── flows
│ │ │ │ │ │ │ ├── _base.ts
│ │ │ │ │ │ │ ├── in-branch.ts
│ │ │ │ │ │ │ └── pull-request.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── platforms
│ │ │ │ │ │ ├── _base.ts
│ │ │ │ │ │ ├── bitbucket.ts
│ │ │ │ │ │ ├── github.ts
│ │ │ │ │ │ ├── gitlab.ts
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── cleanup.ts
│ │ │ │ │ ├── config
│ │ │ │ │ │ ├── get.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── set.ts
│ │ │ │ │ │ └── unset.ts
│ │ │ │ │ ├── i18n.ts
│ │ │ │ │ ├── init.ts
│ │ │ │ │ ├── lockfile.ts
│ │ │ │ │ ├── login.ts
│ │ │ │ │ ├── logout.ts
│ │ │ │ │ ├── may-the-fourth.ts
│ │ │ │ │ ├── mcp.ts
│ │ │ │ │ ├── purge.ts
│ │ │ │ │ ├── run
│ │ │ │ │ │ ├── _const.ts
│ │ │ │ │ │ ├── _types.ts
│ │ │ │ │ │ ├── _utils.ts
│ │ │ │ │ │ ├── execute.spec.ts
│ │ │ │ │ │ ├── execute.ts
│ │ │ │ │ │ ├── frozen.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── plan.ts
│ │ │ │ │ │ ├── setup.ts
│ │ │ │ │ │ └── watch.ts
│ │ │ │ │ ├── show
│ │ │ │ │ │ ├── _shared-key-command.ts
│ │ │ │ │ │ ├── config.ts
│ │ │ │ │ │ ├── files.ts
│ │ │ │ │ │ ├── ignored-keys.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── locale.ts
│ │ │ │ │ │ └── locked-keys.ts
│ │ │ │ │ └── status.ts
│ │ │ │ ├── constants.ts
│ │ │ │ ├── index.spec.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── loaders
│ │ │ │ │ ├── _types.ts
│ │ │ │ │ ├── _utils.ts
│ │ │ │ │ ├── android.spec.ts
│ │ │ │ │ ├── android.ts
│ │ │ │ │ ├── csv.spec.ts
│ │ │ │ │ ├── csv.ts
│ │ │ │ │ ├── dato
│ │ │ │ │ │ ├── _base.ts
│ │ │ │ │ │ ├── _utils.ts
│ │ │ │ │ │ ├── api.ts
│ │ │ │ │ │ ├── extract.ts
│ │ │ │ │ │ ├── filter.ts
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── ejs.spec.ts
│ │ │ │ │ ├── ejs.ts
│ │ │ │ │ ├── ensure-key-order.spec.ts
│ │ │ │ │ ├── ensure-key-order.ts
│ │ │ │ │ ├── flat.spec.ts
│ │ │ │ │ ├── flat.ts
│ │ │ │ │ ├── flutter.spec.ts
│ │ │ │ │ ├── flutter.ts
│ │ │ │ │ ├── formatters
│ │ │ │ │ │ ├── _base.ts
│ │ │ │ │ │ ├── biome.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── prettier.ts
│ │ │ │ │ ├── html.ts
│ │ │ │ │ ├── icu-safety.spec.ts
│ │ │ │ │ ├── ignored-keys-buckets.spec.ts
│ │ │ │ │ ├── ignored-keys.spec.ts
│ │ │ │ │ ├── ignored-keys.ts
│ │ │ │ │ ├── index.spec.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── inject-locale.spec.ts
│ │ │ │ │ ├── inject-locale.ts
│ │ │ │ │ ├── json-dictionary.spec.ts
│ │ │ │ │ ├── json-dictionary.ts
│ │ │ │ │ ├── json-sorting.test.ts
│ │ │ │ │ ├── json-sorting.ts
│ │ │ │ │ ├── json.ts
│ │ │ │ │ ├── json5.spec.ts
│ │ │ │ │ ├── json5.ts
│ │ │ │ │ ├── jsonc.spec.ts
│ │ │ │ │ ├── jsonc.ts
│ │ │ │ │ ├── locked-keys.spec.ts
│ │ │ │ │ ├── locked-keys.ts
│ │ │ │ │ ├── locked-patterns.spec.ts
│ │ │ │ │ ├── locked-patterns.ts
│ │ │ │ │ ├── markdoc.spec.ts
│ │ │ │ │ ├── markdoc.ts
│ │ │ │ │ ├── markdown.ts
│ │ │ │ │ ├── mdx.spec.ts
│ │ │ │ │ ├── mdx.ts
│ │ │ │ │ ├── mdx2
│ │ │ │ │ │ ├── _types.ts
│ │ │ │ │ │ ├── _utils.ts
│ │ │ │ │ │ ├── code-placeholder.spec.ts
│ │ │ │ │ │ ├── code-placeholder.ts
│ │ │ │ │ │ ├── frontmatter-split.spec.ts
│ │ │ │ │ │ ├── frontmatter-split.ts
│ │ │ │ │ │ ├── localizable-document.spec.ts
│ │ │ │ │ │ ├── localizable-document.ts
│ │ │ │ │ │ ├── section-split.spec.ts
│ │ │ │ │ │ ├── section-split.ts
│ │ │ │ │ │ └── sections-split-2.ts
│ │ │ │ │ ├── passthrough.ts
│ │ │ │ │ ├── php.ts
│ │ │ │ │ ├── plutil-json-loader.ts
│ │ │ │ │ ├── po
│ │ │ │ │ │ ├── _types.ts
│ │ │ │ │ │ ├── index.spec.ts
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── properties.ts
│ │ │ │ │ ├── root-key.ts
│ │ │ │ │ ├── srt.ts
│ │ │ │ │ ├── sync.ts
│ │ │ │ │ ├── text-file.ts
│ │ │ │ │ ├── txt.ts
│ │ │ │ │ ├── typescript
│ │ │ │ │ │ ├── cjs-interop.ts
│ │ │ │ │ │ ├── index.spec.ts
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── unlocalizable.spec.ts
│ │ │ │ │ ├── unlocalizable.ts
│ │ │ │ │ ├── variable
│ │ │ │ │ │ ├── index.spec.ts
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── vtt.ts
│ │ │ │ │ ├── vue-json.ts
│ │ │ │ │ ├── xcode-strings
│ │ │ │ │ │ ├── escape.ts
│ │ │ │ │ │ ├── parser.ts
│ │ │ │ │ │ ├── tokenizer.ts
│ │ │ │ │ │ └── types.ts
│ │ │ │ │ ├── xcode-strings.spec.ts
│ │ │ │ │ ├── xcode-strings.ts
│ │ │ │ │ ├── xcode-stringsdict.ts
│ │ │ │ │ ├── xcode-xcstrings-icu.spec.ts
│ │ │ │ │ ├── xcode-xcstrings-icu.ts
│ │ │ │ │ ├── xcode-xcstrings-lock-compatibility.spec.ts
│ │ │ │ │ ├── xcode-xcstrings-v2-loader.ts
│ │ │ │ │ ├── xcode-xcstrings.spec.ts
│ │ │ │ │ ├── xcode-xcstrings.ts
│ │ │ │ │ ├── xliff.spec.ts
│ │ │ │ │ ├── xliff.ts
│ │ │ │ │ ├── xml.ts
│ │ │ │ │ └── yaml.ts
│ │ │ │ ├── localizer
│ │ │ │ │ ├── _types.ts
│ │ │ │ │ ├── explicit.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── lingodotdev.ts
│ │ │ │ ├── processor
│ │ │ │ │ ├── _base.ts
│ │ │ │ │ ├── basic.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── lingo.ts
│ │ │ │ └── utils
│ │ │ │ ├── auth.ts
│ │ │ │ ├── buckets.spec.ts
│ │ │ │ ├── buckets.ts
│ │ │ │ ├── cache.ts
│ │ │ │ ├── cloudflare-status.ts
│ │ │ │ ├── config.ts
│ │ │ │ ├── delta.spec.ts
│ │ │ │ ├── delta.ts
│ │ │ │ ├── ensure-patterns.ts
│ │ │ │ ├── errors.ts
│ │ │ │ ├── exec.spec.ts
│ │ │ │ ├── exec.ts
│ │ │ │ ├── exit-gracefully.spec.ts
│ │ │ │ ├── exit-gracefully.ts
│ │ │ │ ├── exp-backoff.ts
│ │ │ │ ├── find-locale-paths.spec.ts
│ │ │ │ ├── find-locale-paths.ts
│ │ │ │ ├── fs.ts
│ │ │ │ ├── init-ci-cd.ts
│ │ │ │ ├── key-matching.spec.ts
│ │ │ │ ├── key-matching.ts
│ │ │ │ ├── lockfile.ts
│ │ │ │ ├── md5.ts
│ │ │ │ ├── observability.ts
│ │ │ │ ├── plutil-formatter.spec.ts
│ │ │ │ ├── plutil-formatter.ts
│ │ │ │ ├── settings.ts
│ │ │ │ ├── ui.ts
│ │ │ │ └── update-gitignore.ts
│ │ │ ├── compiler
│ │ │ │ └── index.ts
│ │ │ ├── locale-codes
│ │ │ │ └── index.ts
│ │ │ ├── react
│ │ │ │ ├── client.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── react-router.ts
│ │ │ │ └── rsc.ts
│ │ │ ├── sdk
│ │ │ │ └── index.ts
│ │ │ └── spec
│ │ │ └── index.ts
│ │ ├── tests
│ │ │ └── mock-storage.ts
│ │ ├── troubleshooting.md
│ │ ├── tsconfig.json
│ │ ├── tsconfig.test.json
│ │ ├── tsup.config.ts
│ │ ├── types
│ │ │ ├── vtt.d.ts
│ │ │ └── xliff.d.ts
│ │ ├── vitest.config.ts
│ │ └── WATCH_MODE.md
│ ├── compiler
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── _base.ts
│ │ │ ├── _const.ts
│ │ │ ├── _loader-utils.spec.ts
│ │ │ ├── _loader-utils.ts
│ │ │ ├── _utils.spec.ts
│ │ │ ├── _utils.ts
│ │ │ ├── client-dictionary-loader.ts
│ │ │ ├── i18n-directive.spec.ts
│ │ │ ├── i18n-directive.ts
│ │ │ ├── index.spec.ts
│ │ │ ├── index.ts
│ │ │ ├── jsx-attribute-flag.spec.ts
│ │ │ ├── jsx-attribute-flag.ts
│ │ │ ├── jsx-attribute-scope-inject.spec.ts
│ │ │ ├── jsx-attribute-scope-inject.ts
│ │ │ ├── jsx-attribute-scopes-export.spec.ts
│ │ │ ├── jsx-attribute-scopes-export.ts
│ │ │ ├── jsx-attribute.spec.ts
│ │ │ ├── jsx-attribute.ts
│ │ │ ├── jsx-fragment.spec.ts
│ │ │ ├── jsx-fragment.ts
│ │ │ ├── jsx-html-lang.spec.ts
│ │ │ ├── jsx-html-lang.ts
│ │ │ ├── jsx-provider.spec.ts
│ │ │ ├── jsx-provider.ts
│ │ │ ├── jsx-remove-attributes.spec.ts
│ │ │ ├── jsx-remove-attributes.ts
│ │ │ ├── jsx-root-flag.spec.ts
│ │ │ ├── jsx-root-flag.ts
│ │ │ ├── jsx-scope-flag.spec.ts
│ │ │ ├── jsx-scope-flag.ts
│ │ │ ├── jsx-scope-inject.spec.ts
│ │ │ ├── jsx-scope-inject.ts
│ │ │ ├── jsx-scopes-export.spec.ts
│ │ │ ├── jsx-scopes-export.ts
│ │ │ ├── lib
│ │ │ │ └── lcp
│ │ │ │ ├── api
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── prompt.spec.ts
│ │ │ │ │ ├── prompt.ts
│ │ │ │ │ ├── provider-details.spec.ts
│ │ │ │ │ ├── provider-details.ts
│ │ │ │ │ ├── shots.ts
│ │ │ │ │ ├── xml2obj.spec.ts
│ │ │ │ │ └── xml2obj.ts
│ │ │ │ ├── api.spec.ts
│ │ │ │ ├── cache.spec.ts
│ │ │ │ ├── cache.ts
│ │ │ │ ├── index.spec.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── schema.ts
│ │ │ │ ├── server.spec.ts
│ │ │ │ └── server.ts
│ │ │ ├── lingo-turbopack-loader.ts
│ │ │ ├── react-router-dictionary-loader.ts
│ │ │ ├── rsc-dictionary-loader.ts
│ │ │ └── utils
│ │ │ ├── ast-key.spec.ts
│ │ │ ├── ast-key.ts
│ │ │ ├── create-locale-import-map.spec.ts
│ │ │ ├── create-locale-import-map.ts
│ │ │ ├── env.spec.ts
│ │ │ ├── env.ts
│ │ │ ├── hash.spec.ts
│ │ │ ├── hash.ts
│ │ │ ├── index.spec.ts
│ │ │ ├── index.ts
│ │ │ ├── invokations.spec.ts
│ │ │ ├── invokations.ts
│ │ │ ├── jsx-attribute-scope.ts
│ │ │ ├── jsx-attribute.spec.ts
│ │ │ ├── jsx-attribute.ts
│ │ │ ├── jsx-content-whitespace.spec.ts
│ │ │ ├── jsx-content.spec.ts
│ │ │ ├── jsx-content.ts
│ │ │ ├── jsx-element.spec.ts
│ │ │ ├── jsx-element.ts
│ │ │ ├── jsx-expressions.test.ts
│ │ │ ├── jsx-expressions.ts
│ │ │ ├── jsx-functions.spec.ts
│ │ │ ├── jsx-functions.ts
│ │ │ ├── jsx-scope.spec.ts
│ │ │ ├── jsx-scope.ts
│ │ │ ├── jsx-variables.spec.ts
│ │ │ ├── jsx-variables.ts
│ │ │ ├── llm-api-key.ts
│ │ │ ├── llm-api-keys.spec.ts
│ │ │ ├── locales.spec.ts
│ │ │ ├── locales.ts
│ │ │ ├── module-params.spec.ts
│ │ │ ├── module-params.ts
│ │ │ ├── observability.spec.ts
│ │ │ ├── observability.ts
│ │ │ ├── rc.spec.ts
│ │ │ └── rc.ts
│ │ ├── tsconfig.json
│ │ ├── tsup.config.ts
│ │ └── vitest.config.ts
│ ├── locales
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── constants.ts
│ │ │ ├── index.ts
│ │ │ ├── names
│ │ │ │ ├── index.spec.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── integration.spec.ts
│ │ │ │ └── loader.ts
│ │ │ ├── parser.spec.ts
│ │ │ ├── parser.ts
│ │ │ ├── types.ts
│ │ │ ├── validation.spec.ts
│ │ │ └── validation.ts
│ │ ├── tsconfig.json
│ │ └── tsup.config.ts
│ ├── react
│ │ ├── build.config.ts
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── client
│ │ │ │ ├── attribute-component.spec.tsx
│ │ │ │ ├── attribute-component.tsx
│ │ │ │ ├── component.lingo-component.spec.tsx
│ │ │ │ ├── component.spec.tsx
│ │ │ │ ├── component.tsx
│ │ │ │ ├── context.spec.tsx
│ │ │ │ ├── context.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── loader.spec.ts
│ │ │ │ ├── loader.ts
│ │ │ │ ├── locale-switcher.spec.tsx
│ │ │ │ ├── locale-switcher.tsx
│ │ │ │ ├── locale.spec.ts
│ │ │ │ ├── locale.ts
│ │ │ │ ├── provider.spec.tsx
│ │ │ │ ├── provider.tsx
│ │ │ │ ├── utils.spec.ts
│ │ │ │ └── utils.ts
│ │ │ ├── core
│ │ │ │ ├── attribute-component.spec.tsx
│ │ │ │ ├── attribute-component.tsx
│ │ │ │ ├── component.spec.tsx
│ │ │ │ ├── component.tsx
│ │ │ │ ├── const.ts
│ │ │ │ ├── get-dictionary.spec.ts
│ │ │ │ ├── get-dictionary.ts
│ │ │ │ └── index.ts
│ │ │ ├── react-router
│ │ │ │ ├── index.ts
│ │ │ │ ├── loader.spec.ts
│ │ │ │ └── loader.ts
│ │ │ ├── rsc
│ │ │ │ ├── attribute-component.tsx
│ │ │ │ ├── component.lingo-component.spec.tsx
│ │ │ │ ├── component.spec.tsx
│ │ │ │ ├── component.tsx
│ │ │ │ ├── index.ts
│ │ │ │ ├── loader.spec.ts
│ │ │ │ ├── loader.ts
│ │ │ │ ├── provider.spec.tsx
│ │ │ │ ├── provider.tsx
│ │ │ │ ├── utils.spec.ts
│ │ │ │ └── utils.ts
│ │ │ └── test
│ │ │ └── setup.ts
│ │ ├── tsconfig.json
│ │ └── vitest.config.ts
│ ├── sdk
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── abort-controller.specs.ts
│ │ │ ├── index.spec.ts
│ │ │ └── index.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.test.json
│ │ └── tsup.config.ts
│ └── spec
│ ├── CHANGELOG.md
│ ├── package.json
│ ├── README.md
│ ├── src
│ │ ├── config.spec.ts
│ │ ├── config.ts
│ │ ├── formats.ts
│ │ ├── index.spec.ts
│ │ ├── index.ts
│ │ ├── json-schema.ts
│ │ ├── locales.spec.ts
│ │ └── locales.ts
│ ├── tsconfig.json
│ ├── tsconfig.test.json
│ └── tsup.config.ts
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── readme
│ ├── ar.md
│ ├── bn.md
│ ├── de.md
│ ├── en.md
│ ├── es.md
│ ├── fa.md
│ ├── fr.md
│ ├── he.md
│ ├── hi.md
│ ├── it.md
│ ├── ja.md
│ ├── ko.md
│ ├── pl.md
│ ├── pt-BR.md
│ ├── ru.md
│ ├── tr.md
│ ├── uk-UA.md
│ └── zh-Hans.md
├── readme.md
├── scripts
│ ├── docs
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── generate-cli-docs.ts
│ │ │ ├── generate-config-docs.ts
│ │ │ ├── json-schema
│ │ │ │ ├── markdown-renderer.test.ts
│ │ │ │ ├── markdown-renderer.ts
│ │ │ │ ├── parser.test.ts
│ │ │ │ ├── parser.ts
│ │ │ │ └── types.ts
│ │ │ ├── utils.test.ts
│ │ │ └── utils.ts
│ │ ├── tsconfig.json
│ │ └── vitest.config.ts
│ └── packagist-publish.php
└── turbo.json
```
# Files
--------------------------------------------------------------------------------
/packages/cli/src/cli/cmd/i18n.ts:
--------------------------------------------------------------------------------
```typescript
1 | import {
2 | bucketTypeSchema,
3 | I18nConfig,
4 | localeCodeSchema,
5 | resolveOverriddenLocale,
6 | } from "@lingo.dev/_spec";
7 | import { Command } from "interactive-commander";
8 | import Z from "zod";
9 | import _ from "lodash";
10 | import * as path from "path";
11 | import { getConfig } from "../utils/config";
12 | import { getSettings } from "../utils/settings";
13 | import {
14 | ConfigError,
15 | AuthenticationError,
16 | ValidationError,
17 | LocalizationError,
18 | BucketProcessingError,
19 | getCLIErrorType,
20 | isLocalizationError,
21 | isBucketProcessingError,
22 | ErrorDetail,
23 | aggregateErrorAnalytics,
24 | createPreviousErrorContext,
25 | } from "../utils/errors";
26 | import Ora from "ora";
27 | import createBucketLoader from "../loaders";
28 | import { createAuthenticator } from "../utils/auth";
29 | import { getBuckets } from "../utils/buckets";
30 | import chalk from "chalk";
31 | import { createTwoFilesPatch } from "diff";
32 | import inquirer from "inquirer";
33 | import externalEditor from "external-editor";
34 | import updateGitignore from "../utils/update-gitignore";
35 | import createProcessor from "../processor";
36 | import { withExponentialBackoff } from "../utils/exp-backoff";
37 | import trackEvent from "../utils/observability";
38 | import { createDeltaProcessor } from "../utils/delta";
39 | import { isICUPluralObject } from "../loaders/xcode-xcstrings-icu";
40 |
41 | export default new Command()
42 | .command("i18n")
43 | .description(
44 | "DEPRECATED: Run localization pipeline (prefer `run` command instead)",
45 | )
46 | .helpOption("-h, --help", "Show help")
47 | .option(
48 | "--locale <locale>",
49 | "Limit processing to the listed target locale codes from i18n.json. Repeat the flag to include multiple locales. Defaults to all configured target locales",
50 | (val: string, prev: string[]) => (prev ? [...prev, val] : [val]),
51 | )
52 | .option(
53 | "--bucket <bucket>",
54 | "Limit processing to specific bucket types defined in i18n.json (e.g., json, yaml, android). Repeat the flag to include multiple bucket types. Defaults to all buckets",
55 | (val: string, prev: string[]) => (prev ? [...prev, val] : [val]),
56 | )
57 | .option(
58 | "--key <key>",
59 | "Limit processing to a single translation key by exact match. Filters all buckets and locales to process only this key, useful for testing or debugging specific translations. Example: auth.login.title",
60 | (val: string) => encodeURIComponent(val),
61 | )
62 | .option(
63 | "--file [files...]",
64 | "Filter processing to only buckets whose file paths contain these substrings. Example: 'components' to process only files in components directories",
65 | )
66 | .option(
67 | "--frozen",
68 | "Validate translations are up-to-date without making changes - fails if source files, target files, or lockfile are out of sync. Ideal for CI/CD to ensure translation consistency before deployment",
69 | )
70 | .option(
71 | "--force",
72 | "Force re-translation of all keys, bypassing change detection. Useful when you want to regenerate translations with updated AI models or translation settings",
73 | )
74 | .option(
75 | "--verbose",
76 | "Print the translation data being processed as formatted JSON for each bucket and locale",
77 | )
78 | .option(
79 | "--interactive",
80 | "Review and edit AI-generated translations interactively before applying changes to files",
81 | )
82 | .option(
83 | "--api-key <api-key>",
84 | "Override API key from settings or environment variables",
85 | )
86 | .option(
87 | "--debug",
88 | "Pause before processing localization so you can attach a debugger",
89 | )
90 | .option(
91 | "--strict",
92 | "Stop immediately on first error instead of continuing to process remaining buckets and locales (fail-fast mode)",
93 | )
94 | .action(async function (options) {
95 | updateGitignore();
96 |
97 | const ora = Ora();
98 | let flags: ReturnType<typeof parseFlags>;
99 |
100 | try {
101 | flags = parseFlags(options);
102 | } catch (parseError: any) {
103 | // Handle flag validation errors (like invalid locale codes)
104 | await trackEvent("unknown", "cmd.i18n.error", {
105 | errorType: "validation_error",
106 | errorName: parseError.name || "ValidationError",
107 | errorMessage: parseError.message || "Invalid command line options",
108 | errorStack: parseError.stack,
109 | fatal: true,
110 | errorCount: 1,
111 | stage: "flag_validation",
112 | });
113 | throw parseError;
114 | }
115 |
116 | if (flags.debug) {
117 | // wait for user input, use inquirer
118 | const { debug } = await inquirer.prompt([
119 | {
120 | type: "confirm",
121 | name: "debug",
122 | message: "Debug mode. Wait for user input before continuing.",
123 | },
124 | ]);
125 | }
126 |
127 | let hasErrors = false;
128 | let authId: string | null = null;
129 | const errorDetails: ErrorDetail[] = [];
130 | try {
131 | ora.start("Loading configuration...");
132 | const i18nConfig = getConfig();
133 | const settings = getSettings(flags.apiKey);
134 | ora.succeed("Configuration loaded");
135 |
136 | ora.start("Validating localization configuration...");
137 | validateParams(i18nConfig, flags);
138 | ora.succeed("Localization configuration is valid");
139 |
140 | ora.start("Connecting to Lingo.dev Localization Engine...");
141 | const isByokMode = !!i18nConfig?.provider;
142 |
143 | if (isByokMode) {
144 | authId = null;
145 | ora.succeed("Using external provider (BYOK mode)");
146 | } else {
147 | const auth = await validateAuth(settings);
148 | authId = auth.id;
149 | ora.succeed(`Authenticated as ${auth.email}`);
150 | }
151 |
152 | await trackEvent(authId, "cmd.i18n.start", {
153 | i18nConfig,
154 | flags,
155 | });
156 |
157 | let buckets = getBuckets(i18nConfig!);
158 | if (flags.bucket?.length) {
159 | buckets = buckets.filter((bucket: any) =>
160 | flags.bucket!.includes(bucket.type),
161 | );
162 | }
163 | ora.succeed("Buckets retrieved");
164 |
165 | if (flags.file?.length) {
166 | buckets = buckets
167 | .map((bucket: any) => {
168 | const paths = bucket.paths.filter((path: any) =>
169 | flags.file!.find((file) => path.pathPattern?.includes(file)),
170 | );
171 | return { ...bucket, paths };
172 | })
173 | .filter((bucket: any) => bucket.paths.length > 0);
174 | if (buckets.length === 0) {
175 | ora.fail(
176 | "No buckets found. All buckets were filtered out by --file option.",
177 | );
178 | throw new Error(
179 | "No buckets found. All buckets were filtered out by --file option.",
180 | );
181 | } else {
182 | ora.info(`\x1b[36mProcessing only filtered buckets:\x1b[0m`);
183 | buckets.map((bucket: any) => {
184 | ora.info(` ${bucket.type}:`);
185 | bucket.paths.forEach((path: any) => {
186 | ora.info(` - ${path.pathPattern}`);
187 | });
188 | });
189 | }
190 | }
191 |
192 | const targetLocales = flags.locale?.length
193 | ? flags.locale
194 | : i18nConfig!.locale.targets;
195 |
196 | // Ensure the lockfile exists
197 | ora.start("Setting up localization cache...");
198 | const checkLockfileProcessor = createDeltaProcessor("");
199 | const lockfileExists = await checkLockfileProcessor.checkIfLockExists();
200 | if (!lockfileExists) {
201 | ora.start("Creating i18n.lock...");
202 | for (const bucket of buckets) {
203 | for (const bucketPath of bucket.paths) {
204 | const sourceLocale = resolveOverriddenLocale(
205 | i18nConfig!.locale.source,
206 | bucketPath.delimiter,
207 | );
208 | const bucketLoader = createBucketLoader(
209 | bucket.type,
210 | bucketPath.pathPattern,
211 | {
212 | defaultLocale: sourceLocale,
213 | injectLocale: bucket.injectLocale,
214 | formatter: i18nConfig!.formatter,
215 | },
216 | bucket.lockedKeys,
217 | bucket.lockedPatterns,
218 | bucket.ignoredKeys,
219 | );
220 | bucketLoader.setDefaultLocale(sourceLocale);
221 | await bucketLoader.init();
222 |
223 | const sourceData = await bucketLoader.pull(
224 | i18nConfig!.locale.source,
225 | );
226 |
227 | const deltaProcessor = createDeltaProcessor(bucketPath.pathPattern);
228 | const checksums = await deltaProcessor.createChecksums(sourceData);
229 | await deltaProcessor.saveChecksums(checksums);
230 | }
231 | }
232 | ora.succeed("Localization cache initialized");
233 | } else {
234 | ora.succeed("Localization cache loaded");
235 | }
236 |
237 | if (flags.frozen) {
238 | ora.start("Checking for lockfile updates...");
239 | let requiresUpdate: string | null = null;
240 | bucketLoop: for (const bucket of buckets) {
241 | for (const bucketPath of bucket.paths) {
242 | const sourceLocale = resolveOverriddenLocale(
243 | i18nConfig!.locale.source,
244 | bucketPath.delimiter,
245 | );
246 |
247 | const bucketLoader = createBucketLoader(
248 | bucket.type,
249 | bucketPath.pathPattern,
250 | {
251 | defaultLocale: sourceLocale,
252 | returnUnlocalizedKeys: true,
253 | injectLocale: bucket.injectLocale,
254 | },
255 | bucket.lockedKeys,
256 | bucket.lockedPatterns,
257 | bucket.ignoredKeys,
258 | );
259 | bucketLoader.setDefaultLocale(sourceLocale);
260 | await bucketLoader.init();
261 |
262 | const { unlocalizable: sourceUnlocalizable, ...sourceData } =
263 | await bucketLoader.pull(i18nConfig!.locale.source);
264 | const deltaProcessor = createDeltaProcessor(bucketPath.pathPattern);
265 | const sourceChecksums =
266 | await deltaProcessor.createChecksums(sourceData);
267 | const savedChecksums = await deltaProcessor.loadChecksums();
268 |
269 | // Get updated data by comparing current checksums with saved checksums
270 | const updatedSourceData = _.pickBy(
271 | sourceData,
272 | (value, key) => sourceChecksums[key] !== savedChecksums[key],
273 | );
274 |
275 | // translation was updated in the source file
276 | if (Object.keys(updatedSourceData).length > 0) {
277 | requiresUpdate = "updated";
278 | break bucketLoop;
279 | }
280 |
281 | for (const _targetLocale of targetLocales) {
282 | const targetLocale = resolveOverriddenLocale(
283 | _targetLocale,
284 | bucketPath.delimiter,
285 | );
286 | const { unlocalizable: targetUnlocalizable, ...targetData } =
287 | await bucketLoader.pull(targetLocale);
288 |
289 | const missingKeys = _.difference(
290 | Object.keys(sourceData),
291 | Object.keys(targetData),
292 | );
293 | const extraKeys = _.difference(
294 | Object.keys(targetData),
295 | Object.keys(sourceData),
296 | );
297 | const unlocalizableDataDiff = !_.isEqual(
298 | sourceUnlocalizable,
299 | targetUnlocalizable,
300 | );
301 |
302 | // translation is missing in the target file
303 | if (missingKeys.length > 0) {
304 | requiresUpdate = "missing";
305 | break bucketLoop;
306 | }
307 |
308 | // target file has extra translations
309 | if (extraKeys.length > 0) {
310 | requiresUpdate = "extra";
311 | break bucketLoop;
312 | }
313 |
314 | // unlocalizable keys do not match
315 | if (unlocalizableDataDiff) {
316 | requiresUpdate = "unlocalizable";
317 | break bucketLoop;
318 | }
319 | }
320 | }
321 | }
322 |
323 | if (requiresUpdate) {
324 | const message = {
325 | updated: "Source file has been updated.",
326 | missing: "Target file is missing translations.",
327 | extra:
328 | "Target file has extra translations not present in the source file.",
329 | unlocalizable:
330 | "Unlocalizable data (such as booleans, dates, URLs, etc.) do not match.",
331 | }[requiresUpdate];
332 | ora.fail(
333 | `Localization data has changed; please update i18n.lock or run without --frozen.`,
334 | );
335 | ora.fail(` Details: ${message}`);
336 | throw new Error(
337 | `Localization data has changed; please update i18n.lock or run without --frozen. Details: ${message}`,
338 | );
339 | } else {
340 | ora.succeed("No lockfile updates required.");
341 | }
342 | }
343 |
344 | // Process each bucket
345 | for (const bucket of buckets) {
346 | try {
347 | console.log();
348 | ora.info(`Processing bucket: ${bucket.type}`);
349 | for (const bucketPath of bucket.paths) {
350 | const bucketOra = Ora({ indent: 2 }).info(
351 | `Processing path: ${bucketPath.pathPattern}`,
352 | );
353 |
354 | const sourceLocale = resolveOverriddenLocale(
355 | i18nConfig!.locale.source,
356 | bucketPath.delimiter,
357 | );
358 |
359 | const bucketLoader = createBucketLoader(
360 | bucket.type,
361 | bucketPath.pathPattern,
362 | {
363 | defaultLocale: sourceLocale,
364 | injectLocale: bucket.injectLocale,
365 | formatter: i18nConfig!.formatter,
366 | },
367 | bucket.lockedKeys,
368 | bucket.lockedPatterns,
369 | bucket.ignoredKeys,
370 | );
371 | bucketLoader.setDefaultLocale(sourceLocale);
372 | await bucketLoader.init();
373 | let sourceData = await bucketLoader.pull(sourceLocale);
374 |
375 | for (const _targetLocale of targetLocales) {
376 | const targetLocale = resolveOverriddenLocale(
377 | _targetLocale,
378 | bucketPath.delimiter,
379 | );
380 | try {
381 | bucketOra.start(
382 | `[${sourceLocale} -> ${targetLocale}] (0%) Localization in progress...`,
383 | );
384 |
385 | sourceData = await bucketLoader.pull(sourceLocale);
386 |
387 | const targetData = await bucketLoader.pull(targetLocale);
388 | const deltaProcessor = createDeltaProcessor(
389 | bucketPath.pathPattern,
390 | );
391 | const checksums = await deltaProcessor.loadChecksums();
392 | const delta = await deltaProcessor.calculateDelta({
393 | sourceData,
394 | targetData,
395 | checksums,
396 | });
397 | let processableData = _.chain(sourceData)
398 | .entries()
399 | .filter(
400 | ([key, value]) =>
401 | delta.added.includes(key) ||
402 | delta.updated.includes(key) ||
403 | !!flags.force,
404 | )
405 | .fromPairs()
406 | .value();
407 |
408 | if (flags.key) {
409 | processableData = _.pickBy(
410 | processableData,
411 | (_, key) => key === flags.key,
412 | );
413 | }
414 | if (flags.verbose) {
415 | bucketOra.info(JSON.stringify(processableData, null, 2));
416 | }
417 |
418 | bucketOra.start(
419 | `[${sourceLocale} -> ${targetLocale}] [${
420 | Object.keys(processableData).length
421 | } entries] (0%) AI localization in progress...`,
422 | );
423 | let processPayload = createProcessor(i18nConfig!.provider, {
424 | apiKey: settings.auth.apiKey,
425 | apiUrl: settings.auth.apiUrl,
426 | });
427 | processPayload = withExponentialBackoff(
428 | processPayload,
429 | 3,
430 | 1000,
431 | );
432 |
433 | const processedTargetData = await processPayload(
434 | {
435 | sourceLocale,
436 | sourceData,
437 | processableData,
438 | targetLocale,
439 | targetData,
440 | },
441 | (progress, sourceChunk, processedChunk) => {
442 | bucketOra.text = `[${sourceLocale} -> ${targetLocale}] [${
443 | Object.keys(processableData).length
444 | } entries] (${progress}%) AI localization in progress...`;
445 | },
446 | );
447 |
448 | if (flags.verbose) {
449 | bucketOra.info(JSON.stringify(processedTargetData, null, 2));
450 | }
451 |
452 | let finalTargetData = _.merge(
453 | {},
454 | sourceData,
455 | targetData,
456 | processedTargetData,
457 | );
458 |
459 | // rename keys
460 | finalTargetData = _.chain(finalTargetData)
461 | .entries()
462 | .map(([key, value]) => {
463 | const renaming = delta.renamed.find(
464 | ([oldKey, newKey]) => oldKey === key,
465 | );
466 | if (!renaming) {
467 | return [key, value];
468 | }
469 | return [renaming[1], value];
470 | })
471 | .fromPairs()
472 | .value();
473 |
474 | if (flags.interactive) {
475 | bucketOra.stop();
476 | const reviewedData = await reviewChanges({
477 | pathPattern: bucketPath.pathPattern,
478 | targetLocale,
479 | currentData: targetData,
480 | proposedData: finalTargetData,
481 | sourceData,
482 | force: flags.force!,
483 | });
484 |
485 | finalTargetData = reviewedData;
486 | bucketOra.start(
487 | `Applying changes to ${bucketPath} (${targetLocale})`,
488 | );
489 | }
490 |
491 | const finalDiffSize = _.chain(finalTargetData)
492 | .omitBy((value, key) => {
493 | const targetValue = targetData[key];
494 |
495 | // For ICU plural objects, use deep equality (excluding Symbol)
496 | if (
497 | isICUPluralObject(value) &&
498 | isICUPluralObject(targetValue)
499 | ) {
500 | return _.isEqual(
501 | { icu: value.icu, _meta: value._meta },
502 | { icu: targetValue.icu, _meta: targetValue._meta },
503 | );
504 | }
505 |
506 | // Default strict equality for other values
507 | return value === targetValue;
508 | })
509 | .size()
510 | .value();
511 |
512 | // Push to bucket all the time as there might be changes to unlocalizable keys
513 | await bucketLoader.push(targetLocale, finalTargetData);
514 |
515 | if (finalDiffSize > 0 || flags.force) {
516 | bucketOra.succeed(
517 | `[${sourceLocale} -> ${targetLocale}] Localization completed`,
518 | );
519 | } else {
520 | bucketOra.succeed(
521 | `[${sourceLocale} -> ${targetLocale}] Localization completed (no changes).`,
522 | );
523 | }
524 | } catch (_error: any) {
525 | const error = new LocalizationError(
526 | `[${sourceLocale} -> ${targetLocale}] Localization failed: ${_error.message}`,
527 | {
528 | bucket: bucket.type,
529 | sourceLocale,
530 | targetLocale,
531 | pathPattern: bucketPath.pathPattern,
532 | },
533 | );
534 | errorDetails.push({
535 | type: "locale_error",
536 | bucket: bucket.type,
537 | locale: `${sourceLocale} -> ${targetLocale}`,
538 | pathPattern: bucketPath.pathPattern,
539 | message: _error.message,
540 | stack: _error.stack,
541 | });
542 | if (flags.strict) {
543 | throw error;
544 | } else {
545 | bucketOra.fail(error.message);
546 | hasErrors = true;
547 | }
548 | }
549 | }
550 |
551 | const deltaProcessor = createDeltaProcessor(bucketPath.pathPattern);
552 | const checksums = await deltaProcessor.createChecksums(sourceData);
553 | if (!flags.locale?.length) {
554 | await deltaProcessor.saveChecksums(checksums);
555 | }
556 | }
557 | } catch (_error: any) {
558 | const error = new BucketProcessingError(
559 | `Failed to process bucket ${bucket.type}: ${_error.message}`,
560 | bucket.type,
561 | );
562 | errorDetails.push({
563 | type: "bucket_error",
564 | bucket: bucket.type,
565 | message: _error.message,
566 | stack: _error.stack,
567 | });
568 | if (flags.strict) {
569 | throw error;
570 | } else {
571 | ora.fail(error.message);
572 | hasErrors = true;
573 | }
574 | }
575 | }
576 | console.log();
577 | if (!hasErrors) {
578 | ora.succeed("Localization completed.");
579 | await trackEvent(authId, "cmd.i18n.success", {
580 | i18nConfig: {
581 | sourceLocale: i18nConfig!.locale.source,
582 | targetLocales: i18nConfig!.locale.targets,
583 | bucketTypes: Object.keys(i18nConfig!.buckets),
584 | },
585 | flags,
586 | bucketCount: buckets.length,
587 | localeCount: targetLocales.length,
588 | processedSuccessfully: true,
589 | });
590 | } else {
591 | ora.warn("Localization completed with errors.");
592 | await trackEvent(authId || "unknown", "cmd.i18n.error", {
593 | flags,
594 | ...aggregateErrorAnalytics(
595 | errorDetails,
596 | buckets,
597 | targetLocales,
598 | i18nConfig!,
599 | ),
600 | });
601 | }
602 | } catch (error: any) {
603 | ora.fail(error.message);
604 |
605 | // Use robust error type detection
606 | const errorType = getCLIErrorType(error);
607 |
608 | // Extract additional context from typed errors
609 | let errorContext: any = {};
610 | if (isLocalizationError(error)) {
611 | errorContext = {
612 | bucket: error.bucket,
613 | sourceLocale: error.sourceLocale,
614 | targetLocale: error.targetLocale,
615 | pathPattern: error.pathPattern,
616 | };
617 | } else if (isBucketProcessingError(error)) {
618 | errorContext = {
619 | bucket: error.bucket,
620 | };
621 | }
622 |
623 | await trackEvent(authId || "unknown", "cmd.i18n.error", {
624 | flags,
625 | errorType,
626 | errorName: error.name || "Error",
627 | errorMessage: error.message,
628 | errorStack: error.stack,
629 | errorContext,
630 | fatal: true,
631 | errorCount: errorDetails.length + 1,
632 | previousErrors: createPreviousErrorContext(errorDetails),
633 | });
634 | }
635 | });
636 |
637 | function parseFlags(options: any) {
638 | return Z.object({
639 | apiKey: Z.string().optional(),
640 | locale: Z.array(localeCodeSchema).optional(),
641 | bucket: Z.array(bucketTypeSchema).optional(),
642 | force: Z.boolean().optional(),
643 | frozen: Z.boolean().optional(),
644 | verbose: Z.boolean().optional(),
645 | strict: Z.boolean().optional(),
646 | key: Z.string().optional(),
647 | file: Z.array(Z.string()).optional(),
648 | interactive: Z.boolean().default(false),
649 | debug: Z.boolean().default(false),
650 | }).parse(options);
651 | }
652 |
653 | // Export validateAuth for use in other commands
654 | export async function validateAuth(settings: ReturnType<typeof getSettings>) {
655 | if (!settings.auth.apiKey) {
656 | throw new AuthenticationError({
657 | message:
658 | "Not authenticated. Please run `lingo.dev login` to authenticate.",
659 | docUrl: "authError",
660 | });
661 | }
662 |
663 | const authenticator = createAuthenticator({
664 | apiKey: settings.auth.apiKey,
665 | apiUrl: settings.auth.apiUrl,
666 | });
667 | const user = await authenticator.whoami();
668 | if (!user) {
669 | throw new AuthenticationError({
670 | message: "Invalid API key. Please run `lingo.dev login` to authenticate.",
671 | docUrl: "authError",
672 | });
673 | }
674 |
675 | return user;
676 | }
677 |
678 | function validateParams(
679 | i18nConfig: I18nConfig | null,
680 | flags: ReturnType<typeof parseFlags>,
681 | ) {
682 | if (!i18nConfig) {
683 | throw new ConfigError({
684 | message:
685 | "i18n.json not found. Please run `lingo.dev init` to initialize the project.",
686 | docUrl: "i18nNotFound",
687 | });
688 | } else if (!i18nConfig.buckets || !Object.keys(i18nConfig.buckets).length) {
689 | throw new ConfigError({
690 | message:
691 | "No buckets found in i18n.json. Please add at least one bucket containing i18n content.",
692 | docUrl: "bucketNotFound",
693 | });
694 | } else if (
695 | flags.locale?.some((locale) => !i18nConfig.locale.targets.includes(locale))
696 | ) {
697 | throw new ValidationError({
698 | message: `One or more specified locales do not exist in i18n.json locale.targets. Please add them to the list and try again.`,
699 | docUrl: "localeTargetNotFound",
700 | });
701 | } else if (
702 | flags.bucket?.some(
703 | (bucket) =>
704 | !i18nConfig.buckets[bucket as keyof typeof i18nConfig.buckets],
705 | )
706 | ) {
707 | throw new ValidationError({
708 | message: `One or more specified buckets do not exist in i18n.json. Please add them to the list and try again.`,
709 | docUrl: "bucketNotFound",
710 | });
711 | }
712 | }
713 |
714 | async function reviewChanges(args: {
715 | pathPattern: string;
716 | targetLocale: string;
717 | currentData: Record<string, any>;
718 | proposedData: Record<string, any>;
719 | sourceData: Record<string, any>;
720 | force: boolean;
721 | }): Promise<Record<string, any>> {
722 | const currentStr = JSON.stringify(args.currentData, null, 2);
723 | const proposedStr = JSON.stringify(args.proposedData, null, 2);
724 |
725 | // Early return if no changes
726 | if (currentStr === proposedStr && !args.force) {
727 | console.log(
728 | `\n${chalk.blue(args.pathPattern)} (${chalk.yellow(
729 | args.targetLocale,
730 | )}): ${chalk.gray("No changes to review")}`,
731 | );
732 | return args.proposedData;
733 | }
734 |
735 | const patch = createTwoFilesPatch(
736 | `${args.pathPattern} (current)`,
737 | `${args.pathPattern} (proposed)`,
738 | currentStr,
739 | proposedStr,
740 | undefined,
741 | undefined,
742 | { context: 3 },
743 | );
744 |
745 | // Color the diff output
746 | const coloredDiff = patch
747 | .split("\n")
748 | .map((line) => {
749 | if (line.startsWith("+")) return chalk.green(line);
750 | if (line.startsWith("-")) return chalk.red(line);
751 | if (line.startsWith("@")) return chalk.cyan(line);
752 | return line;
753 | })
754 | .join("\n");
755 |
756 | console.log(
757 | `\nReviewing changes for ${chalk.blue(args.pathPattern)} (${chalk.yellow(
758 | args.targetLocale,
759 | )}):`,
760 | );
761 | console.log(coloredDiff);
762 |
763 | const { action } = await inquirer.prompt([
764 | {
765 | type: "list",
766 | name: "action",
767 | message: "Choose action:",
768 | choices: [
769 | { name: "Approve changes", value: "approve" },
770 | { name: "Skip changes", value: "skip" },
771 | { name: "Edit individually", value: "edit" },
772 | ],
773 | default: "approve",
774 | },
775 | ]);
776 |
777 | if (action === "approve") {
778 | return args.proposedData;
779 | }
780 |
781 | if (action === "skip") {
782 | return args.currentData;
783 | }
784 |
785 | // If edit was chosen, prompt for each changed value
786 | const customData = { ...args.currentData };
787 | const changes = _.reduce(
788 | args.proposedData,
789 | (result: string[], value: string, key: string) => {
790 | if (args.currentData[key] !== value) {
791 | result.push(key);
792 | }
793 | return result;
794 | },
795 | [],
796 | );
797 |
798 | for (const key of changes) {
799 | console.log(`\nEditing value for: ${chalk.cyan(key)}`);
800 | console.log(chalk.gray("Source text:"), chalk.blue(args.sourceData[key]));
801 | console.log(
802 | chalk.gray("Current value:"),
803 | chalk.red(args.currentData[key] || "(empty)"),
804 | );
805 | console.log(
806 | chalk.gray("Suggested value:"),
807 | chalk.green(args.proposedData[key]),
808 | );
809 | console.log(
810 | chalk.gray(
811 | "\nYour editor will open. Edit the text and save to continue.",
812 | ),
813 | );
814 | console.log(chalk.gray("------------"));
815 |
816 | try {
817 | // Prepare the editor content with a header comment and the suggested value
818 | const editorContent = [
819 | "# Edit the translation below.",
820 | "# Lines starting with # will be ignored.",
821 | "# Save and exit the editor to continue.",
822 | "#",
823 | `# Source text (${chalk.blue("English")}):`,
824 | `# ${args.sourceData[key]}`,
825 | "#",
826 | `# Current value (${chalk.red(args.targetLocale)}):`,
827 | `# ${args.currentData[key] || "(empty)"}`,
828 | "#",
829 | args.proposedData[key],
830 | ].join("\n");
831 |
832 | const result = externalEditor.edit(editorContent);
833 |
834 | // Clean up the result by removing comments and trimming
835 | const customValue = result
836 | .split("\n")
837 | .filter((line) => !line.startsWith("#"))
838 | .join("\n")
839 | .trim();
840 |
841 | if (customValue) {
842 | customData[key] = customValue;
843 | } else {
844 | console.log(
845 | chalk.yellow("Empty value provided, keeping the current value."),
846 | );
847 | customData[key] = args.currentData[key] || args.proposedData[key];
848 | }
849 | } catch (error) {
850 | console.log(
851 | chalk.red("Error while editing, keeping the suggested value."),
852 | );
853 | customData[key] = args.proposedData[key];
854 | }
855 | }
856 |
857 | return customData;
858 | }
859 |
```
--------------------------------------------------------------------------------
/packages/cli/src/cli/loaders/android.spec.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect, vi, beforeEach } from "vitest";
2 | import fs from "fs/promises";
3 | import createAndroidLoader from "./android";
4 |
5 | describe("android loader", () => {
6 | const setupMocks = (input: string) => {
7 | vi.mock("fs/promises");
8 | vi.mocked(fs.readFile).mockResolvedValue(input);
9 | vi.mocked(fs.writeFile).mockResolvedValue(undefined);
10 | };
11 |
12 | beforeEach(() => {
13 | vi.clearAllMocks();
14 | });
15 |
16 | it("should correctly handle basic string resources", async () => {
17 | const input = `
18 | <resources>
19 | <string name="hello">Hello World</string>
20 | <string name="app_name">My App</string>
21 | </resources>
22 | `.trim();
23 |
24 | const androidLoader = createAndroidLoader().setDefaultLocale("en");
25 | const result = await androidLoader.pull("en", input);
26 |
27 | expect(result).toEqual({
28 | hello: "Hello World",
29 | app_name: "My App",
30 | });
31 | });
32 |
33 | it("should correctly handle string arrays", async () => {
34 | const input = `
35 | <resources>
36 | <string-array name="planets">
37 | <item>Mercury</item>
38 | <item>Venus</item>
39 | <item>Earth</item>
40 | <item>Mars</item>
41 | </string-array>
42 | </resources>
43 | `.trim();
44 |
45 | const androidLoader = createAndroidLoader().setDefaultLocale("en");
46 | const result = await androidLoader.pull("en", input);
47 |
48 | expect(result).toEqual({
49 | planets: ["Mercury", "Venus", "Earth", "Mars"],
50 | });
51 | });
52 |
53 | it("should correctly handle plurals with different quantity types", async () => {
54 | const input = `
55 | <resources>
56 | <plurals name="numberOfSongsAvailable">
57 | <item quantity="zero">No songs found.</item>
58 | <item quantity="one">1 song found.</item>
59 | <item quantity="few">%d songs found.</item>
60 | <item quantity="many">%d songs found.</item>
61 | <item quantity="other">%d songs found.</item>
62 | </plurals>
63 | </resources>
64 | `.trim();
65 |
66 | const androidLoader = createAndroidLoader().setDefaultLocale("en");
67 | const result = await androidLoader.pull("en", input);
68 |
69 | expect(result).toEqual({
70 | numberOfSongsAvailable: {
71 | zero: "No songs found.",
72 | one: "1 song found.",
73 | few: "%d songs found.",
74 | many: "%d songs found.",
75 | other: "%d songs found.",
76 | },
77 | });
78 | });
79 |
80 | it("should correctly handle HTML markup in strings", async () => {
81 | const input = `
82 | <resources>
83 | <string name="welcome">Welcome to <b>Android</b>!</string>
84 | <string name="formatted">This is <i>italic</i> and this is <b>bold</b></string>
85 | </resources>
86 | `.trim();
87 |
88 | const androidLoader = createAndroidLoader().setDefaultLocale("en");
89 | const result = await androidLoader.pull("en", input);
90 |
91 | expect(result).toEqual({
92 | welcome: "Welcome to <b>Android</b>!",
93 | formatted: "This is <i>italic</i> and this is <b>bold</b>",
94 | });
95 | });
96 |
97 | it("should correctly handle format strings", async () => {
98 | const input = `
99 | <resources>
100 | <string name="welcome_messages">Hello, %1$s! You have %2$d new messages.</string>
101 | <string name="complex_format">Value: %1$.2f, Text: %2$s, Number: %3$d</string>
102 | </resources>
103 | `.trim();
104 |
105 | const androidLoader = createAndroidLoader().setDefaultLocale("en");
106 | const result = await androidLoader.pull("en", input);
107 |
108 | expect(result).toEqual({
109 | welcome_messages: "Hello, %1$s! You have %2$d new messages.",
110 | complex_format: "Value: %1$.2f, Text: %2$s, Number: %3$d",
111 | });
112 | });
113 |
114 | it("should correctly handle single quote escaping", async () => {
115 | const input = `
116 | <resources>
117 | <string name="apostrophe">Don\\'t forget me</string>
118 | <string name="escaped_quotes">This has \\'single\\' quotes</string>
119 | </resources>
120 | `.trim();
121 |
122 | const androidLoader = createAndroidLoader().setDefaultLocale("en");
123 | const result = await androidLoader.pull("en", input);
124 |
125 | // Now expect normalized apostrophes in the JS object
126 | expect(result).toEqual({
127 | apostrophe: "Don't forget me",
128 | escaped_quotes: "This has 'single' quotes",
129 | });
130 |
131 | // When pushing back, apostrophes should be escaped again
132 | const pushed = await androidLoader.push("en", result);
133 | expect(pushed).toContain("Don\\'t forget me");
134 | expect(pushed).toContain("This has \\'single\\' quotes");
135 | });
136 |
137 | it("should correctly handle CDATA sections", async () => {
138 | const input = `
139 | <resources>
140 | <string name="html_content"><![CDATA[<html><body><h1>Title</h1><p>Paragraph</p></body></html>]]></string>
141 | </resources>
142 | `.trim();
143 |
144 | const androidLoader = createAndroidLoader().setDefaultLocale("en");
145 | const result = await androidLoader.pull("en", input);
146 |
147 | expect(result).toEqual({
148 | html_content: "<html><body><h1>Title</h1><p>Paragraph</p></body></html>",
149 | });
150 | });
151 |
152 | it("should correctly handle multiple CDATA sections in a single string", async () => {
153 | const input = `
154 | <resources>
155 | <string name="multiple_cdata"><![CDATA[<first>section</first>]]><![CDATA[<second>section</second>]]></string>
156 | </resources>
157 | `.trim();
158 |
159 | const androidLoader = createAndroidLoader().setDefaultLocale("en");
160 | const result = await androidLoader.pull("en", input);
161 |
162 | expect(result).toEqual({
163 | multiple_cdata: "<first>section</first><second>section</second>",
164 | });
165 | });
166 |
167 | it("should correctly handle nested HTML tags with attributes", async () => {
168 | const input = `
169 | <resources>
170 | <string name="complex_html">This is <span style="color:red">red text</span> and <a href="https://example.com">a link</a></string>
171 | </resources>
172 | `.trim();
173 |
174 | const androidLoader = createAndroidLoader().setDefaultLocale("en");
175 | const result = await androidLoader.pull("en", input);
176 |
177 | expect(result).toEqual({
178 | complex_html:
179 | 'This is <span style="color:red">red text</span> and <a href="https://example.com">a link</a>',
180 | });
181 | });
182 |
183 | it("should correctly handle XML entities in strings", async () => {
184 | const input = `
185 | <resources>
186 | <string name="entities">This string contains <brackets> and &ampersands</string>
187 | </resources>
188 | `.trim();
189 |
190 | const androidLoader = createAndroidLoader().setDefaultLocale("en");
191 | const result = await androidLoader.pull("en", input);
192 |
193 | expect(result).toEqual({
194 | entities: "This string contains <brackets> and &ersands",
195 | });
196 | });
197 |
198 | it("should correctly handle empty strings", async () => {
199 | const input = `
200 | <resources>
201 | <string name="empty"></string>
202 | <string name="whitespace"> </string>
203 | </resources>
204 | `.trim();
205 |
206 | const androidLoader = createAndroidLoader().setDefaultLocale("en");
207 | const result = await androidLoader.pull("en", input);
208 |
209 | expect(result).toEqual({
210 | empty: "",
211 | whitespace: " ",
212 | });
213 | });
214 |
215 | it("should correctly handle very long strings", async () => {
216 | const longText = "This is a very long string.".repeat(100);
217 | const input = `
218 | <resources>
219 | <string name="long_text">${longText}</string>
220 | </resources>
221 | `.trim();
222 |
223 | const androidLoader = createAndroidLoader().setDefaultLocale("en");
224 | const result = await androidLoader.pull("en", input);
225 |
226 | expect(result).toEqual({
227 | long_text: longText,
228 | });
229 | });
230 |
231 | it("should correctly handle strings with newlines and whitespace", async () => {
232 | const input = `
233 | <resources>
234 | <string name="multiline">Line 1
235 | Line 2
236 | Line 3 with indent</string>
237 | </resources>
238 | `.trim();
239 |
240 | const androidLoader = createAndroidLoader().setDefaultLocale("en");
241 | const result = await androidLoader.pull("en", input);
242 |
243 | expect(result).toEqual({
244 | multiline: "Line 1\nLine 2\n Line 3 with indent",
245 | });
246 | });
247 |
248 | it("should correctly handle Unicode characters", async () => {
249 | const input = `
250 | <resources>
251 | <string name="unicode">Unicode: 你好, こんにちは, Привет, مرحبا, 안녕하세요</string>
252 | </resources>
253 | `.trim();
254 |
255 | const androidLoader = createAndroidLoader().setDefaultLocale("en");
256 | const result = await androidLoader.pull("en", input);
257 |
258 | expect(result).toEqual({
259 | unicode: "Unicode: 你好, こんにちは, Привет, مرحبا, 안녕하세요",
260 | });
261 | });
262 |
263 | it("should skip non-translatable strings", async () => {
264 | const input = `
265 | <resources>
266 | <string name="app_name" translatable="false">My App</string>
267 | <string name="welcome">Welcome</string>
268 | <string name="version" translatable="false">1.0.0</string>
269 | </resources>
270 | `.trim();
271 |
272 | const androidLoader = createAndroidLoader().setDefaultLocale("en");
273 | const result = await androidLoader.pull("en", input);
274 |
275 | expect(result).toEqual({
276 | welcome: "Welcome",
277 | });
278 | expect(result.app_name).toBeUndefined();
279 | expect(result.version).toBeUndefined();
280 | });
281 |
282 | it("should correctly push string resources back to XML", async () => {
283 | const payload = {
284 | hello: "Hola",
285 | welcome: "Bienvenido",
286 | };
287 |
288 | const androidLoader = createAndroidLoader().setDefaultLocale("en");
289 | await androidLoader.pull(
290 | "en",
291 | `
292 | <resources>
293 | <string name="hello">Hello</string>
294 | <string name="welcome">Welcome</string>
295 | </resources>
296 | `,
297 | );
298 |
299 | const result = await androidLoader.push("es", payload);
300 |
301 | expect(result).toContain('<string name="hello">Hola</string>');
302 | expect(result).toContain('<string name="welcome">Bienvenido</string>');
303 | });
304 |
305 | it("should correctly push string arrays back to XML", async () => {
306 | const payload = {
307 | planets: ["Mercurio", "Venus", "Tierra", "Marte"],
308 | };
309 |
310 | const androidLoader = createAndroidLoader().setDefaultLocale("en");
311 | await androidLoader.pull(
312 | "en",
313 | `
314 | <resources>
315 | <string-array name="planets">
316 | <item>Mercury</item>
317 | <item>Venus</item>
318 | <item>Earth</item>
319 | <item>Mars</item>
320 | </string-array>
321 | </resources>
322 | `,
323 | );
324 |
325 | const result = await androidLoader.push("es", payload);
326 |
327 | expect(result).toContain('<string-array name="planets">');
328 | expect(result).toContain("<item>Mercurio</item>");
329 | expect(result).toContain("<item>Venus</item>");
330 | expect(result).toContain("<item>Tierra</item>");
331 | expect(result).toContain("<item>Marte</item>");
332 | });
333 |
334 | it("should correctly push plurals back to XML", async () => {
335 | const payload = {
336 | numberOfSongsAvailable: {
337 | zero: "No se encontraron canciones.",
338 | one: "1 canción encontrada.",
339 | few: "%d canciones encontradas.",
340 | many: "%d canciones encontradas.",
341 | other: "%d canciones encontradas.",
342 | },
343 | };
344 |
345 | const androidLoader = createAndroidLoader().setDefaultLocale("en");
346 | await androidLoader.pull(
347 | "en",
348 | `
349 | <resources>
350 | <plurals name="numberOfSongsAvailable">
351 | <item quantity="zero">No songs found.</item>
352 | <item quantity="one">1 song found.</item>
353 | <item quantity="few">%d songs found.</item>
354 | <item quantity="many">%d songs found.</item>
355 | <item quantity="other">%d songs found.</item>
356 | </plurals>
357 | </resources>
358 | `,
359 | );
360 |
361 | const result = await androidLoader.push("es", payload);
362 |
363 | expect(result).toContain('<plurals name="numberOfSongsAvailable">');
364 | expect(result).toContain(
365 | '<item quantity="zero">No se encontraron canciones.</item>',
366 | );
367 | expect(result).toContain(
368 | '<item quantity="one">1 canción encontrada.</item>',
369 | );
370 | expect(result).toContain(
371 | '<item quantity="few">%d canciones encontradas.</item>',
372 | );
373 | expect(result).toContain(
374 | '<item quantity="many">%d canciones encontradas.</item>',
375 | );
376 | expect(result).toContain(
377 | '<item quantity="other">%d canciones encontradas.</item>',
378 | );
379 | });
380 |
381 | it("should correctly handle mixed resource types", async () => {
382 | const payload = {
383 | app_name: "Mi Aplicación",
384 | planets: ["Mercurio", "Venus", "Tierra", "Marte"],
385 | numberOfSongsAvailable: {
386 | zero: "No se encontraron canciones.",
387 | one: "1 canción encontrada.",
388 | other: "%d canciones encontradas.",
389 | },
390 | };
391 |
392 | const androidLoader = createAndroidLoader().setDefaultLocale("en");
393 | await androidLoader.pull(
394 | "en",
395 | `
396 | <resources>
397 | <string name="app_name">My App</string>
398 | <string-array name="planets">
399 | <item>Mercury</item>
400 | <item>Venus</item>
401 | <item>Earth</item>
402 | <item>Mars</item>
403 | </string-array>
404 | <plurals name="numberOfSongsAvailable">
405 | <item quantity="zero">No songs found.</item>
406 | <item quantity="one">1 song found.</item>
407 | <item quantity="other">%d songs found.</item>
408 | </plurals>
409 | </resources>
410 | `,
411 | );
412 |
413 | const result = await androidLoader.push("es", payload);
414 |
415 | expect(result).toContain('<string name="app_name">Mi Aplicación</string>');
416 | expect(result).toContain('<string-array name="planets">');
417 | expect(result).toContain('<plurals name="numberOfSongsAvailable">');
418 | });
419 |
420 | it("should correctly handle Unicode escape sequences", async () => {
421 | const input = `
422 | <resources>
423 | <string name="unicode_escape">Unicode escape: \\u0041\\u0042\\u0043 and \\u65e5\\u672c\\u8a9e</string>
424 | </resources>
425 | `.trim();
426 |
427 | const androidLoader = createAndroidLoader().setDefaultLocale("en");
428 | const result = await androidLoader.pull("en", input);
429 |
430 | expect(result).toEqual({
431 | unicode_escape:
432 | "Unicode escape: \\u0041\\u0042\\u0043 and \\u65e5\\u672c\\u8a9e",
433 | });
434 |
435 | const pushed = await androidLoader.push("en", result);
436 | expect(pushed).toContain(
437 | "Unicode escape: \\u0041\\u0042\\u0043 and \\u65e5\\u672c\\u8a9e",
438 | );
439 | });
440 |
441 | it("should correctly handle double quote escaping", async () => {
442 | const input = `
443 | <resources>
444 | <string name="double_quotes">He said, \\"Hello World\\"</string>
445 | </resources>
446 | `.trim();
447 |
448 | const androidLoader = createAndroidLoader().setDefaultLocale("en");
449 | const result = await androidLoader.pull("en", input);
450 |
451 | expect(result).toEqual({
452 | double_quotes: 'He said, \\"Hello World\\"',
453 | });
454 |
455 | const pushed = await androidLoader.push("en", result);
456 | expect(pushed).toContain('He said, \\"Hello World\\"');
457 | });
458 |
459 | it("should correctly handle resource references", async () => {
460 | const input = `
461 | <resources>
462 | <string name="welcome_message">Welcome to @string/app_name</string>
463 | <string name="app_name">My App</string>
464 | </resources>
465 | `.trim();
466 |
467 | const androidLoader = createAndroidLoader().setDefaultLocale("en");
468 | const result = await androidLoader.pull("en", input);
469 |
470 | expect(result).toEqual({
471 | welcome_message: "Welcome to @string/app_name",
472 | app_name: "My App",
473 | });
474 |
475 | const pushed = await androidLoader.push("en", result);
476 | expect(pushed).toContain(
477 | '<string name="welcome_message">Welcome to @string/app_name</string>',
478 | );
479 | });
480 |
481 | it("should correctly handle tools namespace attributes", async () => {
482 | const input = `
483 | <resources>
484 | <string name="debug_only" tools:ignore="MissingTranslation">Debug message</string>
485 | <string name="normal">Normal message</string>
486 | </resources>
487 | `.trim();
488 |
489 | const androidLoader = createAndroidLoader().setDefaultLocale("en");
490 | const result = await androidLoader.pull("en", input);
491 |
492 | expect(result).toEqual({
493 | debug_only: "Debug message",
494 | normal: "Normal message",
495 | });
496 | });
497 |
498 | it("should correctly handle whitespace preservation with double quotes", async () => {
499 | const input = `
500 | <resources>
501 | <string name="preserved_whitespace">" This string preserves whitespace "</string>
502 | </resources>
503 | `.trim();
504 |
505 | const androidLoader = createAndroidLoader().setDefaultLocale("en");
506 | const result = await androidLoader.pull("en", input);
507 |
508 | expect(result).toEqual({
509 | preserved_whitespace: '" This string preserves whitespace "',
510 | });
511 |
512 | const pushed = await androidLoader.push("en", result);
513 | expect(pushed).toContain(
514 | '<string name="preserved_whitespace">" This string preserves whitespace "</string>',
515 | );
516 | });
517 |
518 | it("should correctly handle special characters that need escaping", async () => {
519 | const input = `
520 | <resources>
521 | <string name="special_chars">Special chars: \\@, \\?, \\#, \\$, \\%</string>
522 | </resources>
523 | `.trim();
524 |
525 | const androidLoader = createAndroidLoader().setDefaultLocale("en");
526 | const result = await androidLoader.pull("en", input);
527 |
528 | expect(result).toEqual({
529 | special_chars: "Special chars: \\@, \\?, \\#, \\$, \\%",
530 | });
531 |
532 | const pushed = await androidLoader.push("en", result);
533 | expect(pushed).toContain("Special chars: \\@, \\?, \\#, \\$, \\%");
534 | });
535 |
536 | it("should correctly handle apostrophes in text", async () => {
537 | const input = `
538 | <resources>
539 | <string name="sign_in_agreement_text_1">J\'accepte les</string>
540 | <string name="sign_in_agreement_text_2"> et je reconnais la </string>
541 | </resources>
542 | `.trim();
543 |
544 | const androidLoader = createAndroidLoader().setDefaultLocale("en");
545 | const result = await androidLoader.pull("en", input);
546 |
547 | // During pull, escaped apostrophes should be normalized to simple apostrophes
548 | expect(result).toEqual({
549 | sign_in_agreement_text_1: "J'accepte les",
550 | sign_in_agreement_text_2: " et je reconnais la ",
551 | });
552 |
553 | // When pushing back, apostrophes should be escaped with backslash
554 | const pushed = await androidLoader.push("en", result);
555 | expect(pushed).toContain("J\\'accepte les");
556 | expect(pushed).toContain(" et je reconnais la ");
557 | });
558 |
559 | it("should escape apostrophes even in strings wrapped with double quotes", async () => {
560 | const input = `
561 | <resources>
562 | <string name="quoted_apostrophe">"J'accepte les terms"</string>
563 | </resources>
564 | `.trim();
565 |
566 | const androidLoader = createAndroidLoader().setDefaultLocale("en");
567 | const result = await androidLoader.pull("en", input);
568 |
569 | // During pull, the double quotes around the content should be preserved
570 | expect(result).toEqual({
571 | quoted_apostrophe: '"J\'accepte les terms"',
572 | });
573 |
574 | // When pushing back, apostrophes should be escaped even in double-quoted strings
575 | const pushed = await androidLoader.push("en", result);
576 | expect(pushed).toContain('"J\\\'accepte les terms"');
577 | });
578 |
579 | it("should correctly handle strings with apostrophes and avoid double escaping", async () => {
580 | const input = `
581 | <resources>
582 | <string name="welcome_message">Please don't hesitate to contact us</string>
583 | <item quantity="one">- %d user\'s item</item>
584 | <item quantity="other">- %d user\'s items</item>
585 | </resources>
586 | `.trim();
587 |
588 | const androidLoader = createAndroidLoader().setDefaultLocale("en");
589 | const result = await androidLoader.pull("en", input);
590 |
591 | // During pull, escaped apostrophes should be properly handled
592 | expect(result.welcome_message).toBe("Please don't hesitate to contact us");
593 |
594 | // When pushing back, apostrophes should be escaped but not double-escaped
595 | const pushed = await androidLoader.push("en", {
596 | welcome_message: "Please don't hesitate to contact us",
597 | item_count: {
598 | one: "- %d user's item",
599 | other: "- %d user's items",
600 | },
601 | });
602 |
603 | expect(pushed).toContain("Please don\\'t hesitate to contact us");
604 | expect(pushed).toContain("- %d user\\'s item");
605 | expect(pushed).not.toContain("- %d user\\\\'s item");
606 | });
607 |
608 | // Tests for Issue Fixes
609 |
610 | it("should preserve whitespace in array items during pull and push", async () => {
611 | const input = `
612 | <resources>
613 | <string-array name="mixed_items">
614 | <item> Item with spaces </item>
615 | <item> </item>
616 | </string-array>
617 | </resources>
618 | `.trim();
619 |
620 | const androidLoader = createAndroidLoader().setDefaultLocale("en");
621 | const pulled = await androidLoader.pull("en", input);
622 |
623 | expect(pulled.mixed_items).toEqual([" Item with spaces ", " "]);
624 |
625 | const pushed = await androidLoader.push("en", {
626 | mixed_items: [" Elemento con espacios ", " "],
627 | });
628 |
629 | expect(pushed).toContain("<item> Elemento con espacios </item>");
630 | expect(pushed).toContain("<item> </item>");
631 | });
632 |
633 | it("should retain CDATA wrappers for translated strings", async () => {
634 | const input = `
635 | <resources>
636 | <string name="cdata_example"><![CDATA[Special <tag> ]]></string>
637 | </resources>
638 | `.trim();
639 |
640 | const androidLoader = createAndroidLoader().setDefaultLocale("en");
641 | await androidLoader.pull("en", input);
642 |
643 | const pushed = await androidLoader.push("es", {
644 | cdata_example: "Especial <tag> ",
645 | });
646 |
647 | expect(pushed).toContain(
648 | '<string name="cdata_example"><![CDATA[Especial <tag> ]]></string>',
649 | );
650 | });
651 |
652 | it("should escape apostrophes in CDATA sections", async () => {
653 | const input = `
654 | <resources>
655 | <string name="review_info"><![CDATA[Hosts can't see your review until they've written one. <u>Learn more</u>]]></string>
656 | </resources>
657 | `.trim();
658 |
659 | const androidLoader = createAndroidLoader().setDefaultLocale("en");
660 | const pulled = await androidLoader.pull("en", input);
661 |
662 | expect(pulled.review_info).toBe(
663 | "Hosts can't see your review until they've written one. <u>Learn more</u>",
664 | );
665 |
666 | const pushed = await androidLoader.push("fr", {
667 | review_info:
668 | "Les hôtes ne peuvent voir votre avis qu'après en avoir écrit un. <u>En savoir plus</u>",
669 | });
670 |
671 | // Apostrophes must be escaped even inside CDATA (Android AAPT requirement)
672 | expect(pushed).toContain("qu\\'après");
673 | expect(pushed).toContain("<![CDATA[");
674 | expect(pushed).toContain("]]>");
675 | // HTML tags should NOT be escaped inside CDATA
676 | expect(pushed).toContain("<u>En savoir plus</u>");
677 | expect(pushed).not.toContain("<u>");
678 | });
679 |
680 | it("should preserve resource ordering after push", async () => {
681 | const input = `
682 | <resources>
683 | <string name="first">First</string>
684 | <string-array name="colors">
685 | <item>Red</item>
686 | <item>Green</item>
687 | </string-array>
688 | <plurals name="messages">
689 | <item quantity="one">%d message</item>
690 | <item quantity="other">%d messages</item>
691 | </plurals>
692 | <bool name="show_tutorial">true</bool>
693 | <integer name="retry_count">3</integer>
694 | </resources>
695 | `.trim();
696 |
697 | const androidLoader = createAndroidLoader().setDefaultLocale("en");
698 | const roundTrip = await androidLoader.pull("en", input);
699 | const pushed = await androidLoader.push("en", roundTrip);
700 |
701 | const order = Array.from(
702 | pushed.matchAll(
703 | /<(string|string-array|plurals|bool|integer)\s+name="([^"]+)"/g,
704 | ),
705 | ).map(([, , name]) => name);
706 |
707 | expect(order).toEqual([
708 | "first",
709 | "colors",
710 | "messages",
711 | "show_tutorial",
712 | "retry_count",
713 | ]);
714 | });
715 |
716 | it("should preserve XML declaration from source file", async () => {
717 | const input = `<?xml version="1.0" encoding="utf-8"?>
718 | <resources>
719 | <string name="test">Test</string>
720 | </resources>`;
721 |
722 | const androidLoader = createAndroidLoader().setDefaultLocale("en");
723 | await androidLoader.pull("en", input);
724 |
725 | const result = await androidLoader.push("es", { test: "Prueba" });
726 |
727 | expect(result).toMatch(/^<\?xml version="1\.0" encoding="utf-8"\?>/);
728 | });
729 |
730 | it('should exclude translatable="false" items from target locale', async () => {
731 | const input = `
732 | <resources>
733 | <string name="app_name">My App</string>
734 | <string name="api_url" translatable="false">https://api.example.com</string>
735 | <string name="debug_key" translatable="false">DEBUG_KEY</string>
736 | <string-array name="colors">
737 | <item>Red</item>
738 | </string-array>
739 | <string-array name="urls" translatable="false">
740 | <item>https://example.com</item>
741 | </string-array>
742 | <plurals name="items">
743 | <item quantity="one">%d item</item>
744 | <item quantity="other">%d items</item>
745 | </plurals>
746 | <plurals name="bytes" translatable="false">
747 | <item quantity="one">%d byte</item>
748 | <item quantity="other">%d bytes</item>
749 | </plurals>
750 | <bool name="show_tutorial">true</bool>
751 | <bool name="is_debug" translatable="false">false</bool>
752 | <integer name="timeout">30</integer>
753 | <integer name="version" translatable="false">42</integer>
754 | </resources>
755 | `.trim();
756 |
757 | const androidLoader = createAndroidLoader().setDefaultLocale("en");
758 | await androidLoader.pull("en", input);
759 |
760 | const result = await androidLoader.push("es", {
761 | app_name: "Mi Aplicación",
762 | colors: ["Rojo"],
763 | items: { one: "%d elemento", other: "%d elementos" },
764 | show_tutorial: true,
765 | timeout: 30,
766 | });
767 |
768 | // Check that translatable="false" items are NOT included
769 | expect(result).not.toContain('name="api_url"');
770 | expect(result).not.toContain("https://api.example.com");
771 | expect(result).not.toContain('name="debug_key"');
772 | expect(result).not.toContain("DEBUG_KEY");
773 | expect(result).not.toContain('name="urls"');
774 | expect(result).not.toContain('name="bytes"');
775 | expect(result).not.toContain('name="is_debug"');
776 | expect(result).not.toContain('name="version"');
777 |
778 | // Check that translatable items are translated
779 | expect(result).toContain("Mi Aplicación");
780 | expect(result).toContain("Rojo");
781 | expect(result).toContain("elemento");
782 | expect(result).toContain('name="app_name"');
783 | expect(result).toContain('name="colors"');
784 | expect(result).toContain('name="items"');
785 | expect(result).toContain('name="show_tutorial"');
786 | expect(result).toContain('name="timeout"');
787 | });
788 |
789 | it("should use 4-space indentation by default", async () => {
790 | const input = `<?xml version="1.0" encoding="utf-8"?>
791 | <resources>
792 | <string name="test">Test</string>
793 | <string name="another">Another</string>
794 | </resources>`;
795 |
796 | const androidLoader = createAndroidLoader().setDefaultLocale("en");
797 | await androidLoader.pull("en", input);
798 |
799 | const result = await androidLoader.push("es", {
800 | test: "Prueba",
801 | another: "Otro",
802 | });
803 |
804 | // Check for 4-space indentation (default)
805 | // Note: Users should use formatters (Prettier/Biome) for custom indentation
806 | expect(result).toContain('\n <string name="test">');
807 | expect(result).toContain('\n <string name="another">');
808 | });
809 |
810 | it("should preserve XML declaration encoding from source file", async () => {
811 | const inputUtf8 = `<?xml version="1.0" encoding="utf-8"?>
812 | <resources>
813 | <string name="test">Test</string>
814 | </resources>`;
815 |
816 | const inputUpperUTF8 = `<?xml version="1.0" encoding="UTF-8"?>
817 | <resources>
818 | <string name="test">Test</string>
819 | </resources>`;
820 |
821 | const inputISO = `<?xml version="1.0" encoding="ISO-8859-1"?>
822 | <resources>
823 | <string name="test">Test</string>
824 | </resources>`;
825 |
826 | const androidLoader = createAndroidLoader().setDefaultLocale("en");
827 |
828 | // Test lowercase utf-8
829 | await androidLoader.pull("en", inputUtf8);
830 | let result = await androidLoader.push("es", { test: "Prueba" });
831 | expect(result).toMatch(/^<\?xml version="1\.0" encoding="utf-8"\?>/);
832 |
833 | // Test uppercase UTF-8
834 | await androidLoader.pull("en", inputUpperUTF8);
835 | result = await androidLoader.push("es", { test: "Prueba" });
836 | expect(result).toMatch(/^<\?xml version="1\.0" encoding="UTF-8"\?>/);
837 |
838 | // Test ISO-8859-1
839 | await androidLoader.pull("en", inputISO);
840 | result = await androidLoader.push("es", { test: "Prueba" });
841 | expect(result).toMatch(/^<\?xml version="1\.0" encoding="ISO-8859-1"\?>/);
842 | });
843 |
844 | it("should preserve XML version from source file", async () => {
845 | const inputV10 = `<?xml version="1.0" encoding="utf-8"?>
846 | <resources>
847 | <string name="test">Test</string>
848 | </resources>`;
849 |
850 | const inputV11 = `<?xml version="1.1" encoding="utf-8"?>
851 | <resources>
852 | <string name="test">Test</string>
853 | </resources>`;
854 |
855 | const androidLoader = createAndroidLoader().setDefaultLocale("en");
856 |
857 | // Test version 1.0
858 | await androidLoader.pull("en", inputV10);
859 | let result = await androidLoader.push("es", { test: "Prueba" });
860 | expect(result).toMatch(/^<\?xml version="1\.0"/);
861 |
862 | // Test version 1.1
863 | await androidLoader.pull("en", inputV11);
864 | result = await androidLoader.push("es", { test: "Prueba" });
865 | expect(result).toMatch(/^<\?xml version="1\.1"/);
866 | });
867 |
868 | it("should omit XML declaration when source has none", async () => {
869 | const inputNoDeclaration = `<resources>
870 | <string name="test">Test</string>
871 | </resources>`;
872 |
873 | const androidLoader = createAndroidLoader().setDefaultLocale("en");
874 | await androidLoader.pull("en", inputNoDeclaration);
875 |
876 | const result = await androidLoader.push("es", { test: "Prueba" });
877 |
878 | // Should start immediately with the root element (no declaration)
879 | expect(result).not.toMatch(/^<\?xml/);
880 | expect(result.trim().startsWith("<resources>")).toBe(true);
881 | });
882 | });
883 |
```
--------------------------------------------------------------------------------
/legacy/cli/CHANGELOG.md:
--------------------------------------------------------------------------------
```markdown
1 | # replexica
2 |
3 | ## 0.71.0
4 |
5 | ### Minor Changes
6 |
7 | - [#428](https://github.com/lingodotdev/lingo.dev/pull/428) [`5dd7b65`](https://github.com/lingodotdev/lingo.dev/commit/5dd7b6529ce174d8759e80644c3233927b1ecce4) Thanks [@mathio](https://github.com/mathio)! - map old env vars
8 |
9 | ### Patch Changes
10 |
11 | - Updated dependencies [[`cd836e4`](https://github.com/lingodotdev/lingo.dev/commit/cd836e45cf810f495e2c6e1449824dc84794d571), [`5dd7b65`](https://github.com/lingodotdev/lingo.dev/commit/5dd7b6529ce174d8759e80644c3233927b1ecce4)]:
12 | - [email protected]
13 |
14 | ## 0.70.1
15 |
16 | ### Patch Changes
17 |
18 | - [`5dee9ee`](https://github.com/lingodotdev/lingo.dev/commit/5dee9ee743fbef489fbe342597a768ebd59e5f67) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add proxies to legacy packages
19 |
20 | - [`63eb57b`](https://github.com/lingodotdev/lingo.dev/commit/63eb57b8f4cc37605be196085fafbbfdab71cce5) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add deprecation message to legacy package jsons
21 |
22 | - [`bbf7760`](https://github.com/lingodotdev/lingo.dev/commit/bbf7760580f1631805d68612053ebcd4601bb02b) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add deprecation warning to the legacy package proxies
23 |
24 | - Updated dependencies [[`b4c7f1e`](https://github.com/lingodotdev/lingo.dev/commit/b4c7f1e86334d229bee62219c26f30d0b523926d)]:
25 | - [email protected]
26 |
27 | ## 0.70.0
28 |
29 | ### Minor Changes
30 |
31 | - [`003344f`](https://github.com/lingodotdev/lingo.dev/commit/003344ffcca98a391a298707f18476971c4d4c2b) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add locale delimiter override
32 |
33 | ### Patch Changes
34 |
35 | - Updated dependencies [[`003344f`](https://github.com/lingodotdev/lingo.dev/commit/003344ffcca98a391a298707f18476971c4d4c2b)]:
36 | - @replexica/[email protected]
37 | - @replexica/[email protected]
38 |
39 | ## 0.69.0
40 |
41 | ### Minor Changes
42 |
43 | - [#411](https://github.com/lingodotdev/lingo.dev/pull/411) [`1b0647d`](https://github.com/lingodotdev/lingo.dev/commit/1b0647d91080f4947ba6227c397fb6232d0d1907) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add structure sync loader to cli
44 |
45 | ### Patch Changes
46 |
47 | - Updated dependencies [[`1b0647d`](https://github.com/lingodotdev/lingo.dev/commit/1b0647d91080f4947ba6227c397fb6232d0d1907)]:
48 | - @replexica/[email protected]
49 |
50 | ## 0.68.0
51 |
52 | ### Minor Changes
53 |
54 | - [#408](https://github.com/lingodotdev/lingo.dev/pull/408) [`36fd4af`](https://github.com/lingodotdev/lingo.dev/commit/36fd4af376caf1540dc0a594fd65742c81706aa0) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - disable .po folding
55 |
56 | ### Patch Changes
57 |
58 | - Updated dependencies [[`36fd4af`](https://github.com/lingodotdev/lingo.dev/commit/36fd4af376caf1540dc0a594fd65742c81706aa0)]:
59 | - @replexica/[email protected]
60 |
61 | ## 0.67.0
62 |
63 | ### Minor Changes
64 |
65 | - [#405](https://github.com/lingodotdev/lingo.dev/pull/405) [`446cf9c`](https://github.com/lingodotdev/lingo.dev/commit/446cf9c5c933f71a43fd5d80487b1608023cba8e) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - improved .po loader
66 |
67 | - [#404](https://github.com/lingodotdev/lingo.dev/pull/404) [`3edef26`](https://github.com/lingodotdev/lingo.dev/commit/3edef26ef3a4e2d27394c5eeb2bc94b164e037ac) Thanks [@mathio](https://github.com/mathio)! - interactive init comman
68 |
69 | ### Patch Changes
70 |
71 | - Updated dependencies [[`446cf9c`](https://github.com/lingodotdev/lingo.dev/commit/446cf9c5c933f71a43fd5d80487b1608023cba8e), [`3edef26`](https://github.com/lingodotdev/lingo.dev/commit/3edef26ef3a4e2d27394c5eeb2bc94b164e037ac)]:
72 | - @replexica/[email protected]
73 |
74 | ## 0.66.2
75 |
76 | ### Patch Changes
77 |
78 | - [#399](https://github.com/lingodotdev/lingo.dev/pull/399) [`01ca7bb`](https://github.com/lingodotdev/lingo.dev/commit/01ca7bb9d28d0de903caf44ec6ede2e2bbbb3ba2) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - feat(cli): enhance .po loader to support plural translations and improve loader composition
79 |
80 | - Updated dependencies [[`01ca7bb`](https://github.com/lingodotdev/lingo.dev/commit/01ca7bb9d28d0de903caf44ec6ede2e2bbbb3ba2)]:
81 | - @replexica/[email protected]
82 |
83 | ## 0.66.1
84 |
85 | ### Patch Changes
86 |
87 | - [`aef36b5`](https://github.com/lingodotdev/lingo.dev/commit/aef36b53163efa523f7632786e0f293890f05b23) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - improve .po handling
88 |
89 | - Updated dependencies [[`aef36b5`](https://github.com/lingodotdev/lingo.dev/commit/aef36b53163efa523f7632786e0f293890f05b23)]:
90 | - @replexica/[email protected]
91 |
92 | ## 0.66.0
93 |
94 | ### Minor Changes
95 |
96 | - [`e885fcf`](https://github.com/lingodotdev/lingo.dev/commit/e885fcf8731d9f2a250cf44a534c5556a057ca51) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - single quotes escaping
97 |
98 | ### Patch Changes
99 |
100 | - Updated dependencies [[`e885fcf`](https://github.com/lingodotdev/lingo.dev/commit/e885fcf8731d9f2a250cf44a534c5556a057ca51)]:
101 | - @replexica/[email protected]
102 |
103 | ## 0.65.1
104 |
105 | ### Patch Changes
106 |
107 | - [#390](https://github.com/lingodotdev/lingo.dev/pull/390) [`a2ada16`](https://github.com/lingodotdev/lingo.dev/commit/a2ada16ecf4cd559d3486f0e4756d58808194f7e) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add ieee variables support
108 |
109 | - Updated dependencies [[`a2ada16`](https://github.com/lingodotdev/lingo.dev/commit/a2ada16ecf4cd559d3486f0e4756d58808194f7e)]:
110 | - @replexica/[email protected]
111 | - @replexica/[email protected]
112 |
113 | ## 0.65.0
114 |
115 | ### Minor Changes
116 |
117 | - [`bd577f2`](https://github.com/lingodotdev/lingo.dev/commit/bd577f22da52e7e889bb4b419cb5dab9865512f1) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - remove unlocalizable from dato
118 |
119 | ### Patch Changes
120 |
121 | - Updated dependencies [[`bd577f2`](https://github.com/lingodotdev/lingo.dev/commit/bd577f22da52e7e889bb4b419cb5dab9865512f1)]:
122 | - @replexica/[email protected]
123 |
124 | ## 0.64.0
125 |
126 | ### Minor Changes
127 |
128 | - [#387](https://github.com/lingodotdev/lingo.dev/pull/387) [`8db4527`](https://github.com/lingodotdev/lingo.dev/commit/8db4527d9c3501d97f8bb7b414dd61e8a3ee80f6) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add support for blocks / array of blocks / nested blocks
129 |
130 | ### Patch Changes
131 |
132 | - Updated dependencies [[`8db4527`](https://github.com/lingodotdev/lingo.dev/commit/8db4527d9c3501d97f8bb7b414dd61e8a3ee80f6)]:
133 | - @replexica/[email protected]
134 |
135 | ## 0.63.1
136 |
137 | ### Patch Changes
138 |
139 | - [#382](https://github.com/lingodotdev/lingo.dev/pull/382) [`3320c8c`](https://github.com/lingodotdev/lingo.dev/commit/3320c8c6f9df9671e1002b63a00bf877270a6064) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - fix lockfile resetting when --key flag is applied
140 |
141 | - Updated dependencies [[`3320c8c`](https://github.com/lingodotdev/lingo.dev/commit/3320c8c6f9df9671e1002b63a00bf877270a6064)]:
142 | - @replexica/[email protected]
143 |
144 | ## 0.63.0
145 |
146 | ### Minor Changes
147 |
148 | - [`db2e800`](https://github.com/lingodotdev/lingo.dev/commit/db2e80013e44b478331b6a97008b3e67bae82a1f) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add --key flag for selective updates
149 |
150 | ### Patch Changes
151 |
152 | - Updated dependencies [[`db2e800`](https://github.com/lingodotdev/lingo.dev/commit/db2e80013e44b478331b6a97008b3e67bae82a1f)]:
153 | - @replexica/[email protected]
154 |
155 | ## 0.62.0
156 |
157 | ### Minor Changes
158 |
159 | - [`302afdf`](https://github.com/lingodotdev/lingo.dev/commit/302afdfd3047b781bd9688921eab3dc84173aa20) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - handle C specifiers in localizable content
160 |
161 | ### Patch Changes
162 |
163 | - Updated dependencies [[`302afdf`](https://github.com/lingodotdev/lingo.dev/commit/302afdfd3047b781bd9688921eab3dc84173aa20)]:
164 | - @replexica/[email protected]
165 |
166 | ## 0.61.0
167 |
168 | ### Minor Changes
169 |
170 | - [`9d38df2`](https://github.com/lingodotdev/lingo.dev/commit/9d38df2fdbe23fdcbb1b7e2e207de650e714e433) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - fixed same-file locale rewrites
171 |
172 | ### Patch Changes
173 |
174 | - Updated dependencies [[`9d38df2`](https://github.com/lingodotdev/lingo.dev/commit/9d38df2fdbe23fdcbb1b7e2e207de650e714e433)]:
175 | - @replexica/[email protected]
176 |
177 | ## 0.60.1
178 |
179 | ### Patch Changes
180 |
181 | - [#372](https://github.com/lingodotdev/lingo.dev/pull/372) [`b9a8350`](https://github.com/lingodotdev/lingo.dev/commit/b9a83502803f4a62fc9a62b4348f853f2baff20d) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - fix single-file results overwriting
182 |
183 | - [#371](https://github.com/lingodotdev/lingo.dev/pull/371) [`e6521b8`](https://github.com/lingodotdev/lingo.dev/commit/e6521b86637c254c011aba89a3558802c04ab3ca) Thanks [@mathio](https://github.com/mathio)! - support underscore in locale code
184 |
185 | - Updated dependencies [[`b9a8350`](https://github.com/lingodotdev/lingo.dev/commit/b9a83502803f4a62fc9a62b4348f853f2baff20d), [`e6521b8`](https://github.com/lingodotdev/lingo.dev/commit/e6521b86637c254c011aba89a3558802c04ab3ca)]:
186 | - @replexica/[email protected]
187 | - @replexica/[email protected]
188 |
189 | ## 0.60.0
190 |
191 | ### Minor Changes
192 |
193 | - [#356](https://github.com/lingodotdev/lingo.dev/pull/356) [`cff3c4e`](https://github.com/lingodotdev/lingo.dev/commit/cff3c4eb1a40f82a9c4c095e49cfd9fce053b048) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add dato support
194 |
195 | ### Patch Changes
196 |
197 | - Updated dependencies [[`cff3c4e`](https://github.com/lingodotdev/lingo.dev/commit/cff3c4eb1a40f82a9c4c095e49cfd9fce053b048)]:
198 | - @replexica/[email protected]
199 | - @replexica/[email protected]
200 | - @replexica/[email protected]
201 |
202 | ## 0.59.1
203 |
204 | ### Patch Changes
205 |
206 | - Updated dependencies []:
207 | - @replexica/[email protected]
208 | - @replexica/[email protected]
209 | - @replexica/[email protected]
210 |
211 | ## 0.59.0
212 |
213 | ### Minor Changes
214 |
215 | - [`63daf00`](https://github.com/lingodotdev/lingo.dev/commit/63daf00e80004775f12c9e1d426cdd2bbf10f5a4) Thanks [@vrcprl](https://github.com/vrcprl)! - noop
216 |
217 | ### Patch Changes
218 |
219 | - [`6eb5282`](https://github.com/lingodotdev/lingo.dev/commit/6eb5282063515db93fc76ff3137422862720fa0d) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - noop
220 |
221 | - Updated dependencies [[`63daf00`](https://github.com/lingodotdev/lingo.dev/commit/63daf00e80004775f12c9e1d426cdd2bbf10f5a4), [`3ab5de6`](https://github.com/lingodotdev/lingo.dev/commit/3ab5de66d8a913297b46095c2e73823124cc8c5b), [`3ab5de6`](https://github.com/lingodotdev/lingo.dev/commit/3ab5de66d8a913297b46095c2e73823124cc8c5b), [`6eb5282`](https://github.com/lingodotdev/lingo.dev/commit/6eb5282063515db93fc76ff3137422862720fa0d)]:
222 | - @replexica/[email protected]
223 | - @replexica/[email protected]
224 | - @replexica/[email protected]
225 |
226 | ## 0.58.2
227 |
228 | ### Patch Changes
229 |
230 | - Updated dependencies []:
231 | - @replexica/[email protected]
232 | - @replexica/[email protected]
233 | - @replexica/[email protected]
234 |
235 | ## 0.58.1
236 |
237 | ### Patch Changes
238 |
239 | - Updated dependencies []:
240 | - @replexica/[email protected]
241 | - @replexica/[email protected]
242 |
243 | ## 0.58.0
244 |
245 | ### Minor Changes
246 |
247 | - [`ff0d2d7`](https://github.com/lingodotdev/lingo.dev/commit/ff0d2d7fb12806a7264a72c03e48a8dda3526c23) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add retry with exponential backoff to the cli
248 |
249 | ### Patch Changes
250 |
251 | - [`7ff7f8f`](https://github.com/lingodotdev/lingo.dev/commit/7ff7f8fca7318e4dba929194972d20ccf3487e9d) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - display number of entries in localization completion message
252 |
253 | - Updated dependencies [[`7ff7f8f`](https://github.com/lingodotdev/lingo.dev/commit/7ff7f8fca7318e4dba929194972d20ccf3487e9d), [`ff0d2d7`](https://github.com/lingodotdev/lingo.dev/commit/ff0d2d7fb12806a7264a72c03e48a8dda3526c23)]:
254 | - @replexica/[email protected]
255 |
256 | ## 0.57.1
257 |
258 | ### Patch Changes
259 |
260 | - Updated dependencies []:
261 | - @replexica/[email protected]
262 | - @replexica/[email protected]
263 | - @replexica/[email protected]
264 |
265 | ## 0.57.0
266 |
267 | ### Minor Changes
268 |
269 | - [`8e2cee4`](https://github.com/lingodotdev/lingo.dev/commit/8e2cee4b282c39fef1e00fa429e03e1c1e489cc5) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add `cleanup` command
270 |
271 | ### Patch Changes
272 |
273 | - [`2c5cbcf`](https://github.com/lingodotdev/lingo.dev/commit/2c5cbcfbf6feb28440255cdea0818c8cefa61d91) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - filter out non extistent keys
274 |
275 | - [`ca10072`](https://github.com/lingodotdev/lingo.dev/commit/ca10072f636d8bd1105ed0f6cc84cf0af5a12402) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - improve progress logging in cli
276 |
277 | - Updated dependencies [[`2c5cbcf`](https://github.com/lingodotdev/lingo.dev/commit/2c5cbcfbf6feb28440255cdea0818c8cefa61d91), [`8e2cee4`](https://github.com/lingodotdev/lingo.dev/commit/8e2cee4b282c39fef1e00fa429e03e1c1e489cc5), [`ca10072`](https://github.com/lingodotdev/lingo.dev/commit/ca10072f636d8bd1105ed0f6cc84cf0af5a12402)]:
278 | - @replexica/[email protected]
279 | - @replexica/[email protected]
280 |
281 | ## 0.56.3
282 |
283 | ### Patch Changes
284 |
285 | - [`b8ad864`](https://github.com/lingodotdev/lingo.dev/commit/b8ad8643347088635eeeb568f1818d71d5226269) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - feat(cli): disable safe mode at localizable chunk level
286 |
287 | - Updated dependencies [[`b8ad864`](https://github.com/lingodotdev/lingo.dev/commit/b8ad8643347088635eeeb568f1818d71d5226269)]:
288 | - @replexica/[email protected]
289 |
290 | ## 0.56.2
291 |
292 | ### Patch Changes
293 |
294 | - Updated dependencies []:
295 | - @replexica/[email protected]
296 | - @replexica/[email protected]
297 |
298 | ## 0.56.1
299 |
300 | ### Patch Changes
301 |
302 | - Updated dependencies []:
303 | - @replexica/[email protected]
304 | - @replexica/[email protected]
305 |
306 | ## 0.56.0
307 |
308 | ### Minor Changes
309 |
310 | - [#298](https://github.com/lingodotdev/lingo.dev/pull/298) [`c03437d`](https://github.com/lingodotdev/lingo.dev/commit/c03437dc9cfd8183e40f74926b4ba7f0874ebf81) Thanks [@partik03](https://github.com/partik03)! - implemented xml loader
311 |
312 | - [`42d0a5a`](https://github.com/lingodotdev/lingo.dev/commit/42d0a5a7a53e296192a31e8f1d67c126793ea280) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - added .localizeHtml implementation to SDK
313 |
314 | ### Patch Changes
315 |
316 | - Updated dependencies [[`c03437d`](https://github.com/lingodotdev/lingo.dev/commit/c03437dc9cfd8183e40f74926b4ba7f0874ebf81), [`a6b22a3`](https://github.com/lingodotdev/lingo.dev/commit/a6b22a3237f574455d8119f914d82b0b247b4151), [`42d0a5a`](https://github.com/lingodotdev/lingo.dev/commit/42d0a5a7a53e296192a31e8f1d67c126793ea280)]:
317 | - @replexica/[email protected]
318 | - @replexica/[email protected]
319 | - @replexica/[email protected]
320 |
321 | ## 0.55.0
322 |
323 | ### Minor Changes
324 |
325 | - [`57e395a`](https://github.com/lingodotdev/lingo.dev/commit/57e395aae8ab100ba470bc7d1104ddfa178249e7) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add `--source` and `--target` flags to show files cmd
326 |
327 | ### Patch Changes
328 |
329 | - Updated dependencies [[`57e395a`](https://github.com/lingodotdev/lingo.dev/commit/57e395aae8ab100ba470bc7d1104ddfa178249e7)]:
330 | - @replexica/[email protected]
331 |
332 | ## 0.54.0
333 |
334 | ### Minor Changes
335 |
336 | - [#301](https://github.com/lingodotdev/lingo.dev/pull/301) [`44b4cca`](https://github.com/lingodotdev/lingo.dev/commit/44b4cca2718bd72d55a938bac458d32a4536508a) Thanks [@partik03](https://github.com/partik03)! - --frozen flag
337 |
338 | - [`4fc27da`](https://github.com/lingodotdev/lingo.dev/commit/4fc27daae5810f6167726a28d76a874fd8421a5b) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - replexica show files now shows both source and target paths
339 |
340 | ### Patch Changes
341 |
342 | - Updated dependencies [[`44b4cca`](https://github.com/lingodotdev/lingo.dev/commit/44b4cca2718bd72d55a938bac458d32a4536508a), [`4fc27da`](https://github.com/lingodotdev/lingo.dev/commit/4fc27daae5810f6167726a28d76a874fd8421a5b)]:
343 | - @replexica/[email protected]
344 |
345 | ## 0.53.1
346 |
347 | ### Patch Changes
348 |
349 | - [`44b5c5c`](https://github.com/lingodotdev/lingo.dev/commit/44b5c5c498ca8df3bb814764f40057576c28c941) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - downgrade glob to @10, to allow node 18
350 |
351 | - Updated dependencies [[`44b5c5c`](https://github.com/lingodotdev/lingo.dev/commit/44b5c5c498ca8df3bb814764f40057576c28c941)]:
352 | - @replexica/[email protected]
353 |
354 | ## 0.53.0
355 |
356 | ### Minor Changes
357 |
358 | - [`072e23e`](https://github.com/lingodotdev/lingo.dev/commit/072e23e58fca0da20bfd01f6a0ae600e6fb760a8) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - hide process summary label when there's zero elements to show
359 |
360 | ### Patch Changes
361 |
362 | - Updated dependencies [[`072e23e`](https://github.com/lingodotdev/lingo.dev/commit/072e23e58fca0da20bfd01f6a0ae600e6fb760a8)]:
363 | - @replexica/[email protected]
364 |
365 | ## 0.51.2
366 |
367 | ### Patch Changes
368 |
369 | - Updated dependencies [[`6bc309c`](https://github.com/lingodotdev/lingo.dev/commit/6bc309c56a8e6a468510109182fd75f8f4e61b8f)]:
370 | - @replexica/[email protected]
371 |
372 | ## 0.51.1
373 |
374 | ### Patch Changes
375 |
376 | - [`e511b50`](https://github.com/lingodotdev/lingo.dev/commit/e511b5080dba58728e8650c7bf34d810cccdcf4e) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - added node@18 support
377 |
378 | - Updated dependencies [[`e511b50`](https://github.com/lingodotdev/lingo.dev/commit/e511b5080dba58728e8650c7bf34d810cccdcf4e)]:
379 | - @replexica/[email protected]
380 |
381 | ## 0.51.0
382 |
383 | ### Minor Changes
384 |
385 | - [#275](https://github.com/lingodotdev/lingo.dev/pull/275) [`091ee35`](https://github.com/lingodotdev/lingo.dev/commit/091ee353081795bf8f61c7d41483bc309c7b62ef) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add support for `.po` format
386 |
387 | ### Patch Changes
388 |
389 | - Updated dependencies [[`091ee35`](https://github.com/lingodotdev/lingo.dev/commit/091ee353081795bf8f61c7d41483bc309c7b62ef)]:
390 | - @replexica/[email protected]
391 | - @replexica/[email protected]
392 | - @replexica/[email protected]
393 |
394 | ## 0.50.0
395 |
396 | ### Minor Changes
397 |
398 | - [#268](https://github.com/lingodotdev/lingo.dev/pull/268) [`5e282d7`](https://github.com/lingodotdev/lingo.dev/commit/5e282d7ffa5ca9494aa7046a090bb7c327085a86) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - composable loaders
399 |
400 | ### Patch Changes
401 |
402 | - Updated dependencies [[`5e282d7`](https://github.com/lingodotdev/lingo.dev/commit/5e282d7ffa5ca9494aa7046a090bb7c327085a86)]:
403 | - @replexica/[email protected]
404 | - @replexica/[email protected]
405 | - @replexica/[email protected]
406 |
407 | ## 0.49.1
408 |
409 | ### Patch Changes
410 |
411 | - [`64cd6f3`](https://github.com/lingodotdev/lingo.dev/commit/64cd6f3765bb4524e9f78f93ff283e833a6f26a2) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - fixed path patter relativity
412 |
413 | - Updated dependencies [[`64cd6f3`](https://github.com/lingodotdev/lingo.dev/commit/64cd6f3765bb4524e9f78f93ff283e833a6f26a2)]:
414 | - @replexica/[email protected]
415 |
416 | ## 0.49.0
417 |
418 | ### Minor Changes
419 |
420 | - [`0071cd6`](https://github.com/lingodotdev/lingo.dev/commit/0071cd66b1c868ad3898fc368390a628c5a67767) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add csv format support
421 |
422 | ### Patch Changes
423 |
424 | - [`1cc0796`](https://github.com/lingodotdev/lingo.dev/commit/1cc07961d221e397ad5dd2917bed76cb4f2b1f04) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add path.resolve to text loaders
425 |
426 | - Updated dependencies [[`0071cd6`](https://github.com/lingodotdev/lingo.dev/commit/0071cd66b1c868ad3898fc368390a628c5a67767), [`1cc0796`](https://github.com/lingodotdev/lingo.dev/commit/1cc07961d221e397ad5dd2917bed76cb4f2b1f04)]:
427 | - @replexica/[email protected]
428 | - @replexica/[email protected]
429 | - @replexica/[email protected]
430 |
431 | ## 0.48.0
432 |
433 | ### Minor Changes
434 |
435 | - [#264](https://github.com/lingodotdev/lingo.dev/pull/264) [`cdef5b7`](https://github.com/lingodotdev/lingo.dev/commit/cdef5b7bfbee4670c6de62cf4b4f3e0315748e25) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - added format specific methods to `@replexica/sdk`
436 |
437 | ### Patch Changes
438 |
439 | - [#261](https://github.com/lingodotdev/lingo.dev/pull/261) [`62c464d`](https://github.com/lingodotdev/lingo.dev/commit/62c464d5602909f8f6370dfa5009131a4d6719d0) Thanks [@Nithishvb](https://github.com/Nithishvb)! - This pr introduces a custom error handling base class for the CLI
440 |
441 | - Updated dependencies [[`cdef5b7`](https://github.com/lingodotdev/lingo.dev/commit/cdef5b7bfbee4670c6de62cf4b4f3e0315748e25), [`62c464d`](https://github.com/lingodotdev/lingo.dev/commit/62c464d5602909f8f6370dfa5009131a4d6719d0)]:
442 | - @replexica/[email protected]
443 | - @replexica/[email protected]
444 |
445 | ## 0.47.1
446 |
447 | ### Patch Changes
448 |
449 | - [`2859938`](https://github.com/lingodotdev/lingo.dev/commit/28599388a91bf80cea3813bb4b8999bb4df302c9) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add missing locales
450 |
451 | - Updated dependencies []:
452 | - @replexica/[email protected]
453 | - @replexica/[email protected]
454 | - @replexica/[email protected]
455 |
456 | ## 0.47.0
457 |
458 | ### Minor Changes
459 |
460 | - [`4dfc8d8`](https://github.com/lingodotdev/lingo.dev/commit/4dfc8d8b301a690875401af5d107a88f1716182a) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - added support for android format
461 |
462 | - [`ca9e20e`](https://github.com/lingodotdev/lingo.dev/commit/ca9e20eef9047e20d39ccf9dff74d2f6069d4676) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - .strings support
463 |
464 | - [`2aedf3b`](https://github.com/lingodotdev/lingo.dev/commit/2aedf3bec2d9dffc7b43fc10dea0cab5742d44af) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - added support for .stringsdict
465 |
466 | - [#245](https://github.com/lingodotdev/lingo.dev/pull/245) [`3fc9da7`](https://github.com/lingodotdev/lingo.dev/commit/3fc9da7e3d2ec58e7f278c79a53eae6d3dfa5896) Thanks [@Nithishvb](https://github.com/Nithishvb)! - prevented overwritting of i18n.json with a default template for unsupported locales
467 |
468 | - [`626082a`](https://github.com/lingodotdev/lingo.dev/commit/626082a64b88fb3b589acd950afeafe417ce5ddc) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - added Flutter .arb support
469 |
470 | ### Patch Changes
471 |
472 | - [`2b5e3ae`](https://github.com/lingodotdev/lingo.dev/commit/2b5e3aea3f0745955266f6edf2ce34830242e503) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - fixed yaml-root-key loader
473 |
474 | - [`747847a`](https://github.com/lingodotdev/lingo.dev/commit/747847a86720d4c36f15daeb41d13d0aff129ca9) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - fixed .xcstrings plurals
475 |
476 | - Updated dependencies [[`2b5e3ae`](https://github.com/lingodotdev/lingo.dev/commit/2b5e3aea3f0745955266f6edf2ce34830242e503), [`4dfc8d8`](https://github.com/lingodotdev/lingo.dev/commit/4dfc8d8b301a690875401af5d107a88f1716182a), [`ca9e20e`](https://github.com/lingodotdev/lingo.dev/commit/ca9e20eef9047e20d39ccf9dff74d2f6069d4676), [`2aedf3b`](https://github.com/lingodotdev/lingo.dev/commit/2aedf3bec2d9dffc7b43fc10dea0cab5742d44af), [`747847a`](https://github.com/lingodotdev/lingo.dev/commit/747847a86720d4c36f15daeb41d13d0aff129ca9), [`3fc9da7`](https://github.com/lingodotdev/lingo.dev/commit/3fc9da7e3d2ec58e7f278c79a53eae6d3dfa5896), [`626082a`](https://github.com/lingodotdev/lingo.dev/commit/626082a64b88fb3b589acd950afeafe417ce5ddc)]:
477 | - @replexica/[email protected]
478 | - @replexica/[email protected]
479 | - @replexica/[email protected]
480 |
481 | ## 0.46.0
482 |
483 | ### Minor Changes
484 |
485 | - [`8887ece`](https://github.com/lingodotdev/lingo.dev/commit/8887ece066eccb8da31d42b30a76b005de2219a8) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add node 18 compatibility
486 |
487 | ### Patch Changes
488 |
489 | - Updated dependencies [[`8887ece`](https://github.com/lingodotdev/lingo.dev/commit/8887ece066eccb8da31d42b30a76b005de2219a8)]:
490 | - @replexica/[email protected]
491 |
492 | ## 0.45.0
493 |
494 | ### Minor Changes
495 |
496 | - [`ad78fb2`](https://github.com/lingodotdev/lingo.dev/commit/ad78fb231d4044d09280127ad8d7c7f7141afe1b) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - remove waitlist
497 |
498 | ### Patch Changes
499 |
500 | - Updated dependencies [[`ad78fb2`](https://github.com/lingodotdev/lingo.dev/commit/ad78fb231d4044d09280127ad8d7c7f7141afe1b)]:
501 | - @replexica/[email protected]
502 |
503 | ## 0.44.3
504 |
505 | ### Patch Changes
506 |
507 | - [`1e4cbd9`](https://github.com/lingodotdev/lingo.dev/commit/1e4cbd9670ea330c6938efdda3a965ac1e3e8376) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add support for symlinks in i18n.json
508 |
509 | - Updated dependencies [[`1e4cbd9`](https://github.com/lingodotdev/lingo.dev/commit/1e4cbd9670ea330c6938efdda3a965ac1e3e8376)]:
510 | - @replexica/[email protected]
511 |
512 | ## 0.44.2
513 |
514 | ### Patch Changes
515 |
516 | - [#224](https://github.com/lingodotdev/lingo.dev/pull/224) [`2d019f1`](https://github.com/lingodotdev/lingo.dev/commit/2d019f153bd8cc928c2065c9e0260e9de0a6885c) Thanks [@Absterrg0](https://github.com/Absterrg0)! - Added 2 new github issue forms
517 |
518 | - [#228](https://github.com/lingodotdev/lingo.dev/pull/228) [`38fab73`](https://github.com/lingodotdev/lingo.dev/commit/38fab73377278124dfc85a847326fdc957261c6e) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - avoid stringifying frontmatter dates
519 |
520 | - Updated dependencies [[`38fab73`](https://github.com/lingodotdev/lingo.dev/commit/38fab73377278124dfc85a847326fdc957261c6e)]:
521 | - @replexica/[email protected]
522 |
523 | ## 0.44.1
524 |
525 | ### Patch Changes
526 |
527 | - [`4760f61`](https://github.com/lingodotdev/lingo.dev/commit/4760f617ef5cca7bed742e4fac28044721d33fc1) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - update cli messages
528 |
529 | - Updated dependencies [[`4760f61`](https://github.com/lingodotdev/lingo.dev/commit/4760f617ef5cca7bed742e4fac28044721d33fc1)]:
530 | - @replexica/[email protected]
531 |
532 | ## 0.44.0
533 |
534 | ### Minor Changes
535 |
536 | - [#220](https://github.com/lingodotdev/lingo.dev/pull/220) [`1b11f8e`](https://github.com/lingodotdev/lingo.dev/commit/1b11f8e710d140045be0c4385bad6348f21f4e5c) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add `replexica show files` command
537 |
538 | ### Patch Changes
539 |
540 | - Updated dependencies [[`1b11f8e`](https://github.com/lingodotdev/lingo.dev/commit/1b11f8e710d140045be0c4385bad6348f21f4e5c)]:
541 | - @replexica/[email protected]
542 |
543 | ## 0.43.0
544 |
545 | ### Minor Changes
546 |
547 | - [`fe09f8b`](https://github.com/lingodotdev/lingo.dev/commit/fe09f8b68b1583ba9be83722beceb1596970809f) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add --api-key to the i18n cmd
548 |
549 | ### Patch Changes
550 |
551 | - Updated dependencies [[`fe09f8b`](https://github.com/lingodotdev/lingo.dev/commit/fe09f8b68b1583ba9be83722beceb1596970809f)]:
552 | - @replexica/[email protected]
553 |
554 | ## 0.42.0
555 |
556 | ### Minor Changes
557 |
558 | - [`7c67fc5`](https://github.com/lingodotdev/lingo.dev/commit/7c67fc5d87d66abbf0a174417b938810a112cc1a) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - migrate to the new markdown parser
559 |
560 | ### Patch Changes
561 |
562 | - Updated dependencies [[`7c67fc5`](https://github.com/lingodotdev/lingo.dev/commit/7c67fc5d87d66abbf0a174417b938810a112cc1a)]:
563 | - @replexica/[email protected]
564 |
565 | ## 0.41.3
566 |
567 | ### Patch Changes
568 |
569 | - [#204](https://github.com/lingodotdev/lingo.dev/pull/204) [`99a4d0a`](https://github.com/lingodotdev/lingo.dev/commit/99a4d0a926d6b6ec0821b47e34f337ca5bb05fca) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add additional exception throws
570 |
571 | - Updated dependencies [[`99a4d0a`](https://github.com/lingodotdev/lingo.dev/commit/99a4d0a926d6b6ec0821b47e34f337ca5bb05fca)]:
572 | - @replexica/[email protected]
573 |
574 | ## 0.41.2
575 |
576 | ### Patch Changes
577 |
578 | - [`962ec5e`](https://github.com/lingodotdev/lingo.dev/commit/962ec5e619632d020ff60fb562d3ad7bc8900443) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - avoid rewriting i18n.json when there's no changes
579 |
580 | - Updated dependencies [[`962ec5e`](https://github.com/lingodotdev/lingo.dev/commit/962ec5e619632d020ff60fb562d3ad7bc8900443)]:
581 | - @replexica/[email protected]
582 |
583 | ## 0.41.1
584 |
585 | ### Patch Changes
586 |
587 | - [`6fdc5d5`](https://github.com/lingodotdev/lingo.dev/commit/6fdc5d535a077bb0656d37c5edf3423dd32e6412) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Add json repair to json file loader
588 |
589 | - Updated dependencies [[`6fdc5d5`](https://github.com/lingodotdev/lingo.dev/commit/6fdc5d535a077bb0656d37c5edf3423dd32e6412)]:
590 | - @replexica/[email protected]
591 |
592 | ## 0.41.0
593 |
594 | ### Minor Changes
595 |
596 | - [#181](https://github.com/lingodotdev/lingo.dev/pull/181) [`1601f70`](https://github.com/lingodotdev/lingo.dev/commit/1601f708bdf0ff1786d3bf9b19265ac5b567f740) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Added support for .properties file
597 |
598 | ### Patch Changes
599 |
600 | - Updated dependencies [[`1601f70`](https://github.com/lingodotdev/lingo.dev/commit/1601f708bdf0ff1786d3bf9b19265ac5b567f740)]:
601 | - @replexica/[email protected]
602 | - @replexica/[email protected]
603 | - @replexica/[email protected]
604 |
605 | ## 0.40.1
606 |
607 | ### Patch Changes
608 |
609 | - [`bc5a28c`](https://github.com/lingodotdev/lingo.dev/commit/bc5a28c3c98b619872924b5f913229ac01387524) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Fix spec imports
610 |
611 | - Updated dependencies [[`bc5a28c`](https://github.com/lingodotdev/lingo.dev/commit/bc5a28c3c98b619872924b5f913229ac01387524)]:
612 | - @replexica/[email protected]
613 | - @replexica/[email protected]
614 | - @replexica/[email protected]
615 |
616 | ## 0.40.0
617 |
618 | ### Minor Changes
619 |
620 | - [#165](https://github.com/lingodotdev/lingo.dev/pull/165) [`5c2ca37`](https://github.com/lingodotdev/lingo.dev/commit/5c2ca37114663eaeb529a027e33949ef3839549b) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Update locale code resolution logic
621 |
622 | - [#166](https://github.com/lingodotdev/lingo.dev/pull/166) [`78c4ce4`](https://github.com/lingodotdev/lingo.dev/commit/78c4ce479149d3eeb2f67f9283de54eecf3c35ab) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Add lockfile autogeneration
623 |
624 | ### Patch Changes
625 |
626 | - Updated dependencies [[`5c2ca37`](https://github.com/lingodotdev/lingo.dev/commit/5c2ca37114663eaeb529a027e33949ef3839549b), [`78c4ce4`](https://github.com/lingodotdev/lingo.dev/commit/78c4ce479149d3eeb2f67f9283de54eecf3c35ab)]:
627 | - @replexica/[email protected]
628 | - @replexica/[email protected]
629 | - @replexica/[email protected]
630 |
631 | ## 0.39.1
632 |
633 | ### Patch Changes
634 |
635 | - [#162](https://github.com/lingodotdev/lingo.dev/pull/162) [`c990101`](https://github.com/lingodotdev/lingo.dev/commit/c990101185aa17b036fa5a21db679fc7781bf551) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Add replexica lockfile command for explicit lockfile generation
636 |
637 | - Updated dependencies [[`c990101`](https://github.com/lingodotdev/lingo.dev/commit/c990101185aa17b036fa5a21db679fc7781bf551)]:
638 | - @replexica/[email protected]
639 |
640 | ## 0.39.0
641 |
642 | ### Minor Changes
643 |
644 | - [`6870fc7`](https://github.com/lingodotdev/lingo.dev/commit/6870fc758dae9d1adb641576befbd8cda61cd5ea) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Fix version number bumping in 1.2 config autoupgrade
645 |
646 | ### Patch Changes
647 |
648 | - Updated dependencies [[`6870fc7`](https://github.com/lingodotdev/lingo.dev/commit/6870fc758dae9d1adb641576befbd8cda61cd5ea)]:
649 | - @replexica/[email protected]
650 | - @replexica/[email protected]
651 | - @replexica/[email protected]
652 |
653 | ## 0.38.0
654 |
655 | ### Minor Changes
656 |
657 | - [`d6e6d5c`](https://github.com/lingodotdev/lingo.dev/commit/d6e6d5c24b266de3769e95545f74632e7d75c697) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Add support for multisource localization to the CLI
658 |
659 | ### Patch Changes
660 |
661 | - Updated dependencies [[`d6e6d5c`](https://github.com/lingodotdev/lingo.dev/commit/d6e6d5c24b266de3769e95545f74632e7d75c697)]:
662 | - @replexica/[email protected]
663 | - @replexica/[email protected]
664 | - @replexica/[email protected]
665 |
666 | ## 0.37.0
667 |
668 | ### Minor Changes
669 |
670 | - [#158](https://github.com/lingodotdev/lingo.dev/pull/158) [`73c9250`](https://github.com/lingodotdev/lingo.dev/commit/73c925084989ccea120cae1617ec87776c88e83e) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Configuration spec v1.1: Improved bucket config structure, to support exclusion patterns
671 |
672 | ### Patch Changes
673 |
674 | - Updated dependencies [[`73c9250`](https://github.com/lingodotdev/lingo.dev/commit/73c925084989ccea120cae1617ec87776c88e83e)]:
675 | - @replexica/[email protected]
676 | - @replexica/[email protected]
677 | - @replexica/[email protected]
678 |
679 | ## 0.36.2
680 |
681 | ### Patch Changes
682 |
683 | - [#156](https://github.com/lingodotdev/lingo.dev/pull/156) [`f59380f`](https://github.com/lingodotdev/lingo.dev/commit/f59380f85c98fae4dfb938f842bdf39fe795ddcd) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Preserve order of keys in JSONs
684 |
685 | - Updated dependencies [[`f59380f`](https://github.com/lingodotdev/lingo.dev/commit/f59380f85c98fae4dfb938f842bdf39fe795ddcd)]:
686 | - @replexica/[email protected]
687 |
688 | ## 0.36.1
689 |
690 | ### Patch Changes
691 |
692 | - [`5ad1879`](https://github.com/lingodotdev/lingo.dev/commit/5ad18797f22bc06fe38769120c27bd7c4642fe2d) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Add ascii art
693 |
694 | - Updated dependencies [[`5ad1879`](https://github.com/lingodotdev/lingo.dev/commit/5ad18797f22bc06fe38769120c27bd7c4642fe2d)]:
695 | - @replexica/[email protected]
696 |
697 | ## 0.36.0
698 |
699 | ### Minor Changes
700 |
701 | - [#148](https://github.com/lingodotdev/lingo.dev/pull/148) [`fca3bd9`](https://github.com/lingodotdev/lingo.dev/commit/fca3bd984e5bef20a4a9921d7562980a3401f131) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Add basic glob pattern support for multi-file buckets
702 |
703 | ### Patch Changes
704 |
705 | - Updated dependencies [[`fca3bd9`](https://github.com/lingodotdev/lingo.dev/commit/fca3bd984e5bef20a4a9921d7562980a3401f131)]:
706 | - @replexica/[email protected]
707 |
708 | ## 0.35.0
709 |
710 | ### Minor Changes
711 |
712 | - [`d293f05`](https://github.com/lingodotdev/lingo.dev/commit/d293f059e1bd9131d6d41ceffc713efa8d6fa598) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - New feature: remove unused keys, whenever a key gets deleted in the source file (thanks @quentin-decre!)
713 |
714 | ### Patch Changes
715 |
716 | - Updated dependencies [[`d293f05`](https://github.com/lingodotdev/lingo.dev/commit/d293f059e1bd9131d6d41ceffc713efa8d6fa598)]:
717 | - @replexica/[email protected]
718 |
719 | ## 0.34.0
720 |
721 | ### Minor Changes
722 |
723 | - [#142](https://github.com/lingodotdev/lingo.dev/pull/142) [`d9b0e51`](https://github.com/lingodotdev/lingo.dev/commit/d9b0e512196329cc781a4d33346f8ca0f3a81e7e) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Extract API calling into SDK package
724 |
725 | ### Patch Changes
726 |
727 | - Updated dependencies [[`d9b0e51`](https://github.com/lingodotdev/lingo.dev/commit/d9b0e512196329cc781a4d33346f8ca0f3a81e7e)]:
728 | - @replexica/[email protected]
729 |
730 | ## 0.33.0
731 |
732 | ### Minor Changes
733 |
734 | - [#138](https://github.com/lingodotdev/lingo.dev/pull/138) [`8948266`](https://github.com/lingodotdev/lingo.dev/commit/8948266b0f026da9f656c916bedcedc72e5aedba) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Added JSON flat/unflat for more granular control over lockfile caching and performance
735 |
736 | ### Patch Changes
737 |
738 | - Updated dependencies [[`8948266`](https://github.com/lingodotdev/lingo.dev/commit/8948266b0f026da9f656c916bedcedc72e5aedba)]:
739 | - @replexica/[email protected]
740 |
741 | ## 0.32.0
742 |
743 | ### Minor Changes
744 |
745 | - [`dab6f68`](https://github.com/lingodotdev/lingo.dev/commit/dab6f68b4e564f4f1a757431b5a590f87e30aeca) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Add frontmatter support
746 |
747 | ### Patch Changes
748 |
749 | - Updated dependencies [[`dab6f68`](https://github.com/lingodotdev/lingo.dev/commit/dab6f68b4e564f4f1a757431b5a590f87e30aeca)]:
750 | - @replexica/[email protected]
751 |
752 | ## 0.31.1
753 |
754 | ### Patch Changes
755 |
756 | - [`387b6b7`](https://github.com/lingodotdev/lingo.dev/commit/387b6b74c1718503f50f18991b0337ee87cb53f8) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Fixed extra newline added to markdown results
757 |
758 | - Updated dependencies [[`387b6b7`](https://github.com/lingodotdev/lingo.dev/commit/387b6b74c1718503f50f18991b0337ee87cb53f8)]:
759 | - @replexica/[email protected]
760 |
761 | ## 0.31.0
762 |
763 | ### Minor Changes
764 |
765 | - [`8c8e7dd`](https://github.com/lingodotdev/lingo.dev/commit/8c8e7dd4d35669d484240d643427612ecdaf73eb) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Added new locales
766 |
767 | ### Patch Changes
768 |
769 | - Updated dependencies [[`8c8e7dd`](https://github.com/lingodotdev/lingo.dev/commit/8c8e7dd4d35669d484240d643427612ecdaf73eb)]:
770 | - @replexica/[email protected]
771 | - @replexica/[email protected]
772 | - @replexica/[email protected]
773 |
774 | ## 0.30.0
775 |
776 | ### Minor Changes
777 |
778 | - [`bd2029d`](https://github.com/lingodotdev/lingo.dev/commit/bd2029d5c1241f7355ea08621dbeb7e04b7f5b5c) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Updated markdown processor algo
779 |
780 | ### Patch Changes
781 |
782 | - Updated dependencies [[`bd2029d`](https://github.com/lingodotdev/lingo.dev/commit/bd2029d5c1241f7355ea08621dbeb7e04b7f5b5c)]:
783 | - @replexica/[email protected]
784 |
785 | ## 0.29.0
786 |
787 | ### Minor Changes
788 |
789 | - [`7d83cfc`](https://github.com/lingodotdev/lingo.dev/commit/7d83cfc79921346a47ccef43accee454ba80c83c) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Added retry mechanism to i18n engine calls
790 |
791 | ### Patch Changes
792 |
793 | - Updated dependencies [[`7d83cfc`](https://github.com/lingodotdev/lingo.dev/commit/7d83cfc79921346a47ccef43accee454ba80c83c)]:
794 | - @replexica/[email protected]
795 |
796 | ## 0.24.0
797 |
798 | ### Minor Changes
799 |
800 | - [`37167d6`](https://github.com/lingodotdev/lingo.dev/commit/37167d6d29d747b0dd35e26e5b6f0978f0e156d9) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Added -v, --version flag to print out CLI version
801 |
802 | ### Patch Changes
803 |
804 | - Updated dependencies [[`37167d6`](https://github.com/lingodotdev/lingo.dev/commit/37167d6d29d747b0dd35e26e5b6f0978f0e156d9)]:
805 | - @replexica/[email protected]
806 |
807 | ## 0.23.7
808 |
809 | ### Patch Changes
810 |
811 | - Updated dependencies [[`c0be1a2`](https://github.com/lingodotdev/lingo.dev/commit/c0be1a29e3069ef2c8bdc4e4f52d2fb17abdb1f5), [`a083a55`](https://github.com/lingodotdev/lingo.dev/commit/a083a551cbe755c87a78ad14673f5dbac6d86832)]:
812 | - @replexica/[email protected]
813 | - @replexica/[email protected]
814 | - @replexica/[email protected]
815 |
816 | ## 0.23.6
817 |
818 | ### Patch Changes
819 |
820 | - [`eee21e1`](https://github.com/lingodotdev/lingo.dev/commit/eee21e1913e86f18938f1d6fd0dffaf6c17fb33c) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Improved markdown performance, added support for VERY large markdown content files.
821 |
822 | - Updated dependencies [[`eee21e1`](https://github.com/lingodotdev/lingo.dev/commit/eee21e1913e86f18938f1d6fd0dffaf6c17fb33c)]:
823 | - @replexica/[email protected]
824 |
825 | ## 0.23.5
826 |
827 | ### Patch Changes
828 |
829 | - Updated dependencies [[`ca1dd58`](https://github.com/lingodotdev/lingo.dev/commit/ca1dd58008e31c8aa88ab14362f6506d6efb970a)]:
830 | - @replexica/[email protected]
831 |
832 | ## 0.23.4
833 |
834 | ### Patch Changes
835 |
836 | - Updated dependencies [[`3c7a30c`](https://github.com/lingodotdev/lingo.dev/commit/3c7a30c6be91fb27c00681c998452d7bf1beca0e)]:
837 | - @replexica/[email protected]
838 |
839 | ## 0.23.3
840 |
841 | ### Patch Changes
842 |
843 | - Updated dependencies [[`fbce978`](https://github.com/lingodotdev/lingo.dev/commit/fbce97846eabf00fb1c903b82e7d556480de5d23), [`10252ce`](https://github.com/lingodotdev/lingo.dev/commit/10252ceaa2685cc23f4dbeb6ac985cc2148853e2)]:
844 | - @replexica/[email protected]
845 | - @replexica/[email protected]
846 | - @replexica/[email protected]
847 |
848 | ## 0.23.2
849 |
850 | ### Patch Changes
851 |
852 | - Updated dependencies [[`27bb7fd`](https://github.com/lingodotdev/lingo.dev/commit/27bb7fd7e644e37c59e2cce9b453122097f6362c)]:
853 | - @replexica/[email protected]
854 |
855 | ## 0.23.1
856 |
857 | ### Patch Changes
858 |
859 | - [`088de18`](https://github.com/lingodotdev/lingo.dev/commit/088de18a53f45fa8df5833fe81ed96a2ed231299) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Fix @replexica/config reference
860 |
861 | - Updated dependencies [[`088de18`](https://github.com/lingodotdev/lingo.dev/commit/088de18a53f45fa8df5833fe81ed96a2ed231299)]:
862 | - @replexica/[email protected]
863 | - @replexica/[email protected]
864 | - @replexica/[email protected]
865 |
866 | ## 0.23.0
867 |
868 | ### Minor Changes
869 |
870 | - [#99](https://github.com/lingodotdev/lingo.dev/pull/99) [`4e94058`](https://github.com/lingodotdev/lingo.dev/commit/4e940582ea8ebe5a058b76fb33420729f7bfdcef) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Added support for i18n lockfiles to improve AI localization performance.
871 |
872 | ### Patch Changes
873 |
874 | - Updated dependencies [[`4e94058`](https://github.com/lingodotdev/lingo.dev/commit/4e940582ea8ebe5a058b76fb33420729f7bfdcef)]:
875 | - @replexica/[email protected]
876 | - @replexica/[email protected]
877 | - @replexica/[email protected]
878 |
```