This is page 16 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/sdk/CHANGELOG.md:
--------------------------------------------------------------------------------
```markdown
1 | # @lingo.dev/\_sdk
2 |
3 | ## 0.12.6
4 |
5 | ### Patch Changes
6 |
7 | - [#1230](https://github.com/lingodotdev/lingo.dev/pull/1230) [`b45347c`](https://github.com/lingodotdev/lingo.dev/commit/b45347c38572ee371b2bc494261b7e3e90c4aed1) Thanks [@vrcprl](https://github.com/vrcprl)! - add an xcode-xcstrings-v2 bucket type that supports cldr pluralization rules
8 |
9 | - Updated dependencies [[`b45347c`](https://github.com/lingodotdev/lingo.dev/commit/b45347c38572ee371b2bc494261b7e3e90c4aed1)]:
10 | - @lingo.dev/[email protected]
11 |
12 | ## 0.12.5
13 |
14 | ### Patch Changes
15 |
16 | - Updated dependencies [[`82f5e7c`](https://github.com/lingodotdev/lingo.dev/commit/82f5e7cdde9a2a15b4c2a7fcb8c67ed64eab596b), [`e858174`](https://github.com/lingodotdev/lingo.dev/commit/e858174fd5165e0ea3e3f25fa1fc3edb292bc58f)]:
17 | - @lingo.dev/[email protected]
18 |
19 | ## 0.12.4
20 |
21 | ### Patch Changes
22 |
23 | - Updated dependencies [[`1fa218c`](https://github.com/lingodotdev/lingo.dev/commit/1fa218c13bf90df6d175fb18264f59c1a10b967c)]:
24 | - @lingo.dev/[email protected]
25 |
26 | ## 0.12.3
27 |
28 | ### Patch Changes
29 |
30 | - Updated dependencies [[`bbc71b9`](https://github.com/lingodotdev/lingo.dev/commit/bbc71b9948ccc289c9669d8b0c276c9596f6a5e7)]:
31 | - @lingo.dev/[email protected]
32 |
33 | ## 0.12.2
34 |
35 | ### Patch Changes
36 |
37 | - Updated dependencies [[`6579d70`](https://github.com/lingodotdev/lingo.dev/commit/6579d70bc670c2fdc06c09842d931b07e134151c)]:
38 | - @lingo.dev/[email protected]
39 |
40 | ## 0.12.1
41 |
42 | ### Patch Changes
43 |
44 | - Updated dependencies [[`a35032e`](https://github.com/lingodotdev/lingo.dev/commit/a35032e7e7a188d1f5e774576352068124526e24)]:
45 | - @lingo.dev/[email protected]
46 |
47 | ## 0.12.0
48 |
49 | ### Minor Changes
50 |
51 | - [#1066](https://github.com/lingodotdev/lingo.dev/pull/1066) [`6af91a0`](https://github.com/lingodotdev/lingo.dev/commit/6af91a083d16f85051fb49a4034789abe784017e) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add hints support
52 |
53 | ### Patch Changes
54 |
55 | - Updated dependencies [[`6af91a0`](https://github.com/lingodotdev/lingo.dev/commit/6af91a083d16f85051fb49a4034789abe784017e)]:
56 | - @lingo.dev/[email protected]
57 |
58 | ## 0.11.0
59 |
60 | ### Minor Changes
61 |
62 | - [#1049](https://github.com/lingodotdev/lingo.dev/pull/1049) [`85dfc10`](https://github.com/lingodotdev/lingo.dev/commit/85dfc10961b116e31b2bb478f42013756ca49974) Thanks [@VAIBHAVSING](https://github.com/VAIBHAVSING)! - Added new methods to the SDK:
63 |
64 | 1. `localizeStringArray`: Localizes an array of strings while maintaining their order.
65 |
66 | Also added comprehensive tests for these methods using Vitest.
67 |
68 | ## 0.10.2
69 |
70 | ### Patch Changes
71 |
72 | - Updated dependencies [[`afbb978`](https://github.com/lingodotdev/lingo.dev/commit/afbb978fec83d574f2c43b7d68457e435fca9b57)]:
73 | - @lingo.dev/[email protected]
74 |
75 | ## 0.10.1
76 |
77 | ### Patch Changes
78 |
79 | - [#1023](https://github.com/lingodotdev/lingo.dev/pull/1023) [`9266fd0`](https://github.com/lingodotdev/lingo.dev/commit/9266fd0bcddf4b07ca51d2609af92a9473106f9d) Thanks [@devin-ai-integration](https://github.com/apps/devin-ai-integration)! - Update Zod dependency to version 3.25.76
80 |
81 | - Updated dependencies [[`9266fd0`](https://github.com/lingodotdev/lingo.dev/commit/9266fd0bcddf4b07ca51d2609af92a9473106f9d)]:
82 | - @lingo.dev/[email protected]
83 |
84 | ## 0.10.0
85 |
86 | ### Minor Changes
87 |
88 | - [#998](https://github.com/lingodotdev/lingo.dev/pull/998) [`cb2aa0f`](https://github.com/lingodotdev/lingo.dev/commit/cb2aa0f505d6b7dbc435b526e8a6f62265d1f453) Thanks [@VAIBHAVSING](https://github.com/VAIBHAVSING)! - Added support for AbortController to all public SDK methods, enabling consumers to cancel long-running operations using the standard AbortController API. Refactored internal methods to propagate AbortSignal and check for abortion between batch chunks. Updated fetch calls to use AbortSignal for network request cancellation.
89 |
90 | ## 0.9.6
91 |
92 | ### Patch Changes
93 |
94 | - Updated dependencies [[`acd5356`](https://github.com/lingodotdev/lingo.dev/commit/acd5356b68d2261576240c173fea790864c3c31d)]:
95 | - @lingo.dev/[email protected]
96 |
97 | ## 0.9.5
98 |
99 | ### Patch Changes
100 |
101 | - Updated dependencies [[`f644123`](https://github.com/lingodotdev/lingo.dev/commit/f644123ddf6a6254790d08af50141e4dd78c3677)]:
102 | - @lingo.dev/[email protected]
103 |
104 | ## 0.9.4
105 |
106 | ### Patch Changes
107 |
108 | - Updated dependencies [[`84fd214`](https://github.com/lingodotdev/lingo.dev/commit/84fd214a21766e7683c5d645fcb8c4c0162eb0b6)]:
109 | - @lingo.dev/[email protected]
110 |
111 | ## 0.9.3
112 |
113 | ### Patch Changes
114 |
115 | - Updated dependencies [[`ce8c75c`](https://github.com/lingodotdev/lingo.dev/commit/ce8c75c7fc1a2124d3e18444bc356c4dfce26434)]:
116 | - @lingo.dev/[email protected]
117 |
118 | ## 0.9.2
119 |
120 | ### Patch Changes
121 |
122 | - [#937](https://github.com/lingodotdev/lingo.dev/pull/937) [`4e5983d`](https://github.com/lingodotdev/lingo.dev/commit/4e5983d7e59ebf9eb529c4b7c1c87689432ac873) Thanks [@devin-ai-integration](https://github.com/apps/devin-ai-integration)! - Update documentation URLs from docs.lingo.dev to lingo.dev/cli and lingo.dev/compiler
123 |
124 | ## 0.9.1
125 |
126 | ### Patch Changes
127 |
128 | - Updated dependencies [[`1b9b113`](https://github.com/lingodotdev/lingo.dev/commit/1b9b11301978e8caa2555832d027ff93216aa6e1), [`0329a9c`](https://github.com/lingodotdev/lingo.dev/commit/0329a9cdb5e5a63fcecab4efcd7cce22f155a0e9)]:
129 | - @lingo.dev/[email protected]
130 |
131 | ## 0.9.0
132 |
133 | ### Minor Changes
134 |
135 | - [#915](https://github.com/lingodotdev/lingo.dev/pull/915) [`6b4b9e6`](https://github.com/lingodotdev/lingo.dev/commit/6b4b9e6cc9a0cb5da8a4df9e9ebda474bf2a18ed) Thanks [@devin-ai-integration](https://github.com/apps/devin-ai-integration)! - feat: enhance 5xx error handling with Cloudflare status integration
136 |
137 | - [#915](https://github.com/lingodotdev/lingo.dev/pull/915) [`6b4b9e6`](https://github.com/lingodotdev/lingo.dev/commit/6b4b9e6cc9a0cb5da8a4df9e9ebda474bf2a18ed) Thanks [@devin-ai-integration](https://github.com/apps/devin-ai-integration)! - feat: enhance 5xx error handling with Cloudflare status integration
138 |
139 | ## 0.8.1
140 |
141 | ### Patch Changes
142 |
143 | - Updated dependencies [[`a5da697`](https://github.com/lingodotdev/lingo.dev/commit/a5da697f7efd46de31d17b202d06eb5f655ed9b9)]:
144 | - @lingo.dev/[email protected]
145 |
146 | ## 0.8.0
147 |
148 | ### Minor Changes
149 |
150 | - [`e980e84`](https://github.com/lingodotdev/lingo.dev/commit/e980e84178439ad70417d38b425acf9148cfc4b6) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - added the compiler
151 |
152 | ### Patch Changes
153 |
154 | - Updated dependencies [[`e980e84`](https://github.com/lingodotdev/lingo.dev/commit/e980e84178439ad70417d38b425acf9148cfc4b6)]:
155 | - @lingo.dev/[email protected]
156 |
157 | ## 0.7.43
158 |
159 | ### Patch Changes
160 |
161 | - Updated dependencies [[`0272fbf`](https://github.com/lingodotdev/lingo.dev/commit/0272fbf8847240ed9453130237d5843b918f869f)]:
162 | - @lingo.dev/[email protected]
163 |
164 | ## 0.7.42
165 |
166 | ### Patch Changes
167 |
168 | - [#782](https://github.com/lingodotdev/lingo.dev/pull/782) [`d913c20`](https://github.com/lingodotdev/lingo.dev/commit/d913c20fdf0086741c8b50fd4ddfb38eae304a24) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - parallel processing
169 |
170 | - Updated dependencies [[`d913c20`](https://github.com/lingodotdev/lingo.dev/commit/d913c20fdf0086741c8b50fd4ddfb38eae304a24)]:
171 | - @lingo.dev/[email protected]
172 |
173 | ## 0.7.41
174 |
175 | ### Patch Changes
176 |
177 | - Updated dependencies [[`3f2aba9`](https://github.com/lingodotdev/lingo.dev/commit/3f2aba9c1d5834faf89a26194f1f3d9f9b878d40)]:
178 | - @lingo.dev/[email protected]
179 |
180 | ## 0.7.40
181 |
182 | ### Patch Changes
183 |
184 | - Updated dependencies [[`9aa7004`](https://github.com/lingodotdev/lingo.dev/commit/9aa700491446865dc131b80419f681132b888652)]:
185 | - @lingo.dev/[email protected]
186 |
187 | ## 0.7.39
188 |
189 | ### Patch Changes
190 |
191 | - Updated dependencies [[`5170449`](https://github.com/lingodotdev/lingo.dev/commit/517044905dfc682d6a5fa95b0605b8715e2b72c7)]:
192 | - @lingo.dev/[email protected]
193 |
194 | ## 0.7.38
195 |
196 | ### Patch Changes
197 |
198 | - Updated dependencies [[`c5ccf81`](https://github.com/lingodotdev/lingo.dev/commit/c5ccf81e9c2bd27bae332306da2a41e41bbeb87d)]:
199 | - @lingo.dev/[email protected]
200 |
201 | ## 0.7.37
202 |
203 | ### Patch Changes
204 |
205 | - [#649](https://github.com/lingodotdev/lingo.dev/pull/649) [`409018d`](https://github.com/lingodotdev/lingo.dev/commit/409018de74614a1fd99363c6749b0e4be9e1a278) Thanks [@mathio](https://github.com/mathio)! - refactor dependencies
206 |
207 | - Updated dependencies [[`409018d`](https://github.com/lingodotdev/lingo.dev/commit/409018de74614a1fd99363c6749b0e4be9e1a278)]:
208 | - @lingo.dev/[email protected]
209 |
210 | ## 0.7.36
211 |
212 | ### Patch Changes
213 |
214 | - [#647](https://github.com/lingodotdev/lingo.dev/pull/647) [`235b6d9`](https://github.com/lingodotdev/lingo.dev/commit/235b6d914c5f542ee5f1a8a88085cfd9dea5409e) Thanks [@mathio](https://github.com/mathio)! - update vitest
215 |
216 | - Updated dependencies [[`235b6d9`](https://github.com/lingodotdev/lingo.dev/commit/235b6d914c5f542ee5f1a8a88085cfd9dea5409e)]:
217 | - @lingo.dev/[email protected]
218 |
219 | ## 0.7.35
220 |
221 | ### Patch Changes
222 |
223 | - [#645](https://github.com/lingodotdev/lingo.dev/pull/645) [`d824b10`](https://github.com/lingodotdev/lingo.dev/commit/d824b106631f45fc428cf01f733aab4842b4fa81) Thanks [@mathio](https://github.com/mathio)! - update dependencies
224 |
225 | - Updated dependencies [[`d824b10`](https://github.com/lingodotdev/lingo.dev/commit/d824b106631f45fc428cf01f733aab4842b4fa81)]:
226 | - @lingo.dev/[email protected]
227 |
228 | ## 0.7.34
229 |
230 | ### Patch Changes
231 |
232 | - Updated dependencies [[`82efe61`](https://github.com/lingodotdev/lingo.dev/commit/82efe6176db12cc7c5bbeb84f38bc3261f9eec4f), [`82efe61`](https://github.com/lingodotdev/lingo.dev/commit/82efe6176db12cc7c5bbeb84f38bc3261f9eec4f)]:
233 | - @lingo.dev/[email protected]
234 |
235 | ## 0.7.33
236 |
237 | ### Patch Changes
238 |
239 | - Updated dependencies [[`58f3959`](https://github.com/lingodotdev/lingo.dev/commit/58f39599b3b765ad807e725b4089a5e9b11a01b2)]:
240 | - @lingo.dev/[email protected]
241 |
242 | ## 0.7.32
243 |
244 | ### Patch Changes
245 |
246 | - Updated dependencies [[`fe922a4`](https://github.com/lingodotdev/lingo.dev/commit/fe922a469c2d5dac23a909a4fb67a6efd56d80d6)]:
247 | - @lingo.dev/[email protected]
248 |
249 | ## 0.7.31
250 |
251 | ### Patch Changes
252 |
253 | - Updated dependencies [[`2495afd`](https://github.com/lingodotdev/lingo.dev/commit/2495afd69e23700f96e19e5bbf74e393b29c2033), [`516a79c`](https://github.com/lingodotdev/lingo.dev/commit/516a79c75501c5960ae944379f38591806ca43e2), [`2cc6114`](https://github.com/lingodotdev/lingo.dev/commit/2cc61140fccc69ab73d40c7802a2d0e018889475)]:
254 | - @lingo.dev/[email protected]
255 |
256 | ## 0.7.30
257 |
258 | ### Patch Changes
259 |
260 | - Updated dependencies [[`1dbbfd2`](https://github.com/lingodotdev/lingo.dev/commit/1dbbfd2ed9f5a7e0479dc83f700fb68ee5347a18)]:
261 | - @lingo.dev/[email protected]
262 |
263 | ## 0.7.29
264 |
265 | ### Patch Changes
266 |
267 | - [#596](https://github.com/lingodotdev/lingo.dev/pull/596) [`61b487e`](https://github.com/lingodotdev/lingo.dev/commit/61b487e1e059328a32c3cdf673255d9d2cd480d9) Thanks [@vrcprl](https://github.com/vrcprl)! - add new locale
268 |
269 | - Updated dependencies [[`61b487e`](https://github.com/lingodotdev/lingo.dev/commit/61b487e1e059328a32c3cdf673255d9d2cd480d9)]:
270 | - @lingo.dev/[email protected]
271 |
272 | ## 0.7.28
273 |
274 | ### Patch Changes
275 |
276 | - Updated dependencies [[`743d93e`](https://github.com/lingodotdev/lingo.dev/commit/743d93e554841bbd96d23682d8aec63cb4eb3ec8)]:
277 | - @lingo.dev/[email protected]
278 |
279 | ## 0.7.27
280 |
281 | ### Patch Changes
282 |
283 | - [#574](https://github.com/lingodotdev/lingo.dev/pull/574) [`dde7fbe`](https://github.com/lingodotdev/lingo.dev/commit/dde7fbe57fc9b1d3ce28e192b778921099354dad) Thanks [@mathio](https://github.com/mathio)! - handle errors from i18n when streaming
284 |
285 | ## 0.7.26
286 |
287 | ### Patch Changes
288 |
289 | - [#553](https://github.com/lingodotdev/lingo.dev/pull/553) [`95023f2`](https://github.com/lingodotdev/lingo.dev/commit/95023f2c8da3958e8582628a22bf40674f8d2317) Thanks [@vrcprl](https://github.com/vrcprl)! - Add new locales
290 |
291 | - Updated dependencies [[`95023f2`](https://github.com/lingodotdev/lingo.dev/commit/95023f2c8da3958e8582628a22bf40674f8d2317)]:
292 | - @lingo.dev/[email protected]
293 |
294 | ## 0.7.25
295 |
296 | ### Patch Changes
297 |
298 | - Updated dependencies [[`9089b08`](https://github.com/lingodotdev/lingo.dev/commit/9089b085b968ff3195866e377ecf3016aa06f959)]:
299 | - @lingo.dev/[email protected]
300 |
301 | ## 0.7.24
302 |
303 | ### Patch Changes
304 |
305 | - Updated dependencies [[`0b48be1`](https://github.com/lingodotdev/lingo.dev/commit/0b48be197e88dac581cc4f257789a04b43acf932)]:
306 | - @lingo.dev/[email protected]
307 |
308 | ## 0.7.23
309 |
310 | ### Patch Changes
311 |
312 | - [#537](https://github.com/lingodotdev/lingo.dev/pull/537) [`7597b99`](https://github.com/lingodotdev/lingo.dev/commit/7597b99c4869f63a42e6de3c4ed25424498d15ae) Thanks [@mathio](https://github.com/mathio)! - automatic source locale detection
313 |
314 | ## 0.7.22
315 |
316 | ### Patch Changes
317 |
318 | - Updated dependencies [[`bafa755`](https://github.com/lingodotdev/lingo.dev/commit/bafa755d9681e93741462eb7bcf9b85073d20fd7)]:
319 | - @lingo.dev/[email protected]
320 |
321 | ## 0.7.21
322 |
323 | ### Patch Changes
324 |
325 | - Updated dependencies [[`444a731`](https://github.com/lingodotdev/lingo.dev/commit/444a7319a1351e22e5666504169023b4c8a29d5f)]:
326 | - @lingo.dev/[email protected]
327 |
328 | ## 0.7.20
329 |
330 | ### Patch Changes
331 |
332 | - [#515](https://github.com/lingodotdev/lingo.dev/pull/515) [`fd99a6c`](https://github.com/lingodotdev/lingo.dev/commit/fd99a6ca18ee21774ba5c2b7ce72d1712e374675) Thanks [@mathio](https://github.com/mathio)! - add typesVersions for support of older `moduleResolution`
333 |
334 | ## 0.7.19
335 |
336 | ### Patch Changes
337 |
338 | - Updated dependencies [[`ec2902e`](https://github.com/lingodotdev/lingo.dev/commit/ec2902e5dc31fd79cc3b6fbf478ed1f3c4df0345)]:
339 | - @lingo.dev/[email protected]
340 |
341 | ## 0.7.18
342 |
343 | ### Patch Changes
344 |
345 | - Updated dependencies [[`beb0541`](https://github.com/lingodotdev/lingo.dev/commit/beb05411ee459461e05801a763b1fa28d288e04e)]:
346 | - @lingo.dev/[email protected]
347 |
348 | ## 0.7.17
349 |
350 | ### Patch Changes
351 |
352 | - [#493](https://github.com/lingodotdev/lingo.dev/pull/493) [`81527a4`](https://github.com/lingodotdev/lingo.dev/commit/81527a457ad8ef7fe735232caacdf2cc575e5b20) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - fix payload references
353 |
354 | ## 0.7.16
355 |
356 | ### Patch Changes
357 |
358 | - Updated dependencies [[`a096300`](https://github.com/lingodotdev/lingo.dev/commit/a0963008ea2a8bbc910b0eaeb20f4e3b3cd641a7)]:
359 | - @lingo.dev/[email protected]
360 |
361 | ## 0.7.15
362 |
363 | ### Patch Changes
364 |
365 | - [#473](https://github.com/lingodotdev/lingo.dev/pull/473) [`3a99763`](https://github.com/lingodotdev/lingo.dev/commit/3a99763087512ba82955303d6f0567e813f4fa05) Thanks [@vrcprl](https://github.com/vrcprl)! - add new locales
366 |
367 | - Updated dependencies [[`3a99763`](https://github.com/lingodotdev/lingo.dev/commit/3a99763087512ba82955303d6f0567e813f4fa05)]:
368 | - @lingo.dev/[email protected]
369 |
370 | ## 0.7.14
371 |
372 | ### Patch Changes
373 |
374 | - [#463](https://github.com/lingodotdev/lingo.dev/pull/463) [`f249d8f`](https://github.com/lingodotdev/lingo.dev/commit/f249d8f69d04f0ce40fd94e500e7b829b7ba1ed4) Thanks [@vrcprl](https://github.com/vrcprl)! - set utf-8 encoding explicitly
375 |
376 | ## 0.7.13
377 |
378 | ### Patch Changes
379 |
380 | - [`dc8bfc7`](https://github.com/lingodotdev/lingo.dev/commit/dc8bfc7ddc38ade768b8aa11c56669db7eb446e6) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - publish deps
381 |
382 | - Updated dependencies [[`dc8bfc7`](https://github.com/lingodotdev/lingo.dev/commit/dc8bfc7ddc38ade768b8aa11c56669db7eb446e6)]:
383 | - @lingo.dev/[email protected]
384 |
385 | ## 0.7.12
386 |
387 | ### Patch Changes
388 |
389 | - [`6281dbd`](https://github.com/lingodotdev/lingo.dev/commit/6281dbd96bd5cfe54f194a6a1d055c8255a250de) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - fix sdk/spec exported types
390 |
391 | - Updated dependencies [[`6281dbd`](https://github.com/lingodotdev/lingo.dev/commit/6281dbd96bd5cfe54f194a6a1d055c8255a250de)]:
392 | - @lingo.dev/[email protected]
393 |
394 | ## 0.7.11
395 |
396 | ### Patch Changes
397 |
398 | - [#419](https://github.com/lingodotdev/lingo.dev/pull/419) [`a45feb1`](https://github.com/lingodotdev/lingo.dev/commit/a45feb1d747f8fa32c42c1726953a04c174e754a) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Replexica is now Lingo.dev! 🎉
399 |
400 | - Updated dependencies [[`a45feb1`](https://github.com/lingodotdev/lingo.dev/commit/a45feb1d747f8fa32c42c1726953a04c174e754a)]:
401 | - @lingo.dev/[email protected]
402 |
403 | ## 0.7.10
404 |
405 | ### Patch Changes
406 |
407 | - Updated dependencies [[`003344f`](https://github.com/lingodotdev/lingo.dev/commit/003344ffcca98a391a298707f18476971c4d4c2b)]:
408 | - @replexica/[email protected]
409 |
410 | ## 0.7.9
411 |
412 | ### Patch Changes
413 |
414 | - Updated dependencies [[`a2ada16`](https://github.com/lingodotdev/lingo.dev/commit/a2ada16ecf4cd559d3486f0e4756d58808194f7e)]:
415 | - @replexica/[email protected]
416 |
417 | ## 0.7.8
418 |
419 | ### Patch Changes
420 |
421 | - Updated dependencies [[`e6521b8`](https://github.com/lingodotdev/lingo.dev/commit/e6521b86637c254c011aba89a3558802c04ab3ca)]:
422 | - @replexica/[email protected]
423 |
424 | ## 0.7.7
425 |
426 | ### Patch Changes
427 |
428 | - Updated dependencies [[`cff3c4e`](https://github.com/lingodotdev/lingo.dev/commit/cff3c4eb1a40f82a9c4c095e49cfd9fce053b048)]:
429 | - @replexica/[email protected]
430 |
431 | ## 0.7.6
432 |
433 | ### Patch Changes
434 |
435 | - Updated dependencies [[`58d7b35`](https://github.com/lingodotdev/lingo.dev/commit/58d7b3567e51cc3ef0fad0288c13451381b95a98)]:
436 | - @replexica/[email protected]
437 |
438 | ## 0.7.5
439 |
440 | ### Patch Changes
441 |
442 | - Updated dependencies [[`9cf5299`](https://github.com/lingodotdev/lingo.dev/commit/9cf5299f7efbef70fd83f95177eac49b4d8f8007), [`3ab5de6`](https://github.com/lingodotdev/lingo.dev/commit/3ab5de66d8a913297b46095c2e73823124cc8c5b)]:
443 | - @replexica/[email protected]
444 |
445 | ## 0.7.4
446 |
447 | ### Patch Changes
448 |
449 | - Updated dependencies [[`1556977`](https://github.com/lingodotdev/lingo.dev/commit/1556977332a6f949100283bfa8c9a9ff5e74b156)]:
450 | - @replexica/[email protected]
451 |
452 | ## 0.7.3
453 |
454 | ### Patch Changes
455 |
456 | - [`cbef8f3`](https://github.com/lingodotdev/lingo.dev/commit/cbef8f3cafdc955d61053ce885d98e425acb668d) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - moved jsdom import into the html handler function
457 |
458 | ## 0.7.2
459 |
460 | ### Patch Changes
461 |
462 | - Updated dependencies [[`5cb3c93`](https://github.com/lingodotdev/lingo.dev/commit/5cb3c930fff6e30cff5cc2266b794f75a0db646d)]:
463 | - @replexica/[email protected]
464 |
465 | ## 0.7.1
466 |
467 | ### Patch Changes
468 |
469 | - [`db819a4`](https://github.com/lingodotdev/lingo.dev/commit/db819a42412ceb67fedbe729b7d018952686d60b) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - reduce default batch size to avoid hitting rate limits
470 |
471 | - [`2c5cbcf`](https://github.com/lingodotdev/lingo.dev/commit/2c5cbcfbf6feb28440255cdea0818c8cefa61d91) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - filter out non extistent keys
472 |
473 | ## 0.7.0
474 |
475 | ### Minor Changes
476 |
477 | - [`c42dc2d`](https://github.com/lingodotdev/lingo.dev/commit/c42dc2d5b4efe95e804b5a7e7f6d354cf8622dc7) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add `batchLocalizeText` to sdk
478 |
479 | ## 0.6.0
480 |
481 | ### Minor Changes
482 |
483 | - [`a71a88e`](https://github.com/lingodotdev/lingo.dev/commit/a71a88e5c8bd6601b0838c381433a87763142801) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - fast mode
484 |
485 | ### Patch Changes
486 |
487 | - [`f0a77ad`](https://github.com/lingodotdev/lingo.dev/commit/f0a77ad774a01c30e7e9bc5a0253638176332fd2) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - updated default batch size limits in the SDK
488 |
489 | ## 0.5.0
490 |
491 | ### Minor Changes
492 |
493 | - [`ebf44cb`](https://github.com/lingodotdev/lingo.dev/commit/ebf44cbb462516abfe660c295c04627796c5a3a7) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - implement recognize locale
494 |
495 | - [`42d0a5a`](https://github.com/lingodotdev/lingo.dev/commit/42d0a5a7a53e296192a31e8f1d67c126793ea280) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - added .localizeHtml implementation to SDK
496 |
497 | ### Patch Changes
498 |
499 | - Updated dependencies [[`a6b22a3`](https://github.com/lingodotdev/lingo.dev/commit/a6b22a3237f574455d8119f914d82b0b247b4151)]:
500 | - @replexica/[email protected]
501 |
502 | ## 0.4.3
503 |
504 | ### Patch Changes
505 |
506 | - Updated dependencies [[`091ee35`](https://github.com/lingodotdev/lingo.dev/commit/091ee353081795bf8f61c7d41483bc309c7b62ef)]:
507 | - @replexica/[email protected]
508 |
509 | ## 0.4.2
510 |
511 | ### Patch Changes
512 |
513 | - Updated dependencies [[`5e282d7`](https://github.com/lingodotdev/lingo.dev/commit/5e282d7ffa5ca9494aa7046a090bb7c327085a86)]:
514 | - @replexica/[email protected]
515 |
516 | ## 0.4.1
517 |
518 | ### Patch Changes
519 |
520 | - Updated dependencies [[`0071cd6`](https://github.com/lingodotdev/lingo.dev/commit/0071cd66b1c868ad3898fc368390a628c5a67767)]:
521 | - @replexica/[email protected]
522 |
523 | ## 0.4.0
524 |
525 | ### Minor Changes
526 |
527 | - [#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`
528 |
529 | ## 0.3.4
530 |
531 | ### Patch Changes
532 |
533 | - Updated dependencies [[`2859938`](https://github.com/lingodotdev/lingo.dev/commit/28599388a91bf80cea3813bb4b8999bb4df302c9)]:
534 | - @replexica/[email protected]
535 |
536 | ## 0.3.3
537 |
538 | ### Patch Changes
539 |
540 | - Updated dependencies [[`ca9e20e`](https://github.com/lingodotdev/lingo.dev/commit/ca9e20eef9047e20d39ccf9dff74d2f6069d4676), [`2aedf3b`](https://github.com/lingodotdev/lingo.dev/commit/2aedf3bec2d9dffc7b43fc10dea0cab5742d44af), [`626082a`](https://github.com/lingodotdev/lingo.dev/commit/626082a64b88fb3b589acd950afeafe417ce5ddc)]:
541 | - @replexica/[email protected]
542 |
543 | ## 0.3.2
544 |
545 | ### Patch Changes
546 |
547 | - Updated dependencies [[`1601f70`](https://github.com/lingodotdev/lingo.dev/commit/1601f708bdf0ff1786d3bf9b19265ac5b567f740)]:
548 | - @replexica/[email protected]
549 |
550 | ## 0.3.1
551 |
552 | ### Patch Changes
553 |
554 | - Updated dependencies [[`bc5a28c`](https://github.com/lingodotdev/lingo.dev/commit/bc5a28c3c98b619872924b5f913229ac01387524)]:
555 | - @replexica/[email protected]
556 |
557 | ## 0.3.0
558 |
559 | ### Minor Changes
560 |
561 | - [#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
562 |
563 | ### Patch Changes
564 |
565 | - Updated dependencies [[`5c2ca37`](https://github.com/lingodotdev/lingo.dev/commit/5c2ca37114663eaeb529a027e33949ef3839549b)]:
566 | - @replexica/[email protected]
567 |
568 | ## 0.2.1
569 |
570 | ### Patch Changes
571 |
572 | - Updated dependencies [[`6870fc7`](https://github.com/lingodotdev/lingo.dev/commit/6870fc758dae9d1adb641576befbd8cda61cd5ea)]:
573 | - @replexica/[email protected]
574 |
575 | ## 0.2.0
576 |
577 | ### Minor Changes
578 |
579 | - [`d6e6d5c`](https://github.com/lingodotdev/lingo.dev/commit/d6e6d5c24b266de3769e95545f74632e7d75c697) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Add support for multisource localization to the CLI
580 |
581 | ### Patch Changes
582 |
583 | - Updated dependencies [[`d6e6d5c`](https://github.com/lingodotdev/lingo.dev/commit/d6e6d5c24b266de3769e95545f74632e7d75c697)]:
584 | - @replexica/[email protected]
585 |
586 | ## 0.1.1
587 |
588 | ### Patch Changes
589 |
590 | - Updated dependencies [[`73c9250`](https://github.com/lingodotdev/lingo.dev/commit/73c925084989ccea120cae1617ec87776c88e83e)]:
591 | - @replexica/[email protected]
592 |
593 | ## 0.1.0
594 |
595 | ### Minor Changes
596 |
597 | - [#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
598 |
```
--------------------------------------------------------------------------------
/packages/spec/CHANGELOG.md:
--------------------------------------------------------------------------------
```markdown
1 | # @lingo.dev/\_spec
2 |
3 | ## 0.41.1
4 |
5 | ### Patch Changes
6 |
7 | - [#1230](https://github.com/lingodotdev/lingo.dev/pull/1230) [`b45347c`](https://github.com/lingodotdev/lingo.dev/commit/b45347c38572ee371b2bc494261b7e3e90c4aed1) Thanks [@vrcprl](https://github.com/vrcprl)! - add an xcode-xcstrings-v2 bucket type that supports cldr pluralization rules
8 |
9 | ## 0.41.0
10 |
11 | ### Minor Changes
12 |
13 | - [#1186](https://github.com/lingodotdev/lingo.dev/pull/1186) [`82f5e7c`](https://github.com/lingodotdev/lingo.dev/commit/82f5e7cdde9a2a15b4c2a7fcb8c67ed64eab596b) Thanks [@davidturnbull](https://github.com/davidturnbull)! - Add Markdoc support
14 |
15 | ### Patch Changes
16 |
17 | - [#1215](https://github.com/lingodotdev/lingo.dev/pull/1215) [`e858174`](https://github.com/lingodotdev/lingo.dev/commit/e858174fd5165e0ea3e3f25fa1fc3edb292bc58f) Thanks [@vrcprl](https://github.com/vrcprl)! - add provider settings
18 |
19 | ## 0.40.4
20 |
21 | ### Patch Changes
22 |
23 | - [#1201](https://github.com/lingodotdev/lingo.dev/pull/1201) [`1fa218c`](https://github.com/lingodotdev/lingo.dev/commit/1fa218c13bf90df6d175fb18264f59c1a10b967c) Thanks [@vrcprl](https://github.com/vrcprl)! - add new languages Malayalam (India), Armenian (Armenia), Macedonian (Macedonia)
24 |
25 | ## 0.40.3
26 |
27 | ### Patch Changes
28 |
29 | - [#1192](https://github.com/lingodotdev/lingo.dev/pull/1192) [`bbc71b9`](https://github.com/lingodotdev/lingo.dev/commit/bbc71b9948ccc289c9669d8b0c276c9596f6a5e7) Thanks [@vrcprl](https://github.com/vrcprl)! - Add biome support
30 |
31 | ## 0.40.2
32 |
33 | ### Patch Changes
34 |
35 | - [#1171](https://github.com/lingodotdev/lingo.dev/pull/1171) [`6579d70`](https://github.com/lingodotdev/lingo.dev/commit/6579d70bc670c2fdc06c09842d931b07e134151c) Thanks [@vrcprl](https://github.com/vrcprl)! - add el-CY en-IE fr-LU locales
36 |
37 | ## 0.40.1
38 |
39 | ### Patch Changes
40 |
41 | - [#1016](https://github.com/lingodotdev/lingo.dev/pull/1016) [`a35032e`](https://github.com/lingodotdev/lingo.dev/commit/a35032e7e7a188d1f5e774576352068124526e24) Thanks [@davidturnbull](https://github.com/davidturnbull)! - feat: add automated config documentation generator for i18n.json schema
42 |
43 | ## 0.40.0
44 |
45 | ### Minor Changes
46 |
47 | - [#1066](https://github.com/lingodotdev/lingo.dev/pull/1066) [`6af91a0`](https://github.com/lingodotdev/lingo.dev/commit/6af91a083d16f85051fb49a4034789abe784017e) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add hints support for xcode and jsonc buckets
48 |
49 | ## 0.39.3
50 |
51 | ### Patch Changes
52 |
53 | - [#1031](https://github.com/lingodotdev/lingo.dev/pull/1031) [`afbb978`](https://github.com/lingodotdev/lingo.dev/commit/afbb978fec83d574f2c43b7d68457e435fca9b57) Thanks [@mathio](https://github.com/mathio)! - add json-dictionary loader support
54 |
55 | ## 0.39.2
56 |
57 | ### Patch Changes
58 |
59 | - [#1023](https://github.com/lingodotdev/lingo.dev/pull/1023) [`9266fd0`](https://github.com/lingodotdev/lingo.dev/commit/9266fd0bcddf4b07ca51d2609af92a9473106f9d) Thanks [@devin-ai-integration](https://github.com/apps/devin-ai-integration)! - Update Zod dependency to version 3.25.76
60 |
61 | ## 0.39.1
62 |
63 | ### Patch Changes
64 |
65 | - [#995](https://github.com/lingodotdev/lingo.dev/pull/995) [`acd5356`](https://github.com/lingodotdev/lingo.dev/commit/acd5356b68d2261576240c173fea790864c3c31d) Thanks [@devin-ai-integration](https://github.com/apps/devin-ai-integration)! - Add Icelandic (is) locale support with is-IS regional variant
66 |
67 | ## 0.39.0
68 |
69 | ### Minor Changes
70 |
71 | - [#981](https://github.com/lingodotdev/lingo.dev/pull/981) [`f644123`](https://github.com/lingodotdev/lingo.dev/commit/f644123ddf6a6254790d08af50141e4dd78c3677) Thanks [@devin-ai-integration](https://github.com/apps/devin-ai-integration)! - Add support for plain TXT files to enable translation of fastlane App Store metadata and other plain text content
72 |
73 | ## 0.38.0
74 |
75 | ### Minor Changes
76 |
77 | - [#958](https://github.com/lingodotdev/lingo.dev/pull/958) [`84fd214`](https://github.com/lingodotdev/lingo.dev/commit/84fd214a21766e7683c5d645fcb8c4c0162eb0b6) Thanks [@chrissiwaffler](https://github.com/chrissiwaffler)! - feat: add Mistral AI as a supported LLM provider
78 |
79 | - Added Mistral AI provider support across the entire lingo.dev ecosystem
80 | - Users can now use Mistral models for localization by setting MISTRAL_API_KEY
81 | - Supports all Mistral models available through the @ai-sdk/mistral package
82 | - Configuration via environment variable or user-wide config: `npx lingo.dev@latest config set llm.mistralApiKey <key>`
83 |
84 | ## 0.37.0
85 |
86 | ### Minor Changes
87 |
88 | - [#956](https://github.com/lingodotdev/lingo.dev/pull/956) [`ce8c75c`](https://github.com/lingodotdev/lingo.dev/commit/ce8c75c7fc1a2124d3e18444bc356c4dfce26434) Thanks [@VAIBHAVSING](https://github.com/VAIBHAVSING)! - feat: add EJS (Embedded JavaScript) templating engine support
89 |
90 | - Added EJS loader to support parsing and translating EJS template files
91 | - EJS loader extracts translatable text while preserving EJS tags and expressions
92 | - Updated spec package to include "ejs" in supported bucket types
93 | - Added comprehensive test suite covering various EJS scenarios including conditionals, loops, includes, and mixed content
94 | - Automatically installed EJS dependency (@types/ejs) for TypeScript support
95 |
96 | ## 0.36.0
97 |
98 | ### Minor Changes
99 |
100 | - [#913](https://github.com/lingodotdev/lingo.dev/pull/913) [`1b9b113`](https://github.com/lingodotdev/lingo.dev/commit/1b9b11301978e8caa2555832d027ff93216aa6e1) Thanks [@The-Best-Codes](https://github.com/The-Best-Codes)! - Add support for Ollama as a CLI and Compiler provider.
101 |
102 | ### Patch Changes
103 |
104 | - [#922](https://github.com/lingodotdev/lingo.dev/pull/922) [`0329a9c`](https://github.com/lingodotdev/lingo.dev/commit/0329a9cdb5e5a63fcecab4efcd7cce22f155a0e9) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add openrouter ais support for compiler
105 |
106 | ## 0.35.0
107 |
108 | ### Minor Changes
109 |
110 | - [#897](https://github.com/lingodotdev/lingo.dev/pull/897) [`a5da697`](https://github.com/lingodotdev/lingo.dev/commit/a5da697f7efd46de31d17b202d06eb5f655ed9b9) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Add support for other providers in the compiler and implement Google AI as a provider.
111 |
112 | ## 0.34.0
113 |
114 | ### Minor Changes
115 |
116 | - [`e980e84`](https://github.com/lingodotdev/lingo.dev/commit/e980e84178439ad70417d38b425acf9148cfc4b6) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - added the compiler
117 |
118 | ## 0.33.3
119 |
120 | ### Patch Changes
121 |
122 | - [#805](https://github.com/lingodotdev/lingo.dev/pull/805) [`0272fbf`](https://github.com/lingodotdev/lingo.dev/commit/0272fbf8847240ed9453130237d5843b918f869f) Thanks [@Vicentesan](https://github.com/Vicentesan)! - Introduce the gregorian language (ka-GE)
123 |
124 | ## 0.33.2
125 |
126 | ### Patch Changes
127 |
128 | - [#782](https://github.com/lingodotdev/lingo.dev/pull/782) [`d913c20`](https://github.com/lingodotdev/lingo.dev/commit/d913c20fdf0086741c8b50fd4ddfb38eae304a24) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - parallel processing
129 |
130 | ## 0.33.1
131 |
132 | ### Patch Changes
133 |
134 | - [#778](https://github.com/lingodotdev/lingo.dev/pull/778) [`3f2aba9`](https://github.com/lingodotdev/lingo.dev/commit/3f2aba9c1d5834faf89a26194f1f3d9f9b878d40) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add ignoredKeys
135 |
136 | ## 0.33.0
137 |
138 | ### Minor Changes
139 |
140 | - [#759](https://github.com/lingodotdev/lingo.dev/pull/759) [`9aa7004`](https://github.com/lingodotdev/lingo.dev/commit/9aa700491446865dc131b80419f681132b888652) Thanks [@devin-ai-integration](https://github.com/apps/devin-ai-integration)! - Enhance TypeScript loader to support nested fields and arrays
141 |
142 | ## 0.32.0
143 |
144 | ### Minor Changes
145 |
146 | - [#757](https://github.com/lingodotdev/lingo.dev/pull/757) [`5170449`](https://github.com/lingodotdev/lingo.dev/commit/517044905dfc682d6a5fa95b0605b8715e2b72c7) Thanks [@devin-ai-integration](https://github.com/apps/devin-ai-integration)! - Add TypeScript loader for .ts files that extracts string literals from default exports
147 |
148 | ## 0.31.0
149 |
150 | ### Minor Changes
151 |
152 | - [#700](https://github.com/lingodotdev/lingo.dev/pull/700) [`c5ccf81`](https://github.com/lingodotdev/lingo.dev/commit/c5ccf81e9c2bd27bae332306da2a41e41bbeb87d) Thanks [@devin-ai-integration](https://github.com/apps/devin-ai-integration)! - Add support for locked patterns in MDX loader
153 |
154 | This change adds support for preserving specific patterns in MDX files during translation, including:
155 |
156 | - !params syntax for parameter documentation
157 | - !! parameter_name headings
158 | - !type declarations
159 | - !required flags
160 | - !values lists
161 |
162 | The implementation adds a new config version 1.7 with a "lockedPatterns" field that accepts an array of regex patterns to be preserved during translation.
163 |
164 | ## 0.30.3
165 |
166 | ### Patch Changes
167 |
168 | - [#649](https://github.com/lingodotdev/lingo.dev/pull/649) [`409018d`](https://github.com/lingodotdev/lingo.dev/commit/409018de74614a1fd99363c6749b0e4be9e1a278) Thanks [@mathio](https://github.com/mathio)! - refactor dependencies
169 |
170 | ## 0.30.2
171 |
172 | ### Patch Changes
173 |
174 | - [#647](https://github.com/lingodotdev/lingo.dev/pull/647) [`235b6d9`](https://github.com/lingodotdev/lingo.dev/commit/235b6d914c5f542ee5f1a8a88085cfd9dea5409e) Thanks [@mathio](https://github.com/mathio)! - update vitest
175 |
176 | ## 0.30.1
177 |
178 | ### Patch Changes
179 |
180 | - [#645](https://github.com/lingodotdev/lingo.dev/pull/645) [`d824b10`](https://github.com/lingodotdev/lingo.dev/commit/d824b106631f45fc428cf01f733aab4842b4fa81) Thanks [@mathio](https://github.com/mathio)! - update dependencies
181 |
182 | ## 0.30.0
183 |
184 | ### Minor Changes
185 |
186 | - [#631](https://github.com/lingodotdev/lingo.dev/pull/631) [`82efe61`](https://github.com/lingodotdev/lingo.dev/commit/82efe6176db12cc7c5bbeb84f38bc3261f9eec4f) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - double formatting for mdx
187 |
188 | - [#631](https://github.com/lingodotdev/lingo.dev/pull/631) [`82efe61`](https://github.com/lingodotdev/lingo.dev/commit/82efe6176db12cc7c5bbeb84f38bc3261f9eec4f) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - advanced mdx support (shout out to @ZYJLiu!)
189 |
190 | ## 0.29.0
191 |
192 | ### Minor Changes
193 |
194 | - [#629](https://github.com/lingodotdev/lingo.dev/pull/629) [`58f3959`](https://github.com/lingodotdev/lingo.dev/commit/58f39599b3b765ad807e725b4089a5e9b11a01b2) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - advanced mdx support (shout out to @ZYJLiu!)
195 |
196 | ## 0.28.0
197 |
198 | ### Minor Changes
199 |
200 | - [#627](https://github.com/lingodotdev/lingo.dev/pull/627) [`fe922a4`](https://github.com/lingodotdev/lingo.dev/commit/fe922a469c2d5dac23a909a4fb67a6efd56d80d6) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add support for json/yaml key locking
201 |
202 | ## 0.27.0
203 |
204 | ### Minor Changes
205 |
206 | - [#614](https://github.com/lingodotdev/lingo.dev/pull/614) [`2495afd`](https://github.com/lingodotdev/lingo.dev/commit/2495afd69e23700f96e19e5bbf74e393b29c2033) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add basic translators
207 |
208 | ### Patch Changes
209 |
210 | - [#616](https://github.com/lingodotdev/lingo.dev/pull/616) [`516a79c`](https://github.com/lingodotdev/lingo.dev/commit/516a79c75501c5960ae944379f38591806ca43e2) Thanks [@mathio](https://github.com/mathio)! - po files --frozen flag
211 |
212 | - [`2cc6114`](https://github.com/lingodotdev/lingo.dev/commit/2cc61140fccc69ab73d40c7802a2d0e018889475) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add Welsh language support
213 |
214 | ## 0.26.6
215 |
216 | ### Patch Changes
217 |
218 | - [#605](https://github.com/lingodotdev/lingo.dev/pull/605) [`1dbbfd2`](https://github.com/lingodotdev/lingo.dev/commit/1dbbfd2ed9f5a7e0479dc83f700fb68ee5347a18) Thanks [@mathio](https://github.com/mathio)! - inject locale
219 |
220 | ## 0.26.5
221 |
222 | ### Patch Changes
223 |
224 | - [#596](https://github.com/lingodotdev/lingo.dev/pull/596) [`61b487e`](https://github.com/lingodotdev/lingo.dev/commit/61b487e1e059328a32c3cdf673255d9d2cd480d9) Thanks [@vrcprl](https://github.com/vrcprl)! - add new locale
225 |
226 | ## 0.26.4
227 |
228 | ### Patch Changes
229 |
230 | - [#584](https://github.com/lingodotdev/lingo.dev/pull/584) [`743d93e`](https://github.com/lingodotdev/lingo.dev/commit/743d93e554841bbd96d23682d8aec63cb4eb3ec8) Thanks [@khalatevarun](https://github.com/khalatevarun)! - Add unit test for utility function in locales.ts
231 |
232 | ## 0.26.3
233 |
234 | ### Patch Changes
235 |
236 | - [#553](https://github.com/lingodotdev/lingo.dev/pull/553) [`95023f2`](https://github.com/lingodotdev/lingo.dev/commit/95023f2c8da3958e8582628a22bf40674f8d2317) Thanks [@vrcprl](https://github.com/vrcprl)! - Add new locales
237 |
238 | ## 0.26.2
239 |
240 | ### Patch Changes
241 |
242 | - [#546](https://github.com/lingodotdev/lingo.dev/pull/546) [`9089b08`](https://github.com/lingodotdev/lingo.dev/commit/9089b085b968ff3195866e377ecf3016aa06f959) Thanks [@mathio](https://github.com/mathio)! - add helper method to spec
243 |
244 | ## 0.26.1
245 |
246 | ### Patch Changes
247 |
248 | - [`0b48be1`](https://github.com/lingodotdev/lingo.dev/commit/0b48be197e88dac581cc4f257789a04b43acf932) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add Kinyarwanda and Kiswahili
249 |
250 | ## 0.26.0
251 |
252 | ### Minor Changes
253 |
254 | - [#530](https://github.com/lingodotdev/lingo.dev/pull/530) [`bafa755`](https://github.com/lingodotdev/lingo.dev/commit/bafa755d9681e93741462eb7bcf9b85073d20fd7) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Add Kazakh (Kazakhstan) locale (localization engine passed the benchmarks!)
255 |
256 | ## 0.25.3
257 |
258 | ### Patch Changes
259 |
260 | - [#518](https://github.com/lingodotdev/lingo.dev/pull/518) [`444a731`](https://github.com/lingodotdev/lingo.dev/commit/444a7319a1351e22e5666504169023b4c8a29d5f) Thanks [@mathio](https://github.com/mathio)! - support JSON messages in <i18n> block of .vue files
261 |
262 | ## 0.25.2
263 |
264 | ### Patch Changes
265 |
266 | - [#498](https://github.com/lingodotdev/lingo.dev/pull/498) [`ec2902e`](https://github.com/lingodotdev/lingo.dev/commit/ec2902e5dc31fd79cc3b6fbf478ed1f3c4df0345) Thanks [@mathio](https://github.com/mathio)! - build json schema for config
267 |
268 | ## 0.25.1
269 |
270 | ### Patch Changes
271 |
272 | - [#496](https://github.com/lingodotdev/lingo.dev/pull/496) [`beb0541`](https://github.com/lingodotdev/lingo.dev/commit/beb05411ee459461e05801a763b1fa28d288e04e) Thanks [@mathio](https://github.com/mathio)! - po files
273 |
274 | ## 0.25.0
275 |
276 | ### Minor Changes
277 |
278 | - [#485](https://github.com/lingodotdev/lingo.dev/pull/485) [`a096300`](https://github.com/lingodotdev/lingo.dev/commit/a0963008ea2a8bbc910b0eaeb20f4e3b3cd641a7) Thanks [@mathio](https://github.com/mathio)! - add support for php buckets
279 |
280 | ## 0.24.4
281 |
282 | ### Patch Changes
283 |
284 | - [#473](https://github.com/lingodotdev/lingo.dev/pull/473) [`3a99763`](https://github.com/lingodotdev/lingo.dev/commit/3a99763087512ba82955303d6f0567e813f4fa05) Thanks [@vrcprl](https://github.com/vrcprl)! - add new locales
285 |
286 | ## 0.24.3
287 |
288 | ### Patch Changes
289 |
290 | - [`dc8bfc7`](https://github.com/lingodotdev/lingo.dev/commit/dc8bfc7ddc38ade768b8aa11c56669db7eb446e6) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - publish deps
291 |
292 | ## 0.24.2
293 |
294 | ### Patch Changes
295 |
296 | - [`6281dbd`](https://github.com/lingodotdev/lingo.dev/commit/6281dbd96bd5cfe54f194a6a1d055c8255a250de) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - fix sdk/spec exported types
297 |
298 | ## 0.24.1
299 |
300 | ### Patch Changes
301 |
302 | - [#419](https://github.com/lingodotdev/lingo.dev/pull/419) [`a45feb1`](https://github.com/lingodotdev/lingo.dev/commit/a45feb1d747f8fa32c42c1726953a04c174e754a) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Replexica is now Lingo.dev! 🎉
303 |
304 | ## 0.24.0
305 |
306 | ### Minor Changes
307 |
308 | - [`003344f`](https://github.com/lingodotdev/lingo.dev/commit/003344ffcca98a391a298707f18476971c4d4c2b) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add locale delimiter override
309 |
310 | ## 0.23.0
311 |
312 | ### Minor Changes
313 |
314 | - [#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 explicit regional flag support
315 |
316 | ## 0.22.1
317 |
318 | ### Patch Changes
319 |
320 | - [#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
321 |
322 | ## 0.22.0
323 |
324 | ### Minor Changes
325 |
326 | - [#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
327 |
328 | ## 0.21.1
329 |
330 | ### Patch Changes
331 |
332 | - [`58d7b35`](https://github.com/lingodotdev/lingo.dev/commit/58d7b3567e51cc3ef0fad0288c13451381b95a98) Thanks [@vrcprl](https://github.com/vrcprl)! - Added Telugu (India) locale
333 |
334 | ## 0.21.0
335 |
336 | ### Minor Changes
337 |
338 | - [#327](https://github.com/lingodotdev/lingo.dev/pull/327) [`3ab5de6`](https://github.com/lingodotdev/lingo.dev/commit/3ab5de66d8a913297b46095c2e73823124cc8c5b) Thanks [@partik03](https://github.com/partik03)! - added support for xliff loader
339 |
340 | ### Patch Changes
341 |
342 | - [`9cf5299`](https://github.com/lingodotdev/lingo.dev/commit/9cf5299f7efbef70fd83f95177eac49b4d8f8007) Thanks [@vrcprl](https://github.com/vrcprl)! - Add Tagalog
343 |
344 | ## 0.20.0
345 |
346 | ### Minor Changes
347 |
348 | - [`1556977`](https://github.com/lingodotdev/lingo.dev/commit/1556977332a6f949100283bfa8c9a9ff5e74b156) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add new locales
349 |
350 | ## 0.19.0
351 |
352 | ### Minor Changes
353 |
354 | - [`5cb3c93`](https://github.com/lingodotdev/lingo.dev/commit/5cb3c930fff6e30cff5cc2266b794f75a0db646d) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - added Latin / Cyrilic modifiers for Serbian
355 |
356 | ## 0.18.0
357 |
358 | ### Minor Changes
359 |
360 | - [#300](https://github.com/lingodotdev/lingo.dev/pull/300) [`a6b22a3`](https://github.com/lingodotdev/lingo.dev/commit/a6b22a3237f574455d8119f914d82b0b247b4151) Thanks [@partik03](https://github.com/partik03)! - implemented srt file loader and added support for srt file format in spec
361 |
362 | ## 0.17.0
363 |
364 | ### Minor Changes
365 |
366 | - [#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
367 |
368 | ## 0.16.0
369 |
370 | ### Minor Changes
371 |
372 | - [#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
373 |
374 | ## 0.15.0
375 |
376 | ### Minor Changes
377 |
378 | - [`0071cd6`](https://github.com/lingodotdev/lingo.dev/commit/0071cd66b1c868ad3898fc368390a628c5a67767) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add csv format support
379 |
380 | ## 0.14.1
381 |
382 | ### Patch Changes
383 |
384 | - [`2859938`](https://github.com/lingodotdev/lingo.dev/commit/28599388a91bf80cea3813bb4b8999bb4df302c9) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add missing locales
385 |
386 | ## 0.14.0
387 |
388 | ### Minor Changes
389 |
390 | - [`ca9e20e`](https://github.com/lingodotdev/lingo.dev/commit/ca9e20eef9047e20d39ccf9dff74d2f6069d4676) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - .strings support
391 |
392 | - [`2aedf3b`](https://github.com/lingodotdev/lingo.dev/commit/2aedf3bec2d9dffc7b43fc10dea0cab5742d44af) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - added support for .stringsdict
393 |
394 | - [`626082a`](https://github.com/lingodotdev/lingo.dev/commit/626082a64b88fb3b589acd950afeafe417ce5ddc) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - added Flutter .arb support
395 |
396 | ## 0.13.0
397 |
398 | ### Minor Changes
399 |
400 | - [#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
401 |
402 | ## 0.12.1
403 |
404 | ### Patch Changes
405 |
406 | - [`bc5a28c`](https://github.com/lingodotdev/lingo.dev/commit/bc5a28c3c98b619872924b5f913229ac01387524) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Fix spec imports
407 |
408 | ## 0.12.0
409 |
410 | ### Minor Changes
411 |
412 | - [#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
413 |
414 | ## 0.11.0
415 |
416 | ### Minor Changes
417 |
418 | - [`6870fc7`](https://github.com/lingodotdev/lingo.dev/commit/6870fc758dae9d1adb641576befbd8cda61cd5ea) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Fix version number bumping in 1.2 config autoupgrade
419 |
420 | ## 0.10.0
421 |
422 | ### Minor Changes
423 |
424 | - [`d6e6d5c`](https://github.com/lingodotdev/lingo.dev/commit/d6e6d5c24b266de3769e95545f74632e7d75c697) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Add support for multisource localization to the CLI
425 |
426 | ## 0.9.0
427 |
428 | ### Minor Changes
429 |
430 | - [#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
431 |
432 | ## 0.8.0
433 |
434 | ### Minor Changes
435 |
436 | - [`8c8e7dd`](https://github.com/lingodotdev/lingo.dev/commit/8c8e7dd4d35669d484240d643427612ecdaf73eb) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Added new locales
437 |
438 | ## 0.7.0
439 |
440 | ### Minor Changes
441 |
442 | - [`c0be1a2`](https://github.com/lingodotdev/lingo.dev/commit/c0be1a29e3069ef2c8bdc4e4f52d2fb17abdb1f5) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Replaced `replexica config` with `replexica show config`. Added `replexica show locale sources` and `replexica show locale targets`.
443 |
444 | ## 0.6.0
445 |
446 | ### Minor Changes
447 |
448 | - [`10252ce`](https://github.com/lingodotdev/lingo.dev/commit/10252ceaa2685cc23f4dbeb6ac985cc2148853e2) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Add android support
449 |
450 | ## 0.5.1
451 |
452 | ### Patch Changes
453 |
454 | - [`088de18`](https://github.com/lingodotdev/lingo.dev/commit/088de18a53f45fa8df5833fe81ed96a2ed231299) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Fix @replexica/config reference
455 |
456 | ## 0.5.0
457 |
458 | ### Minor Changes
459 |
460 | - [#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.
461 |
462 | ## 0.4.1
463 |
464 | ### Patch Changes
465 |
466 | - [#94](https://github.com/lingodotdev/lingo.dev/pull/94) [`abab45c`](https://github.com/lingodotdev/lingo.dev/commit/abab45cc91675f507499bf84350b080cd647c464) Thanks [@vrcprl](https://github.com/vrcprl)! - Locales mapping (ex. en -> en-US)
467 |
468 | ## 0.4.0
469 |
470 | ### Minor Changes
471 |
472 | - [#87](https://github.com/lingodotdev/lingo.dev/pull/87) [`07657c6`](https://github.com/lingodotdev/lingo.dev/commit/07657c611306797d605718e13ce6b2c920a5a94e) Thanks [@vrcprl](https://github.com/vrcprl)! - added new core locales : ja de pt it ru uk hi zh ko tr ar and source locales yue pl sk th
473 |
474 | ## 0.3.0
475 |
476 | ### Minor Changes
477 |
478 | - [`830d4a4`](https://github.com/lingodotdev/lingo.dev/commit/830d4a441c4d1177c9356756a9e9afc170a386d6) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add support for shyriiwook language
479 |
480 | ## 0.2.0
481 |
482 | ### Minor Changes
483 |
484 | - [#76](https://github.com/lingodotdev/lingo.dev/pull/76) [`69d487c`](https://github.com/lingodotdev/lingo.dev/commit/69d487c0b4c8e22f9c86867ebf6cc55ea2875dbf) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - enable french, catalan in source/target mode, and czech in source-only mode
485 |
486 | ## 0.1.0
487 |
488 | ### Minor Changes
489 |
490 | - [#73](https://github.com/lingodotdev/lingo.dev/pull/73) [`94ab265`](https://github.com/lingodotdev/lingo.dev/commit/94ab26551577b5dfab629ffee3c82e59b56ce25d) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - intro a `@replexica/spec` package containing common definitions, constants, schemas, and types
491 |
492 | - [#75](https://github.com/lingodotdev/lingo.dev/pull/75) [`b11b48e`](https://github.com/lingodotdev/lingo.dev/commit/b11b48e7c3ab05dd8de0ddcfe5cb4589786abbf9) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - framework-agnostic i18n support
493 |
```
--------------------------------------------------------------------------------
/packages/cli/src/cli/cmd/status.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 { CLIError } from "../utils/errors";
14 | import Ora from "ora";
15 | import createBucketLoader from "../loaders";
16 | import { createAuthenticator } from "../utils/auth";
17 | import { getBuckets } from "../utils/buckets";
18 | import chalk from "chalk";
19 | import Table from "cli-table3";
20 | import { createDeltaProcessor } from "../utils/delta";
21 | import trackEvent from "../utils/observability";
22 | import { minimatch } from "minimatch";
23 | import { exitGracefully } from "../utils/exit-gracefully";
24 |
25 | // Define types for our language stats
26 | interface LanguageStats {
27 | complete: number;
28 | missing: number;
29 | updated: number;
30 | words: number;
31 | }
32 |
33 | export default new Command()
34 | .command("status")
35 | .description("Show the status of the localization process")
36 | .helpOption("-h, --help", "Show help")
37 | .option(
38 | "--locale <locale>",
39 | "Limit the report to specific target locales from i18n.json. Repeat the flag to include multiple locales. Defaults to all configured target locales",
40 | (val: string, prev: string[]) => (prev ? [...prev, val] : [val]),
41 | )
42 | .option(
43 | "--bucket <bucket>",
44 | "Limit the report 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",
45 | (val: string, prev: string[]) => (prev ? [...prev, val] : [val]),
46 | )
47 | .option(
48 | "--file [files...]",
49 | "Filter the status report to only include files whose paths contain these substrings. Example: 'components' to match any file path containing 'components'",
50 | )
51 | .option(
52 | "--force",
53 | "Force all keys to be counted as needing translation, bypassing change detection. Shows word estimates for a complete retranslation regardless of current translation status",
54 | )
55 | .option(
56 | "--verbose",
57 | "Print detailed output showing missing and updated key counts with example key names for each file and locale",
58 | )
59 | .option(
60 | "--api-key <api-key>",
61 | "Override the API key from settings or environment variables for this run",
62 | )
63 | .action(async function (options) {
64 | const ora = Ora();
65 | const flags = parseFlags(options);
66 | let authId: string | null = null;
67 |
68 | try {
69 | ora.start("Loading configuration...");
70 | const i18nConfig = getConfig();
71 | const settings = getSettings(flags.apiKey);
72 | ora.succeed("Configuration loaded");
73 |
74 | // Try to authenticate, but continue even if not authenticated
75 | try {
76 | ora.start("Checking authentication status...");
77 | const auth = await tryAuthenticate(settings);
78 | if (auth) {
79 | authId = auth.id;
80 | ora.succeed(`Authenticated as ${auth.email}`);
81 | } else {
82 | ora.info(
83 | "Not authenticated. Continuing without authentication. (Run `lingo.dev login` to authenticate)",
84 | );
85 | }
86 | } catch (error) {
87 | ora.info("Authentication failed. Continuing without authentication.");
88 | }
89 |
90 | ora.start("Validating localization configuration...");
91 | validateParams(i18nConfig, flags);
92 | ora.succeed("Localization configuration is valid");
93 |
94 | // Track event with or without authentication
95 | trackEvent(authId || "status", "cmd.status.start", {
96 | i18nConfig,
97 | flags,
98 | });
99 |
100 | let buckets = getBuckets(i18nConfig!);
101 | if (flags.bucket?.length) {
102 | buckets = buckets.filter((bucket: any) =>
103 | flags.bucket!.includes(bucket.type),
104 | );
105 | }
106 | ora.succeed("Buckets retrieved");
107 |
108 | if (flags.file?.length) {
109 | buckets = buckets
110 | .map((bucket: any) => {
111 | const paths = bucket.paths.filter((path: any) =>
112 | flags.file!.find(
113 | (file) =>
114 | path.pathPattern?.includes(file) ||
115 | path.pathPattern?.match(file) ||
116 | minimatch(path.pathPattern, file),
117 | ),
118 | );
119 | return { ...bucket, paths };
120 | })
121 | .filter((bucket: any) => bucket.paths.length > 0);
122 | if (buckets.length === 0) {
123 | ora.fail(
124 | "No buckets found. All buckets were filtered out by --file option.",
125 | );
126 | process.exit(1);
127 | } else {
128 | ora.info(`\x1b[36mProcessing only filtered buckets:\x1b[0m`);
129 | buckets.map((bucket: any) => {
130 | ora.info(` ${bucket.type}:`);
131 | bucket.paths.forEach((path: any) => {
132 | ora.info(` - ${path.pathPattern}`);
133 | });
134 | });
135 | }
136 | }
137 |
138 | const targetLocales = flags.locale?.length
139 | ? flags.locale
140 | : i18nConfig!.locale.targets;
141 |
142 | // Global stats
143 | let totalSourceKeyCount = 0;
144 | let uniqueKeysToTranslate = 0;
145 | let totalExistingTranslations = 0;
146 | const totalWordCount = new Map<string, number>(); // Words per language
147 | const languageStats: Record<string, LanguageStats> = {};
148 |
149 | // Initialize per-language stats
150 | for (const locale of targetLocales) {
151 | languageStats[locale] = {
152 | complete: 0,
153 | missing: 0,
154 | updated: 0,
155 | words: 0,
156 | };
157 | totalWordCount.set(locale, 0);
158 | }
159 |
160 | // Per-file stats
161 | const fileStats: Record<
162 | string,
163 | {
164 | path: string;
165 | sourceKeys: number;
166 | wordCount: number;
167 | languageStats: Record<
168 | string,
169 | {
170 | complete: number;
171 | missing: number;
172 | updated: number;
173 | words: number;
174 | }
175 | >;
176 | }
177 | > = {};
178 |
179 | // Process each bucket
180 | for (const bucket of buckets) {
181 | try {
182 | console.log();
183 | ora.info(`Analyzing bucket: ${bucket.type}`);
184 |
185 | for (const bucketPath of bucket.paths) {
186 | const bucketOra = Ora({ indent: 2 }).info(
187 | `Analyzing path: ${bucketPath.pathPattern}`,
188 | );
189 |
190 | const sourceLocale = resolveOverriddenLocale(
191 | i18nConfig!.locale.source,
192 | bucketPath.delimiter,
193 | );
194 | const bucketLoader = createBucketLoader(
195 | bucket.type,
196 | bucketPath.pathPattern,
197 | {
198 | defaultLocale: sourceLocale,
199 | injectLocale: bucket.injectLocale,
200 | formatter: i18nConfig!.formatter,
201 | },
202 | bucket.lockedKeys,
203 | bucket.lockedPatterns,
204 | bucket.ignoredKeys,
205 | );
206 |
207 | bucketLoader.setDefaultLocale(sourceLocale);
208 | await bucketLoader.init();
209 |
210 | // Initialize file stats
211 | const filePath = bucketPath.pathPattern;
212 | if (!fileStats[filePath]) {
213 | fileStats[filePath] = {
214 | path: filePath,
215 | sourceKeys: 0,
216 | wordCount: 0,
217 | languageStats: {},
218 | };
219 |
220 | for (const locale of targetLocales) {
221 | fileStats[filePath].languageStats[locale] = {
222 | complete: 0,
223 | missing: 0,
224 | updated: 0,
225 | words: 0,
226 | };
227 | }
228 | }
229 |
230 | // Get source data and count source keys
231 | const sourceData = await bucketLoader.pull(sourceLocale);
232 | const sourceKeys = Object.keys(sourceData);
233 | fileStats[filePath].sourceKeys = sourceKeys.length;
234 | totalSourceKeyCount += sourceKeys.length;
235 |
236 | // Calculate source word count
237 | let sourceWordCount = 0;
238 | for (const key of sourceKeys) {
239 | const value = sourceData[key];
240 | if (typeof value === "string") {
241 | const words = value.trim().split(/\s+/).length;
242 | sourceWordCount += words;
243 | }
244 | }
245 | fileStats[filePath].wordCount = sourceWordCount;
246 |
247 | // Process each target locale
248 | for (const _targetLocale of targetLocales) {
249 | const targetLocale = resolveOverriddenLocale(
250 | _targetLocale,
251 | bucketPath.delimiter,
252 | );
253 | bucketOra.start(
254 | `[${sourceLocale} -> ${targetLocale}] Analyzing translation status...`,
255 | );
256 |
257 | let targetData = {};
258 | let fileExists = true;
259 |
260 | try {
261 | targetData = await bucketLoader.pull(targetLocale);
262 | } catch (error) {
263 | fileExists = false;
264 | bucketOra.info(
265 | `[${sourceLocale} -> ${targetLocale}] Target file not found, assuming all keys need translation.`,
266 | );
267 | }
268 |
269 | if (!fileExists) {
270 | // All keys are missing for this locale
271 | fileStats[filePath].languageStats[_targetLocale].missing =
272 | sourceKeys.length;
273 | fileStats[filePath].languageStats[_targetLocale].words =
274 | sourceWordCount;
275 | languageStats[_targetLocale].missing += sourceKeys.length;
276 | languageStats[_targetLocale].words += sourceWordCount;
277 | totalWordCount.set(
278 | _targetLocale,
279 | (totalWordCount.get(_targetLocale) || 0) + sourceWordCount,
280 | );
281 |
282 | bucketOra.succeed(
283 | `[${sourceLocale} -> ${targetLocale}] ${chalk.red(
284 | `0% complete`,
285 | )} (0/${sourceKeys.length} keys) - file not found`,
286 | );
287 | continue;
288 | }
289 |
290 | // Calculate delta for existing file
291 | const deltaProcessor = createDeltaProcessor(
292 | bucketPath.pathPattern,
293 | );
294 | const checksums = await deltaProcessor.loadChecksums();
295 | const delta = await deltaProcessor.calculateDelta({
296 | sourceData,
297 | targetData,
298 | checksums,
299 | });
300 |
301 | const missingKeys = delta.added;
302 | const updatedKeys = delta.updated;
303 | const completeKeys = sourceKeys.filter(
304 | (key) =>
305 | !missingKeys.includes(key) && !updatedKeys.includes(key),
306 | );
307 |
308 | // Count words that need translation
309 | let wordsToTranslate = 0;
310 | const keysToProcess = flags.force
311 | ? sourceKeys
312 | : [...missingKeys, ...updatedKeys];
313 |
314 | for (const key of keysToProcess) {
315 | const value = sourceData[String(key)];
316 | if (typeof value === "string") {
317 | const words = value.trim().split(/\s+/).length;
318 | wordsToTranslate += words;
319 | }
320 | }
321 |
322 | // Update file stats
323 | fileStats[filePath].languageStats[_targetLocale].missing =
324 | missingKeys.length;
325 | fileStats[filePath].languageStats[_targetLocale].updated =
326 | updatedKeys.length;
327 | fileStats[filePath].languageStats[_targetLocale].complete =
328 | completeKeys.length;
329 | fileStats[filePath].languageStats[_targetLocale].words =
330 | wordsToTranslate;
331 |
332 | // Update global stats
333 | languageStats[_targetLocale].missing += missingKeys.length;
334 | languageStats[_targetLocale].updated += updatedKeys.length;
335 | languageStats[_targetLocale].complete += completeKeys.length;
336 | languageStats[_targetLocale].words += wordsToTranslate;
337 | totalWordCount.set(
338 | _targetLocale,
339 | (totalWordCount.get(_targetLocale) || 0) + wordsToTranslate,
340 | );
341 |
342 | // Display progress
343 | const totalKeysInFile = sourceKeys.length;
344 | const completionPercent = (
345 | (completeKeys.length / totalKeysInFile) *
346 | 100
347 | ).toFixed(1);
348 |
349 | if (missingKeys.length === 0 && updatedKeys.length === 0) {
350 | bucketOra.succeed(
351 | `[${sourceLocale} -> ${targetLocale}] ${chalk.green(
352 | `100% complete`,
353 | )} (${completeKeys.length}/${totalKeysInFile} keys)`,
354 | );
355 | } else {
356 | const message = `[${sourceLocale} -> ${targetLocale}] ${
357 | parseFloat(completionPercent) > 50
358 | ? chalk.yellow(`${completionPercent}% complete`)
359 | : chalk.red(`${completionPercent}% complete`)
360 | } (${completeKeys.length}/${totalKeysInFile} keys)`;
361 |
362 | bucketOra.succeed(message);
363 |
364 | if (flags.verbose) {
365 | if (missingKeys.length > 0) {
366 | console.log(
367 | ` ${chalk.red(`Missing:`)} ${missingKeys.length} keys, ~${wordsToTranslate} words`,
368 | );
369 | console.log(
370 | ` ${chalk.red(`Missing:`)} ${
371 | missingKeys.length
372 | } keys, ~${wordsToTranslate} words`,
373 | );
374 | console.log(
375 | ` ${chalk.dim(
376 | `Example missing: ${missingKeys
377 | .slice(0, 2)
378 | .join(", ")}${missingKeys.length > 2 ? "..." : ""}`,
379 | )}`,
380 | );
381 | }
382 | if (updatedKeys.length > 0) {
383 | console.log(
384 | ` ${chalk.yellow(`Updated:`)} ${
385 | updatedKeys.length
386 | } keys that changed in source`,
387 | );
388 | }
389 | }
390 | }
391 | }
392 | }
393 | } catch (error: any) {
394 | ora.fail(`Failed to analyze bucket ${bucket.type}: ${error.message}`);
395 | }
396 | }
397 |
398 | // Calculate unique keys needing translation and keys fully translated
399 | // Count unique keys that need translation
400 | const totalKeysNeedingTranslation = Object.values(languageStats).reduce(
401 | (sum, stats) => {
402 | return sum + stats.missing + stats.updated;
403 | },
404 | 0,
405 | );
406 |
407 | // Calculate keys that are completely translated
408 | const totalCompletedKeys =
409 | totalSourceKeyCount -
410 | totalKeysNeedingTranslation / targetLocales.length;
411 |
412 | // Summary output
413 | console.log();
414 | ora.succeed(chalk.green(`Localization status completed.`));
415 |
416 | // Create a visually impactful main header
417 | console.log(chalk.bold.cyan(`\n╔════════════════════════════════════╗`));
418 | console.log(chalk.bold.cyan(`║ LOCALIZATION STATUS REPORT ║`));
419 | console.log(chalk.bold.cyan(`╚════════════════════════════════════╝`));
420 |
421 | // Source content overview
422 | console.log(chalk.bold(`\n📝 SOURCE CONTENT:`));
423 | console.log(
424 | `• Source language: ${chalk.green(i18nConfig!.locale.source)}`,
425 | );
426 | console.log(
427 | `• Source keys: ${chalk.yellow(
428 | totalSourceKeyCount.toString(),
429 | )} keys across all files`,
430 | );
431 |
432 | // Create a language-by-language breakdown table
433 | console.log(chalk.bold(`\n🌐 LANGUAGE BY LANGUAGE BREAKDOWN:`));
434 |
435 | // Create a new table instance with cli-table3
436 | const table = new Table({
437 | head: [
438 | "Language",
439 | "Status",
440 | "Complete",
441 | "Missing",
442 | "Updated",
443 | "Total Keys",
444 | "Words to Translate",
445 | ],
446 | style: {
447 | head: ["white"], // White color for headers
448 | border: [], // No color for borders
449 | },
450 | colWidths: [12, 20, 18, 12, 12, 12, 15], // Explicit column widths, making Status column wider
451 | });
452 |
453 | // Data rows
454 | let totalWordsToTranslate = 0;
455 | for (const locale of targetLocales) {
456 | const stats = languageStats[locale];
457 | const percentComplete = (
458 | (stats.complete / totalSourceKeyCount) *
459 | 100
460 | ).toFixed(1);
461 | const totalNeeded = stats.missing + stats.updated;
462 |
463 | // Determine status text and color
464 | let statusText;
465 | let statusColor;
466 | if (stats.missing === totalSourceKeyCount) {
467 | statusText = "🔴 Not started";
468 | statusColor = chalk.red;
469 | } else if (stats.missing === 0 && stats.updated === 0) {
470 | statusText = "✅ Complete";
471 | statusColor = chalk.green;
472 | } else if (parseFloat(percentComplete) > 80) {
473 | statusText = "🟡 Almost done";
474 | statusColor = chalk.yellow;
475 | } else if (parseFloat(percentComplete) > 0) {
476 | statusText = "🟠 In progress";
477 | statusColor = chalk.yellow;
478 | } else {
479 | statusText = "🔴 Not started";
480 | statusColor = chalk.red;
481 | }
482 |
483 | // Create row data
484 | const words = totalWordCount.get(locale) || 0;
485 | totalWordsToTranslate += words;
486 |
487 | // Add row to the table
488 | table.push([
489 | locale,
490 | statusColor(statusText),
491 | `${stats.complete}/${totalSourceKeyCount} (${percentComplete}%)`,
492 | stats.missing > 0 ? chalk.red(stats.missing.toString()) : "0",
493 | stats.updated > 0 ? chalk.yellow(stats.updated.toString()) : "0",
494 | totalNeeded > 0 ? chalk.magenta(totalNeeded.toString()) : "0",
495 | words > 0 ? `~${words.toLocaleString()}` : "0",
496 | ]);
497 | }
498 |
499 | // Display the table
500 | console.log(table.toString());
501 |
502 | // Total usage summary
503 | console.log(chalk.bold(`\n📊 USAGE ESTIMATE:`));
504 | console.log(
505 | `• WORDS TO BE CONSUMED: ~${chalk.yellow.bold(
506 | totalWordsToTranslate.toLocaleString(),
507 | )} words across all languages`,
508 | );
509 | console.log(
510 | ` (Words are counted from source language for keys that need translation in target languages)`,
511 | );
512 |
513 | // Breakdown by language if we have multiple languages
514 | if (targetLocales.length > 1) {
515 | console.log(`• Per-language breakdown:`);
516 | for (const locale of targetLocales) {
517 | const words = totalWordCount.get(locale) || 0;
518 | const percent = ((words / totalWordsToTranslate) * 100).toFixed(1);
519 | console.log(
520 | ` - ${locale}: ~${words.toLocaleString()} words (${percent}% of total)`,
521 | );
522 | }
523 | }
524 |
525 | // Detailed stats if flags.confirm is specified
526 | if (flags.confirm && Object.keys(fileStats).length > 0) {
527 | console.log(chalk.bold(`\n📑 BREAKDOWN BY FILE:`));
528 |
529 | Object.entries(fileStats)
530 | .sort((a, b) => b[1].wordCount - a[1].wordCount) // Sort by word count
531 | .forEach(([path, stats]) => {
532 | // Skip files with no source keys
533 | if (stats.sourceKeys === 0) return;
534 |
535 | console.log(chalk.bold(`\n• ${path}:`));
536 | console.log(
537 | ` ${
538 | stats.sourceKeys
539 | } source keys, ~${stats.wordCount.toLocaleString()} source words`,
540 | );
541 |
542 | // Create file detail table
543 | const fileTable = new Table({
544 | head: ["Language", "Status", "Details"],
545 | style: {
546 | head: ["white"],
547 | border: [],
548 | },
549 | colWidths: [12, 20, 50], // Explicit column widths for file detail table
550 | });
551 |
552 | for (const locale of targetLocales) {
553 | const langStats = stats.languageStats[locale];
554 | const complete = langStats.complete;
555 | const total = stats.sourceKeys;
556 | const completion = ((complete / total) * 100).toFixed(1);
557 |
558 | let status = "✅ Complete";
559 | let statusColor = chalk.green;
560 |
561 | if (langStats.missing === total) {
562 | status = "❌ Not started";
563 | statusColor = chalk.red;
564 | } else if (langStats.missing > 0 || langStats.updated > 0) {
565 | status = `⚠️ ${completion}% complete`;
566 | statusColor = chalk.yellow;
567 | }
568 |
569 | // Show counts only if there's something missing or updated
570 | let details = "";
571 | if (langStats.missing > 0 || langStats.updated > 0) {
572 | const parts = [];
573 | if (langStats.missing > 0)
574 | parts.push(`${langStats.missing} missing`);
575 | if (langStats.updated > 0)
576 | parts.push(`${langStats.updated} changed`);
577 | details = `${parts.join(", ")}, ~${langStats.words} words`;
578 | } else {
579 | details = "All keys translated";
580 | }
581 |
582 | fileTable.push([locale, statusColor(status), details]);
583 | }
584 |
585 | console.log(fileTable.toString());
586 | });
587 | }
588 |
589 | // Find fully translated and missing languages
590 | const completeLanguages = targetLocales.filter(
591 | (locale) =>
592 | languageStats[locale].missing === 0 &&
593 | languageStats[locale].updated === 0,
594 | );
595 |
596 | const missingLanguages = targetLocales.filter(
597 | (locale) => languageStats[locale].complete === 0,
598 | );
599 |
600 | // Add optimization tips
601 | console.log(chalk.bold.green(`\n💡 OPTIMIZATION TIPS:`));
602 |
603 | if (missingLanguages.length > 0) {
604 | console.log(
605 | `• ${chalk.yellow(missingLanguages.join(", "))} ${
606 | missingLanguages.length === 1 ? "has" : "have"
607 | } no translations yet`,
608 | );
609 | }
610 |
611 | if (completeLanguages.length > 0) {
612 | console.log(
613 | `• ${chalk.green(completeLanguages.join(", "))} ${
614 | completeLanguages.length === 1 ? "is" : "are"
615 | } completely translated`,
616 | );
617 | }
618 |
619 | // Other tips
620 | if (targetLocales.length > 1) {
621 | console.log(`• Translating one language at a time reduces complexity`);
622 | console.log(
623 | `• Try 'lingo.dev@latest i18n --locale ${targetLocales[0]}' to process just one language`,
624 | );
625 | }
626 |
627 | // Track successful completion
628 | trackEvent(authId || "status", "cmd.status.success", {
629 | i18nConfig,
630 | flags,
631 | totalSourceKeyCount,
632 | languageStats,
633 | totalWordsToTranslate,
634 | authenticated: !!authId,
635 | });
636 | exitGracefully();
637 | } catch (error: any) {
638 | ora.fail(error.message);
639 | trackEvent(authId || "status", "cmd.status.error", {
640 | flags,
641 | error: error.message,
642 | authenticated: !!authId,
643 | });
644 | process.exit(1);
645 | }
646 | });
647 |
648 | function parseFlags(options: any) {
649 | return Z.object({
650 | locale: Z.array(localeCodeSchema).optional(),
651 | bucket: Z.array(bucketTypeSchema).optional(),
652 | force: Z.boolean().optional(),
653 | confirm: Z.boolean().optional(),
654 | verbose: Z.boolean().optional(),
655 | file: Z.array(Z.string()).optional(),
656 | apiKey: Z.string().optional(),
657 | }).parse(options);
658 | }
659 |
660 | async function tryAuthenticate(settings: ReturnType<typeof getSettings>) {
661 | if (!settings.auth.apiKey) {
662 | return null;
663 | }
664 |
665 | try {
666 | const authenticator = createAuthenticator({
667 | apiKey: settings.auth.apiKey,
668 | apiUrl: settings.auth.apiUrl,
669 | });
670 | const user = await authenticator.whoami();
671 | return user;
672 | } catch (error) {
673 | return null;
674 | }
675 | }
676 |
677 | function validateParams(
678 | i18nConfig: I18nConfig | null,
679 | flags: ReturnType<typeof parseFlags>,
680 | ) {
681 | if (!i18nConfig) {
682 | throw new CLIError({
683 | message:
684 | "i18n.json not found. Please run `lingo.dev init` to initialize the project.",
685 | docUrl: "i18nNotFound",
686 | });
687 | } else if (!i18nConfig.buckets || !Object.keys(i18nConfig.buckets).length) {
688 | throw new CLIError({
689 | message:
690 | "No buckets found in i18n.json. Please add at least one bucket containing i18n content.",
691 | docUrl: "bucketNotFound",
692 | });
693 | } else if (
694 | flags.locale?.some((locale) => !i18nConfig.locale.targets.includes(locale))
695 | ) {
696 | throw new CLIError({
697 | message: `One or more specified locales do not exist in i18n.json locale.targets. Please add them to the list and try again.`,
698 | docUrl: "localeTargetNotFound",
699 | });
700 | } else if (
701 | flags.bucket?.some(
702 | (bucket) =>
703 | !i18nConfig.buckets[bucket as keyof typeof i18nConfig.buckets],
704 | )
705 | ) {
706 | throw new CLIError({
707 | message: `One or more specified buckets do not exist in i18n.json. Please add them to the list and try again.`,
708 | docUrl: "bucketNotFound",
709 | });
710 | }
711 | }
712 |
```
--------------------------------------------------------------------------------
/packages/cli/src/cli/loaders/mdx2/code-placeholder.spec.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect } from "vitest";
2 | import createMdxCodePlaceholderLoader from "./code-placeholder";
3 | import dedent from "dedent";
4 | import { md5 } from "../../utils/md5";
5 |
6 | const PLACEHOLDER_REGEX = /---CODE-PLACEHOLDER-[0-9a-f]+---/g;
7 |
8 | const sampleContent = dedent`
9 | Paragraph with some code:
10 |
11 | \`\`\`js
12 | console.log("foo");
13 | \`\`\`
14 | `;
15 |
16 | describe("MDX Code Placeholder Loader", () => {
17 | const loader = createMdxCodePlaceholderLoader();
18 | loader.setDefaultLocale("en");
19 |
20 | it("should replace fenced code with placeholder on pull", async () => {
21 | const result = await loader.pull("en", sampleContent);
22 | const hash = md5('```js\nconsole.log("foo");\n```');
23 | const expected = `Paragraph with some code:\n\n---CODE-PLACEHOLDER-${hash}---`;
24 | expect(result.trim()).toBe(expected);
25 | });
26 |
27 | it("should restore fenced code from placeholder on push", async () => {
28 | const pulled = await loader.pull("en", sampleContent);
29 | const translated = pulled.replace("Paragraph", "Párrafo");
30 | const output = await loader.push("es", translated);
31 | const expected = dedent`
32 | Párrafo with some code:
33 |
34 | \`\`\`js
35 | console.log("foo");
36 | \`\`\`
37 | `;
38 | expect(output.trim()).toBe(expected.trim());
39 | });
40 |
41 | describe("round-trip scenarios", () => {
42 | it("round-trips a fenced block with language tag", async () => {
43 | const md = dedent`
44 | Example:
45 |
46 | \`\`\`js
47 | console.log()
48 | \`\`\`
49 | `;
50 | const pulled = await loader.pull("en", md);
51 | const pushed = await loader.push("es", pulled);
52 | expect(pushed).toBe(md);
53 | });
54 |
55 | it("round-trips a fenced block without language tag", async () => {
56 | const md = dedent`
57 | Intro:
58 |
59 | \`\`\`
60 | generic code
61 | \`\`\`
62 | `;
63 | const pulled = await loader.pull("en", md);
64 | const pushed = await loader.push("es", pulled);
65 | expect(pushed).toBe(md);
66 | });
67 |
68 | it("round-trips a meta-tagged fenced block", async () => {
69 | const md = dedent`
70 | Meta:
71 |
72 | \`\`\`js {1,2} title="Sample"
73 | line1
74 | line2
75 | \`\`\`
76 | `;
77 | const pulled = await loader.pull("en", md);
78 | const pushed = await loader.push("es", pulled);
79 | expect(pushed).toBe(md);
80 | });
81 |
82 | it("round-trips a fenced block inside a blockquote", async () => {
83 | const md = dedent`
84 | > Quote start
85 | > \`\`\`ts
86 | > let x = 42;
87 | > \`\`\`
88 | > Quote end
89 | `;
90 | const pulled = await loader.pull("en", md);
91 | const pushed = await loader.push("es", pulled);
92 | expect(pushed).toBe(md);
93 | });
94 |
95 | it("round-trips multiple separated fenced blocks", async () => {
96 | const md = dedent`
97 | A:
98 |
99 | \`\`\`js
100 | 1
101 | \`\`\`
102 |
103 | B:
104 |
105 | \`\`\`js
106 | 2
107 | \`\`\`
108 | `;
109 | const pulled = await loader.pull("en", md);
110 | const pushed = await loader.push("es", pulled);
111 | expect(pushed).toBe(md);
112 | });
113 |
114 | it("round-trips adjacent fenced blocks", async () => {
115 | const md = dedent`
116 | \`\`\`
117 | a()
118 | \`\`\`
119 | \`\`\`
120 | b()
121 | \`\`\`
122 | `;
123 | const expected = dedent`
124 | \`\`\`
125 | a()
126 | \`\`\`
127 |
128 | \`\`\`
129 | b()
130 | \`\`\`
131 | `;
132 | const pulled = await loader.pull("en", md);
133 | const pushed = await loader.push("es", pulled);
134 | expect(pushed).toBe(expected);
135 | });
136 |
137 | it("round-trips an indented fenced block", async () => {
138 | const md = dedent`
139 | Outer:
140 |
141 | \`\`\`py
142 | pass
143 | \`\`\`
144 | `;
145 | const pulled = await loader.pull("en", md);
146 | const pushed = await loader.push("es", pulled);
147 | expect(pushed).toBe(md);
148 | });
149 |
150 | it("round-trips a fenced block after a heading", async () => {
151 | const md = dedent`
152 | # Title
153 |
154 | \`\`\`bash
155 | echo hi
156 | \`\`\`
157 | `;
158 | const pulled = await loader.pull("en", md);
159 | const pushed = await loader.push("es", pulled);
160 | expect(pushed).toBe(md);
161 | });
162 |
163 | it("round-trips a fenced block inside a list item", async () => {
164 | const md = `
165 | - item:
166 |
167 | \`\`\`js
168 | io()
169 | \`\`\`
170 | `.trim();
171 | const pulled = await loader.pull("en", md);
172 | const pushed = await loader.push("es", pulled);
173 | expect(pushed).toBe(md);
174 | });
175 |
176 | it("round-trips a fenced block inside JSX component", async () => {
177 | const md = dedent`
178 | <Component>
179 |
180 | \`\`\`js
181 | x
182 | \`\`\`
183 |
184 | </Component>
185 | `;
186 | const pulled = await loader.pull("en", md);
187 | const pushed = await loader.push("es", pulled);
188 | expect(pushed).toBe(md);
189 | });
190 |
191 | it("round-trips a fenced block inside JSX component - adds new lines", async () => {
192 | const md = dedent`
193 | <Component>
194 | \`\`\`js
195 | x
196 | \`\`\`
197 | </Component>
198 | `;
199 | const pulled = await loader.pull("en", md);
200 | const pushed = await loader.push("es", pulled);
201 | expect(pushed).toBe(
202 | dedent`
203 | <Component>
204 |
205 | \`\`\`js
206 | x
207 | \`\`\`
208 |
209 | </Component>
210 | `,
211 | );
212 | });
213 |
214 | it("round-trips a large JSON fenced block", async () => {
215 | const md = dedent`
216 | \`\`\`shell
217 | { "key": [1,2,3] }
218 | \`\`\`
219 | `;
220 | const pulled = await loader.pull("en", md);
221 | const pushed = await loader.push("es", pulled);
222 | expect(pushed).toBe(md);
223 | });
224 |
225 | it("handles identical code snippets correctly", async () => {
226 | const md = dedent`
227 | First paragraph:
228 |
229 | \`\`\`shell
230 | echo "hello world"
231 | \`\`\`
232 |
233 | Second paragraph:
234 | \`\`\`shell
235 | echo "hello world"
236 | \`\`\`
237 | `;
238 | const pulled = await loader.pull("en", md);
239 | const pushed = await loader.push("es", pulled);
240 | expect(pushed).toBe(
241 | dedent`
242 | First paragraph:
243 |
244 | \`\`\`shell
245 | echo "hello world"
246 | \`\`\`
247 |
248 | Second paragraph:
249 |
250 | \`\`\`shell
251 | echo "hello world"
252 | \`\`\`
253 | `,
254 | );
255 | });
256 |
257 | it("handles fenced code blocks inside quotes correctly", async () => {
258 | const md = dedent`
259 | > Code snippet inside quote:
260 | >
261 | > \`\`\`shell
262 | > npx -y mucho@latest install
263 | > \`\`\`
264 | `;
265 | const pulled = await loader.pull("en", md);
266 | const pushed = await loader.push("es", pulled);
267 | expect(pushed).toBe(md);
268 | });
269 |
270 | it("round-trips an image block with surrounding blank lines unchanged", async () => {
271 | const md = dedent`
272 | Text above.
273 |
274 | 
275 |
276 | Text below.
277 | `;
278 |
279 | const pulled = await loader.pull("en", md);
280 | const pushed = await loader.push("es", pulled);
281 | expect(pushed).toBe(md);
282 | });
283 |
284 | it("round-trips and adds blank lines around an image block when missing", async () => {
285 | const md = dedent`
286 | Text above.
287 | 
288 | Text below.
289 | `;
290 |
291 | const expected = dedent`
292 | Text above.
293 |
294 | 
295 |
296 | Text below.
297 | `;
298 |
299 | const pulled = await loader.pull("en", md);
300 | const pushed = await loader.push("es", pulled);
301 | expect(pushed).toBe(expected);
302 | });
303 |
304 | it("keeps image inside blockquote as-is", async () => {
305 | const md = dedent`
306 | > 
307 | `;
308 |
309 | const pulled = await loader.pull("en", md);
310 | const pushed = await loader.push("es", pulled);
311 | expect(pushed).toBe(md);
312 | });
313 |
314 | it("leaves incomplete fences untouched", async () => {
315 | const md = "```js\nno close";
316 | const pulled = await loader.pull("en", md);
317 | expect(pulled).toBe(md);
318 |
319 | const pushed = await loader.push("es", pulled);
320 | expect(pushed).toBe(md);
321 | });
322 |
323 | // Edge cases for image spacing
324 |
325 | it("adds blank line after image when only before exists", async () => {
326 | const md = dedent`
327 | Before.
328 |
329 | 
330 | After.
331 | `;
332 |
333 | const expected = dedent`
334 | Before.
335 |
336 | 
337 |
338 | After.
339 | `;
340 |
341 | const pulled = await loader.pull("en", md);
342 | const pushed = await loader.push("es", pulled);
343 | expect(pushed).toBe(expected);
344 | });
345 |
346 | it("adds blank line before image when only after exists", async () => {
347 | const md = dedent`
348 | Before.
349 | 
350 |
351 | After.
352 | `;
353 |
354 | const expected = dedent`
355 | Before.
356 |
357 | 
358 |
359 | After.
360 | `;
361 |
362 | const pulled = await loader.pull("en", md);
363 | const pushed = await loader.push("es", pulled);
364 | expect(pushed).toBe(expected);
365 | });
366 |
367 | it("inserts spacing between consecutive images", async () => {
368 | const md = dedent`
369 | 
370 | 
371 | `;
372 |
373 | const expected = dedent`
374 | 
375 |
376 | 
377 | `;
378 |
379 | const pulled = await loader.pull("en", md);
380 | const pushed = await loader.push("es", pulled);
381 | expect(pushed).toBe(expected);
382 | });
383 |
384 | it("handles image inside JSX component - adds blank lines", async () => {
385 | const md = dedent`
386 | <Wrapper>
387 | 
388 | </Wrapper>
389 | `;
390 |
391 | const expected = dedent`
392 | <Wrapper>
393 |
394 | 
395 |
396 | </Wrapper>
397 | `;
398 |
399 | const pulled = await loader.pull("en", md);
400 | const pushed = await loader.push("es", pulled);
401 | expect(pushed).toBe(expected);
402 | });
403 | });
404 |
405 | describe("inline code placeholder", () => {
406 | it("should replace inline code with placeholder on pull", async () => {
407 | const md = "This is some `inline()` code.";
408 | const pulled = await loader.pull("en", md);
409 | const hash = md5("`inline()`");
410 | const expected = `This is some ---INLINE-CODE-PLACEHOLDER-${hash}--- code.`;
411 | expect(pulled).toBe(expected);
412 | });
413 |
414 | it("should restore inline code from placeholder on push", async () => {
415 | const md = "Some `code` here.";
416 | const pulled = await loader.pull("en", md);
417 | const translated = pulled.replace("Some", "Algún");
418 | const pushed = await loader.push("es", translated);
419 | expect(pushed).toBe("Algún `code` here.");
420 | });
421 |
422 | it("round-trips multiple inline code snippets", async () => {
423 | const md = "Use `a` and `b` and `c`.";
424 | const pulled = await loader.pull("en", md);
425 | const pushed = await loader.push("es", pulled);
426 | expect(pushed).toBe(md);
427 | });
428 |
429 | it("handles identical inline snippets correctly", async () => {
430 | const md = "Repeat `x` and `x` again.";
431 | const pulled = await loader.pull("en", md);
432 | const pushed = await loader.push("es", pulled);
433 | expect(pushed).toBe(md);
434 | });
435 |
436 | it("retains custom inline code in target locale when it differs from source", async () => {
437 | const enMd = "Use `foo` function.";
438 | const ruMd = "Используйте `бар` функцию.";
439 |
440 | // Pull English source to establish originalInput in loader state
441 | await loader.pull("en", enMd);
442 |
443 | // Pull Russian content (with its own inline code value)
444 | const ruPulled = await loader.pull("ru", ruMd);
445 | // Simulate translator editing surrounding text but keeping placeholder intact
446 | const ruTranslated = ruPulled.replace("Используйте", "Примените");
447 |
448 | // Push back to Russian locale and ensure inline code is preserved
449 | const ruPushed = await loader.push("ru", ruTranslated);
450 | expect(ruPushed).toBe("Примените `бар` функцию.");
451 | });
452 | });
453 |
454 | describe("Image URLs with Parentheses", () => {
455 | it("should handle image URLs with parentheses", async () => {
456 | const md = dedent`
457 | Text above.
458 |
459 | parentheses.jpg)
460 |
461 | Text below.
462 | `;
463 |
464 | const pulled = await loader.pull("en", md);
465 | const pushed = await loader.push("es", pulled);
466 | expect(pushed).toBe(md);
467 | });
468 |
469 | it("should handle image URLs with nested parentheses", async () => {
470 | const md = dedent`
471 | Text above.
472 |
473 | parentheses).jpg)
474 |
475 | Text below.
476 | `;
477 |
478 | const pulled = await loader.pull("en", md);
479 | const pushed = await loader.push("es", pulled);
480 | expect(pushed).toBe(md);
481 | });
482 |
483 | it("should handle image URLs with parentheses in blockquotes", async () => {
484 | const md = dedent`
485 | > blockquote.jpg)
486 | `;
487 |
488 | const pulled = await loader.pull("en", md);
489 | const pushed = await loader.push("es", pulled);
490 | expect(pushed).toBe(md);
491 | });
492 |
493 | it("should handle image URLs with parentheses in JSX components", async () => {
494 | const md = dedent`
495 | <Component>
496 | component.jpg)
497 | </Component>
498 | `;
499 |
500 | const expected = dedent`
501 | <Component>
502 |
503 | component.jpg)
504 |
505 | </Component>
506 | `;
507 |
508 | const pulled = await loader.pull("en", md);
509 | const pushed = await loader.push("es", pulled);
510 | expect(pushed).toBe(expected);
511 | });
512 | });
513 |
514 | describe("placeholder replacement bugs", () => {
515 | it("should handle special $ characters in code content correctly", async () => {
516 | const loader = createMdxCodePlaceholderLoader();
517 | loader.setDefaultLocale("en");
518 |
519 | // Code containing special $ characters that have special meaning in replaceAll
520 | const content = dedent`
521 | Text before.
522 |
523 | \`\`\`js
524 | const price = "$100";
525 | const template = "$\`text\`";
526 | const special = "$&$'$\`";
527 | \`\`\`
528 |
529 | Text after.
530 | `;
531 |
532 | // Pull and then push the same content
533 | const pulled = await loader.pull("en", content);
534 | const translated = pulled.replace("Text before", "Texto antes");
535 | const pushed = await loader.push("en", translated);
536 |
537 | // Should not contain any placeholders
538 | expect(pushed).not.toMatch(/---CODE-PLACEHOLDER-[0-9a-f]+---/);
539 |
540 | // Should preserve all special $ characters exactly as they were
541 | expect(pushed).toContain('const price = "$100";');
542 | expect(pushed).toContain('const template = "$`text`";');
543 | expect(pushed).toContain('const special = "$&$\'$`";');
544 | expect(pushed).toContain("Texto antes");
545 | });
546 |
547 | it("should handle inline code with $ characters correctly", async () => {
548 | const loader = createMdxCodePlaceholderLoader();
549 | loader.setDefaultLocale("en");
550 |
551 | const content = "Use `$price` and `$&` and `$\`` in your code.";
552 |
553 | // Pull and then push the same content
554 | const pulled = await loader.pull("en", content);
555 | const translated = pulled.replace("Use", "Utilize");
556 | const pushed = await loader.push("en", translated);
557 |
558 | // Should not contain any placeholders
559 | expect(pushed).not.toMatch(/---INLINE-CODE-PLACEHOLDER-[0-9a-f]+---/);
560 |
561 | // Should preserve all special $ characters
562 | expect(pushed).toContain("`$price`");
563 | expect(pushed).toContain("`$&`");
564 | expect(pushed).toContain("`$\``");
565 | expect(pushed).toContain("Utilize");
566 | });
567 |
568 | it("should not leave placeholders when content matches", async () => {
569 | const loader = createMdxCodePlaceholderLoader();
570 | loader.setDefaultLocale("en");
571 |
572 | const content = "Use the `getData()` function.";
573 |
574 | // Pull and then push the same content - should work correctly
575 | const pulled = await loader.pull("en", content);
576 | const translated = pulled.replace("Use", "Utilize");
577 | const pushed = await loader.push("en", translated);
578 |
579 | // Should not contain any placeholders
580 | expect(pushed).not.toMatch(/---INLINE-CODE-PLACEHOLDER-[0-9a-f]+---/);
581 | expect(pushed).not.toMatch(/---CODE-PLACEHOLDER-[0-9a-f]+---/);
582 | expect(pushed).toContain("`getData()`");
583 | expect(pushed).toContain("Utilize");
584 | });
585 |
586 | it("should replace all placeholders including those from different sources", async () => {
587 | const loader = createMdxCodePlaceholderLoader();
588 | loader.setDefaultLocale("en");
589 |
590 | // Simulate the exact scenario from the user's bug report
591 | const englishContent = "Use the `getData()` function.";
592 | const arabicContent = "استخدم `الحصول_على_البيانات()` الدالة.";
593 |
594 | // First pull English (required as default locale)
595 | await loader.pull("en", englishContent);
596 |
597 | // Pull Arabic content to create placeholders
598 | const arabicPulled = await loader.pull("ar", arabicContent);
599 |
600 | // Simulate translation: translator changes text but keeps placeholder
601 | const arabicTranslated = arabicPulled.replace("استخدم", "قم بتطبيق");
602 |
603 | // Push back - this should now work correctly with the fix
604 | const pushedResult = await loader.push("ar", arabicTranslated);
605 |
606 | // The fix: ALL placeholders should be replaced, including Arabic ones
607 | expect(pushedResult).not.toMatch(
608 | /---INLINE-CODE-PLACEHOLDER-[0-9a-f]+---/,
609 | );
610 | expect(pushedResult).not.toMatch(/---CODE-PLACEHOLDER-[0-9a-f]+---/);
611 |
612 | // The Arabic inline code should be preserved and translated text should be there
613 | expect(pushedResult).toContain("`الحصول_على_البيانات()`");
614 | expect(pushedResult).toContain("قم بتطبيق");
615 | });
616 |
617 | it("should replace placeholders even when pullInput state is overwritten", async () => {
618 | const loader = createMdxCodePlaceholderLoader();
619 | loader.setDefaultLocale("en");
620 |
621 | const englishContent = "Use the `getData()` function.";
622 | const arabicContent = "استخدم `الحصول_على_البيانات()` الدالة.";
623 |
624 | // First pull English (required as default locale)
625 | await loader.pull("en", englishContent);
626 |
627 | // Pull Arabic content to create placeholders
628 | const arabicPulled = await loader.pull("ar", arabicContent);
629 |
630 | // Simulate translation: translator changes text but keeps placeholder
631 | const arabicTranslated = arabicPulled.replace("استخدم", "قم بتطبيق");
632 |
633 | // Now pull English again, overwriting pullInput state
634 | // This simulates the real-world scenario where the loader state gets out of sync
635 | await loader.pull("en", englishContent);
636 |
637 | // Push the Arabic translation - should work despite state being overwritten
638 | const pushedResult = await loader.push("ar", arabicTranslated);
639 |
640 | // All placeholders should be replaced, even when not in current pullInput
641 | expect(pushedResult).not.toMatch(
642 | /---INLINE-CODE-PLACEHOLDER-[0-9a-f]+---/,
643 | );
644 | expect(pushedResult).not.toMatch(/---CODE-PLACEHOLDER-[0-9a-f]+---/);
645 | expect(pushedResult).toContain("`الحصول_على_البيانات()`");
646 | expect(pushedResult).toContain("قم بتطبيق");
647 | });
648 | });
649 |
650 | describe("raw code outside fences", () => {
651 | it("should handle raw JavaScript code outside fences", async () => {
652 | const loader = createMdxCodePlaceholderLoader();
653 | loader.setDefaultLocale("en");
654 |
655 | // Test case matching user's file structure - raw JS between JSX components
656 | const md = dedent`
657 | </Tabs>
658 |
659 | // Attach to button click
660 | document.getElementById('executeBtn')?.addEventListener('click', executeClientSideWorkflow);
661 |
662 | <Callout type="warning">
663 | Content here
664 | </Callout>
665 | `;
666 |
667 | const pulled = await loader.pull("en", md);
668 | const pushed = await loader.push("en", pulled);
669 |
670 | // Should round-trip correctly
671 | expect(pushed).toBe(md);
672 | });
673 |
674 | it("should handle mixed code blocks and raw code", async () => {
675 | const loader = createMdxCodePlaceholderLoader();
676 | loader.setDefaultLocale("en");
677 |
678 | const md = dedent`
679 | Here's a code block:
680 |
681 | \`\`\`typescript
682 | const x = 1;
683 | \`\`\`
684 |
685 | Now some raw code outside:
686 | // This is outside
687 | const y = 2;
688 |
689 | And another block:
690 |
691 | \`\`\`javascript
692 | const z = 3;
693 | \`\`\`
694 | `;
695 |
696 | const pulled = await loader.pull("en", md);
697 | const pushed = await loader.push("en", pulled);
698 |
699 | // Should preserve raw code outside fences
700 | expect(pushed).toContain("// This is outside");
701 | expect(pushed).toContain("const y = 2;");
702 | });
703 |
704 | it("should handle code blocks with extra blank lines added by translation", async () => {
705 | const loader = createMdxCodePlaceholderLoader();
706 | loader.setDefaultLocale("en");
707 |
708 | // English source - no extra blank lines
709 | const enMd = dedent`
710 | <Tab value="npm">
711 | \`\`\`bash
712 | npm install
713 | \`\`\`
714 | </Tab>
715 | `;
716 |
717 | // Pull English to establish placeholders
718 | const enPulled = await loader.pull("en", enMd);
719 |
720 | // German translation with extra blank lines (simulating AI translation behavior)
721 | const deMd = dedent`
722 | <Tab value="npm">
723 |
724 | \`\`\`bash
725 | npm install
726 | \`\`\`
727 |
728 | </Tab>
729 | `;
730 |
731 | // Pull German version
732 | const dePulled = await loader.pull("de", deMd);
733 |
734 | // Push back - should restore code blocks correctly
735 | const dePushed = await loader.push("de", dePulled);
736 |
737 | // The code block should be present and not replaced with placeholder
738 | expect(dePushed).toContain("```bash");
739 | expect(dePushed).toContain("npm install");
740 | expect(dePushed).not.toMatch(/---CODE-PLACEHOLDER-/);
741 | });
742 |
743 | it("should preserve double newlines around placeholders for section splitting", async () => {
744 | const loader = createMdxCodePlaceholderLoader();
745 | loader.setDefaultLocale("en");
746 |
747 | // Test that placeholders maintain double newlines so section-split works correctly
748 | const md = dedent`
749 | Text before.
750 |
751 | \`\`\`typescript
752 | code1
753 | \`\`\`
754 |
755 | Text between.
756 |
757 | \`\`\`javascript
758 | code2
759 | \`\`\`
760 |
761 | Text after.
762 | `;
763 |
764 | const pulled = await loader.pull("en", md);
765 |
766 | // Verify placeholders are surrounded by double newlines for proper section splitting
767 | const placeholders = pulled.match(/---CODE-PLACEHOLDER-[a-f0-9]+---/g);
768 | expect(placeholders).toHaveLength(2);
769 |
770 | // Check that each placeholder has double newlines around it
771 | for (const placeholder of placeholders!) {
772 | // Should have \n\n before (except at start) and \n\n after (except at end)
773 | const placeholderIndex = pulled.indexOf(placeholder);
774 |
775 | // Check for double newline after (unless at end)
776 | const afterPlaceholder = pulled.substring(
777 | placeholderIndex + placeholder.length,
778 | placeholderIndex + placeholder.length + 2,
779 | );
780 | if (placeholderIndex + placeholder.length < pulled.length - 2) {
781 | expect(afterPlaceholder).toBe("\n\n");
782 | }
783 | }
784 |
785 | // Ensure we can split on \n\n and get separate sections
786 | const sections = pulled.split("\n\n").filter(Boolean);
787 | expect(sections.length).toBeGreaterThanOrEqual(5); // Text + placeholder + text + placeholder + text
788 | });
789 | });
790 | });
791 |
792 | describe("adjacent code blocks bug", () => {
793 | it("should handle closing fence followed immediately by opening fence", async () => {
794 | const loader = createMdxCodePlaceholderLoader();
795 | loader.setDefaultLocale("en");
796 |
797 | // This reproduces the actual bug from the user's file
798 | const md = dedent`
799 | \`\`\`typescript
800 | function example() {
801 | return true;
802 | }
803 | \`\`\`
804 |
805 | \`\`\`typescript
806 | import { Something } from 'somewhere';
807 | \`\`\`
808 | `;
809 |
810 | const pulled = await loader.pull("en", md);
811 |
812 | console.log("PULLED CONTENT:");
813 | console.log(pulled);
814 | console.log("---");
815 |
816 | // The bug: placeholder is concatenated with "typescript" from next block
817 | const bugPattern = /---CODE-PLACEHOLDER-[a-f0-9]+---typescript/;
818 | expect(pulled).not.toMatch(bugPattern);
819 |
820 | // Should have proper separation
821 | expect(pulled).toMatch(
822 | /---CODE-PLACEHOLDER-[a-f0-9]+---\n\n---CODE-PLACEHOLDER-[a-f0-9]+---/,
823 | );
824 | });
825 | });
826 |
827 | describe("$ special character handling in replacement functions", () => {
828 | it("should preserve $ characters in ensureTrailingFenceNewline", async () => {
829 | const loader = createMdxCodePlaceholderLoader();
830 | loader.setDefaultLocale("en");
831 |
832 | // Tests fix for lines 38, 68: replaceAll(match, () => replacement)
833 | // Code block with $ that would trigger special replacement behavior if not using function replacer
834 | const content = dedent`
835 | Some text
836 | \`\`\`js
837 | console.log('Current period cost: $' + amount);
838 | const template = \`Price: $\${price}\`;
839 | \`\`\`
840 | More text
841 | `;
842 |
843 | const pulled = await loader.pull("en", content);
844 | const pushed = await loader.push("en", pulled);
845 |
846 | // All $ characters should be preserved exactly
847 | expect(pushed).toContain("console.log('Current period cost: $' + amount);");
848 | expect(pushed).toContain("const template = `Price: $");
849 | });
850 |
851 | it("should preserve $ characters in ensureSurroundingImageNewlines", async () => {
852 | const loader = createMdxCodePlaceholderLoader();
853 | loader.setDefaultLocale("en");
854 |
855 | // Tests fix for line 38: replaceAll(match, () => replacement) in image handling
856 | // Image with $ in URL and alt text that would break with string replacer
857 | const content = dedent`
858 | Here is an image:
859 | 
860 | End of text
861 | `;
862 |
863 | const pulled = await loader.pull("en", content);
864 | const pushed = await loader.push("en", pulled);
865 |
866 | // All $ characters in URL and alt text should be preserved
867 | expect(pushed).toContain("![Price: $100]");
868 | expect(pushed).toContain("price=$500¤cy=$USD");
869 | });
870 | });
871 |
```