#
tokens: 43503/50000 5/626 files (page 15/20)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 15 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/compiler/src/lib/lcp/api/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { createGroq } from "@ai-sdk/groq";
  2 | import { createGoogleGenerativeAI } from "@ai-sdk/google";
  3 | import { createOpenRouter } from "@openrouter/ai-sdk-provider";
  4 | import { createOllama } from "ollama-ai-provider";
  5 | import { createMistral } from "@ai-sdk/mistral";
  6 | import { generateText } from "ai";
  7 | import { LingoDotDevEngine } from "@lingo.dev/_sdk";
  8 | import { DictionarySchema } from "../schema";
  9 | import _ from "lodash";
 10 | import { getLocaleModel } from "../../../utils/locales";
 11 | import getSystemPrompt from "./prompt";
 12 | import { obj2xml, xml2obj } from "./xml2obj";
 13 | import shots from "./shots";
 14 | import {
 15 |   getGroqKey,
 16 |   getGroqKeyFromEnv,
 17 |   getGoogleKey,
 18 |   getGoogleKeyFromEnv,
 19 |   getOpenRouterKey,
 20 |   getOpenRouterKeyFromEnv,
 21 |   getMistralKey,
 22 |   getMistralKeyFromEnv,
 23 |   getLingoDotDevKeyFromEnv,
 24 |   getLingoDotDevKey,
 25 | } from "../../../utils/llm-api-key";
 26 | import dedent from "dedent";
 27 | import { isRunningInCIOrDocker } from "../../../utils/env";
 28 | import { LanguageModel } from "ai";
 29 | import { providerDetails } from "./provider-details";
 30 | 
 31 | export class LCPAPI {
 32 |   static async translate(
 33 |     models: "lingo.dev" | Record<string, string>,
 34 |     sourceDictionary: DictionarySchema,
 35 |     sourceLocale: string,
 36 |     targetLocale: string,
 37 |     prompt?: string | null,
 38 |   ): Promise<DictionarySchema> {
 39 |     const timeLabel = `LCPAPI.translate: ${targetLocale}`;
 40 |     console.time(timeLabel);
 41 |     const chunks = this._chunkDictionary(sourceDictionary);
 42 |     const translatedChunks = [];
 43 |     for (const chunk of chunks) {
 44 |       const translatedChunk = await this._translateChunk(
 45 |         models,
 46 |         chunk,
 47 |         sourceLocale,
 48 |         targetLocale,
 49 |         prompt,
 50 |       );
 51 |       translatedChunks.push(translatedChunk);
 52 |     }
 53 |     const result = this._mergeDictionaries(translatedChunks);
 54 |     console.timeEnd(timeLabel);
 55 |     return result;
 56 |   }
 57 | 
 58 |   private static _chunkDictionary(
 59 |     dictionary: DictionarySchema,
 60 |   ): DictionarySchema[] {
 61 |     const MAX_ENTRIES_PER_CHUNK = 100;
 62 |     const { files, ...rest } = dictionary;
 63 |     const chunks: DictionarySchema[] = [];
 64 | 
 65 |     let currentChunk: DictionarySchema = {
 66 |       ...rest,
 67 |       files: {},
 68 |     };
 69 |     let currentEntryCount = 0;
 70 | 
 71 |     Object.entries(files).forEach(([fileName, file]) => {
 72 |       const entries = file.entries;
 73 |       const entryPairs = Object.entries(entries);
 74 | 
 75 |       let currentIndex = 0;
 76 |       while (currentIndex < entryPairs.length) {
 77 |         const remainingSpace = MAX_ENTRIES_PER_CHUNK - currentEntryCount;
 78 |         const entriesToAdd = entryPairs.slice(
 79 |           currentIndex,
 80 |           currentIndex + remainingSpace,
 81 |         );
 82 | 
 83 |         if (entriesToAdd.length > 0) {
 84 |           currentChunk.files[fileName] = currentChunk.files[fileName] || {
 85 |             entries: {},
 86 |           };
 87 |           currentChunk.files[fileName].entries = {
 88 |             ...currentChunk.files[fileName].entries,
 89 |             ...Object.fromEntries(entriesToAdd),
 90 |           };
 91 |           currentEntryCount += entriesToAdd.length;
 92 |         }
 93 | 
 94 |         currentIndex += entriesToAdd.length;
 95 | 
 96 |         if (
 97 |           currentEntryCount >= MAX_ENTRIES_PER_CHUNK ||
 98 |           (currentIndex < entryPairs.length &&
 99 |             currentEntryCount + (entryPairs.length - currentIndex) >
100 |               MAX_ENTRIES_PER_CHUNK)
101 |         ) {
102 |           chunks.push(currentChunk);
103 |           currentChunk = { ...rest, files: {} };
104 |           currentEntryCount = 0;
105 |         }
106 |       }
107 |     });
108 | 
109 |     if (currentEntryCount > 0) {
110 |       chunks.push(currentChunk);
111 |     }
112 | 
113 |     return chunks;
114 |   }
115 | 
116 |   private static _mergeDictionaries(dictionaries: DictionarySchema[]) {
117 |     const fileNames = _.uniq(
118 |       _.flatMap(dictionaries, (dict) => Object.keys(dict.files)),
119 |     );
120 |     const files = _(fileNames)
121 |       .map((fileName) => {
122 |         const entries = dictionaries.reduce((entries, dict) => {
123 |           const file = dict.files[fileName];
124 |           if (file) {
125 |             entries = _.merge(entries, file.entries);
126 |           }
127 |           return entries;
128 |         }, {});
129 |         return [fileName, { entries }];
130 |       })
131 |       .fromPairs()
132 |       .value();
133 |     const dictionary = {
134 |       version: dictionaries[0].version,
135 |       locale: dictionaries[0].locale,
136 |       files,
137 |     };
138 |     return dictionary;
139 |   }
140 | 
141 |   private static _createLingoDotDevEngine() {
142 |     // Specific check for CI/CD or Docker missing GROQ key
143 |     if (isRunningInCIOrDocker()) {
144 |       const apiKeyFromEnv = getLingoDotDevKeyFromEnv();
145 |       if (!apiKeyFromEnv) {
146 |         this._failMissingLLMKeyCi("lingo.dev");
147 |       }
148 |     }
149 |     const apiKey = getLingoDotDevKey();
150 |     if (!apiKey) {
151 |       throw new Error(
152 |         "⚠️  Lingo.dev API key not found. Please set LINGODOTDEV_API_KEY environment variable or configure it user-wide.",
153 |       );
154 |     }
155 |     console.log(`Creating Lingo.dev client`);
156 |     return new LingoDotDevEngine({
157 |       apiKey,
158 |     });
159 |   }
160 | 
161 |   private static async _translateChunk(
162 |     models: "lingo.dev" | Record<string, string>,
163 |     sourceDictionary: DictionarySchema,
164 |     sourceLocale: string,
165 |     targetLocale: string,
166 |     prompt?: string | null,
167 |   ): Promise<DictionarySchema> {
168 |     if (models === "lingo.dev") {
169 |       try {
170 |         const lingoDotDevEngine = this._createLingoDotDevEngine();
171 | 
172 |         console.log(
173 |           `✨ Using Lingo.dev Engine to localize from "${sourceLocale}" to "${targetLocale}"`,
174 |         );
175 | 
176 |         const result = await lingoDotDevEngine.localizeObject(
177 |           sourceDictionary,
178 |           {
179 |             sourceLocale: sourceLocale,
180 |             targetLocale: targetLocale,
181 |           },
182 |         );
183 | 
184 |         return result as DictionarySchema;
185 |       } catch (error) {
186 |         this._failLLMFailureLocal(
187 |           "lingo.dev",
188 |           targetLocale,
189 |           error instanceof Error ? error.message : "Unknown error",
190 |         );
191 |         // This throw is unreachable because the failure method exits,
192 |         // but it helps satisfy the TypeScript compiler.
193 |         throw error;
194 |       }
195 |     } else {
196 |       const { provider, model } = getLocaleModel(
197 |         models,
198 |         sourceLocale,
199 |         targetLocale,
200 |       );
201 | 
202 |       if (!provider || !model) {
203 |         throw new Error(
204 |           dedent`
205 |             🚫  Lingo.dev Localization Engine Not Configured!
206 | 
207 |             The "models" parameter is missing or incomplete in your Lingo.dev configuration.
208 | 
209 |             👉 To fix this, set the "models" parameter to either:
210 |                • "lingo.dev" (for the default engine)
211 |                • a map of locale-to-model, e.g. { "models": { "en:es": "openai:gpt-3.5-turbo" } }
212 | 
213 |             Example:
214 |               {
215 |                 // ...
216 |                 "models": "lingo.dev"
217 |               }
218 | 
219 |             For more details, see: https://lingo.dev/compiler
220 |             To get help, join our Discord: https://lingo.dev/go/discord
221 |             `,
222 |         );
223 |       }
224 | 
225 |       try {
226 |         const aiModel = this._createAiModel(provider, model, targetLocale);
227 | 
228 |         console.log(
229 |           `ℹ️ Using raw LLM API ("${provider}":"${model}") to translate from "${sourceLocale}" to "${targetLocale}"`,
230 |         );
231 | 
232 |         const response = await generateText({
233 |           model: aiModel,
234 |           messages: [
235 |             {
236 |               role: "system",
237 |               content: getSystemPrompt({
238 |                 sourceLocale,
239 |                 targetLocale,
240 |                 prompt: prompt ?? undefined,
241 |               }),
242 |             },
243 |             ...shots.flatMap((shotsTuple) => [
244 |               {
245 |                 role: "user" as const,
246 |                 content: obj2xml(shotsTuple[0]),
247 |               },
248 |               {
249 |                 role: "assistant" as const,
250 |                 content: obj2xml(shotsTuple[1]),
251 |               },
252 |             ]),
253 |             {
254 |               role: "user",
255 |               content: obj2xml(sourceDictionary),
256 |             },
257 |           ],
258 |         });
259 | 
260 |         console.log("Response text received for", targetLocale);
261 |         let responseText = response.text;
262 |         // Extract XML content
263 |         responseText = responseText.substring(
264 |           responseText.indexOf("<"),
265 |           responseText.lastIndexOf(">") + 1,
266 |         );
267 | 
268 |         return xml2obj(responseText);
269 |       } catch (error) {
270 |         this._failLLMFailureLocal(
271 |           provider,
272 |           targetLocale,
273 |           error instanceof Error ? error.message : "Unknown error",
274 |         );
275 |         // This throw is unreachable because the failure method exits,
276 |         // but it helps satisfy the TypeScript compiler.
277 |         throw error;
278 |       }
279 |     }
280 |   }
281 | 
282 |   /**
283 |    * Instantiates an AI model based on provider and model ID.
284 |    * Includes CI/CD API key checks.
285 |    * @param providerId The ID of the AI provider (e.g., "groq", "google").
286 |    * @param modelId The ID of the specific model (e.g., "llama3-8b-8192", "gemini-2.0-flash").
287 |    * @param targetLocale The target locale being translated to (for logging/error messages).
288 |    * @returns An instantiated AI LanguageModel.
289 |    * @throws Error if the provider is not supported or API key is missing in CI/CD.
290 |    */
291 |   private static _createAiModel(
292 |     providerId: string,
293 |     modelId: string,
294 |     targetLocale: string,
295 |   ): LanguageModel {
296 |     switch (providerId) {
297 |       case "groq": {
298 |         // Specific check for CI/CD or Docker missing GROQ key
299 |         if (isRunningInCIOrDocker()) {
300 |           const groqFromEnv = getGroqKeyFromEnv();
301 |           if (!groqFromEnv) {
302 |             this._failMissingLLMKeyCi(providerId);
303 |           }
304 |         }
305 |         const groqKey = getGroqKey();
306 |         if (!groqKey) {
307 |           throw new Error(
308 |             "⚠️  GROQ API key not found. Please set GROQ_API_KEY environment variable or configure it user-wide.",
309 |           );
310 |         }
311 |         console.log(
312 |           `Creating Groq client for ${targetLocale} using model ${modelId}`,
313 |         );
314 |         return createGroq({ apiKey: groqKey })(modelId);
315 |       }
316 | 
317 |       case "google": {
318 |         // Specific check for CI/CD or Docker missing Google key
319 |         if (isRunningInCIOrDocker()) {
320 |           const googleFromEnv = getGoogleKeyFromEnv();
321 |           if (!googleFromEnv) {
322 |             this._failMissingLLMKeyCi(providerId);
323 |           }
324 |         }
325 |         const googleKey = getGoogleKey();
326 |         if (!googleKey) {
327 |           throw new Error(
328 |             "⚠️  Google API key not found. Please set GOOGLE_API_KEY environment variable or configure it user-wide.",
329 |           );
330 |         }
331 |         console.log(
332 |           `Creating Google Generative AI client for ${targetLocale} using model ${modelId}`,
333 |         );
334 |         return createGoogleGenerativeAI({ apiKey: googleKey })(modelId);
335 |       }
336 |       case "openrouter": {
337 |         // Specific check for CI/CD or Docker missing OpenRouter key
338 |         if (isRunningInCIOrDocker()) {
339 |           const openRouterFromEnv = getOpenRouterKeyFromEnv();
340 |           if (!openRouterFromEnv) {
341 |             this._failMissingLLMKeyCi(providerId);
342 |           }
343 |         }
344 |         const openRouterKey = getOpenRouterKey();
345 |         if (!openRouterKey) {
346 |           throw new Error(
347 |             "⚠️  OpenRouter API key not found. Please set OPENROUTER_API_KEY environment variable or configure it user-wide.",
348 |           );
349 |         }
350 |         console.log(
351 |           `Creating OpenRouter client for ${targetLocale} using model ${modelId}`,
352 |         );
353 |         return createOpenRouter({
354 |           apiKey: openRouterKey,
355 |         })(modelId);
356 |       }
357 | 
358 |       case "ollama": {
359 |         // No API key check needed for Ollama
360 |         console.log(
361 |           `Creating Ollama client for ${targetLocale} using model ${modelId} at default Ollama address`,
362 |         );
363 |         return createOllama()(modelId);
364 |       }
365 | 
366 |       case "mistral": {
367 |         // Specific check for CI/CD or Docker missing Mistral key
368 |         if (isRunningInCIOrDocker()) {
369 |           const mistralFromEnv = getMistralKeyFromEnv();
370 |           if (!mistralFromEnv) {
371 |             this._failMissingLLMKeyCi(providerId);
372 |           }
373 |         }
374 |         const mistralKey = getMistralKey();
375 |         if (!mistralKey) {
376 |           throw new Error(
377 |             "⚠️  Mistral API key not found. Please set MISTRAL_API_KEY environment variable or configure it user-wide.",
378 |           );
379 |         }
380 |         console.log(
381 |           `Creating Mistral client for ${targetLocale} using model ${modelId}`,
382 |         );
383 |         return createMistral({ apiKey: mistralKey })(modelId);
384 |       }
385 | 
386 |       default: {
387 |         throw new Error(
388 |           `⚠️  Provider "${providerId}" for locale "${targetLocale}" is not supported. Only "groq", "google", "openrouter", "ollama", and "mistral" providers are supported at the moment.`,
389 |         );
390 |       }
391 |     }
392 |   }
393 | 
394 |   /**
395 |    * Show an actionable error message and exit the process when the compiler
396 |    * is running in CI/CD without a required LLM API key.
397 |    * The message explains why this situation is unusual and how to fix it.
398 |    * @param providerId The ID of the LLM provider whose key is missing.
399 |    */
400 |   private static _failMissingLLMKeyCi(providerId: string): never {
401 |     let details = providerDetails[providerId];
402 |     if (!details) {
403 |       // Fallback for unsupported provider in failure message logic
404 |       throw new Error(
405 |         `Internal Error: Missing details for provider "${providerId}" when reporting missing key in CI/CD. You might be using an unsupported provider.`,
406 |       );
407 |     }
408 | 
409 |     const errorMessage = dedent`
410 |       💡 You're using Lingo.dev Localization Compiler, and it detected unlocalized components in your app.
411 | 
412 |       The compiler needs a ${details.name} API key to translate missing strings, but ${details.apiKeyEnvVar} is not set in the environment.
413 | 
414 |       This is unexpected: typically you run a full build locally, commit the generated translation files, and push them to CI/CD.
415 | 
416 |       However, If you want CI/CD to translate the new strings, provide the key with:
417 |       • Session-wide: export ${details.apiKeyEnvVar}=<your-api-key>
418 |       • Project-wide / CI: add ${details.apiKeyEnvVar}=<your-api-key> to your pipeline environment variables
419 | 
420 |       ⭐️ Also:
421 |       1. If you don't yet have a ${details.name} API key, get one for free at ${details.getKeyLink}
422 |       2. If you want to use a different LLM, update your configuration. Refer to documentation for help: https://lingo.dev/compiler
423 |       3. If the model you want to use isn't supported yet, raise an issue in our open-source repo: https://lingo.dev/go/gh
424 |     `;
425 |     console.log(errorMessage);
426 |     throw new Error(`Missing ${details.name} API key in CI/CD environment.`);
427 |   }
428 | 
429 |   /**
430 |    * Show an actionable error message and exit the process when an LLM API call
431 |    * fails during local compilation.
432 |    * @param providerId The ID of the LLM provider that failed.
433 |    * @param targetLocale The target locale being translated to.
434 |    * @param errorMessage The error message received from the API.
435 |    */
436 |   private static _failLLMFailureLocal(
437 |     providerId: string,
438 |     targetLocale: string,
439 |     errorMessage: string,
440 |   ): never {
441 |     const details = providerDetails[providerId];
442 |     if (!details) {
443 |       // Fallback
444 |       throw new Error(
445 |         `Internal Error: Missing details for provider "${providerId}" when reporting local failure. Original Error: ${errorMessage}`,
446 |       );
447 |     }
448 | 
449 |     const isInvalidApiKey = errorMessage.match("Invalid API Key"); // TODO: This may change per-provider, so might update this later
450 | 
451 |     if (isInvalidApiKey) {
452 |       const message = dedent`
453 |         ⚠️  Lingo.dev Compiler requires a valid ${details.name} API key to translate your application.
454 | 
455 |         It looks like you set ${details.name} API key but it is not valid. Please check your API key and try again.
456 | 
457 |         Error details from ${details.name} API: ${errorMessage}
458 | 
459 |         👉 You can set the API key in one of the following ways:
460 |         1. User-wide: Run npx lingo.dev@latest config set ${details.apiKeyConfigKey} <your-api-key>
461 |         2. Project-wide: Add ${details.apiKeyEnvVar}=<your-api-key> to .env file in every project that uses Lingo.dev Localization Compiler
462 |         3 Session-wide: Run export ${details.apiKeyEnvVar}=<your-api-key> in your terminal before running the compiler to set the API key for the current session
463 | 
464 |         ⭐️ Also:
465 |         1. If you don't yet have a ${details.name} API key, get one for free at ${details.getKeyLink}
466 |         2. If you want to use a different LLM, raise an issue in our open-source repo: https://lingo.dev/go/gh
467 |         3. If you have questions, feature requests, or would like to contribute, join our Discord: https://lingo.dev/go/discord
468 |       `;
469 |       console.log(message);
470 |       throw new Error(`Invalid ${details.name} API key.`);
471 |     } else {
472 |       const message = dedent`
473 |         ⚠️  Lingo.dev Compiler tried to translate your application to "${targetLocale}" locale via ${
474 |           details.name
475 |         } but it failed.
476 | 
477 |         Error details from ${details.name} API: ${errorMessage}
478 | 
479 |         This error comes from the ${
480 |           details.name
481 |         } API, please check their documentation for more details: ${
482 |           details.docsLink
483 |         }
484 | 
485 |         ⭐️ Also:
486 |         1. Did you set ${
487 |           details.apiKeyEnvVar
488 |             ? `${details.apiKeyEnvVar}`
489 |             : "the provider API key"
490 |         } environment variable correctly ${
491 |           !details.apiKeyEnvVar ? "(if required)" : ""
492 |         }?
493 |         2. Did you reach any limits of your ${details.name} account?
494 |         3. If you have questions, feature requests, or would like to contribute, join our Discord: https://lingo.dev/go/discord
495 |       `;
496 |       console.log(message);
497 |       throw new Error(
498 |         `Translation failed for locale "${targetLocale}" using ${details.name}: ${errorMessage}`,
499 |       );
500 |     }
501 |   }
502 | }
503 | 
```

--------------------------------------------------------------------------------
/packages/cli/src/cli/loaders/ignored-keys-buckets.spec.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, beforeEach, vi } from "vitest";
  2 | import fs from "fs/promises";
  3 | import dedent from "dedent";
  4 | import createBucketLoader from "./index";
  5 | 
  6 | describe("ignored keys support across buckets", () => {
  7 |   beforeEach(() => {
  8 |     vi.clearAllMocks();
  9 |     vi.resetModules();
 10 |     setupFileMocks();
 11 |   });
 12 | 
 13 |   it("android: should omit ignored keys on pull", async () => {
 14 |     const input = `
 15 |       <resources>
 16 |         <string name="button.title">Submit</string>
 17 |         <string name="button.description">Description</string>
 18 |       </resources>
 19 |     `.trim();
 20 |     mockFileOperations(input);
 21 | 
 22 |     const loader = createBucketLoader(
 23 |       "android",
 24 |       "values-[locale]/strings.xml",
 25 |       { defaultLocale: "en" },
 26 |       [],
 27 |       [],
 28 |       ["button.description"],
 29 |     );
 30 |     loader.setDefaultLocale("en");
 31 |     const data = await loader.pull("en");
 32 |     expect(data).toEqual({ "button.title": "Submit" });
 33 |   });
 34 | 
 35 |   it("csv: should omit ignored keys on pull", async () => {
 36 |     const input = `id,en\nbutton.title,Submit\nbutton.description,Description`;
 37 |     mockFileOperations(input);
 38 | 
 39 |     const loader = createBucketLoader(
 40 |       "csv",
 41 |       "i18n.csv",
 42 |       { defaultLocale: "en" },
 43 |       [],
 44 |       [],
 45 |       ["button.description"],
 46 |     );
 47 |     loader.setDefaultLocale("en");
 48 |     const data = await loader.pull("en");
 49 |     expect(data).toEqual({ "button.title": "Submit" });
 50 |   });
 51 | 
 52 |   it("html: should omit ignored keys (by prefix) on pull", async () => {
 53 |     const input = dedent`
 54 |       <html>
 55 |         <head>
 56 |           <title>My Page</title>
 57 |           <meta name="description" content="Page description" />
 58 |         </head>
 59 |         <body>
 60 |           <h1>Hello</h1>
 61 |           <p>Paragraph</p>
 62 |         </body>
 63 |       </html>
 64 |     `;
 65 |     mockFileOperations(input);
 66 | 
 67 |     const loader = createBucketLoader(
 68 |       "html",
 69 |       "i18n/[locale].html",
 70 |       { defaultLocale: "en" },
 71 |       [],
 72 |       [],
 73 |       ["head"],
 74 |     );
 75 |     loader.setDefaultLocale("en");
 76 |     const data = await loader.pull("en");
 77 |     expect(Object.keys(data).some((k) => k.startsWith("head"))).toBe(false);
 78 |   });
 79 | 
 80 |   it("ejs: should omit ignored keys on pull", async () => {
 81 |     const input = `<h1>Welcome</h1><p>Hello <%= name %></p>`;
 82 |     mockFileOperations(input);
 83 | 
 84 |     const loader = createBucketLoader(
 85 |       "ejs",
 86 |       "templates/[locale].ejs",
 87 |       { defaultLocale: "en" },
 88 |       [],
 89 |       [],
 90 |       ["text_*"],
 91 |     );
 92 |     loader.setDefaultLocale("en");
 93 |     const data = await loader.pull("en");
 94 |     expect(data).toEqual({});
 95 |   });
 96 | 
 97 |   it("json: should omit ignored keys on pull", async () => {
 98 |     const input = JSON.stringify({ title: "Submit", description: "Desc" });
 99 |     mockFileOperations(input);
100 | 
101 |     const loader = createBucketLoader(
102 |       "json",
103 |       "i18n/[locale].json",
104 |       { defaultLocale: "en" },
105 |       [],
106 |       [],
107 |       ["description"],
108 |     );
109 |     loader.setDefaultLocale("en");
110 |     const data = await loader.pull("en");
111 |     expect(data).toEqual({ title: "Submit" });
112 |   });
113 | 
114 |   it("json5: should omit ignored keys on pull", async () => {
115 |     const input = `{
116 |       // comment
117 |       title: "Submit",
118 |       description: "Desc"
119 |     }`;
120 |     mockFileOperations(input);
121 | 
122 |     const loader = createBucketLoader(
123 |       "json5",
124 |       "i18n/[locale].json5",
125 |       { defaultLocale: "en" },
126 |       [],
127 |       [],
128 |       ["description"],
129 |     );
130 |     loader.setDefaultLocale("en");
131 |     const data = await loader.pull("en");
132 |     expect(data).toEqual({ title: "Submit" });
133 |   });
134 | 
135 |   it("jsonc: should omit ignored keys on pull", async () => {
136 |     const input = `{
137 |       // comment
138 |       "title": "Submit",
139 |       "description": "Desc"
140 |     }`;
141 |     mockFileOperations(input);
142 | 
143 |     const loader = createBucketLoader(
144 |       "jsonc",
145 |       "i18n/[locale].jsonc",
146 |       { defaultLocale: "en" },
147 |       [],
148 |       [],
149 |       ["description"],
150 |     );
151 |     loader.setDefaultLocale("en");
152 |     const data = await loader.pull("en");
153 |     expect(data).toEqual({ title: "Submit" });
154 |   });
155 | 
156 |   it("markdown: should omit ignored keys (frontmatter) on pull", async () => {
157 |     const input = dedent`
158 |       ---
159 |       title: Test Markdown
160 |       date: 2023-05-25
161 |       ---
162 | 
163 |       # Heading 1
164 | 
165 |       Content.
166 |     `;
167 |     mockFileOperations(input);
168 | 
169 |     const loader = createBucketLoader(
170 |       "markdown",
171 |       "i18n/[locale].md",
172 |       { defaultLocale: "en" },
173 |       [],
174 |       [],
175 |       ["fm-attr-title"],
176 |     );
177 |     loader.setDefaultLocale("en");
178 |     const data = await loader.pull("en");
179 |     expect(Object.keys(data)).not.toContain("fm-attr-title");
180 |   });
181 | 
182 |   it("markdoc: should omit ignored keys by semantic prefix on pull", async () => {
183 |     const input = dedent`
184 |       ---
185 |       title: My Page
186 |       ---
187 | 
188 |       # Heading 1
189 | 
190 |       Hello world
191 |     `;
192 |     mockFileOperations(input);
193 | 
194 |     const loader = createBucketLoader(
195 |       "markdoc",
196 |       "docs/[locale].md",
197 |       { defaultLocale: "en" },
198 |       [],
199 |       [],
200 |       ["heading"],
201 |     );
202 |     loader.setDefaultLocale("en");
203 |     const data = await loader.pull("en");
204 |     expect(Object.keys(data).some((k) => k.startsWith("heading"))).toBe(false);
205 |   });
206 | 
207 |   it("mdx: should omit ignored section keys on pull", async () => {
208 |     const input = dedent`
209 |       ---
210 |       title: Hello
211 |       ---
212 | 
213 |       # Title
214 | 
215 |       Paragraph
216 |     `;
217 |     mockFileOperations(input);
218 | 
219 |     const loader = createBucketLoader(
220 |       "mdx",
221 |       "i18n/[locale].mdx",
222 |       { defaultLocale: "en", formatter: undefined },
223 |       [],
224 |       [],
225 |       ["md-section-0"],
226 |     );
227 |     loader.setDefaultLocale("en");
228 |     const data = await loader.pull("en");
229 |     expect(Object.keys(data)).not.toContain("md-section-0");
230 |   });
231 | 
232 |   it("po: should omit ignored keys on pull", async () => {
233 |     const input = dedent`
234 |       #: hello.py:1
235 |       msgid "Hello"
236 |       msgstr ""
237 |     `;
238 |     mockFileOperations(input);
239 | 
240 |     const loader = createBucketLoader(
241 |       "po",
242 |       "i18n/[locale].po",
243 |       { defaultLocale: "en" },
244 |       [],
245 |       [],
246 |       ["Hello"],
247 |     );
248 |     loader.setDefaultLocale("en");
249 |     const data = await loader.pull("en");
250 |     expect(data).toEqual({});
251 |   });
252 | 
253 |   it("properties: should omit ignored keys on pull", async () => {
254 |     const input = dedent`
255 |       welcome.message=Welcome
256 |       error.message=Error
257 |     `;
258 |     mockFileOperations(input);
259 | 
260 |     const loader = createBucketLoader(
261 |       "properties",
262 |       "i18n/[locale].properties",
263 |       { defaultLocale: "en" },
264 |       [],
265 |       [],
266 |       ["error.message"],
267 |     );
268 |     loader.setDefaultLocale("en");
269 |     const data = await loader.pull("en");
270 |     expect(data).toEqual({ "welcome.message": "Welcome" });
271 |   });
272 | 
273 |   it("xcode-strings: should omit ignored keys on pull", async () => {
274 |     const input = `"hello" = "Hello!";\n"bye" = "Bye!";`;
275 |     mockFileOperations(input);
276 | 
277 |     const loader = createBucketLoader(
278 |       "xcode-strings",
279 |       "i18n/[locale].strings",
280 |       { defaultLocale: "en" },
281 |       [],
282 |       [],
283 |       ["bye"],
284 |     );
285 |     loader.setDefaultLocale("en");
286 |     const data = await loader.pull("en");
287 |     expect(data).toEqual({ hello: "Hello!" });
288 |   });
289 | 
290 |   it("xcode-stringsdict: should omit ignored keys on pull", async () => {
291 |     const input = dedent`
292 |       <?xml version="1.0" encoding="UTF-8"?>
293 |       <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
294 |       <plist version="1.0">
295 |       <dict>
296 |         <key>greeting</key>
297 |         <string>Hello!</string>
298 |         <key>items_count</key>
299 |         <dict>
300 |           <key>NSStringLocalizedFormatKey</key>
301 |           <string>%#@items@</string>
302 |           <key>items</key>
303 |           <dict>
304 |             <key>NSStringFormatSpecTypeKey</key>
305 |             <string>NSStringPluralRuleType</string>
306 |             <key>NSStringFormatValueTypeKey</key>
307 |             <string>d</string>
308 |             <key>one</key>
309 |             <string>%d item</string>
310 |             <key>other</key>
311 |             <string>%d items</string>
312 |           </dict>
313 |         </dict>
314 |       </dict>
315 |       </plist>
316 |     `;
317 |     mockFileOperations(input);
318 | 
319 |     const loader = createBucketLoader(
320 |       "xcode-stringsdict",
321 |       "i18n/[locale].stringsdict",
322 |       { defaultLocale: "en" },
323 |       [],
324 |       [],
325 |       ["items_count"],
326 |     );
327 |     loader.setDefaultLocale("en");
328 |     const data = await loader.pull("en");
329 |     expect(Object.keys(data)).toContain("greeting");
330 |     expect(Object.keys(data).some((k) => k.startsWith("items_count"))).toBe(
331 |       false,
332 |     );
333 |   });
334 | 
335 |   it("xcode-xcstrings: should omit ignored keys on pull", async () => {
336 |     const input = dedent`
337 |       {
338 |         "sourceLanguage": "en",
339 |         "strings": {
340 |           "greeting": {
341 |             "extractionState": "manual",
342 |             "localizations": {
343 |               "en": { "stringUnit": { "state": "translated", "value": "Hello!" } }
344 |             }
345 |           },
346 |           "message": {
347 |             "extractionState": "manual",
348 |             "localizations": {
349 |               "en": { "stringUnit": { "state": "translated", "value": "Welcome" } }
350 |             }
351 |           }
352 |         }
353 |       }
354 |     `;
355 |     mockFileOperations(input);
356 | 
357 |     const loader = createBucketLoader(
358 |       "xcode-xcstrings",
359 |       "i18n/[locale].xcstrings",
360 |       { defaultLocale: "en" },
361 |       [],
362 |       [],
363 |       ["message"],
364 |     );
365 |     loader.setDefaultLocale("en");
366 |     const data = await loader.pull("en");
367 |     expect(data).toEqual({ greeting: "Hello!" });
368 |   });
369 | 
370 |   it("xcode-xcstrings-v2: should omit ignored string keys on pull", async () => {
371 |     const input = dedent`
372 |       {
373 |         "sourceLanguage": "en",
374 |         "strings": {
375 |           "hello": {
376 |             "extractionState": "manual",
377 |             "localizations": {
378 |               "en": { "stringUnit": { "state": "translated", "value": "Hello" } }
379 |             }
380 |           },
381 |           "world": {
382 |             "extractionState": "manual",
383 |             "localizations": {
384 |               "en": { "stringUnit": { "state": "translated", "value": "World" } }
385 |             }
386 |           }
387 |         }
388 |       }
389 |     `;
390 |     mockFileOperations(input);
391 | 
392 |     const loader = createBucketLoader(
393 |       "xcode-xcstrings-v2",
394 |       "i18n/[locale].xcstrings",
395 |       { defaultLocale: "en" },
396 |       [],
397 |       [],
398 |       ["world"],
399 |     );
400 |     loader.setDefaultLocale("en");
401 |     const data = await loader.pull("en");
402 |     expect(Object.keys(data)).toContain("hello");
403 |     expect(Object.keys(data)).not.toContain("world");
404 |   });
405 | 
406 |   it("yaml: should omit ignored keys on pull", async () => {
407 |     const input = dedent`
408 |       title: Submit
409 |       description: Desc
410 |     `;
411 |     mockFileOperations(input);
412 | 
413 |     const loader = createBucketLoader(
414 |       "yaml",
415 |       "i18n/[locale].yml",
416 |       { defaultLocale: "en" },
417 |       [],
418 |       [],
419 |       ["description"],
420 |     );
421 |     loader.setDefaultLocale("en");
422 |     const data = await loader.pull("en");
423 |     expect(data).toEqual({ title: "Submit" });
424 |   });
425 | 
426 |   it("yaml-root-key: should omit ignored keys on pull", async () => {
427 |     const input = dedent`
428 |       en:
429 |         title: Submit
430 |         description: Desc
431 |     `;
432 |     mockFileOperations(input);
433 | 
434 |     const loader = createBucketLoader(
435 |       "yaml-root-key",
436 |       "i18n/[locale].yml",
437 |       { defaultLocale: "en" },
438 |       [],
439 |       [],
440 |       ["description"],
441 |     );
442 |     loader.setDefaultLocale("en");
443 |     const data = await loader.pull("en");
444 |     expect(data).toEqual({ title: "Submit" });
445 |   });
446 | 
447 |   it("flutter: should omit ignored keys on pull", async () => {
448 |     const input = JSON.stringify(
449 |       {
450 |         "@@locale": "en",
451 |         greeting: "Hello, {name}!",
452 |         "@greeting": { description: "d" },
453 |         farewell: "Goodbye!",
454 |       },
455 |       null,
456 |       2,
457 |     );
458 |     mockFileOperations(input);
459 | 
460 |     const loader = createBucketLoader(
461 |       "flutter",
462 |       "lib/l10n/app_[locale].arb",
463 |       { defaultLocale: "en" },
464 |       [],
465 |       [],
466 |       ["farewell"],
467 |     );
468 |     loader.setDefaultLocale("en");
469 |     const data = await loader.pull("en");
470 |     expect(Object.keys(data)).toContain("greeting");
471 |     expect(Object.keys(data)).not.toContain("farewell");
472 |   });
473 | 
474 |   it("xliff: should omit ignored keys on pull", async () => {
475 |     const input = dedent`
476 |       <?xml version="1.0" encoding="utf-8"?>
477 |       <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
478 |         <file original="" source-language="en" datatype="plaintext">
479 |           <body>
480 |             <trans-unit id="greeting" resname="greeting"><source>Hello</source></trans-unit>
481 |             <trans-unit id="farewell" resname="farewell"><source>Goodbye</source></trans-unit>
482 |           </body>
483 |         </file>
484 |       </xliff>
485 |     `;
486 |     mockFileOperations(input);
487 | 
488 |     const loader = createBucketLoader(
489 |       "xliff",
490 |       "i18n/[locale].xliff",
491 |       { defaultLocale: "en" },
492 |       [],
493 |       [],
494 |       ["farewell"],
495 |     );
496 |     loader.setDefaultLocale("en");
497 |     const data = await loader.pull("en");
498 |     expect(Object.keys(data)).toContain("greeting");
499 |     expect(Object.keys(data)).not.toContain("farewell");
500 |   });
501 | 
502 |   it("xml: should omit ignored keys on pull", async () => {
503 |     const input = `<root><title>Hello</title><description>Desc</description></root>`;
504 |     mockFileOperations(input);
505 | 
506 |     const loader = createBucketLoader(
507 |       "xml",
508 |       "i18n/[locale].xml",
509 |       { defaultLocale: "en" },
510 |       [],
511 |       [],
512 |       ["root/description"],
513 |     );
514 |     loader.setDefaultLocale("en");
515 |     const data = await loader.pull("en");
516 |     expect(Object.keys(data)).toContain("root/title");
517 |     expect(Object.keys(data)).not.toContain("root/description");
518 |   });
519 | 
520 |   it("srt: should omit ignored keys on pull", async () => {
521 |     const input = dedent`
522 |       1
523 |       00:00:01,000 --> 00:00:04,000
524 |       Hello
525 | 
526 |       2
527 |       00:00:05,000 --> 00:00:06,000
528 |       World
529 |     `;
530 |     mockFileOperations(input);
531 | 
532 |     const loader = createBucketLoader(
533 |       "srt",
534 |       "i18n/[locale].srt",
535 |       { defaultLocale: "en" },
536 |       [],
537 |       [],
538 |       ["1#*"],
539 |     );
540 |     loader.setDefaultLocale("en");
541 |     const data = await loader.pull("en");
542 |     // Expect only entry 2 remains
543 |     const keys = Object.keys(data);
544 |     expect(keys.length).toBe(1);
545 |     expect(keys[0].startsWith("2#")).toBe(true);
546 |   });
547 | 
548 |   it("vtt: should omit ignored keys on pull", async () => {
549 |     const input = dedent`
550 |       WEBVTT
551 | 
552 |       00:00:00.000 --> 00:00:02.000
553 |       First
554 | 
555 |       00:00:02.000 --> 00:00:04.000
556 |       Second
557 |     `;
558 |     mockFileOperations(input);
559 | 
560 |     const loader = createBucketLoader(
561 |       "vtt",
562 |       "i18n/[locale].vtt",
563 |       { defaultLocale: "en" },
564 |       [],
565 |       [],
566 |       ["0#*"],
567 |     );
568 |     loader.setDefaultLocale("en");
569 |     const data = await loader.pull("en");
570 |     // One cue should be filtered
571 |     expect(Object.keys(data).length).toBe(1);
572 |   });
573 | 
574 |   it("php: should omit ignored keys on pull", async () => {
575 |     const input = dedent`
576 |       <?php
577 |       return [
578 |         'title' => 'Submit',
579 |         'description' => 'Desc',
580 |       ];
581 |     `;
582 |     mockFileOperations(input);
583 | 
584 |     const loader = createBucketLoader(
585 |       "php",
586 |       "i18n/[locale].php",
587 |       { defaultLocale: "en" },
588 |       [],
589 |       [],
590 |       ["description"],
591 |     );
592 |     loader.setDefaultLocale("en");
593 |     const data = await loader.pull("en");
594 |     expect(data).toEqual({ title: "Submit" });
595 |   });
596 | 
597 |   it("vue-json: should omit ignored keys on pull", async () => {
598 |     const input = dedent`
599 |       <template></template>
600 |       <i18n>
601 |       {"en": {"title": "Hello", "description": "Desc"}}
602 |       </i18n>
603 |       <script setup></script>
604 |     `;
605 |     mockFileOperations(input);
606 | 
607 |     const loader = createBucketLoader(
608 |       "vue-json",
609 |       "i18n/App.vue",
610 |       { defaultLocale: "en" },
611 |       [],
612 |       [],
613 |       ["description"],
614 |     );
615 |     loader.setDefaultLocale("en");
616 |     const data = await loader.pull("en");
617 |     expect(data).toEqual({ title: "Hello" });
618 |   });
619 | 
620 |   it("typescript: should omit ignored keys on pull", async () => {
621 |     const input = dedent`
622 |       export default {
623 |         title: "Submit",
624 |         description: "Desc"
625 |       };
626 |     `;
627 |     mockFileOperations(input);
628 | 
629 |     const loader = createBucketLoader(
630 |       "typescript",
631 |       "i18n/[locale].ts",
632 |       { defaultLocale: "en" },
633 |       [],
634 |       [],
635 |       ["description"],
636 |     );
637 |     loader.setDefaultLocale("en");
638 |     const data = await loader.pull("en");
639 |     expect(data).toEqual({ title: "Submit" });
640 |   });
641 | 
642 |   it("txt: should omit ignored keys on pull", async () => {
643 |     const input = dedent`
644 |       First line
645 |       Second line
646 |     `;
647 |     mockFileOperations(input);
648 | 
649 |     const loader = createBucketLoader(
650 |       "txt",
651 |       "fastlane/metadata/[locale]/description.txt",
652 |       { defaultLocale: "en" },
653 |       [],
654 |       [],
655 |       ["1"],
656 |     );
657 |     loader.setDefaultLocale("en");
658 |     const data = await loader.pull("en");
659 |     expect(Object.keys(data)).toEqual(["2"]);
660 |   });
661 | 
662 |   it("json-dictionary: should omit ignored keys on pull (wildcard)", async () => {
663 |     const input = JSON.stringify(
664 |       {
665 |         title: { en: "Title" },
666 |         pages: [
667 |           {
668 |             elements: [
669 |               { title: { en: "E1" }, description: { en: "D1" } },
670 |               { title: { en: "E2" }, description: { en: "D2" } },
671 |             ],
672 |           },
673 |         ],
674 |       },
675 |       null,
676 |       2,
677 |     );
678 |     mockFileOperations(input);
679 | 
680 |     const loader = createBucketLoader(
681 |       "json-dictionary",
682 |       "i18n/[locale].json",
683 |       { defaultLocale: "en" },
684 |       [],
685 |       [],
686 |       ["pages/*/elements/*/description"],
687 |     );
688 |     loader.setDefaultLocale("en");
689 |     const data = await loader.pull("en");
690 |     const keys = Object.keys(data);
691 |     expect(keys).toContain("title");
692 |     expect(keys).toContain("pages/0/elements/0/title");
693 |     expect(keys.find((k) => k.includes("/description"))).toBeUndefined();
694 |   });
695 | });
696 | 
697 | function setupFileMocks() {
698 |   vi.mock("fs/promises", () => ({
699 |     default: {
700 |       readFile: vi.fn(),
701 |       writeFile: vi.fn(),
702 |       mkdir: vi.fn(),
703 |       access: vi.fn(),
704 |     },
705 |   }));
706 | 
707 |   vi.mock("path", () => ({
708 |     default: {
709 |       resolve: vi.fn((path) => path),
710 |       dirname: vi.fn((path) => path.split("/").slice(0, -1).join("/")),
711 |     },
712 |   }));
713 | }
714 | 
715 | function mockFileOperations(input: string) {
716 |   (fs.access as any).mockImplementation(() => Promise.resolve());
717 |   (fs.readFile as any).mockImplementation(() => Promise.resolve(input));
718 |   (fs.writeFile as any).mockImplementation(() => Promise.resolve());
719 | }
720 | 
```

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

```typescript
  1 | import { describe, it, expect } from "vitest";
  2 | import createXcodeXcstringsLoader, { _removeLocale } from "./xcode-xcstrings";
  3 | 
  4 | describe("loaders/xcode-xcstrings", () => {
  5 |   const defaultLocale = "en";
  6 |   const mockInput = {
  7 |     sourceLanguage: "en",
  8 |     strings: {
  9 |       "app.title": {
 10 |         localizations: {
 11 |           en: {
 12 |             stringUnit: {
 13 |               state: "translated",
 14 |               value: "My App",
 15 |             },
 16 |           },
 17 |           es: {
 18 |             stringUnit: {
 19 |               state: "translated",
 20 |               value: "Mi App",
 21 |             },
 22 |           },
 23 |         },
 24 |       },
 25 |       "items.count": {
 26 |         localizations: {
 27 |           en: {
 28 |             variations: {
 29 |               plural: {
 30 |                 one: {
 31 |                   stringUnit: {
 32 |                     state: "translated",
 33 |                     value: "1 item",
 34 |                   },
 35 |                 },
 36 |                 other: {
 37 |                   stringUnit: {
 38 |                     state: "translated",
 39 |                     value: "%d items",
 40 |                   },
 41 |                 },
 42 |               },
 43 |             },
 44 |           },
 45 |           es: {
 46 |             variations: {
 47 |               plural: {
 48 |                 one: {
 49 |                   stringUnit: {
 50 |                     state: "translated",
 51 |                     value: "1 artículo",
 52 |                   },
 53 |                 },
 54 |                 other: {
 55 |                   stringUnit: {
 56 |                     state: "translated",
 57 |                     value: "%d artículos",
 58 |                   },
 59 |                 },
 60 |               },
 61 |             },
 62 |           },
 63 |         },
 64 |       },
 65 |       "key.no-translate": {
 66 |         shouldTranslate: false,
 67 |         localizations: {
 68 |           en: {
 69 |             stringUnit: {
 70 |               state: "translated",
 71 |               value: "Do not translate",
 72 |             },
 73 |           },
 74 |         },
 75 |       },
 76 |       "key.source-only": {
 77 |         localizations: {},
 78 |       },
 79 |       "key.missing-localization": {
 80 |         localizations: {
 81 |           es: {
 82 |             stringUnit: {
 83 |               state: "translated",
 84 |               value: "solo español",
 85 |             },
 86 |           },
 87 |         },
 88 |       },
 89 |     },
 90 |     version: "1.0",
 91 |   };
 92 | 
 93 |   describe("pull", () => {
 94 |     it("should pull simple string translations for a given locale", async () => {
 95 |       const loader = createXcodeXcstringsLoader(defaultLocale);
 96 |       loader.setDefaultLocale(defaultLocale);
 97 |       await loader.pull(defaultLocale, mockInput);
 98 |       const result = await loader.pull("es", mockInput);
 99 |       expect(result).toEqual({
100 |         "app.title": "Mi App",
101 |         "items.count": {
102 |           one: "1 artículo",
103 |           other: "%d artículos",
104 |         },
105 |         "key.missing-localization": "solo español",
106 |       });
107 |     });
108 | 
109 |     it("should pull plural translations for a given locale", async () => {
110 |       const loader = createXcodeXcstringsLoader(defaultLocale);
111 |       loader.setDefaultLocale(defaultLocale);
112 |       const result = await loader.pull("en", mockInput);
113 |       expect(result["items.count"]).toEqual({
114 |         one: "1 item",
115 |         other: "%d items",
116 |       });
117 |     });
118 | 
119 |     it("should use the key as value for the source language if no translation is available", async () => {
120 |       const loader = createXcodeXcstringsLoader(defaultLocale);
121 |       loader.setDefaultLocale(defaultLocale);
122 |       const result = await loader.pull("en", mockInput);
123 |       expect(result["key.source-only"]).toBe("key.source-only");
124 |       expect(result["key.missing-localization"]).toBe(
125 |         "key.missing-localization",
126 |       );
127 |     });
128 | 
129 |     it("should not use key as value if not source language", async () => {
130 |       const loader = createXcodeXcstringsLoader(defaultLocale);
131 |       loader.setDefaultLocale(defaultLocale);
132 |       await loader.pull(defaultLocale, mockInput);
133 |       const result = await loader.pull("es", mockInput);
134 |       expect(result["key.source-only"]).toBeUndefined();
135 |     });
136 | 
137 |     it("should skip keys marked with shouldTranslate: false", async () => {
138 |       const loader = createXcodeXcstringsLoader(defaultLocale);
139 |       loader.setDefaultLocale(defaultLocale);
140 |       const result = await loader.pull("en", mockInput);
141 |       expect(result["key.no-translate"]).toBeUndefined();
142 |     });
143 | 
144 |     it("should return an empty object for a locale with no translations", async () => {
145 |       const loader = createXcodeXcstringsLoader(defaultLocale);
146 |       loader.setDefaultLocale(defaultLocale);
147 |       await loader.pull(defaultLocale, mockInput);
148 |       const result = await loader.pull("fr", mockInput);
149 |       expect(result).toEqual({});
150 |     });
151 |   });
152 | 
153 |   describe("push", () => {
154 |     it("should push simple string translations", async () => {
155 |       const loader = createXcodeXcstringsLoader(defaultLocale);
156 |       loader.setDefaultLocale(defaultLocale);
157 |       await loader.pull(defaultLocale, mockInput);
158 |       const payload = {
159 |         "app.title": "Mon App",
160 |       };
161 |       const result = await loader.push("fr", payload);
162 |       expect(result).not.toBeNull();
163 |       expect(result!.version).toBe("1.0");
164 |       expect(result!.strings["app.title"].localizations.fr).toEqual({
165 |         stringUnit: {
166 |           state: "translated",
167 |           value: "Mon App",
168 |         },
169 |       });
170 |     });
171 | 
172 |     it("should push plural translations in plain object format", async () => {
173 |       const loader = createXcodeXcstringsLoader(defaultLocale);
174 |       loader.setDefaultLocale(defaultLocale);
175 |       await loader.pull(defaultLocale, mockInput);
176 |       const payload = {
177 |         "items.count": {
178 |           one: "1 article",
179 |           other: "%d articles",
180 |         },
181 |       };
182 |       const result = await loader.push("fr", payload);
183 |       expect(result).not.toBeNull();
184 |       expect(result!.strings["items.count"].localizations.fr).toEqual({
185 |         variations: {
186 |           plural: {
187 |             one: {
188 |               stringUnit: {
189 |                 state: "translated",
190 |                 value: "1 article",
191 |               },
192 |             },
193 |             other: {
194 |               stringUnit: {
195 |                 state: "translated",
196 |                 value: "%d articles",
197 |               },
198 |             },
199 |           },
200 |         },
201 |       });
202 |     });
203 | 
204 |     it("should merge translations into existing input", async () => {
205 |       const loader = createXcodeXcstringsLoader(defaultLocale);
206 |       loader.setDefaultLocale(defaultLocale);
207 |       await loader.pull(defaultLocale, mockInput);
208 |       const payload = {
209 |         "app.title": "Mi App (actualizado)",
210 |       };
211 |       const result = await loader.push("es", payload);
212 |       expect(result).not.toBeNull();
213 |       // check new value
214 |       expect(
215 |         result!.strings["app.title"].localizations.es.stringUnit.value,
216 |       ).toBe("Mi App (actualizado)");
217 |       // check existing value is untouched
218 |       expect(
219 |         result!.strings["app.title"].localizations.en.stringUnit.value,
220 |       ).toBe("My App");
221 |     });
222 | 
223 |     it("should preserve the shouldTranslate: false flag", async () => {
224 |       const loader = createXcodeXcstringsLoader(defaultLocale);
225 |       loader.setDefaultLocale(defaultLocale);
226 |       await loader.pull(defaultLocale, mockInput);
227 |       const payload = {
228 |         "key.no-translate": "Ne pas traduire",
229 |       };
230 |       const result = await loader.push("fr", payload);
231 |       expect(result).not.toBeNull();
232 |       expect(result!.strings["key.no-translate"].shouldTranslate).toBe(false);
233 |       expect(
234 |         result!.strings["key.no-translate"].localizations.fr.stringUnit.value,
235 |       ).toBe("Ne pas traduire");
236 |     });
237 | 
238 |     it("should handle pushing to a null or undefined originalInput", async () => {
239 |       const loader = createXcodeXcstringsLoader(defaultLocale);
240 |       loader.setDefaultLocale(defaultLocale);
241 |       await loader.pull(defaultLocale, { strings: {} });
242 |       const payload = {
243 |         greeting: "Hello",
244 |       };
245 |       const result = await loader.push("en", payload);
246 |       expect(result).toEqual({
247 |         strings: {
248 |           greeting: {
249 |             localizations: {
250 |               en: {
251 |                 stringUnit: {
252 |                   state: "translated",
253 |                   value: "Hello",
254 |                 },
255 |               },
256 |             },
257 |           },
258 |         },
259 |       });
260 |     });
261 | 
262 |     it("should skip null and undefined values in payload", async () => {
263 |       const loader = createXcodeXcstringsLoader(defaultLocale);
264 |       loader.setDefaultLocale(defaultLocale);
265 |       await loader.pull(defaultLocale, mockInput);
266 |       const payload = {
267 |         "app.title": "new title",
268 |         "key.null": null,
269 |         "key.undefined": undefined,
270 |       };
271 |       const result = await loader.push("en", payload);
272 |       expect(result).not.toBeNull();
273 |       expect(Object.keys(result!.strings)).not.toContain("key.null");
274 |       expect(Object.keys(result!.strings)).not.toContain("key.undefined");
275 |       expect(
276 |         result!.strings["app.title"].localizations.en.stringUnit.value,
277 |       ).toBe("new title");
278 |     });
279 | 
280 |     it("should remove the pushed locale from original input", async () => {
281 |       const loader = createXcodeXcstringsLoader(defaultLocale);
282 |       loader.setDefaultLocale(defaultLocale);
283 |       await loader.pull(defaultLocale, mockInput);
284 |       const payload = {
285 |         "app.title": "new title",
286 |       };
287 |       const result = await loader.push("en", payload);
288 |       expect(result).not.toBeNull();
289 |       expect(result!.strings["app.title"].localizations.en.stringUnit).toEqual({
290 |         state: "translated",
291 |         value: "new title",
292 |       });
293 |       expect(result!.strings["items.count"].localizations.en).toBeUndefined();
294 |       expect(
295 |         result!.strings["key.no-translate"].localizations.en,
296 |       ).toBeUndefined();
297 |       expect(
298 |         result!.strings["key.source-only"].localizations.en,
299 |       ).toBeUndefined();
300 |       expect(
301 |         result!.strings["key.missing-localization"].localizations.en,
302 |       ).toBeUndefined();
303 |     });
304 |   });
305 | 
306 |   describe("_removeLocale", () => {
307 |     it("should remove the locale from the input", () => {
308 |       const input = {
309 |         sourceLanguage: "en",
310 |         strings: {
311 |           key1: {
312 |             localizations: {
313 |               en: { stringUnit: { state: "translated", value: "Hello" } },
314 |               es: { stringUnit: { state: "translated", value: "Hola" } },
315 |             },
316 |           },
317 |           key2: {
318 |             localizations: {
319 |               en: { stringUnit: { state: "translated", value: "World" } },
320 |               fr: { stringUnit: { state: "translated", value: "Monde" } },
321 |             },
322 |           },
323 |           key3: {
324 |             localizations: {
325 |               en: {
326 |                 variations: {
327 |                   plural: {
328 |                     one: {
329 |                       stringUnit: { state: "translated", value: "1 item" },
330 |                     },
331 |                   },
332 |                 },
333 |               },
334 |               fr: {
335 |                 variations: {
336 |                   plural: {
337 |                     one: {
338 |                       stringUnit: { state: "translated", value: "1 article" },
339 |                     },
340 |                   },
341 |                 },
342 |               },
343 |             },
344 |           },
345 |         },
346 |       };
347 |       const result = _removeLocale(input, "en");
348 |       expect(result).toEqual({
349 |         sourceLanguage: "en",
350 |         strings: {
351 |           key1: {
352 |             localizations: {
353 |               es: { stringUnit: { state: "translated", value: "Hola" } },
354 |             },
355 |           },
356 |           key2: {
357 |             localizations: {
358 |               fr: { stringUnit: { state: "translated", value: "Monde" } },
359 |             },
360 |           },
361 |           key3: {
362 |             localizations: {
363 |               fr: {
364 |                 variations: {
365 |                   plural: {
366 |                     one: {
367 |                       stringUnit: { state: "translated", value: "1 article" },
368 |                     },
369 |                   },
370 |                 },
371 |               },
372 |             },
373 |           },
374 |         },
375 |       });
376 |     });
377 | 
378 |     it("should do nothing if the locale does not exist", () => {
379 |       const input = {
380 |         sourceLanguage: "en",
381 |         strings: {
382 |           key1: {
383 |             localizations: {
384 |               en: { stringUnit: { state: "translated", value: "Hello" } },
385 |               es: { stringUnit: { state: "translated", value: "Hola" } },
386 |             },
387 |           },
388 |         },
389 |       };
390 |       const result = _removeLocale(input, "fr");
391 |       expect(result).toEqual({
392 |         sourceLanguage: "en",
393 |         strings: {
394 |           key1: {
395 |             localizations: {
396 |               en: { stringUnit: { state: "translated", value: "Hello" } },
397 |               es: { stringUnit: { state: "translated", value: "Hola" } },
398 |             },
399 |           },
400 |         },
401 |       });
402 |     });
403 | 
404 |     it("should handle empty strings object", () => {
405 |       const input = {
406 |         sourceLanguage: "en",
407 |         strings: {},
408 |       };
409 |       const result = _removeLocale(input, "en");
410 |       expect(result).toEqual({
411 |         sourceLanguage: "en",
412 |         strings: {},
413 |       });
414 |     });
415 | 
416 |     it("should handle keys with no localizations", () => {
417 |       const input = {
418 |         sourceLanguage: "en",
419 |         strings: {
420 |           key1: {
421 |             localizations: {},
422 |           },
423 |         },
424 |       };
425 |       const result = _removeLocale(input, "en");
426 |       expect(result).toEqual({
427 |         sourceLanguage: "en",
428 |         strings: {
429 |           key1: {
430 |             localizations: {},
431 |           },
432 |         },
433 |       });
434 |     });
435 |   });
436 | 
437 |   describe("pullHints", () => {
438 |     it("should extract comments from xcstrings format", async () => {
439 |       const inputWithComments = {
440 |         sourceLanguage: "en",
441 |         strings: {
442 |           welcome_message: {
443 |             comment: "Greeting shown on the main screen",
444 |             extractionState: "manual",
445 |             localizations: {
446 |               en: {
447 |                 stringUnit: {
448 |                   state: "translated",
449 |                   value: "Welcome!",
450 |                 },
451 |               },
452 |             },
453 |           },
454 |           user_count: {
455 |             comment: "Number of active users",
456 |             extractionState: "manual",
457 |             localizations: {
458 |               en: {
459 |                 variations: {
460 |                   plural: {
461 |                     one: {
462 |                       stringUnit: {
463 |                         state: "translated",
464 |                         value: "1 user",
465 |                       },
466 |                     },
467 |                     other: {
468 |                       stringUnit: {
469 |                         state: "translated",
470 |                         value: "%d users",
471 |                       },
472 |                     },
473 |                   },
474 |                 },
475 |               },
476 |             },
477 |           },
478 |           no_comment_key: {
479 |             extractionState: "manual",
480 |             localizations: {
481 |               en: {
482 |                 stringUnit: {
483 |                   state: "translated",
484 |                   value: "No comment",
485 |                 },
486 |               },
487 |             },
488 |           },
489 |         },
490 |       };
491 | 
492 |       const loader = createXcodeXcstringsLoader(defaultLocale);
493 |       loader.setDefaultLocale(defaultLocale);
494 |       await loader.pull(defaultLocale, inputWithComments);
495 | 
496 |       const hints = await loader.pullHints(inputWithComments);
497 | 
498 |       expect(hints).toEqual({
499 |         welcome_message: { hint: "Greeting shown on the main screen" },
500 |         user_count: { hint: "Number of active users" },
501 |         "user_count/one": { hint: "Number of active users" },
502 |         "user_count/other": { hint: "Number of active users" },
503 |       });
504 |     });
505 | 
506 |     it("should handle empty input", async () => {
507 |       const loader = createXcodeXcstringsLoader(defaultLocale);
508 |       loader.setDefaultLocale(defaultLocale);
509 | 
510 |       const hints1 = await loader.pullHints({});
511 |       expect(hints1).toEqual({});
512 | 
513 |       const hints2 = await loader.pullHints(null as any);
514 |       expect(hints2).toEqual({});
515 | 
516 |       const hints3 = await loader.pullHints(undefined as any);
517 |       expect(hints3).toEqual({});
518 |     });
519 | 
520 |     it("should handle xcstrings without comments", async () => {
521 |       const loader = createXcodeXcstringsLoader(defaultLocale);
522 |       loader.setDefaultLocale(defaultLocale);
523 |       await loader.pull(defaultLocale, mockInput);
524 | 
525 |       const hints = await loader.pullHints(mockInput);
526 |       expect(hints).toEqual({});
527 |     });
528 | 
529 |     it("should handle strings with only some having comments", async () => {
530 |       const inputWithMixedComments = {
531 |         sourceLanguage: "en",
532 |         strings: {
533 |           with_comment: {
534 |             comment: "This has a comment",
535 |             localizations: {
536 |               en: {
537 |                 stringUnit: {
538 |                   state: "translated",
539 |                   value: "Value with comment",
540 |                 },
541 |               },
542 |             },
543 |           },
544 |           without_comment: {
545 |             localizations: {
546 |               en: {
547 |                 stringUnit: {
548 |                   state: "translated",
549 |                   value: "Value without comment",
550 |                 },
551 |               },
552 |             },
553 |           },
554 |         },
555 |       };
556 | 
557 |       const loader = createXcodeXcstringsLoader(defaultLocale);
558 |       loader.setDefaultLocale(defaultLocale);
559 |       await loader.pull(defaultLocale, inputWithMixedComments);
560 | 
561 |       const hints = await loader.pullHints(inputWithMixedComments);
562 | 
563 |       expect(hints).toEqual({
564 |         with_comment: { hint: "This has a comment" },
565 |       });
566 |     });
567 | 
568 |     it("should handle multiple locales with same comment", async () => {
569 |       const inputWithMultipleLocales = {
570 |         sourceLanguage: "en",
571 |         strings: {
572 |           multi_locale: {
573 |             comment: "Available in multiple languages",
574 |             localizations: {
575 |               en: {
576 |                 stringUnit: {
577 |                   state: "translated",
578 |                   value: "English",
579 |                 },
580 |               },
581 |               es: {
582 |                 stringUnit: {
583 |                   state: "translated",
584 |                   value: "Español",
585 |                 },
586 |               },
587 |               fr: {
588 |                 variations: {
589 |                   plural: {
590 |                     one: {
591 |                       stringUnit: {
592 |                         state: "translated",
593 |                         value: "1 français",
594 |                       },
595 |                     },
596 |                     other: {
597 |                       stringUnit: {
598 |                         state: "translated",
599 |                         value: "%d français",
600 |                       },
601 |                     },
602 |                   },
603 |                 },
604 |               },
605 |             },
606 |           },
607 |         },
608 |       };
609 | 
610 |       const loader = createXcodeXcstringsLoader(defaultLocale);
611 |       loader.setDefaultLocale(defaultLocale);
612 |       await loader.pull(defaultLocale, inputWithMultipleLocales);
613 | 
614 |       const hints = await loader.pullHints(inputWithMultipleLocales);
615 | 
616 |       expect(hints).toEqual({
617 |         multi_locale: { hint: "Available in multiple languages" },
618 |         "multi_locale/one": { hint: "Available in multiple languages" },
619 |         "multi_locale/other": { hint: "Available in multiple languages" },
620 |       });
621 |     });
622 |   });
623 | });
624 | 
```

--------------------------------------------------------------------------------
/demo/next-app/CHANGELOG.md:
--------------------------------------------------------------------------------

```markdown
  1 | # next-app
  2 | 
  3 | ## 0.2.81
  4 | 
  5 | ### Patch Changes
  6 | 
  7 | - Updated dependencies [[`68fb3ea`](https://github.com/lingodotdev/lingo.dev/commit/68fb3ea64fc0191ecee66403432e0c8efabab2b9)]:
  8 |   - [email protected]
  9 | 
 10 | ## 0.2.80
 11 | 
 12 | ### Patch Changes
 13 | 
 14 | - Updated dependencies [[`e70385b`](https://github.com/lingodotdev/lingo.dev/commit/e70385bd1ac676bf5bd31b212d8510e6b7ebf793)]:
 15 |   - [email protected]
 16 | 
 17 | ## 0.2.79
 18 | 
 19 | ### Patch Changes
 20 | 
 21 | - Updated dependencies [[`f7215c1`](https://github.com/lingodotdev/lingo.dev/commit/f7215c1e435378aac8fc953765335cd478cbf507)]:
 22 |   - [email protected]
 23 | 
 24 | ## 0.2.78
 25 | 
 26 | ### Patch Changes
 27 | 
 28 | - Updated dependencies [[`898bd36`](https://github.com/lingodotdev/lingo.dev/commit/898bd36cc2e444641560d2ad2b28065a57072183)]:
 29 |   - [email protected]
 30 | 
 31 | ## 0.2.77
 32 | 
 33 | ### Patch Changes
 34 | 
 35 | - Updated dependencies [[`060680c`](https://github.com/lingodotdev/lingo.dev/commit/060680cd13c05dd77dd9d5447c064d948bd21cb0), [`f102356`](https://github.com/lingodotdev/lingo.dev/commit/f102356e1ea12c800399ac11f074c42708c304b1), [`a956e53`](https://github.com/lingodotdev/lingo.dev/commit/a956e537d0d45565c3243dd0c5ba4eec8bed69c6), [`3fd38c2`](https://github.com/lingodotdev/lingo.dev/commit/3fd38c2d38e4b22dcd824c865fe31abbc56bc862)]:
 36 |   - [email protected]
 37 | 
 38 | ## 0.2.76
 39 | 
 40 | ### Patch Changes
 41 | 
 42 | - Updated dependencies [[`03671f7`](https://github.com/lingodotdev/lingo.dev/commit/03671f7cb252d6bee3debce2f4a4eb989dc0050b)]:
 43 |   - [email protected]
 44 | 
 45 | ## 0.2.75
 46 | 
 47 | ### Patch Changes
 48 | 
 49 | - Updated dependencies [[`4f5ffe6`](https://github.com/lingodotdev/lingo.dev/commit/4f5ffe62189949bb26a6c7825cb72c217aefa32f)]:
 50 |   - [email protected]
 51 | 
 52 | ## 0.2.74
 53 | 
 54 | ### Patch Changes
 55 | 
 56 | - Updated dependencies [[`be8de32`](https://github.com/lingodotdev/lingo.dev/commit/be8de3280bb5dc5f409fc7680c0e5ff6a53e2fe5)]:
 57 |   - [email protected]
 58 | 
 59 | ## 0.2.73
 60 | 
 61 | ### Patch Changes
 62 | 
 63 | - Updated dependencies [[`79c4c00`](https://github.com/lingodotdev/lingo.dev/commit/79c4c00108b9c102cf53e1c090b286070a43e3d5)]:
 64 |   - [email protected]
 65 | 
 66 | ## 0.2.72
 67 | 
 68 | ### Patch Changes
 69 | 
 70 | - Updated dependencies [[`b45347c`](https://github.com/lingodotdev/lingo.dev/commit/b45347c38572ee371b2bc494261b7e3e90c4aed1)]:
 71 |   - [email protected]
 72 | 
 73 | ## 0.2.71
 74 | 
 75 | ### Patch Changes
 76 | 
 77 | - Updated dependencies [[`74d8efe`](https://github.com/lingodotdev/lingo.dev/commit/74d8efef8d4789f9baa5b7837e053c2571df0308)]:
 78 |   - [email protected]
 79 | 
 80 | ## 0.2.70
 81 | 
 82 | ### Patch Changes
 83 | 
 84 | - Updated dependencies [[`3d3c3d7`](https://github.com/lingodotdev/lingo.dev/commit/3d3c3d783a61443da50a5d182391db33a0d29c84)]:
 85 |   - [email protected]
 86 | 
 87 | ## 0.2.69
 88 | 
 89 | ### Patch Changes
 90 | 
 91 | - Updated dependencies [[`38139c8`](https://github.com/lingodotdev/lingo.dev/commit/38139c81a85001739cece60873c0c6ad711327a4)]:
 92 |   - [email protected]
 93 | 
 94 | ## 0.2.68
 95 | 
 96 | ### Patch Changes
 97 | 
 98 | - Updated dependencies [[`3413dad`](https://github.com/lingodotdev/lingo.dev/commit/3413dad22af688a6d26649c4f25e18304b3caee6)]:
 99 |   - [email protected]
100 | 
101 | ## 0.2.67
102 | 
103 | ### Patch Changes
104 | 
105 | - Updated dependencies [[`26d2ec1`](https://github.com/lingodotdev/lingo.dev/commit/26d2ec155c5868a5bdce1027cd76a5a2d4f8f2b1)]:
106 |   - [email protected]
107 | 
108 | ## 0.2.66
109 | 
110 | ### Patch Changes
111 | 
112 | - Updated dependencies [[`82f5e7c`](https://github.com/lingodotdev/lingo.dev/commit/82f5e7cdde9a2a15b4c2a7fcb8c67ed64eab596b), [`e858174`](https://github.com/lingodotdev/lingo.dev/commit/e858174fd5165e0ea3e3f25fa1fc3edb292bc58f)]:
113 |   - [email protected]
114 | 
115 | ## 0.2.65
116 | 
117 | ### Patch Changes
118 | 
119 | - Updated dependencies [[`f3d4987`](https://github.com/lingodotdev/lingo.dev/commit/f3d4987ddc393c28d488f030c087f3e99a667975), [`a933b81`](https://github.com/lingodotdev/lingo.dev/commit/a933b8102763e0481f088c847da53e0eee3f0617)]:
120 |   - [email protected]
121 | 
122 | ## 0.2.64
123 | 
124 | ### Patch Changes
125 | 
126 | - Updated dependencies []:
127 |   - [email protected]
128 | 
129 | ## 0.2.63
130 | 
131 | ### Patch Changes
132 | 
133 | - Updated dependencies [[`dd0663f`](https://github.com/lingodotdev/lingo.dev/commit/dd0663fdcdd0ff4fd5748386758a8c20f9e52a4b)]:
134 |   - [email protected]
135 | 
136 | ## 0.2.62
137 | 
138 | ### Patch Changes
139 | 
140 | - Updated dependencies [[`762396b`](https://github.com/lingodotdev/lingo.dev/commit/762396bb37110dbe3e4e000edb27892b318aa3ef)]:
141 |   - [email protected]
142 | 
143 | ## 0.2.61
144 | 
145 | ### Patch Changes
146 | 
147 | - Updated dependencies [[`468a59b`](https://github.com/lingodotdev/lingo.dev/commit/468a59b89736c72253b1f32abbf30a950e5434ec)]:
148 |   - [email protected]
149 | 
150 | ## 0.2.60
151 | 
152 | ### Patch Changes
153 | 
154 | - Updated dependencies [[`bbc71b9`](https://github.com/lingodotdev/lingo.dev/commit/bbc71b9948ccc289c9669d8b0c276c9596f6a5e7)]:
155 |   - [email protected]
156 | 
157 | ## 0.2.59
158 | 
159 | ### Patch Changes
160 | 
161 | - Updated dependencies [[`0e6d605`](https://github.com/lingodotdev/lingo.dev/commit/0e6d605a9ad6835bef26c40895760c652a69b7a2)]:
162 |   - [email protected]
163 | 
164 | ## 0.2.58
165 | 
166 | ### Patch Changes
167 | 
168 | - Updated dependencies [[`03138da`](https://github.com/lingodotdev/lingo.dev/commit/03138dac37e869e2e99702ffd3c76532f1c58aa6), [`9557fe5`](https://github.com/lingodotdev/lingo.dev/commit/9557fe572d3e4a1a4d8c1e35417fe3b7531c3d52)]:
169 |   - [email protected]
170 | 
171 | ## 0.2.57
172 | 
173 | ### Patch Changes
174 | 
175 | - Updated dependencies [[`64225d0`](https://github.com/lingodotdev/lingo.dev/commit/64225d073999d599ba86f65fee8e08e3e5f2800b)]:
176 |   - [email protected]
177 | 
178 | ## 0.2.56
179 | 
180 | ### Patch Changes
181 | 
182 | - Updated dependencies []:
183 |   - [email protected]
184 | 
185 | ## 0.2.55
186 | 
187 | ### Patch Changes
188 | 
189 | - Updated dependencies [[`88b7e31`](https://github.com/lingodotdev/lingo.dev/commit/88b7e3132c77d0a1e823de4ee6ef5a96a3098b97)]:
190 |   - [email protected]
191 | 
192 | ## 0.2.54
193 | 
194 | ### Patch Changes
195 | 
196 | - Updated dependencies [[`d9294c0`](https://github.com/lingodotdev/lingo.dev/commit/d9294c0bbb993454ad3654f77dd48d82211e0465)]:
197 |   - [email protected]
198 | 
199 | ## 0.2.53
200 | 
201 | ### Patch Changes
202 | 
203 | - Updated dependencies [[`100b141`](https://github.com/lingodotdev/lingo.dev/commit/100b141d2143e33b603830475ba55089dc421e3d)]:
204 |   - [email protected]
205 | 
206 | ## 0.2.52
207 | 
208 | ### Patch Changes
209 | 
210 | - Updated dependencies [[`8741a20`](https://github.com/lingodotdev/lingo.dev/commit/8741a20dcaa3983131a1919f875dd2c264cb29fb)]:
211 |   - [email protected]
212 | 
213 | ## 0.2.51
214 | 
215 | ### Patch Changes
216 | 
217 | - Updated dependencies [[`bd3f69d`](https://github.com/lingodotdev/lingo.dev/commit/bd3f69dde76814146f775bc87241fa2fad012ab0)]:
218 |   - [email protected]
219 | 
220 | ## 0.2.50
221 | 
222 | ### Patch Changes
223 | 
224 | - Updated dependencies [[`6c174c3`](https://github.com/lingodotdev/lingo.dev/commit/6c174c38f3cf28c2af24ead18503658c3c641026)]:
225 |   - [email protected]
226 | 
227 | ## 0.2.49
228 | 
229 | ### Patch Changes
230 | 
231 | - Updated dependencies [[`3a642f3`](https://github.com/lingodotdev/lingo.dev/commit/3a642f33c04378706a8382aa0fde36e747fd6af5)]:
232 |   - [email protected]
233 | 
234 | ## 0.2.48
235 | 
236 | ### Patch Changes
237 | 
238 | - Updated dependencies [[`bc7b08e`](https://github.com/lingodotdev/lingo.dev/commit/bc7b08ef1245d1af0c68813cb18193d4f14bc7e0)]:
239 |   - [email protected]
240 | 
241 | ## 0.2.47
242 | 
243 | ### Patch Changes
244 | 
245 | - Updated dependencies [[`b6071e4`](https://github.com/lingodotdev/lingo.dev/commit/b6071e4f19dd1823f4f2ce54ba5495538a94d4fd)]:
246 |   - [email protected]
247 | 
248 | ## 0.2.46
249 | 
250 | ### Patch Changes
251 | 
252 | - Updated dependencies [[`e898c1e`](https://github.com/lingodotdev/lingo.dev/commit/e898c1eeb34e4dd3e74df26465802b520018acf9)]:
253 |   - [email protected]
254 | 
255 | ## 0.2.45
256 | 
257 | ### Patch Changes
258 | 
259 | - Updated dependencies [[`410825c`](https://github.com/lingodotdev/lingo.dev/commit/410825c8bf0029d8ee458514d6f203a7397c8f22)]:
260 |   - [email protected]
261 | 
262 | ## 0.2.44
263 | 
264 | ### Patch Changes
265 | 
266 | - Updated dependencies [[`555384d`](https://github.com/lingodotdev/lingo.dev/commit/555384dacf79167e1bb8b9e6871e153fea763471)]:
267 |   - [email protected]
268 | 
269 | ## 0.2.43
270 | 
271 | ### Patch Changes
272 | 
273 | - Updated dependencies [[`c0486ca`](https://github.com/lingodotdev/lingo.dev/commit/c0486ca9b0451ea75d070e199f502507ba418e5e)]:
274 |   - [email protected]
275 | 
276 | ## 0.2.42
277 | 
278 | ### Patch Changes
279 | 
280 | - Updated dependencies [[`99aae2d`](https://github.com/lingodotdev/lingo.dev/commit/99aae2d09a26060c810913f740893a4a5874d9d4)]:
281 |   - [email protected]
282 | 
283 | ## 0.2.41
284 | 
285 | ### Patch Changes
286 | 
287 | - Updated dependencies []:
288 |   - [email protected]
289 | 
290 | ## 0.2.40
291 | 
292 | ### Patch Changes
293 | 
294 | - Updated dependencies [[`6af91a0`](https://github.com/lingodotdev/lingo.dev/commit/6af91a083d16f85051fb49a4034789abe784017e)]:
295 |   - [email protected]
296 | 
297 | ## 0.2.39
298 | 
299 | ### Patch Changes
300 | 
301 | - Updated dependencies [[`1ff847b`](https://github.com/lingodotdev/lingo.dev/commit/1ff847b9273a3082178553e70c22524f5831ad36), [`55e9e68`](https://github.com/lingodotdev/lingo.dev/commit/55e9e687a3d0efa84b808818a848a276b1a42015), [`b9e2551`](https://github.com/lingodotdev/lingo.dev/commit/b9e2551f349e33542212f941b3407e8517b5fb27)]:
302 |   - [email protected]
303 | 
304 | ## 0.2.38
305 | 
306 | ### Patch Changes
307 | 
308 | - Updated dependencies []:
309 |   - [email protected]
310 | 
311 | ## 0.2.37
312 | 
313 | ### Patch Changes
314 | 
315 | - Updated dependencies []:
316 |   - [email protected]
317 | 
318 | ## 0.2.36
319 | 
320 | ### Patch Changes
321 | 
322 | - Updated dependencies [[`20a3737`](https://github.com/lingodotdev/lingo.dev/commit/20a3737ddb50b2a97699e57e03ea353b8912b78f)]:
323 |   - [email protected]
324 | 
325 | ## 0.2.35
326 | 
327 | ### Patch Changes
328 | 
329 | - Updated dependencies [[`afbb978`](https://github.com/lingodotdev/lingo.dev/commit/afbb978fec83d574f2c43b7d68457e435fca9b57)]:
330 |   - [email protected]
331 | 
332 | ## 0.2.34
333 | 
334 | ### Patch Changes
335 | 
336 | - Updated dependencies [[`1f1e33f`](https://github.com/lingodotdev/lingo.dev/commit/1f1e33fe4d0767c2f026214a505a2aa9f3785996), [`9266fd0`](https://github.com/lingodotdev/lingo.dev/commit/9266fd0bcddf4b07ca51d2609af92a9473106f9d)]:
337 |   - [email protected]
338 | 
339 | ## 0.2.33
340 | 
341 | ### Patch Changes
342 | 
343 | - Updated dependencies [[`6baa1a7`](https://github.com/lingodotdev/lingo.dev/commit/6baa1a7e88dbfac3783d1d49695595077fd8d209)]:
344 |   - [email protected]
345 | 
346 | ## 0.2.32
347 | 
348 | ### Patch Changes
349 | 
350 | - Updated dependencies [[`925997d`](https://github.com/lingodotdev/lingo.dev/commit/925997d75a1edbb4211a3be8db2b186cb139327e)]:
351 |   - [email protected]
352 | 
353 | ## 0.2.31
354 | 
355 | ### Patch Changes
356 | 
357 | - Updated dependencies [[`cb2aa0f`](https://github.com/lingodotdev/lingo.dev/commit/cb2aa0f505d6b7dbc435b526e8a6f62265d1f453)]:
358 |   - [email protected]
359 | 
360 | ## 0.2.30
361 | 
362 | ### Patch Changes
363 | 
364 | - Updated dependencies [[`bfcb424`](https://github.com/lingodotdev/lingo.dev/commit/bfcb424eb4479d0d3b767e062d30f02c5bcaeb14)]:
365 |   - [email protected]
366 | 
367 | ## 0.2.29
368 | 
369 | ### Patch Changes
370 | 
371 | - Updated dependencies [[`2b297ba`](https://github.com/lingodotdev/lingo.dev/commit/2b297babe76f9799c5154d9421fecd1ebbe1bb72)]:
372 |   - [email protected]
373 | 
374 | ## 0.2.28
375 | 
376 | ### Patch Changes
377 | 
378 | - Updated dependencies [[`30faa6d`](https://github.com/lingodotdev/lingo.dev/commit/30faa6d10e851a38ced86ae403b3a1fd48440bca)]:
379 |   - [email protected]
380 | 
381 | ## 0.2.27
382 | 
383 | ### Patch Changes
384 | 
385 | - Updated dependencies []:
386 |   - [email protected]
387 | 
388 | ## 0.2.26
389 | 
390 | ### Patch Changes
391 | 
392 | - Updated dependencies [[`4e9e368`](https://github.com/lingodotdev/lingo.dev/commit/4e9e36830ee4277ef9d65eee9ee92380a95a622c)]:
393 |   - [email protected]
394 | 
395 | ## 0.2.25
396 | 
397 | ### Patch Changes
398 | 
399 | - Updated dependencies [[`65701e5`](https://github.com/lingodotdev/lingo.dev/commit/65701e5b9694e811587ef600227251a1ff1384a0), [`4e55355`](https://github.com/lingodotdev/lingo.dev/commit/4e5535535029743b7a0edc4fdab3d4ee71374035)]:
400 |   - [email protected]
401 | 
402 | ## 0.2.24
403 | 
404 | ### Patch Changes
405 | 
406 | - Updated dependencies [[`f644123`](https://github.com/lingodotdev/lingo.dev/commit/f644123ddf6a6254790d08af50141e4dd78c3677)]:
407 |   - [email protected]
408 | 
409 | ## 0.2.23
410 | 
411 | ### Patch Changes
412 | 
413 | - Updated dependencies [[`29cf6a7`](https://github.com/lingodotdev/lingo.dev/commit/29cf6a7359707e0e341c11942d1ce6dedf7e66e5)]:
414 |   - [email protected]
415 | 
416 | ## 0.2.22
417 | 
418 | ### Patch Changes
419 | 
420 | - Updated dependencies [[`b249484`](https://github.com/lingodotdev/lingo.dev/commit/b249484d6f0060e29cd5b50b3d8ce68b857ccad5)]:
421 |   - [email protected]
422 | 
423 | ## 0.2.21
424 | 
425 | ### Patch Changes
426 | 
427 | - Updated dependencies [[`f7debef`](https://github.com/lingodotdev/lingo.dev/commit/f7debef9f004e670bb1f6a45ae17067a72a6e53f)]:
428 |   - [email protected]
429 | 
430 | ## 0.2.20
431 | 
432 | ### Patch Changes
433 | 
434 | - Updated dependencies [[`da6f0c8`](https://github.com/lingodotdev/lingo.dev/commit/da6f0c85e69687615df943323d261078742ba3f2)]:
435 |   - [email protected]
436 | 
437 | ## 0.2.19
438 | 
439 | ### Patch Changes
440 | 
441 | - Updated dependencies [[`8b306bc`](https://github.com/lingodotdev/lingo.dev/commit/8b306bcd0a3231ffd8bde283414b6d069b7a5b99), [`013fca0`](https://github.com/lingodotdev/lingo.dev/commit/013fca0f4252103ee3009fe3cdcfce2a87c80058)]:
442 |   - [email protected]
443 | 
444 | ## 0.2.18
445 | 
446 | ### Patch Changes
447 | 
448 | - Updated dependencies [[`84fd214`](https://github.com/lingodotdev/lingo.dev/commit/84fd214a21766e7683c5d645fcb8c4c0162eb0b6), [`0fc6385`](https://github.com/lingodotdev/lingo.dev/commit/0fc63856c6f49ac68a220b6e2f1c4f060e7ce78e), [`cac5429`](https://github.com/lingodotdev/lingo.dev/commit/cac54296d512d436dc3861441d5d1a3f1076792b)]:
449 |   - [email protected]
450 | 
451 | ## 0.2.17
452 | 
453 | ### Patch Changes
454 | 
455 | - Updated dependencies [[`ce0e5cd`](https://github.com/lingodotdev/lingo.dev/commit/ce0e5cd6d1ec17f5c593d394ceb63a28666df924)]:
456 |   - [email protected]
457 | 
458 | ## 0.2.16
459 | 
460 | ### Patch Changes
461 | 
462 | - Updated dependencies [[`ce8c75c`](https://github.com/lingodotdev/lingo.dev/commit/ce8c75c7fc1a2124d3e18444bc356c4dfce26434)]:
463 |   - [email protected]
464 | 
465 | ## 0.2.15
466 | 
467 | ### Patch Changes
468 | 
469 | - Updated dependencies []:
470 |   - [email protected]
471 | 
472 | ## 0.2.14
473 | 
474 | ### Patch Changes
475 | 
476 | - Updated dependencies [[`d80285a`](https://github.com/lingodotdev/lingo.dev/commit/d80285a9b12bd85425564cb00e558812fd0aee40)]:
477 |   - [email protected]
478 | 
479 | ## 0.2.13
480 | 
481 | ### Patch Changes
482 | 
483 | - Updated dependencies [[`81eff21`](https://github.com/lingodotdev/lingo.dev/commit/81eff2104a4401b1c1b6cdf4dcc7ca75b7411ba4)]:
484 |   - [email protected]
485 | 
486 | ## 0.2.12
487 | 
488 | ### Patch Changes
489 | 
490 | - Updated dependencies [[`b39b04a`](https://github.com/lingodotdev/lingo.dev/commit/b39b04ad83d3c8001008c3cefe309d8e762b2adc), [`4e5983d`](https://github.com/lingodotdev/lingo.dev/commit/4e5983d7e59ebf9eb529c4b7c1c87689432ac873)]:
491 |   - [email protected]
492 | 
493 | ## 0.2.11
494 | 
495 | ### Patch Changes
496 | 
497 | - Updated dependencies [[`1a3cbc1`](https://github.com/lingodotdev/lingo.dev/commit/1a3cbc1751c64e5617e91812506b3c061475f16a)]:
498 |   - [email protected]
499 | 
500 | ## 0.2.10
501 | 
502 | ### Patch Changes
503 | 
504 | - Updated dependencies []:
505 |   - [email protected]
506 | 
507 | ## 0.2.9
508 | 
509 | ### Patch Changes
510 | 
511 | - Updated dependencies []:
512 |   - [email protected]
513 | 
514 | ## 0.2.8
515 | 
516 | ### Patch Changes
517 | 
518 | - [`8e97256`](https://github.com/lingodotdev/lingo.dev/commit/8e97256ca4e78dd09a967539ca9dec359bd558ef) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - fix dictionary merging
519 | 
520 | - Updated dependencies []:
521 |   - [email protected]
522 | 
523 | ## 0.2.7
524 | 
525 | ### Patch Changes
526 | 
527 | - [#925](https://github.com/lingodotdev/lingo.dev/pull/925) [`215af19`](https://github.com/lingodotdev/lingo.dev/commit/215af1944667cce66e9c5966f4fb627186687b74) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - improved compiler concurrency, caching, added lingo.dev engine to the compiler, and updated demo apps
528 | 
529 | - Updated dependencies [[`1b9b113`](https://github.com/lingodotdev/lingo.dev/commit/1b9b11301978e8caa2555832d027ff93216aa6e1), [`0329a9c`](https://github.com/lingodotdev/lingo.dev/commit/0329a9cdb5e5a63fcecab4efcd7cce22f155a0e9)]:
530 |   - [email protected]
531 | 
532 | ## 0.2.6
533 | 
534 | ### Patch Changes
535 | 
536 | - Updated dependencies [[`3b6574f`](https://github.com/lingodotdev/lingo.dev/commit/3b6574f0499f3f4d3c48f66ba2b828d2c1c0ceb0), [`6b4b9e6`](https://github.com/lingodotdev/lingo.dev/commit/6b4b9e6cc9a0cb5da8a4df9e9ebda474bf2a18ed), [`6b4b9e6`](https://github.com/lingodotdev/lingo.dev/commit/6b4b9e6cc9a0cb5da8a4df9e9ebda474bf2a18ed)]:
537 |   - [email protected]
538 | 
539 | ## 0.2.5
540 | 
541 | ### Patch Changes
542 | 
543 | - Updated dependencies []:
544 |   - [email protected]
545 | 
546 | ## 0.2.4
547 | 
548 | ### Patch Changes
549 | 
550 | - Updated dependencies [[`2dd8170`](https://github.com/lingodotdev/lingo.dev/commit/2dd8170ff0101268f2253c9248409d184da5f75c)]:
551 |   - [email protected]
552 | 
553 | ## 0.2.3
554 | 
555 | ### Patch Changes
556 | 
557 | - Updated dependencies []:
558 |   - [email protected]
559 | 
560 | ## 0.2.2
561 | 
562 | ### Patch Changes
563 | 
564 | - Updated dependencies [[`cc232eb`](https://github.com/lingodotdev/lingo.dev/commit/cc232eb72d0e54b3571bbb70e88cdad24ba6372a)]:
565 |   - [email protected]
566 | 
567 | ## 0.2.1
568 | 
569 | ### Patch Changes
570 | 
571 | - Updated dependencies [[`fead8e0`](https://github.com/lingodotdev/lingo.dev/commit/fead8e08dc2b2869a093cb25a04f6e0aa78cf6b7)]:
572 |   - [email protected]
573 | 
574 | ## 0.2.0
575 | 
576 | ### Minor Changes
577 | 
578 | - [#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.
579 | 
580 | ### Patch Changes
581 | 
582 | - Updated dependencies [[`a5da697`](https://github.com/lingodotdev/lingo.dev/commit/a5da697f7efd46de31d17b202d06eb5f655ed9b9), [`10a0139`](https://github.com/lingodotdev/lingo.dev/commit/10a0139edc9ffbc1c52ac2226f6b0f345cc19878)]:
583 |   - [email protected]
584 | 
585 | ## 0.1.23
586 | 
587 | ### Patch Changes
588 | 
589 | - Updated dependencies [[`3bd4045`](https://github.com/lingodotdev/lingo.dev/commit/3bd40450cbb5c8aabce61d7f1f3ab9c7293323d9)]:
590 |   - [email protected]
591 | 
592 | ## 0.1.22
593 | 
594 | ### Patch Changes
595 | 
596 | - Updated dependencies [[`f140f82`](https://github.com/lingodotdev/lingo.dev/commit/f140f820d00b15f99214a7eece1a9c7f0d098e90)]:
597 |   - [email protected]
598 | 
599 | ## 0.1.21
600 | 
601 | ### Patch Changes
602 | 
603 | - Updated dependencies [[`145fb74`](https://github.com/lingodotdev/lingo.dev/commit/145fb74c09b42c8810f351be5a641b1366881ae1), [`0c45acc`](https://github.com/lingodotdev/lingo.dev/commit/0c45accfc45e63f597758c47033bc58d2f6059b5)]:
604 |   - [email protected]
605 | 
606 | ## 0.1.20
607 | 
608 | ### Patch Changes
609 | 
610 | - Updated dependencies [[`511a2ec`](https://github.com/lingodotdev/lingo.dev/commit/511a2ecd68a9c5e2800035d5c6a6b5b31b2dc80f)]:
611 |   - [email protected]
612 | 
613 | ## 0.1.19
614 | 
615 | ### Patch Changes
616 | 
617 | - Updated dependencies [[`7191444`](https://github.com/lingodotdev/lingo.dev/commit/7191444f67864ea5b5a91a9be759b2445bf186d3)]:
618 |   - [email protected]
619 | 
620 | ## 0.1.18
621 | 
622 | ### Patch Changes
623 | 
624 | - Updated dependencies []:
625 |   - [email protected]
626 | 
627 | ## 0.1.17
628 | 
629 | ### Patch Changes
630 | 
631 | - Updated dependencies [[`af011b1`](https://github.com/lingodotdev/lingo.dev/commit/af011b18fe96f15287609278f4d4d2b343b6c2cc)]:
632 |   - [email protected]
633 | 
634 | ## 0.1.16
635 | 
636 | ### Patch Changes
637 | 
638 | - Updated dependencies []:
639 |   - [email protected]
640 | 
641 | ## 0.1.15
642 | 
643 | ### Patch Changes
644 | 
645 | - Updated dependencies [[`3750c9c`](https://github.com/lingodotdev/lingo.dev/commit/3750c9ca25a78280b04e4a2b2e6641dd21f9f3b0)]:
646 |   - [email protected]
647 | 
648 | ## 0.1.14
649 | 
650 | ### Patch Changes
651 | 
652 | - Updated dependencies []:
653 |   - [email protected]
654 | 
655 | ## 0.1.13
656 | 
657 | ### Patch Changes
658 | 
659 | - Updated dependencies []:
660 |   - [email protected]
661 | 
662 | ## 0.1.12
663 | 
664 | ### Patch Changes
665 | 
666 | - Updated dependencies []:
667 |   - [email protected]
668 | 
669 | ## 0.1.11
670 | 
671 | ### Patch Changes
672 | 
673 | - Updated dependencies []:
674 |   - [email protected]
675 | 
676 | ## 0.1.10
677 | 
678 | ### Patch Changes
679 | 
680 | - Updated dependencies []:
681 |   - [email protected]
682 | 
683 | ## 0.1.9
684 | 
685 | ### Patch Changes
686 | 
687 | - Updated dependencies [[`cb7d5e2`](https://github.com/lingodotdev/lingo.dev/commit/cb7d5e213282c00af658159472183a763f84ca3d)]:
688 |   - [email protected]
689 | 
690 | ## 0.1.8
691 | 
692 | ### Patch Changes
693 | 
694 | - Updated dependencies [[`5d27455`](https://github.com/lingodotdev/lingo.dev/commit/5d2745545044cbaddb099f7920c96fe198879ba3)]:
695 |   - [email protected]
696 | 
697 | ## 0.1.7
698 | 
699 | ### Patch Changes
700 | 
701 | - Updated dependencies [[`b67a331`](https://github.com/lingodotdev/lingo.dev/commit/b67a33141253fa755b5531e52cd690bf5824d4b6)]:
702 |   - [email protected]
703 | 
704 | ## 0.1.6
705 | 
706 | ### Patch Changes
707 | 
708 | - Updated dependencies []:
709 |   - [email protected]
710 | 
711 | ## 0.1.5
712 | 
713 | ### Patch Changes
714 | 
715 | - Updated dependencies [[`f42cff8`](https://github.com/lingodotdev/lingo.dev/commit/f42cff8355b1ff7bba1445bd04d11ee4672903c2)]:
716 |   - [email protected]
717 | 
718 | ## 0.1.4
719 | 
720 | ### Patch Changes
721 | 
722 | - Updated dependencies [[`920e3f5`](https://github.com/lingodotdev/lingo.dev/commit/920e3f5c3ca1fd51b0919db13a4787cfd616de54)]:
723 |   - [email protected]
724 | 
725 | ## 0.1.3
726 | 
727 | ### Patch Changes
728 | 
729 | - Updated dependencies [[`cdb59dd`](https://github.com/lingodotdev/lingo.dev/commit/cdb59dddcd14da1ba3181a33c4c119af877cb4f3)]:
730 |   - [email protected]
731 | 
732 | ## 0.1.2
733 | 
734 | ### Patch Changes
735 | 
736 | - Updated dependencies [[`caef325`](https://github.com/lingodotdev/lingo.dev/commit/caef3253bc99fa7bf7a0b40e5604c3590dcb4958)]:
737 |   - [email protected]
738 | 
739 | ## 0.1.1
740 | 
741 | ### Patch Changes
742 | 
743 | - Updated dependencies [[`e980e84`](https://github.com/lingodotdev/lingo.dev/commit/e980e84178439ad70417d38b425acf9148cfc4b6)]:
744 |   - [email protected]
745 | 
```

--------------------------------------------------------------------------------
/packages/sdk/src/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import Z from "zod";
  2 | import { LocaleCode, localeCodeSchema } from "@lingo.dev/_spec";
  3 | import { createId } from "@paralleldrive/cuid2";
  4 | 
  5 | const engineParamsSchema = Z.object({
  6 |   apiKey: Z.string(),
  7 |   apiUrl: Z.string().url().default("https://engine.lingo.dev"),
  8 |   batchSize: Z.number().int().gt(0).lte(250).default(25),
  9 |   idealBatchItemSize: Z.number().int().gt(0).lte(2500).default(250),
 10 | }).passthrough();
 11 | 
 12 | const payloadSchema = Z.record(Z.string(), Z.any());
 13 | const referenceSchema = Z.record(localeCodeSchema, payloadSchema);
 14 | const hintsSchema = Z.record(Z.string(), Z.array(Z.string()));
 15 | 
 16 | const localizationParamsSchema = Z.object({
 17 |   sourceLocale: Z.union([localeCodeSchema, Z.null()]),
 18 |   targetLocale: localeCodeSchema,
 19 |   fast: Z.boolean().optional(),
 20 |   reference: referenceSchema.optional(),
 21 |   hints: hintsSchema.optional(),
 22 | });
 23 | 
 24 | /**
 25 |  * LingoDotDevEngine class for interacting with the LingoDotDev API
 26 |  * A powerful localization engine that supports various content types including
 27 |  * plain text, objects, chat sequences, and HTML documents.
 28 |  */
 29 | export class LingoDotDevEngine {
 30 |   protected config: Z.infer<typeof engineParamsSchema>;
 31 | 
 32 |   /**
 33 |    * Create a new LingoDotDevEngine instance
 34 |    * @param config - Configuration options for the Engine
 35 |    */
 36 |   constructor(config: Partial<Z.infer<typeof engineParamsSchema>>) {
 37 |     this.config = engineParamsSchema.parse(config);
 38 |   }
 39 | 
 40 |   /**
 41 |    * Localize content using the Lingo.dev API
 42 |    * @param payload - The content to be localized
 43 |    * @param params - Localization parameters including source/target locales and fast mode option
 44 |    * @param progressCallback - Optional callback function to report progress (0-100)
 45 |    * @param signal - Optional AbortSignal to cancel the operation
 46 |    * @returns Localized content
 47 |    * @internal
 48 |    */
 49 |   async _localizeRaw(
 50 |     payload: Z.infer<typeof payloadSchema>,
 51 |     params: Z.infer<typeof localizationParamsSchema>,
 52 |     progressCallback?: (
 53 |       progress: number,
 54 |       sourceChunk: Record<string, string>,
 55 |       processedChunk: Record<string, string>,
 56 |     ) => void,
 57 |     signal?: AbortSignal,
 58 |   ): Promise<Record<string, string>> {
 59 |     const finalPayload = payloadSchema.parse(payload);
 60 |     const finalParams = localizationParamsSchema.parse(params);
 61 | 
 62 |     const chunkedPayload = this.extractPayloadChunks(finalPayload);
 63 |     const processedPayloadChunks: Record<string, string>[] = [];
 64 | 
 65 |     const workflowId = createId();
 66 |     for (let i = 0; i < chunkedPayload.length; i++) {
 67 |       const chunk = chunkedPayload[i];
 68 |       const percentageCompleted = Math.round(
 69 |         ((i + 1) / chunkedPayload.length) * 100,
 70 |       );
 71 | 
 72 |       const processedPayloadChunk = await this.localizeChunk(
 73 |         finalParams.sourceLocale,
 74 |         finalParams.targetLocale,
 75 |         { data: chunk, reference: params.reference, hints: params.hints },
 76 |         workflowId,
 77 |         params.fast || false,
 78 |         signal,
 79 |       );
 80 | 
 81 |       if (progressCallback) {
 82 |         progressCallback(percentageCompleted, chunk, processedPayloadChunk);
 83 |       }
 84 | 
 85 |       processedPayloadChunks.push(processedPayloadChunk);
 86 |     }
 87 | 
 88 |     return Object.assign({}, ...processedPayloadChunks);
 89 |   }
 90 | 
 91 |   /**
 92 |    * Localize a single chunk of content
 93 |    * @param sourceLocale - Source locale
 94 |    * @param targetLocale - Target locale
 95 |    * @param payload - Payload containing the chunk to be localized
 96 |    * @param workflowId - Workflow ID for tracking
 97 |    * @param fast - Whether to use fast mode
 98 |    * @param signal - Optional AbortSignal to cancel the operation
 99 |    * @returns Localized chunk
100 |    */
101 |   private async localizeChunk(
102 |     sourceLocale: string | null,
103 |     targetLocale: string,
104 |     payload: {
105 |       data: Z.infer<typeof payloadSchema>;
106 |       reference?: Z.infer<typeof referenceSchema>;
107 |       hints?: Z.infer<typeof hintsSchema>;
108 |     },
109 |     workflowId: string,
110 |     fast: boolean,
111 |     signal?: AbortSignal,
112 |   ): Promise<Record<string, string>> {
113 |     const res = await fetch(`${this.config.apiUrl}/i18n`, {
114 |       method: "POST",
115 |       headers: {
116 |         "Content-Type": "application/json; charset=utf-8",
117 |         Authorization: `Bearer ${this.config.apiKey}`,
118 |       },
119 |       body: JSON.stringify(
120 |         {
121 |           params: { workflowId, fast },
122 |           locale: {
123 |             source: sourceLocale,
124 |             target: targetLocale,
125 |           },
126 |           data: payload.data,
127 |           reference: payload.reference,
128 |           hints: payload.hints,
129 |         },
130 |         null,
131 |         2,
132 |       ),
133 |       signal,
134 |     });
135 | 
136 |     if (!res.ok) {
137 |       if (res.status >= 500 && res.status < 600) {
138 |         const errorText = await res.text();
139 |         throw new Error(
140 |           `Server error (${res.status}): ${res.statusText}. ${errorText}. This may be due to temporary service issues.`,
141 |         );
142 |       } else if (res.status === 400) {
143 |         throw new Error(`Invalid request: ${res.statusText}`);
144 |       } else {
145 |         const errorText = await res.text();
146 |         throw new Error(errorText);
147 |       }
148 |     }
149 | 
150 |     const jsonResponse = await res.json();
151 | 
152 |     // when streaming the error is returned in the response body
153 |     if (!jsonResponse.data && jsonResponse.error) {
154 |       throw new Error(jsonResponse.error);
155 |     }
156 | 
157 |     return jsonResponse.data || {};
158 |   }
159 | 
160 |   /**
161 |    * Extract payload chunks based on the ideal chunk size
162 |    * @param payload - The payload to be chunked
163 |    * @returns An array of payload chunks
164 |    */
165 |   private extractPayloadChunks(
166 |     payload: Record<string, string>,
167 |   ): Record<string, string>[] {
168 |     const result: Record<string, string>[] = [];
169 |     let currentChunk: Record<string, string> = {};
170 |     let currentChunkItemCount = 0;
171 | 
172 |     const payloadEntries = Object.entries(payload);
173 |     for (let i = 0; i < payloadEntries.length; i++) {
174 |       const [key, value] = payloadEntries[i];
175 |       currentChunk[key] = value;
176 |       currentChunkItemCount++;
177 | 
178 |       const currentChunkSize = this.countWordsInRecord(currentChunk);
179 |       if (
180 |         currentChunkSize > this.config.idealBatchItemSize ||
181 |         currentChunkItemCount >= this.config.batchSize ||
182 |         i === payloadEntries.length - 1
183 |       ) {
184 |         result.push(currentChunk);
185 |         currentChunk = {};
186 |         currentChunkItemCount = 0;
187 |       }
188 |     }
189 | 
190 |     return result;
191 |   }
192 | 
193 |   /**
194 |    * Count words in a record or array
195 |    * @param payload - The payload to count words in
196 |    * @returns The total number of words
197 |    */
198 |   private countWordsInRecord(
199 |     payload: any | Record<string, any> | Array<any>,
200 |   ): number {
201 |     if (Array.isArray(payload)) {
202 |       return payload.reduce(
203 |         (acc, item) => acc + this.countWordsInRecord(item),
204 |         0,
205 |       );
206 |     } else if (typeof payload === "object" && payload !== null) {
207 |       return Object.values(payload).reduce(
208 |         (acc: number, item) => acc + this.countWordsInRecord(item),
209 |         0,
210 |       );
211 |     } else if (typeof payload === "string") {
212 |       return payload.trim().split(/\s+/).filter(Boolean).length;
213 |     } else {
214 |       return 0;
215 |     }
216 |   }
217 | 
218 |   /**
219 |    * Localize a typical JavaScript object
220 |    * @param obj - The object to be localized (strings will be extracted and translated)
221 |    * @param params - Localization parameters:
222 |    *   - sourceLocale: The source language code (e.g., 'en')
223 |    *   - targetLocale: The target language code (e.g., 'es')
224 |    *   - fast: Optional boolean to enable fast mode (faster but potentially lower quality)
225 |    * @param progressCallback - Optional callback function to report progress (0-100)
226 |    * @param signal - Optional AbortSignal to cancel the operation
227 |    * @returns A new object with the same structure but localized string values
228 |    */
229 |   async localizeObject(
230 |     obj: Record<string, any>,
231 |     params: Z.infer<typeof localizationParamsSchema>,
232 |     progressCallback?: (
233 |       progress: number,
234 |       sourceChunk: Record<string, string>,
235 |       processedChunk: Record<string, string>,
236 |     ) => void,
237 |     signal?: AbortSignal,
238 |   ): Promise<Record<string, any>> {
239 |     return this._localizeRaw(obj, params, progressCallback, signal);
240 |   }
241 | 
242 |   /**
243 |    * Localize a single text string
244 |    * @param text - The text string to be localized
245 |    * @param params - Localization parameters:
246 |    *   - sourceLocale: The source language code (e.g., 'en')
247 |    *   - targetLocale: The target language code (e.g., 'es')
248 |    *   - fast: Optional boolean to enable fast mode (faster for bigger batches)
249 |    * @param progressCallback - Optional callback function to report progress (0-100)
250 |    * @param signal - Optional AbortSignal to cancel the operation
251 |    * @returns The localized text string
252 |    */
253 |   async localizeText(
254 |     text: string,
255 |     params: Z.infer<typeof localizationParamsSchema>,
256 |     progressCallback?: (progress: number) => void,
257 |     signal?: AbortSignal,
258 |   ): Promise<string> {
259 |     const response = await this._localizeRaw(
260 |       { text },
261 |       params,
262 |       progressCallback,
263 |       signal,
264 |     );
265 |     return response.text || "";
266 |   }
267 | 
268 |   /**
269 |    * Localize a text string to multiple target locales
270 |    * @param text - The text string to be localized
271 |    * @param params - Localization parameters:
272 |    *   - sourceLocale: The source language code (e.g., 'en')
273 |    *   - targetLocales: An array of target language codes (e.g., ['es', 'fr'])
274 |    *   - fast: Optional boolean to enable fast mode (for bigger batches)
275 |    * @param signal - Optional AbortSignal to cancel the operation
276 |    * @returns An array of localized text strings
277 |    */
278 |   async batchLocalizeText(
279 |     text: string,
280 |     params: {
281 |       sourceLocale: LocaleCode;
282 |       targetLocales: LocaleCode[];
283 |       fast?: boolean;
284 |     },
285 |     signal?: AbortSignal,
286 |   ) {
287 |     const responses = await Promise.all(
288 |       params.targetLocales.map((targetLocale) =>
289 |         this.localizeText(
290 |           text,
291 |           {
292 |             sourceLocale: params.sourceLocale,
293 |             targetLocale,
294 |             fast: params.fast,
295 |           },
296 |           undefined,
297 |           signal,
298 |         ),
299 |       ),
300 |     );
301 | 
302 |     return responses;
303 |   }
304 | 
305 |   /**
306 |    * Localize an array of strings
307 |    * @param strings - An array of strings to be localized
308 |    * @param params - Localization parameters:
309 |    *   - sourceLocale: The source language code (e.g., 'en')
310 |    *   - targetLocale: The target language code (e.g., 'es')
311 |    *   - fast: Optional boolean to enable fast mode (faster for bigger batches)
312 |    * @returns An array of localized strings in the same order
313 |    */
314 |   async localizeStringArray(
315 |     strings: string[],
316 |     params: Z.infer<typeof localizationParamsSchema>,
317 |   ): Promise<string[]> {
318 |     const mapped = strings.reduce(
319 |       (acc, str, i) => {
320 |         acc[`item_${i}`] = str;
321 |         return acc;
322 |       },
323 |       {} as Record<string, string>,
324 |     );
325 | 
326 |     const result = await this.localizeObject(mapped, params);
327 |     return Object.values(result);
328 |   }
329 | 
330 |   /**
331 |    * Localize a chat sequence while preserving speaker names
332 |    * @param chat - Array of chat messages, each with 'name' and 'text' properties
333 |    * @param params - Localization parameters:
334 |    *   - sourceLocale: The source language code (e.g., 'en')
335 |    *   - targetLocale: The target language code (e.g., 'es')
336 |    *   - fast: Optional boolean to enable fast mode (faster but potentially lower quality)
337 |    * @param progressCallback - Optional callback function to report progress (0-100)
338 |    * @param signal - Optional AbortSignal to cancel the operation
339 |    * @returns Array of localized chat messages with preserved structure
340 |    */
341 |   async localizeChat(
342 |     chat: Array<{ name: string; text: string }>,
343 |     params: Z.infer<typeof localizationParamsSchema>,
344 |     progressCallback?: (progress: number) => void,
345 |     signal?: AbortSignal,
346 |   ): Promise<Array<{ name: string; text: string }>> {
347 |     const localized = await this._localizeRaw(
348 |       { chat },
349 |       params,
350 |       progressCallback,
351 |       signal,
352 |     );
353 | 
354 |     return Object.entries(localized).map(([key, value]) => ({
355 |       name: chat[parseInt(key.split("_")[1])].name,
356 |       text: value,
357 |     }));
358 |   }
359 | 
360 |   /**
361 |    * Localize an HTML document while preserving structure and formatting
362 |    * Handles both text content and localizable attributes (alt, title, placeholder, meta content)
363 |    * @param html - The HTML document string to be localized
364 |    * @param params - Localization parameters:
365 |    *   - sourceLocale: The source language code (e.g., 'en')
366 |    *   - targetLocale: The target language code (e.g., 'es')
367 |    *   - fast: Optional boolean to enable fast mode (faster but potentially lower quality)
368 |    * @param progressCallback - Optional callback function to report progress (0-100)
369 |    * @param signal - Optional AbortSignal to cancel the operation
370 |    * @returns The localized HTML document as a string, with updated lang attribute
371 |    */
372 |   async localizeHtml(
373 |     html: string,
374 |     params: Z.infer<typeof localizationParamsSchema>,
375 |     progressCallback?: (progress: number) => void,
376 |     signal?: AbortSignal,
377 |   ): Promise<string> {
378 |     const jsdomPackage = await import("jsdom");
379 |     const { JSDOM } = jsdomPackage;
380 |     const dom = new JSDOM(html);
381 |     const document = dom.window.document;
382 | 
383 |     const LOCALIZABLE_ATTRIBUTES: Record<string, string[]> = {
384 |       meta: ["content"],
385 |       img: ["alt"],
386 |       input: ["placeholder"],
387 |       a: ["title"],
388 |     };
389 |     const UNLOCALIZABLE_TAGS = ["script", "style"];
390 | 
391 |     const extractedContent: Record<string, string> = {};
392 | 
393 |     const getPath = (node: Node, attribute?: string): string => {
394 |       const indices: number[] = [];
395 |       let current = node as ChildNode;
396 |       let rootParent = "";
397 | 
398 |       while (current) {
399 |         const parent = current.parentElement as Element;
400 |         if (!parent) break;
401 | 
402 |         if (parent === document.documentElement) {
403 |           rootParent = current.nodeName.toLowerCase();
404 |           break;
405 |         }
406 | 
407 |         const siblings = Array.from(parent.childNodes).filter(
408 |           (n) =>
409 |             n.nodeType === 1 || (n.nodeType === 3 && n.textContent?.trim()),
410 |         );
411 |         const index = siblings.indexOf(current);
412 |         if (index !== -1) {
413 |           indices.unshift(index);
414 |         }
415 |         current = parent;
416 |       }
417 | 
418 |       const basePath = rootParent
419 |         ? `${rootParent}/${indices.join("/")}`
420 |         : indices.join("/");
421 |       return attribute ? `${basePath}#${attribute}` : basePath;
422 |     };
423 | 
424 |     const processNode = (node: Node) => {
425 |       let parent = node.parentElement;
426 |       while (parent) {
427 |         if (UNLOCALIZABLE_TAGS.includes(parent.tagName.toLowerCase())) {
428 |           return;
429 |         }
430 |         parent = parent.parentElement;
431 |       }
432 | 
433 |       if (node.nodeType === 3) {
434 |         const text = node.textContent?.trim() || "";
435 |         if (text) {
436 |           extractedContent[getPath(node)] = text;
437 |         }
438 |       } else if (node.nodeType === 1) {
439 |         const element = node as Element;
440 |         const tagName = element.tagName.toLowerCase();
441 | 
442 |         const attributes = LOCALIZABLE_ATTRIBUTES[tagName] || [];
443 |         attributes.forEach((attr) => {
444 |           const value = element.getAttribute(attr);
445 |           if (value) {
446 |             extractedContent[getPath(element, attr)] = value;
447 |           }
448 |         });
449 | 
450 |         Array.from(element.childNodes)
451 |           .filter(
452 |             (n) =>
453 |               n.nodeType === 1 || (n.nodeType === 3 && n.textContent?.trim()),
454 |           )
455 |           .forEach(processNode);
456 |       }
457 |     };
458 | 
459 |     Array.from(document.head.childNodes)
460 |       .filter(
461 |         (n) => n.nodeType === 1 || (n.nodeType === 3 && n.textContent?.trim()),
462 |       )
463 |       .forEach(processNode);
464 |     Array.from(document.body.childNodes)
465 |       .filter(
466 |         (n) => n.nodeType === 1 || (n.nodeType === 3 && n.textContent?.trim()),
467 |       )
468 |       .forEach(processNode);
469 | 
470 |     const localizedContent = await this._localizeRaw(
471 |       extractedContent,
472 |       params,
473 |       progressCallback,
474 |       signal,
475 |     );
476 | 
477 |     // Update the DOM with localized content
478 |     document.documentElement.setAttribute("lang", params.targetLocale);
479 | 
480 |     Object.entries(localizedContent).forEach(([path, value]) => {
481 |       const [nodePath, attribute] = path.split("#");
482 |       const [rootTag, ...indices] = nodePath.split("/");
483 | 
484 |       let parent: Element = rootTag === "head" ? document.head : document.body;
485 |       let current: Node | null = parent;
486 | 
487 |       for (const index of indices) {
488 |         const siblings = Array.from(parent.childNodes).filter(
489 |           (n) =>
490 |             n.nodeType === 1 || (n.nodeType === 3 && n.textContent?.trim()),
491 |         );
492 |         current = siblings[parseInt(index)] || null;
493 |         if (current?.nodeType === 1) {
494 |           parent = current as Element;
495 |         }
496 |       }
497 | 
498 |       if (current) {
499 |         if (attribute) {
500 |           (current as Element).setAttribute(attribute, value);
501 |         } else {
502 |           current.textContent = value;
503 |         }
504 |       }
505 |     });
506 | 
507 |     return dom.serialize();
508 |   }
509 | 
510 |   /**
511 |    * Detect the language of a given text
512 |    * @param text - The text to analyze
513 |    * @param signal - Optional AbortSignal to cancel the operation
514 |    * @returns Promise resolving to a locale code (e.g., 'en', 'es', 'fr')
515 |    */
516 |   async recognizeLocale(
517 |     text: string,
518 |     signal?: AbortSignal,
519 |   ): Promise<LocaleCode> {
520 |     const response = await fetch(`${this.config.apiUrl}/recognize`, {
521 |       method: "POST",
522 |       headers: {
523 |         "Content-Type": "application/json; charset=utf-8",
524 |         Authorization: `Bearer ${this.config.apiKey}`,
525 |       },
526 |       body: JSON.stringify({ text }),
527 |       signal,
528 |     });
529 | 
530 |     if (!response.ok) {
531 |       if (response.status >= 500 && response.status < 600) {
532 |         throw new Error(
533 |           `Server error (${response.status}): ${response.statusText}. This may be due to temporary service issues.`,
534 |         );
535 |       }
536 |       throw new Error(`Error recognizing locale: ${response.statusText}`);
537 |     }
538 | 
539 |     const jsonResponse = await response.json();
540 |     return jsonResponse.locale;
541 |   }
542 | 
543 |   async whoami(
544 |     signal?: AbortSignal,
545 |   ): Promise<{ email: string; id: string } | null> {
546 |     try {
547 |       const res = await fetch(`${this.config.apiUrl}/whoami`, {
548 |         method: "POST",
549 |         headers: {
550 |           Authorization: `Bearer ${this.config.apiKey}`,
551 |           ContentType: "application/json",
552 |         },
553 |         signal,
554 |       });
555 | 
556 |       if (res.ok) {
557 |         const payload = await res.json();
558 |         if (!payload?.email) {
559 |           return null;
560 |         }
561 | 
562 |         return {
563 |           email: payload.email,
564 |           id: payload.id,
565 |         };
566 |       }
567 | 
568 |       if (res.status >= 500 && res.status < 600) {
569 |         throw new Error(
570 |           `Server error (${res.status}): ${res.statusText}. This may be due to temporary service issues.`,
571 |         );
572 |       }
573 | 
574 |       return null;
575 |     } catch (error) {
576 |       if (error instanceof Error && error.message.includes("Server error")) {
577 |         throw error;
578 |       }
579 |       return null;
580 |     }
581 |   }
582 | }
583 | 
584 | /**
585 |  * @deprecated Use LingoDotDevEngine instead. This class is maintained for backwards compatibility.
586 |  */
587 | export class ReplexicaEngine extends LingoDotDevEngine {
588 |   private static hasWarnedDeprecation = false;
589 | 
590 |   constructor(config: Partial<Z.infer<typeof engineParamsSchema>>) {
591 |     super(config);
592 |     if (!ReplexicaEngine.hasWarnedDeprecation) {
593 |       console.warn(
594 |         "ReplexicaEngine is deprecated and will be removed in a future release. " +
595 |           "Please use LingoDotDevEngine instead. " +
596 |           "See https://lingo.dev/cli for more information.",
597 |       );
598 |       ReplexicaEngine.hasWarnedDeprecation = true;
599 |     }
600 |   }
601 | }
602 | 
603 | /**
604 |  * @deprecated Use LingoDotDevEngine instead. This class is maintained for backwards compatibility.
605 |  */
606 | export class LingoEngine extends LingoDotDevEngine {
607 |   private static hasWarnedDeprecation = false;
608 | 
609 |   constructor(config: Partial<Z.infer<typeof engineParamsSchema>>) {
610 |     super(config);
611 |     if (!LingoEngine.hasWarnedDeprecation) {
612 |       console.warn(
613 |         "LingoEngine is deprecated and will be removed in a future release. " +
614 |           "Please use LingoDotDevEngine instead. " +
615 |           "See https://lingo.dev/cli for more information.",
616 |       );
617 |       LingoEngine.hasWarnedDeprecation = true;
618 |     }
619 |   }
620 | }
621 | 
```
Page 15/20FirstPrevNextLast