This is page 11 of 16. Use http://codebase.md/lingodotdev/lingo.dev?lines=false&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/CHANGELOG.md:
--------------------------------------------------------------------------------
```markdown
# @lingo.dev/\_compiler
## 0.7.15
### Patch Changes
- [#1231](https://github.com/lingodotdev/lingo.dev/pull/1231) [`44a928b`](https://github.com/lingodotdev/lingo.dev/commit/44a928b473802cd07bec64f94a273ee1b845a0d0) Thanks [@davidturnbull](https://github.com/davidturnbull)! - Compiler now throws errors instead of abruptly exiting the process, allowing parent applications to handle errors gracefully
## 0.7.14
### Patch Changes
- Updated dependencies [[`b45347c`](https://github.com/lingodotdev/lingo.dev/commit/b45347c38572ee371b2bc494261b7e3e90c4aed1)]:
- @lingo.dev/[email protected]
- @lingo.dev/[email protected]
## 0.7.13
### Patch Changes
- [#1222](https://github.com/lingodotdev/lingo.dev/pull/1222) [`38139c8`](https://github.com/lingodotdev/lingo.dev/commit/38139c81a85001739cece60873c0c6ad711327a4) Thanks [@vrcprl](https://github.com/vrcprl)! - fix regex replacement
## 0.7.12
### Patch Changes
- Updated dependencies [[`82f5e7c`](https://github.com/lingodotdev/lingo.dev/commit/82f5e7cdde9a2a15b4c2a7fcb8c67ed64eab596b), [`e858174`](https://github.com/lingodotdev/lingo.dev/commit/e858174fd5165e0ea3e3f25fa1fc3edb292bc58f)]:
- @lingo.dev/[email protected]
- @lingo.dev/[email protected]
## 0.7.11
### Patch Changes
- Updated dependencies [[`1fa218c`](https://github.com/lingodotdev/lingo.dev/commit/1fa218c13bf90df6d175fb18264f59c1a10b967c)]:
- @lingo.dev/[email protected]
- @lingo.dev/[email protected]
## 0.7.10
### Patch Changes
- Updated dependencies [[`bbc71b9`](https://github.com/lingodotdev/lingo.dev/commit/bbc71b9948ccc289c9669d8b0c276c9596f6a5e7)]:
- @lingo.dev/[email protected]
- @lingo.dev/[email protected]
## 0.7.9
### Patch Changes
- Updated dependencies [[`6579d70`](https://github.com/lingodotdev/lingo.dev/commit/6579d70bc670c2fdc06c09842d931b07e134151c)]:
- @lingo.dev/[email protected]
- @lingo.dev/[email protected]
## 0.7.8
### Patch Changes
- Updated dependencies [[`a35032e`](https://github.com/lingodotdev/lingo.dev/commit/a35032e7e7a188d1f5e774576352068124526e24)]:
- @lingo.dev/[email protected]
- @lingo.dev/[email protected]
## 0.7.7
### Patch Changes
- [#1130](https://github.com/lingodotdev/lingo.dev/pull/1130) [`bc7b08e`](https://github.com/lingodotdev/lingo.dev/commit/bc7b08ef1245d1af0c68813cb18193d4f14bc7e0) Thanks [@mathio](https://github.com/mathio)! - dictionary path calculation
## 0.7.6
### Patch Changes
- [#1121](https://github.com/lingodotdev/lingo.dev/pull/1121) [`b6071e4`](https://github.com/lingodotdev/lingo.dev/commit/b6071e4f19dd1823f4f2ce54ba5495538a94d4fd) Thanks [@mathio](https://github.com/mathio)! - compiler: prevent duplicate props
## 0.7.5
### Patch Changes
- [#1118](https://github.com/lingodotdev/lingo.dev/pull/1118) [`410825c`](https://github.com/lingodotdev/lingo.dev/commit/410825c8bf0029d8ee458514d6f203a7397c8f22) Thanks [@mathio](https://github.com/mathio)! - support Turbopack in Next.js v14 by Compiler
- [#1116](https://github.com/lingodotdev/lingo.dev/pull/1116) [`bc419ae`](https://github.com/lingodotdev/lingo.dev/commit/bc419aeeb4211d80d3c0ddd65deeab62ad68fea8) Thanks [@devin-ai-integration](https://github.com/apps/devin-ai-integration)! - fix: move vitest from dependencies to devDependencies
## 0.7.4
### Patch Changes
- [#1072](https://github.com/lingodotdev/lingo.dev/pull/1072) [`3cb1ebe`](https://github.com/lingodotdev/lingo.dev/commit/3cb1ebec5441882678ab30a7d1b532bc2fc397b6) Thanks [@The-Best-Codes](https://github.com/The-Best-Codes)! - Fixed compiler handling of namespace imports (import \* as React from "react") and default imports.
## 0.7.3
### Patch Changes
- Updated dependencies [[`6af91a0`](https://github.com/lingodotdev/lingo.dev/commit/6af91a083d16f85051fb49a4034789abe784017e), [`6af91a0`](https://github.com/lingodotdev/lingo.dev/commit/6af91a083d16f85051fb49a4034789abe784017e)]:
- @lingo.dev/[email protected]
- @lingo.dev/[email protected]
## 0.7.2
### Patch Changes
- Updated dependencies [[`85dfc10`](https://github.com/lingodotdev/lingo.dev/commit/85dfc10961b116e31b2bb478f42013756ca49974)]:
- @lingo.dev/[email protected]
## 0.7.1
### Patch Changes
- [#1040](https://github.com/lingodotdev/lingo.dev/pull/1040) [`f897a7d`](https://github.com/lingodotdev/lingo.dev/commit/f897a7d0a3f7a236fb64f19bce9a8d00626d09ca) Thanks [@The-Best-Codes](https://github.com/The-Best-Codes)! - Fixed the compiler to handle type-only react imports.
## 0.7.0
### Minor Changes
- [#997](https://github.com/lingodotdev/lingo.dev/pull/997) [`bd9538a`](https://github.com/lingodotdev/lingo.dev/commit/bd9538ac6eba0ffc91ffc1fef5db6366c13e9e06) Thanks [@VAIBHAVSING](https://github.com/VAIBHAVSING)! - ### Whitespace Normalization Fix
- Improved `normalizeJsxWhitespace` logic to preserve leading spaces inside JSX elements while removing unnecessary formatting whitespace and extra lines.
- Ensured explicit whitespace (e.g., `{" "}`) is handled correctly without introducing double spaces.
- Added targeted tests (`jsx-content-whitespace.spec.ts`) to verify whitespace handling.
- Cleaned up unnecessary debug/test files created during development.
## 0.6.3
### Patch Changes
- Updated dependencies [[`afbb978`](https://github.com/lingodotdev/lingo.dev/commit/afbb978fec83d574f2c43b7d68457e435fca9b57)]:
- @lingo.dev/[email protected]
- @lingo.dev/[email protected]
## 0.6.2
### Patch Changes
- [#1023](https://github.com/lingodotdev/lingo.dev/pull/1023) [`9266fd0`](https://github.com/lingodotdev/lingo.dev/commit/9266fd0bcddf4b07ca51d2609af92a9473106f9d) Thanks [@devin-ai-integration](https://github.com/apps/devin-ai-integration)! - Update Zod dependency to version 3.25.76
- Updated dependencies [[`9266fd0`](https://github.com/lingodotdev/lingo.dev/commit/9266fd0bcddf4b07ca51d2609af92a9473106f9d)]:
- @lingo.dev/[email protected]
- @lingo.dev/[email protected]
## 0.6.1
### Patch Changes
- [#1021](https://github.com/lingodotdev/lingo.dev/pull/1021) [`6baa1a7`](https://github.com/lingodotdev/lingo.dev/commit/6baa1a7e88dbfac3783d1d49695595077fd8d209) Thanks [@mathio](https://github.com/mathio)! - add lingo.dev provider details
## 0.6.0
### Minor Changes
- [#1010](https://github.com/lingodotdev/lingo.dev/pull/1010) [`864c305`](https://github.com/lingodotdev/lingo.dev/commit/864c30586510e6b69739c20fa42efdf45d8881ed) Thanks [@davidturnbull](https://github.com/davidturnbull)! - improve type safety of compiler params
### Patch Changes
- Updated dependencies [[`cb2aa0f`](https://github.com/lingodotdev/lingo.dev/commit/cb2aa0f505d6b7dbc435b526e8a6f62265d1f453)]:
- @lingo.dev/[email protected]
## 0.5.5
### Patch Changes
- [#1011](https://github.com/lingodotdev/lingo.dev/pull/1011) [`bfcb424`](https://github.com/lingodotdev/lingo.dev/commit/bfcb424eb4479d0d3b767e062d30f02c5bcaeb14) Thanks [@mathio](https://github.com/mathio)! - replace elements with dot in name
## 0.5.4
### Patch Changes
- [#1002](https://github.com/lingodotdev/lingo.dev/pull/1002) [`2b297ba`](https://github.com/lingodotdev/lingo.dev/commit/2b297babe76f9799c5154d9421fecd1ebbe1bb72) Thanks [@mathio](https://github.com/mathio)! - support custom prompts in compiler
## 0.5.3
### Patch Changes
- Updated dependencies []:
- @lingo.dev/[email protected]
## 0.5.2
### Patch Changes
- Updated dependencies []:
- @lingo.dev/[email protected]
## 0.5.1
### Patch Changes
- [#972](https://github.com/lingodotdev/lingo.dev/pull/972) [`b249484`](https://github.com/lingodotdev/lingo.dev/commit/b249484d6f0060e29cd5b50b3d8ce68b857ccad5) Thanks [@mathio](https://github.com/mathio)! - support components with dot in name
## 0.5.0
### Minor Changes
- [#958](https://github.com/lingodotdev/lingo.dev/pull/958) [`84fd214`](https://github.com/lingodotdev/lingo.dev/commit/84fd214a21766e7683c5d645fcb8c4c0162eb0b6) Thanks [@chrissiwaffler](https://github.com/chrissiwaffler)! - feat: add Mistral AI as a supported LLM provider
- Added Mistral AI provider support across the entire lingo.dev ecosystem
- Users can now use Mistral models for localization by setting MISTRAL_API_KEY
- Supports all Mistral models available through the @ai-sdk/mistral package
- Configuration via environment variable or user-wide config: `npx lingo.dev@latest config set llm.mistralApiKey <key>`
### Patch Changes
- Updated dependencies []:
- @lingo.dev/[email protected]
## 0.4.1
### Patch Changes
- Updated dependencies []:
- @lingo.dev/[email protected]
## 0.4.0
### Minor Changes
- [#932](https://github.com/lingodotdev/lingo.dev/pull/932) [`1bba8ee`](https://github.com/lingodotdev/lingo.dev/commit/1bba8eed6272ae166ceb9b92963404bfe90a4aaa) Thanks [@The-Best-Codes](https://github.com/The-Best-Codes)! - Add support for Next.js Turbopack with the Lingo.dev compiler.
## 0.3.5
### Patch Changes
- [#947](https://github.com/lingodotdev/lingo.dev/pull/947) [`d80285a`](https://github.com/lingodotdev/lingo.dev/commit/d80285a9b12bd85425564cb00e558812fd0aee40) Thanks [@mathio](https://github.com/mathio)! - remove local variable cache
## 0.3.4
### Patch Changes
- [#937](https://github.com/lingodotdev/lingo.dev/pull/937) [`4e5983d`](https://github.com/lingodotdev/lingo.dev/commit/4e5983d7e59ebf9eb529c4b7c1c87689432ac873) Thanks [@devin-ai-integration](https://github.com/apps/devin-ai-integration)! - Update documentation URLs from docs.lingo.dev to lingo.dev/cli and lingo.dev/compiler
- Updated dependencies [[`4e5983d`](https://github.com/lingodotdev/lingo.dev/commit/4e5983d7e59ebf9eb529c4b7c1c87689432ac873)]:
- @lingo.dev/[email protected]
## 0.3.3
### Patch Changes
- [`76cbd9b`](https://github.com/lingodotdev/lingo.dev/commit/76cbd9b2f2e1217421ad1f671bed5b3d64b43333) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - dictionary merging
## 0.3.2
### Patch Changes
- [`01f253d`](https://github.com/lingodotdev/lingo.dev/commit/01f253dd9759b518f400dff03ab51b460b9b8997) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - fix dictionary merging
## 0.3.1
### Patch Changes
- [`8e97256`](https://github.com/lingodotdev/lingo.dev/commit/8e97256ca4e78dd09a967539ca9dec359bd558ef) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - fix dictionary merging
## 0.3.0
### Minor Changes
- [#913](https://github.com/lingodotdev/lingo.dev/pull/913) [`1b9b113`](https://github.com/lingodotdev/lingo.dev/commit/1b9b11301978e8caa2555832d027ff93216aa6e1) Thanks [@The-Best-Codes](https://github.com/The-Best-Codes)! - Add support for Ollama as a CLI and Compiler provider.
- [#922](https://github.com/lingodotdev/lingo.dev/pull/922) [`0329a9c`](https://github.com/lingodotdev/lingo.dev/commit/0329a9cdb5e5a63fcecab4efcd7cce22f155a0e9) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add openrouter ais support for compiler
### Patch Changes
- [#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
- Updated dependencies []:
- @lingo.dev/[email protected]
## 0.2.4
### Patch Changes
- [#919](https://github.com/lingodotdev/lingo.dev/pull/919) [`3b6574f`](https://github.com/lingodotdev/lingo.dev/commit/3b6574f0499f3f4d3c48f66ba2b828d2c1c0ceb0) Thanks [@mathio](https://github.com/mathio)! - update package import names
## 0.2.3
### Patch Changes
- [#911](https://github.com/lingodotdev/lingo.dev/pull/911) [`d7e74c6`](https://github.com/lingodotdev/lingo.dev/commit/d7e74c6cc724da8ae759ba8d8fdb1a64867d505c) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - fix hyphens in locale names
## 0.2.2
### Patch Changes
- [#905](https://github.com/lingodotdev/lingo.dev/pull/905) [`1a235a1`](https://github.com/lingodotdev/lingo.dev/commit/1a235a17455fb2631f7426283aa8431209999758) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - remove @/ path mapping in compiler
## 0.2.1
### Patch Changes
- [#900](https://github.com/lingodotdev/lingo.dev/pull/900) [`fead8e0`](https://github.com/lingodotdev/lingo.dev/commit/fead8e08dc2b2869a093cb25a04f6e0aa78cf6b7) Thanks [@mathio](https://github.com/mathio)! - load API key from env var and env files
## 0.2.0
### Minor Changes
- [#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.
## 0.1.13
### Patch Changes
- [#890](https://github.com/lingodotdev/lingo.dev/pull/890) [`145fb74`](https://github.com/lingodotdev/lingo.dev/commit/145fb74c09b42c8810f351be5a641b1366881ae1) Thanks [@mathio](https://github.com/mathio)! - do not parse LingoProvider component
- [#889](https://github.com/lingodotdev/lingo.dev/pull/889) [`0c45acc`](https://github.com/lingodotdev/lingo.dev/commit/0c45accfc45e63f597758c47033bc58d2f6059b5) Thanks [@mathio](https://github.com/mathio)! - update Groq API error handling
## 0.1.12
### Patch Changes
- [#887](https://github.com/lingodotdev/lingo.dev/pull/887) [`511a2ec`](https://github.com/lingodotdev/lingo.dev/commit/511a2ecd68a9c5e2800035d5c6a6b5b31b2dc80f) Thanks [@mathio](https://github.com/mathio)! - handle when lingo dir is deleted
## 0.1.11
### Patch Changes
- [#883](https://github.com/lingodotdev/lingo.dev/pull/883) [`7191444`](https://github.com/lingodotdev/lingo.dev/commit/7191444f67864ea5b5a91a9be759b2445bf186d3) Thanks [@mathio](https://github.com/mathio)! - client-side loading state
## 0.1.10
### Patch Changes
- [#876](https://github.com/lingodotdev/lingo.dev/pull/876) [`152e96a`](https://github.com/lingodotdev/lingo.dev/commit/152e96a46b98dd25d558ff0e7e20b18b954d375a) Thanks [@vrcprl](https://github.com/vrcprl)! - fix for triggering reload on Windows
## 0.1.9
### Patch Changes
- [#866](https://github.com/lingodotdev/lingo.dev/pull/866) [`77461a7`](https://github.com/lingodotdev/lingo.dev/commit/77461a7872eec3ea188b3ca6c6f7ce1fd13fdfbb) Thanks [@vrcprl](https://github.com/vrcprl)! - normalize paths in dictionaries
## 0.1.8
### Patch Changes
- [#861](https://github.com/lingodotdev/lingo.dev/pull/861) [`1bccb7e`](https://github.com/lingodotdev/lingo.dev/commit/1bccb7ed51ac1f13ea79e618bbee551d5529efdc) Thanks [@vrcprl](https://github.com/vrcprl)! - support filePath on Windows
## 0.1.7
### Patch Changes
- [`5b68641`](https://github.com/lingodotdev/lingo.dev/commit/5b686414f363f8ee4b79fd4e804a434db5cfcb36) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - feat: unshift the plugins
## 0.1.6
### Patch Changes
- [`7a5898b`](https://github.com/lingodotdev/lingo.dev/commit/7a5898b12dcd0015a5e57236bf65172cedb8a6ee) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - merge dictionaries
## 0.1.5
### Patch Changes
- [`7013b53`](https://github.com/lingodotdev/lingo.dev/commit/7013b5300d6c2c26f39da62b5ad2c7cf11158c74) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - value.trim() issue
## 0.1.4
### Patch Changes
- [#853](https://github.com/lingodotdev/lingo.dev/pull/853) [`cb7d5e2`](https://github.com/lingodotdev/lingo.dev/commit/cb7d5e213282c00af658159472183a763f84ca3d) Thanks [@vrcprl](https://github.com/vrcprl)! - Fix groq api key retrieval from .env
## 0.1.3
### Patch Changes
- [`f42cff8`](https://github.com/lingodotdev/lingo.dev/commit/f42cff8355b1ff7bba1445bd04d11ee4672903c2) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - flat reexports
## 0.1.2
### Patch Changes
- [`920e3f5`](https://github.com/lingodotdev/lingo.dev/commit/920e3f5c3ca1fd51b0919db13a4787cfd616de54) Thanks [@mathio](https://github.com/mathio)! - remove cloneDeep for optimization
## 0.1.1
### Patch Changes
- [`caef325`](https://github.com/lingodotdev/lingo.dev/commit/caef3253bc99fa7bf7a0b40e5604c3590dcb4958) Thanks [@mathio](https://github.com/mathio)! - release fix
## 0.1.0
### Minor Changes
- [`e980e84`](https://github.com/lingodotdev/lingo.dev/commit/e980e84178439ad70417d38b425acf9148cfc4b6) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - added the compiler
```
--------------------------------------------------------------------------------
/packages/react/src/core/component.spec.tsx:
--------------------------------------------------------------------------------
```typescript
import { describe, it, expect } from "vitest";
import { render } from "@testing-library/react";
import { LingoComponent } from "./component";
describe("LingoComponent", () => {
const dictionary = {
files: {
messages: {
entries: {
greeting: "Hello {user.profile.name} you have {count} messages",
welcome:
"Welcome <element:a>incredible <element:span>fantastic <element:em>wonderful <element:strong>amazing</element:strong></element:em></element:span> user</element:a> <element:Icons.Rocket></element:Icons.Rocket>",
complex:
"<element:a>Hello {user.profile.name}, welcome to <element:span>wonderful <element:strong><element:em>{placeholder}</element:em> nested</element:strong></element:span> world</element:a> of the <element:u>universe number {count}</element:u>",
},
},
},
};
it("replaces variables in text", () => {
const { container } = render(
<LingoComponent
$dictionary={dictionary}
$as="div"
$fileKey="messages"
$entryKey="greeting"
$variables={{ "user.profile.name": "John", count: 69 }}
/>,
);
expect(container.textContent).toBe("Hello John you have 69 messages");
});
it("replaces variables with JSX", () => {
const { container } = render(
<LingoComponent
$dictionary={dictionary}
$as="div"
$fileKey="messages"
$entryKey="greeting"
$variables={{
"user.profile.name": <strong>John</strong>,
count: <em>69</em>,
}}
/>,
);
expect(container.innerHTML).toBe(
"<div>Hello <strong>John</strong> you have <em>69</em> messages</div>",
);
});
it("replaces element placeholders", () => {
const Icons = {
Rocket: () => <span>🚀</span>,
};
const { container } = render(
<LingoComponent
$dictionary={dictionary}
$as="div"
$fileKey="messages"
$entryKey="welcome"
$elements={[
({ children }: any) => <a href="#">{children}</a>,
({ children }: any) => <span>{children}</span>,
({ children }: any) => <em>{children}</em>,
({ children }: any) => <strong className="red">{children}</strong>,
({ children }: any) => <Icons.Rocket />,
]}
/>,
);
expect(container.innerHTML).toBe(
'<div>Welcome <a href="#">incredible <span>fantastic <em>wonderful <strong class="red">amazing</strong></em></span> user</a> <span>🚀</span></div>',
);
});
it("handles both variables and elements", () => {
const { container } = render(
<LingoComponent
$dictionary={dictionary}
$as="div"
$fileKey="messages"
$entryKey="complex"
$variables={{
"user.profile.name": "John",
count: 42,
placeholder: "very",
}}
$elements={[
({ children }: any) => <a>{children}</a>,
({ children }: any) => <span>{children}</span>,
({ children }: any) => <strong>{children}</strong>,
({ children }: any) => <em>{children}</em>,
({ children }: any) => <u>{children}</u>,
]}
/>,
);
expect(container.innerHTML).toBe(
"<div><a>Hello John, welcome to <span>wonderful <strong><em>very</em> nested</strong></span> world</a> of the <u>universe number 42</u></div>",
);
});
it("falls back to entryKey if value not found", () => {
const { container } = render(
<LingoComponent
$dictionary={dictionary}
$as="div"
$fileKey="messages"
$entryKey="nonexistent"
$variables={{}}
$elements={[]}
/>,
);
expect(container.textContent).toBe("nonexistent");
});
describe("function replacement", () => {
const getName = () => "John";
const getCount = () => 42;
const formatName = () => "John Doe";
const getUnread = () => 3;
const fnDictionary = {
files: {
messages: {
entries: {
simple:
"Hello <function:getName/>, you have <function:getCount/> items",
chained: "Hello <function:user.details.profile.name/>",
mixed:
"Welcome <function:formatName/>, you have {count} items and <function:getUnread/> unread",
nested:
"<element:strong>User <function:getName/></element:strong> has <element:em><function:getCount/></element:em>",
},
},
},
};
it("replaces function calls in text", () => {
const { container } = render(
<LingoComponent
$dictionary={fnDictionary}
$as="div"
$fileKey="messages"
$entryKey="simple"
$functions={{
getName: [getName()],
getCount: [getCount()],
}}
/>,
);
expect(container.textContent).toBe("Hello John, you have 42 items");
});
it("handles mixed variables and functions", () => {
const { container } = render(
<LingoComponent
$dictionary={fnDictionary}
$as="div"
$fileKey="messages"
$entryKey="mixed"
$variables={{
count: 5,
}}
$functions={{
formatName: [formatName()],
getUnread: [getUnread()],
}}
/>,
);
expect(container.textContent).toBe(
"Welcome John Doe, you have 5 items and 3 unread",
);
});
it("handles functions with nested elements", () => {
const { container } = render(
<LingoComponent
$dictionary={fnDictionary}
$as="div"
$fileKey="messages"
$entryKey="nested"
$functions={{
getName: [getName()],
getCount: [getCount()],
}}
$elements={[
({ children }: any) => <strong>{children}</strong>,
({ children }: any) => <em>{children}</em>,
]}
/>,
);
expect(container.innerHTML).toBe(
"<div><strong>User John</strong> has <em>42</em></div>",
);
});
it("handles function with chained names", () => {
const { container } = render(
<LingoComponent
$dictionary={fnDictionary}
$as="div"
$fileKey="messages"
$entryKey="chained"
$functions={{
"user.details.profile.name": [getName()],
}}
/>,
);
expect(container.textContent).toBe("Hello John");
});
it("preserves function placeholder if function not provided", () => {
const { container } = render(
<LingoComponent
$dictionary={fnDictionary}
$as="div"
$fileKey="messages"
$entryKey="simple"
$functions={{
getName: [getName()],
// fn1:getCount not provided
}}
/>,
);
expect(container.textContent).toBe(
"Hello John, you have <function:getCount/> items",
);
});
it("replaces function calls with JSX", () => {
const { container } = render(
<LingoComponent
$dictionary={fnDictionary}
$as="div"
$fileKey="messages"
$entryKey="simple"
$functions={{
getName: [<strong>John</strong>],
getCount: [<em>42</em>],
}}
/>,
);
expect(container.innerHTML).toBe(
"<div>Hello <strong>John</strong>, you have <em>42</em> items</div>",
);
});
});
describe("expression replacement", () => {
const exprDictionary = {
files: {
messages: {
entries: {
simple: "Result: <expression/>",
multiple: "First: <expression/>, Second: <expression/>",
mixed:
"Count: <expression/>, User: {user.name}, Items: <expression/>",
nested:
"<element:strong>Value: <expression/></element:strong> and <element:em>Total: <expression/></element:em>",
},
},
},
};
it("replaces simple expressions", () => {
const { container } = render(
<LingoComponent
$dictionary={exprDictionary}
$as="div"
$fileKey="messages"
$entryKey="simple"
$expressions={[42]}
/>,
);
expect(container.textContent).toBe("Result: 42");
});
it("handles multiple expressions", () => {
const { container } = render(
<LingoComponent
$dictionary={exprDictionary}
$as="div"
$fileKey="messages"
$entryKey="multiple"
$expressions={[42 * 2, "hello".toUpperCase()]}
/>,
);
expect(container.textContent).toBe("First: 84, Second: HELLO");
});
it("handles mixed variables and expressions", () => {
const { container } = render(
<LingoComponent
$dictionary={exprDictionary}
$as="div"
$fileKey="messages"
$entryKey="mixed"
$variables={{
"user.name": "John",
}}
$expressions={[42 + 1, [1, 2, 3].length]}
/>,
);
expect(container.textContent).toBe("Count: 43, User: John, Items: 3");
});
it("handles expressions with nested elements", () => {
const { container } = render(
<LingoComponent
$dictionary={exprDictionary}
$as="div"
$fileKey="messages"
$entryKey="nested"
$expressions={[42 * 2, [1, 2, 3].reduce((a, b) => a + b, 0)]}
$elements={[
({ children }: any) => <strong>{children}</strong>,
({ children }: any) => <em>{children}</em>,
]}
/>,
);
expect(container.innerHTML).toBe(
"<div><strong>Value: 84</strong> and <em>Total: 6</em></div>",
);
});
it("preserves expression placeholder if not provided", () => {
const { container } = render(
<LingoComponent
$dictionary={exprDictionary}
$as="div"
$fileKey="messages"
$entryKey="multiple"
$expressions={[
42,
// second expression not provided
]}
/>,
);
expect(container.textContent).toBe("First: 42, Second: <expression/>");
});
it("replaces expressions with JSX", () => {
const { container } = render(
<LingoComponent
$dictionary={exprDictionary}
$as="div"
$fileKey="messages"
$entryKey="multiple"
$expressions={[<strong>foo</strong>, <code>bar</code>]}
/>,
);
expect(container.innerHTML).toBe(
"<div>First: <strong>foo</strong>, Second: <code>bar</code></div>",
);
});
});
describe("array mutation prevention (shift() bug fix)", () => {
const mutationDictionary = {
files: {
test: {
entries: {
elements:
"First <element:0>text</element:0> and <element:1>more</element:1>",
functions: "Call <function:fn1/> then <function:fn2/>",
expressions: "Value <expression/> and <expression/>",
mixed:
"Element <element:0>content</element:0> with <function:fn1/> and <expression/>",
},
},
},
};
it("does not mutate elements array during processing", () => {
const elements = [
({ children }: any) => <span>{children}</span>,
({ children }: any) => <strong>{children}</strong>,
];
const originalElements = [...elements];
render(
<LingoComponent
$dictionary={mutationDictionary}
$as="div"
$fileKey="test"
$entryKey="elements"
$elements={elements}
/>,
);
expect(elements).toEqual(originalElements);
expect(elements.length).toBe(2);
});
it("does not mutate functions arrays during processing", () => {
const functions = {
fn1: ["result1", "result2"],
fn2: ["result3", "result4"],
};
const originalFunctions = {
fn1: [...functions.fn1],
fn2: [...functions.fn2],
};
render(
<LingoComponent
$dictionary={mutationDictionary}
$as="div"
$fileKey="test"
$entryKey="functions"
$functions={functions}
/>,
);
expect(functions.fn1).toEqual(originalFunctions.fn1);
expect(functions.fn2).toEqual(originalFunctions.fn2);
expect(functions.fn1.length).toBe(2);
expect(functions.fn2.length).toBe(2);
});
it("does not mutate expressions array during processing", () => {
const expressions = ["value1", "value2"];
const originalExpressions = [...expressions];
render(
<LingoComponent
$dictionary={mutationDictionary}
$as="div"
$fileKey="test"
$entryKey="expressions"
$expressions={expressions}
/>,
);
expect(expressions).toEqual(originalExpressions);
expect(expressions.length).toBe(2);
});
it("produces consistent output across multiple renders", () => {
const elements = [
({ children }: any) => <span>{children}</span>,
({ children }: any) => <strong>{children}</strong>,
];
const functions = { fn1: ["result1"] };
const expressions = ["value1"];
const { container: container1 } = render(
<LingoComponent
$dictionary={mutationDictionary}
$as="div"
$fileKey="test"
$entryKey="mixed"
$elements={elements}
$functions={functions}
$expressions={expressions}
/>,
);
const { container: container2 } = render(
<LingoComponent
$dictionary={mutationDictionary}
$as="div"
$fileKey="test"
$entryKey="mixed"
$elements={elements}
$functions={functions}
$expressions={expressions}
/>,
);
expect(container1.innerHTML).toBe(container2.innerHTML);
});
it("handles shared arrays across multiple component instances", () => {
const sharedElements = [
({ children }: any) => <span>{children}</span>,
({ children }: any) => <strong>{children}</strong>,
];
const { container: container1 } = render(
<div>
<LingoComponent
$dictionary={mutationDictionary}
$as="div"
$fileKey="test"
$entryKey="elements"
$elements={sharedElements}
/>
</div>,
);
const { container: container2 } = render(
<div>
<LingoComponent
$dictionary={mutationDictionary}
$as="div"
$fileKey="test"
$entryKey="elements"
$elements={sharedElements}
/>
</div>,
);
expect(container1.innerHTML).toBe(container2.innerHTML);
expect(sharedElements.length).toBe(2);
});
it("extracts inner content when elements array is exhausted", () => {
const { container } = render(
<LingoComponent
$dictionary={mutationDictionary}
$as="div"
$fileKey="test"
$entryKey="elements"
$elements={[({ children }: any) => <span>{children}</span>]}
/>,
);
expect(container.textContent).toBe("First text and more");
expect(container.innerHTML).toBe(
"<div>First <span>text</span> and more</div>",
);
});
it("handles completely empty elements array gracefully", () => {
const { container } = render(
<LingoComponent
$dictionary={mutationDictionary}
$as="div"
$fileKey="test"
$entryKey="elements"
$elements={[]}
/>,
);
expect(container.textContent).toBe("First text and more");
expect(container.innerHTML).toBe("<div>First text and more</div>");
});
it("maintains function index tracking per function name", () => {
const multiCallDictionary = {
files: {
test: {
entries: {
multiCall:
"First <function:fn1/>, second <function:fn1/>, third <function:fn2/>",
},
},
},
};
const { container } = render(
<LingoComponent
$dictionary={multiCallDictionary}
$as="div"
$fileKey="test"
$entryKey="multiCall"
$functions={{
fn1: ["A", "B"],
fn2: ["C"],
}}
/>,
);
expect(container.textContent).toBe("First A, second B, third C");
});
});
});
```
--------------------------------------------------------------------------------
/packages/cli/src/cli/loaders/xliff.ts:
--------------------------------------------------------------------------------
```typescript
import { ILoader } from "./_types";
import { createLoader } from "./_utils";
import { JSDOM } from "jsdom";
/**
* Creates a comprehensive XLIFF loader supporting versions 1.2 and 2.0
* with deterministic key generation and structure preservation
*/
export default function createXliffLoader(): ILoader<
string,
Record<string, string>
> {
return createLoader({
async pull(locale, input, _ctx, originalLocale) {
const trimmedInput = (input ?? "").trim();
if (!trimmedInput) {
return createEmptyResult(originalLocale, locale);
}
try {
const dom = new JSDOM(trimmedInput, { contentType: "text/xml" });
const document = dom.window.document;
// Check for parsing errors
const parserError = document.querySelector("parsererror");
if (parserError) {
throw new Error(`XML parsing failed: ${parserError.textContent}`);
}
const xliffElement = document.documentElement;
if (!xliffElement || xliffElement.tagName !== "xliff") {
throw new Error("Invalid XLIFF: missing root <xliff> element");
}
const version = xliffElement.getAttribute("version") || "1.2";
const isV2 = version === "2.0";
if (isV2) {
return pullV2(xliffElement, locale, originalLocale);
} else {
return pullV1(xliffElement, locale, originalLocale);
}
} catch (error: any) {
throw new Error(`Failed to parse XLIFF file: ${error.message}`);
}
},
async push(locale, translations, originalInput, originalLocale, pullInput) {
if (!originalInput) {
// Create new file from scratch
return pushNewFile(locale, translations, originalLocale);
}
try {
const dom = new JSDOM(originalInput, { contentType: "text/xml" });
const document = dom.window.document;
const xliffElement = document.documentElement;
const version = xliffElement.getAttribute("version") || "1.2";
const isV2 = version === "2.0";
if (isV2) {
return pushV2(
dom,
xliffElement,
locale,
translations,
originalLocale,
originalInput,
);
} else {
return pushV1(
dom,
xliffElement,
locale,
translations,
originalLocale,
originalInput,
);
}
} catch (error: any) {
throw new Error(`Failed to update XLIFF file: ${error.message}`);
}
},
});
}
/* -------------------------------------------------------------------------- */
/* Version 1.2 Support */
/* -------------------------------------------------------------------------- */
function pullV1(
xliffElement: Element,
locale: string,
originalLocale: string,
): Record<string, string> {
const result: Record<string, string> = {};
const fileElement = xliffElement.querySelector("file");
if (!fileElement) {
return result;
}
const sourceLanguage =
fileElement.getAttribute("source-language") || originalLocale;
const isSourceLocale = sourceLanguage === locale;
const bodyElement = fileElement.querySelector("body");
if (!bodyElement) {
return result;
}
const transUnits = bodyElement.querySelectorAll("trans-unit");
const seenKeys = new Set<string>();
transUnits.forEach((unit) => {
let key = getTransUnitKey(unit as Element);
if (!key) return;
// Handle duplicates deterministically
if (seenKeys.has(key)) {
const id = (unit as Element).getAttribute("id")?.trim();
if (id) {
key = `${key}#${id}`;
} else {
let counter = 1;
let newKey = `${key}__${counter}`;
while (seenKeys.has(newKey)) {
counter++;
newKey = `${key}__${counter}`;
}
key = newKey;
}
}
seenKeys.add(key);
const elementName = isSourceLocale ? "source" : "target";
const textElement = (unit as Element).querySelector(elementName);
if (textElement) {
result[key] = extractTextContent(textElement);
} else if (isSourceLocale) {
result[key] = key; // fallback for source
} else {
result[key] = ""; // empty for missing target
}
});
return result;
}
function pushV1(
dom: JSDOM,
xliffElement: Element,
locale: string,
translations: Record<string, string>,
originalLocale: string,
originalInput?: string,
): string {
const document = dom.window.document;
const fileElement = xliffElement.querySelector("file");
if (!fileElement) {
throw new Error("Invalid XLIFF 1.2: missing <file> element");
}
// Update language attributes
const sourceLanguage =
fileElement.getAttribute("source-language") || originalLocale;
const isSourceLocale = sourceLanguage === locale;
if (!isSourceLocale) {
fileElement.setAttribute("target-language", locale);
}
let bodyElement = fileElement.querySelector("body");
if (!bodyElement) {
bodyElement = document.createElement("body");
fileElement.appendChild(bodyElement);
}
// Build current index
const existingUnits = new Map<string, Element>();
const seenKeys = new Set<string>();
bodyElement.querySelectorAll("trans-unit").forEach((unit) => {
let key = getTransUnitKey(unit as Element);
if (!key) return;
if (seenKeys.has(key)) {
const id = (unit as Element).getAttribute("id")?.trim();
if (id) {
key = `${key}#${id}`;
} else {
let counter = 1;
let newKey = `${key}__${counter}`;
while (seenKeys.has(newKey)) {
counter++;
newKey = `${key}__${counter}`;
}
key = newKey;
}
}
seenKeys.add(key);
existingUnits.set(key, unit as Element);
});
// Update/create translation units
Object.entries(translations).forEach(([key, value]) => {
let unit = existingUnits.get(key);
if (!unit) {
unit = document.createElement("trans-unit");
unit.setAttribute("id", key);
unit.setAttribute("resname", key);
unit.setAttribute("restype", "string");
unit.setAttribute("datatype", "plaintext");
const sourceElement = document.createElement("source");
setTextContent(sourceElement, isSourceLocale ? value : key);
unit.appendChild(sourceElement);
if (!isSourceLocale) {
const targetElement = document.createElement("target");
targetElement.setAttribute("state", value ? "translated" : "new");
setTextContent(targetElement, value);
unit.appendChild(targetElement);
}
bodyElement.appendChild(unit);
existingUnits.set(key, unit);
} else {
updateTransUnitV1(unit, key, value, isSourceLocale);
}
});
// Remove orphaned units
const translationKeys = new Set(Object.keys(translations));
existingUnits.forEach((unit, key) => {
if (!translationKeys.has(key)) {
unit.parentNode?.removeChild(unit);
}
});
return serializeWithDeclaration(
dom,
extractXmlDeclaration(originalInput || ""),
);
}
function updateTransUnitV1(
unit: Element,
key: string,
value: string,
isSourceLocale: boolean,
): void {
const document = unit.ownerDocument!;
if (isSourceLocale) {
let sourceElement = unit.querySelector("source");
if (!sourceElement) {
sourceElement = document.createElement("source");
unit.appendChild(sourceElement);
}
setTextContent(sourceElement, value);
} else {
let targetElement = unit.querySelector("target");
if (!targetElement) {
targetElement = document.createElement("target");
unit.appendChild(targetElement);
}
setTextContent(targetElement, value);
targetElement.setAttribute("state", value.trim() ? "translated" : "new");
}
}
/* -------------------------------------------------------------------------- */
/* Version 2.0 Support */
/* -------------------------------------------------------------------------- */
function pullV2(
xliffElement: Element,
locale: string,
originalLocale: string,
): Record<string, string> {
const result: Record<string, string> = {};
// Add source language metadata
const srcLang = xliffElement.getAttribute("srcLang") || originalLocale;
result.sourceLanguage = srcLang;
const fileElements = xliffElement.querySelectorAll("file");
fileElements.forEach((fileElement) => {
const fileId = fileElement.getAttribute("id");
if (!fileId) return;
traverseUnitsV2(fileElement, fileId, "", result);
});
return result;
}
function traverseUnitsV2(
container: Element,
fileId: string,
currentPath: string,
result: Record<string, string>,
): void {
Array.from(container.children).forEach((child) => {
const tagName = child.tagName;
if (tagName === "unit") {
const unitId = child.getAttribute("id")?.trim();
if (!unitId) return;
const key = `resources/${fileId}/${currentPath}${unitId}/source`;
const segment = child.querySelector("segment");
const source = segment?.querySelector("source");
if (source) {
result[key] = extractTextContent(source);
} else {
result[key] = unitId; // fallback
}
} else if (tagName === "group") {
const groupId = child.getAttribute("id")?.trim();
const newPath = groupId
? `${currentPath}${groupId}/groupUnits/`
: currentPath;
traverseUnitsV2(child, fileId, newPath, result);
}
});
}
function pushV2(
dom: JSDOM,
xliffElement: Element,
locale: string,
translations: Record<string, string>,
originalLocale: string,
originalInput?: string,
): string {
const document = dom.window.document;
// Handle sourceLanguage metadata
if (translations.sourceLanguage) {
xliffElement.setAttribute("srcLang", translations.sourceLanguage);
delete translations.sourceLanguage; // Don't process as regular translation
}
// Build index of existing units
const existingUnits = new Map<string, Element>();
const fileElements = xliffElement.querySelectorAll("file");
fileElements.forEach((fileElement) => {
const fileId = fileElement.getAttribute("id");
if (!fileId) return;
indexUnitsV2(fileElement, fileId, "", existingUnits);
});
// Update existing units
Object.entries(translations).forEach(([key, value]) => {
const unit = existingUnits.get(key);
if (unit) {
updateUnitV2(unit, value);
} else {
// For new units, we'd need to create the structure
// This is complex in V2 due to the hierarchical nature
console.warn(`Cannot create new unit for key: ${key} in XLIFF 2.0`);
}
});
return serializeWithDeclaration(
dom,
extractXmlDeclaration(originalInput || ""),
);
}
function indexUnitsV2(
container: Element,
fileId: string,
currentPath: string,
index: Map<string, Element>,
): void {
Array.from(container.children).forEach((child) => {
const tagName = child.tagName;
if (tagName === "unit") {
const unitId = child.getAttribute("id")?.trim();
if (!unitId) return;
const key = `resources/${fileId}/${currentPath}${unitId}/source`;
index.set(key, child);
} else if (tagName === "group") {
const groupId = child.getAttribute("id")?.trim();
const newPath = groupId
? `${currentPath}${groupId}/groupUnits/`
: currentPath;
indexUnitsV2(child, fileId, newPath, index);
}
});
}
function updateUnitV2(unit: Element, value: string): void {
const document = unit.ownerDocument!;
let segment = unit.querySelector("segment");
if (!segment) {
segment = document.createElement("segment");
unit.appendChild(segment);
}
let source = segment.querySelector("source");
if (!source) {
source = document.createElement("source");
segment.appendChild(source);
}
setTextContent(source, value);
}
/* -------------------------------------------------------------------------- */
/* Utilities */
/* -------------------------------------------------------------------------- */
function getTransUnitKey(transUnit: Element): string {
const resname = transUnit.getAttribute("resname")?.trim();
if (resname) return resname;
const id = transUnit.getAttribute("id")?.trim();
if (id) return id;
const sourceElement = transUnit.querySelector("source");
if (sourceElement) {
const sourceText = extractTextContent(sourceElement).trim();
if (sourceText) return sourceText;
}
return "";
}
function extractTextContent(element: Element): string {
// Handle CDATA sections
const cdataNode = Array.from(element.childNodes).find(
(node) => node.nodeType === element.CDATA_SECTION_NODE,
);
if (cdataNode) {
return cdataNode.nodeValue || "";
}
return element.textContent || "";
}
function setTextContent(element: Element, content: string): void {
const document = element.ownerDocument!;
// Clear existing content
while (element.firstChild) {
element.removeChild(element.firstChild);
}
// Use CDATA if content contains XML-sensitive characters
if (/[<>&"']/.test(content)) {
const cdataSection = document.createCDATASection(content);
element.appendChild(cdataSection);
} else {
element.textContent = content;
}
}
function extractXmlDeclaration(xmlContent: string): string {
const match = xmlContent.match(/^<\?xml[^>]*\?>/);
return match ? match[0] : "";
}
function serializeWithDeclaration(dom: JSDOM, declaration: string): string {
let serialized = dom.serialize();
// Add proper indentation for readability
serialized = formatXml(serialized);
if (declaration) {
serialized = `${declaration}\n${serialized}`;
}
return serialized;
}
function formatXml(xml: string): string {
// Parse and reformat XML with proper indentation using JSDOM
const dom = new JSDOM(xml, { contentType: "text/xml" });
const doc = dom.window.document;
function formatElement(element: Element, depth: number = 0): string {
const indent = " ".repeat(depth);
const tagName = element.tagName;
const attributes = Array.from(element.attributes)
.map((attr) => `${attr.name}="${attr.value}"`)
.join(" ");
const openTag = attributes ? `<${tagName} ${attributes}>` : `<${tagName}>`;
// Check for CDATA sections first
const cdataNode = Array.from(element.childNodes).find(
(node) => node.nodeType === element.CDATA_SECTION_NODE,
);
if (cdataNode) {
return `${indent}${openTag}<![CDATA[${cdataNode.nodeValue}]]></${tagName}>`;
}
// Check if element has only text content
const textContent = element.textContent?.trim() || "";
const hasOnlyText =
element.childNodes.length === 1 && element.childNodes[0].nodeType === 3;
if (hasOnlyText && textContent) {
return `${indent}${openTag}${textContent}</${tagName}>`;
}
// Element has child elements
const children = Array.from(element.children);
if (children.length === 0) {
return `${indent}${openTag}</${tagName}>`;
}
let result = `${indent}${openTag}\n`;
for (const child of children) {
result += formatElement(child, depth + 1) + "\n";
}
result += `${indent}</${tagName}>`;
return result;
}
return formatElement(doc.documentElement);
}
function createEmptyResult(
originalLocale: string,
locale: string,
): Record<string, string> {
return {};
}
function pushNewFile(
locale: string,
translations: Record<string, string>,
originalLocale: string,
): string {
const skeleton = `<?xml version="1.0" encoding="utf-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
<file original="" source-language="${originalLocale}" target-language="${locale}" datatype="plaintext">
<header></header>
<body></body>
</file>
</xliff>`;
const dom = new JSDOM(skeleton, { contentType: "text/xml" });
const document = dom.window.document;
const bodyElement = document.querySelector("body")!;
Object.entries(translations).forEach(([key, value]) => {
const unit = document.createElement("trans-unit");
unit.setAttribute("id", key);
unit.setAttribute("resname", key);
unit.setAttribute("restype", "string");
unit.setAttribute("datatype", "plaintext");
const sourceElement = document.createElement("source");
setTextContent(sourceElement, key);
unit.appendChild(sourceElement);
const targetElement = document.createElement("target");
targetElement.setAttribute("state", value ? "translated" : "new");
setTextContent(targetElement, value);
unit.appendChild(targetElement);
bodyElement.appendChild(unit);
});
return serializeWithDeclaration(
dom,
'<?xml version="1.0" encoding="utf-8"?>',
);
}
```
--------------------------------------------------------------------------------
/packages/cli/src/cli/loaders/index.ts:
--------------------------------------------------------------------------------
```typescript
import Z from "zod";
import jsdom from "jsdom";
import { bucketTypeSchema } from "@lingo.dev/_spec";
import { composeLoaders } from "./_utils";
import createJsonLoader from "./json";
import createJson5Loader from "./json5";
import createJsoncLoader from "./jsonc";
import createFlatLoader from "./flat";
import createTextFileLoader from "./text-file";
import createYamlLoader from "./yaml";
import createRootKeyLoader from "./root-key";
import createFlutterLoader from "./flutter";
import { ILoader } from "./_types";
import createAndroidLoader from "./android";
import createCsvLoader from "./csv";
import createHtmlLoader from "./html";
import createMarkdownLoader from "./markdown";
import createMarkdocLoader from "./markdoc";
import createPropertiesLoader from "./properties";
import createXcodeStringsLoader from "./xcode-strings";
import createXcodeStringsdictLoader from "./xcode-stringsdict";
import createXcodeXcstringsLoader from "./xcode-xcstrings";
import createXcodeXcstringsV2Loader from "./xcode-xcstrings-v2-loader";
import { isICUPluralObject } from "./xcode-xcstrings-icu";
import createUnlocalizableLoader from "./unlocalizable";
import { createFormatterLoader, FormatterType } from "./formatters";
import createPoLoader from "./po";
import createXliffLoader from "./xliff";
import createXmlLoader from "./xml";
import createSrtLoader from "./srt";
import createDatoLoader from "./dato";
import createVttLoader from "./vtt";
import createVariableLoader from "./variable";
import createSyncLoader from "./sync";
import createPlutilJsonTextLoader from "./plutil-json-loader";
import createPhpLoader from "./php";
import createVueJsonLoader from "./vue-json";
import createTypescriptLoader from "./typescript";
import createInjectLocaleLoader from "./inject-locale";
import createLockedKeysLoader from "./locked-keys";
import createMdxFrontmatterSplitLoader from "./mdx2/frontmatter-split";
import createMdxCodePlaceholderLoader from "./mdx2/code-placeholder";
import createLocalizableMdxDocumentLoader from "./mdx2/localizable-document";
import createMdxSectionsSplit2Loader from "./mdx2/sections-split-2";
import createLockedPatternsLoader from "./locked-patterns";
import createIgnoredKeysLoader from "./ignored-keys";
import createEjsLoader from "./ejs";
import createEnsureKeyOrderLoader from "./ensure-key-order";
import createTxtLoader from "./txt";
import createJsonKeysLoader from "./json-dictionary";
type BucketLoaderOptions = {
returnUnlocalizedKeys?: boolean;
defaultLocale: string;
injectLocale?: string[];
targetLocale?: string;
formatter?: FormatterType;
};
export default function createBucketLoader(
bucketType: Z.infer<typeof bucketTypeSchema>,
bucketPathPattern: string,
options: BucketLoaderOptions,
lockedKeys?: string[],
lockedPatterns?: string[],
ignoredKeys?: string[],
): ILoader<void, Record<string, any>> {
switch (bucketType) {
default:
throw new Error(`Unsupported bucket type: ${bucketType}`);
case "android":
return composeLoaders(
createTextFileLoader(bucketPathPattern),
createLockedPatternsLoader(lockedPatterns),
createAndroidLoader(),
createEnsureKeyOrderLoader(),
createFlatLoader(),
createLockedKeysLoader(lockedKeys || []),
createIgnoredKeysLoader(ignoredKeys || []),
createSyncLoader(),
createUnlocalizableLoader(options.returnUnlocalizedKeys),
);
case "csv":
return composeLoaders(
createTextFileLoader(bucketPathPattern),
createLockedPatternsLoader(lockedPatterns),
createCsvLoader(),
createEnsureKeyOrderLoader(),
createFlatLoader(),
createLockedKeysLoader(lockedKeys || []),
createIgnoredKeysLoader(ignoredKeys || []),
createSyncLoader(),
createUnlocalizableLoader(options.returnUnlocalizedKeys),
);
case "html":
return composeLoaders(
createTextFileLoader(bucketPathPattern),
createFormatterLoader(options.formatter, "html", bucketPathPattern),
createLockedPatternsLoader(lockedPatterns),
createHtmlLoader(),
createLockedKeysLoader(lockedKeys || []),
createIgnoredKeysLoader(ignoredKeys || []),
createSyncLoader(),
createUnlocalizableLoader(options.returnUnlocalizedKeys),
);
case "ejs":
return composeLoaders(
createTextFileLoader(bucketPathPattern),
createLockedPatternsLoader(lockedPatterns),
createEjsLoader(),
createLockedKeysLoader(lockedKeys || []),
createIgnoredKeysLoader(ignoredKeys || []),
createSyncLoader(),
createUnlocalizableLoader(options.returnUnlocalizedKeys),
);
case "json":
return composeLoaders(
createTextFileLoader(bucketPathPattern),
createFormatterLoader(options.formatter, "json", bucketPathPattern),
createLockedPatternsLoader(lockedPatterns),
createJsonLoader(),
createEnsureKeyOrderLoader(),
createFlatLoader(),
createInjectLocaleLoader(options.injectLocale),
createLockedKeysLoader(lockedKeys || []),
createIgnoredKeysLoader(ignoredKeys || []),
createSyncLoader(),
createUnlocalizableLoader(options.returnUnlocalizedKeys),
);
case "json5":
return composeLoaders(
createTextFileLoader(bucketPathPattern),
createLockedPatternsLoader(lockedPatterns),
createJson5Loader(),
createEnsureKeyOrderLoader(),
createFlatLoader(),
createInjectLocaleLoader(options.injectLocale),
createLockedKeysLoader(lockedKeys || []),
createIgnoredKeysLoader(ignoredKeys || []),
createSyncLoader(),
createUnlocalizableLoader(options.returnUnlocalizedKeys),
);
case "jsonc":
return composeLoaders(
createTextFileLoader(bucketPathPattern),
createLockedPatternsLoader(lockedPatterns),
createJsoncLoader(),
createEnsureKeyOrderLoader(),
createFlatLoader(),
createInjectLocaleLoader(options.injectLocale),
createLockedKeysLoader(lockedKeys || []),
createIgnoredKeysLoader(ignoredKeys || []),
createSyncLoader(),
createUnlocalizableLoader(options.returnUnlocalizedKeys),
);
case "markdown":
return composeLoaders(
createTextFileLoader(bucketPathPattern),
createFormatterLoader(options.formatter, "markdown", bucketPathPattern),
createLockedPatternsLoader(lockedPatterns),
createMarkdownLoader(),
createLockedKeysLoader(lockedKeys || []),
createIgnoredKeysLoader(ignoredKeys || []),
createSyncLoader(),
createUnlocalizableLoader(options.returnUnlocalizedKeys),
);
case "markdoc":
return composeLoaders(
createTextFileLoader(bucketPathPattern),
createLockedPatternsLoader(lockedPatterns),
createMarkdocLoader(),
createFlatLoader(),
createEnsureKeyOrderLoader(),
createLockedKeysLoader(lockedKeys || []),
createIgnoredKeysLoader(ignoredKeys || []),
createSyncLoader(),
createUnlocalizableLoader(options.returnUnlocalizedKeys),
);
case "mdx":
return composeLoaders(
createTextFileLoader(bucketPathPattern),
createFormatterLoader(options.formatter, "mdx", bucketPathPattern),
createMdxCodePlaceholderLoader(),
createLockedPatternsLoader(lockedPatterns),
createMdxFrontmatterSplitLoader(),
createMdxSectionsSplit2Loader(),
createLocalizableMdxDocumentLoader(),
createFlatLoader(),
createEnsureKeyOrderLoader(),
createLockedKeysLoader(lockedKeys || []),
createIgnoredKeysLoader(ignoredKeys || []),
createSyncLoader(),
createUnlocalizableLoader(options.returnUnlocalizedKeys),
);
case "po":
return composeLoaders(
createTextFileLoader(bucketPathPattern),
createLockedPatternsLoader(lockedPatterns),
createPoLoader(),
createFlatLoader(),
createEnsureKeyOrderLoader(),
createLockedKeysLoader(lockedKeys || []),
createIgnoredKeysLoader(ignoredKeys || []),
createSyncLoader(),
createVariableLoader({ type: "python" }),
createUnlocalizableLoader(options.returnUnlocalizedKeys),
);
case "properties":
return composeLoaders(
createTextFileLoader(bucketPathPattern),
createLockedPatternsLoader(lockedPatterns),
createPropertiesLoader(),
createLockedKeysLoader(lockedKeys || []),
createIgnoredKeysLoader(ignoredKeys || []),
createSyncLoader(),
createUnlocalizableLoader(options.returnUnlocalizedKeys),
);
case "xcode-strings":
return composeLoaders(
createTextFileLoader(bucketPathPattern),
createLockedPatternsLoader(lockedPatterns),
createXcodeStringsLoader(),
createLockedKeysLoader(lockedKeys || []),
createIgnoredKeysLoader(ignoredKeys || []),
createSyncLoader(),
createUnlocalizableLoader(options.returnUnlocalizedKeys),
);
case "xcode-stringsdict":
return composeLoaders(
createTextFileLoader(bucketPathPattern),
createLockedPatternsLoader(lockedPatterns),
createXcodeStringsdictLoader(),
createFlatLoader(),
createEnsureKeyOrderLoader(),
createLockedKeysLoader(lockedKeys || []),
createIgnoredKeysLoader(ignoredKeys || []),
createSyncLoader(),
createUnlocalizableLoader(options.returnUnlocalizedKeys),
);
case "xcode-xcstrings":
return composeLoaders(
createTextFileLoader(bucketPathPattern),
createPlutilJsonTextLoader(),
createLockedPatternsLoader(lockedPatterns),
createJsonLoader(),
createXcodeXcstringsLoader(options.defaultLocale),
createFlatLoader(),
createEnsureKeyOrderLoader(),
createLockedKeysLoader(lockedKeys || []),
createIgnoredKeysLoader(ignoredKeys || []),
createSyncLoader(),
createVariableLoader({ type: "ieee" }),
createUnlocalizableLoader(options.returnUnlocalizedKeys),
);
case "xcode-xcstrings-v2":
return composeLoaders(
createTextFileLoader(bucketPathPattern),
createPlutilJsonTextLoader(),
createLockedPatternsLoader(lockedPatterns),
createJsonLoader(),
createXcodeXcstringsLoader(options.defaultLocale),
createXcodeXcstringsV2Loader(options.defaultLocale),
createFlatLoader({ shouldPreserveObject: isICUPluralObject }),
createEnsureKeyOrderLoader(),
createLockedKeysLoader(lockedKeys || []),
createIgnoredKeysLoader(ignoredKeys || []),
createSyncLoader(),
createVariableLoader({ type: "ieee" }),
createUnlocalizableLoader(options.returnUnlocalizedKeys),
);
case "yaml":
return composeLoaders(
createTextFileLoader(bucketPathPattern),
createFormatterLoader(options.formatter, "yaml", bucketPathPattern),
createLockedPatternsLoader(lockedPatterns),
createYamlLoader(),
createFlatLoader(),
createEnsureKeyOrderLoader(),
createLockedKeysLoader(lockedKeys || []),
createIgnoredKeysLoader(ignoredKeys || []),
createSyncLoader(),
createUnlocalizableLoader(options.returnUnlocalizedKeys),
);
case "yaml-root-key":
return composeLoaders(
createTextFileLoader(bucketPathPattern),
createFormatterLoader(options.formatter, "yaml", bucketPathPattern),
createLockedPatternsLoader(lockedPatterns),
createYamlLoader(),
createRootKeyLoader(true),
createFlatLoader(),
createEnsureKeyOrderLoader(),
createLockedKeysLoader(lockedKeys || []),
createIgnoredKeysLoader(ignoredKeys || []),
createSyncLoader(),
createUnlocalizableLoader(options.returnUnlocalizedKeys),
);
case "flutter":
return composeLoaders(
createTextFileLoader(bucketPathPattern),
createFormatterLoader(options.formatter, "json", bucketPathPattern),
createLockedPatternsLoader(lockedPatterns),
createJsonLoader(),
createEnsureKeyOrderLoader(),
createFlutterLoader(),
createFlatLoader(),
createLockedKeysLoader(lockedKeys || []),
createIgnoredKeysLoader(ignoredKeys || []),
createSyncLoader(),
createUnlocalizableLoader(options.returnUnlocalizedKeys),
);
case "xliff":
return composeLoaders(
createTextFileLoader(bucketPathPattern),
createLockedPatternsLoader(lockedPatterns),
createXliffLoader(),
createFlatLoader(),
createEnsureKeyOrderLoader(),
createLockedKeysLoader(lockedKeys || []),
createIgnoredKeysLoader(ignoredKeys || []),
createSyncLoader(),
createUnlocalizableLoader(options.returnUnlocalizedKeys),
);
case "xml":
return composeLoaders(
createTextFileLoader(bucketPathPattern),
createLockedPatternsLoader(lockedPatterns),
createXmlLoader(),
createFlatLoader(),
createEnsureKeyOrderLoader(),
createLockedKeysLoader(lockedKeys || []),
createIgnoredKeysLoader(ignoredKeys || []),
createSyncLoader(),
createUnlocalizableLoader(options.returnUnlocalizedKeys),
);
case "srt":
return composeLoaders(
createTextFileLoader(bucketPathPattern),
createLockedPatternsLoader(lockedPatterns),
createSrtLoader(),
createLockedKeysLoader(lockedKeys || []),
createIgnoredKeysLoader(ignoredKeys || []),
createSyncLoader(),
createUnlocalizableLoader(options.returnUnlocalizedKeys),
);
case "dato":
return composeLoaders(
createDatoLoader(bucketPathPattern),
createSyncLoader(),
createFlatLoader(),
createEnsureKeyOrderLoader(),
createLockedKeysLoader(lockedKeys || []),
createIgnoredKeysLoader(ignoredKeys || []),
createUnlocalizableLoader(options.returnUnlocalizedKeys),
);
case "vtt":
return composeLoaders(
createTextFileLoader(bucketPathPattern),
createLockedPatternsLoader(lockedPatterns),
createVttLoader(),
createLockedKeysLoader(lockedKeys || []),
createIgnoredKeysLoader(ignoredKeys || []),
createSyncLoader(),
createUnlocalizableLoader(options.returnUnlocalizedKeys),
);
case "php":
return composeLoaders(
createTextFileLoader(bucketPathPattern),
createLockedPatternsLoader(lockedPatterns),
createPhpLoader(),
createSyncLoader(),
createFlatLoader(),
createEnsureKeyOrderLoader(),
createLockedKeysLoader(lockedKeys || []),
createIgnoredKeysLoader(ignoredKeys || []),
createUnlocalizableLoader(options.returnUnlocalizedKeys),
);
case "vue-json":
return composeLoaders(
createTextFileLoader(bucketPathPattern),
createLockedPatternsLoader(lockedPatterns),
createVueJsonLoader(),
createSyncLoader(),
createFlatLoader(),
createEnsureKeyOrderLoader(),
createLockedKeysLoader(lockedKeys || []),
createIgnoredKeysLoader(ignoredKeys || []),
createUnlocalizableLoader(options.returnUnlocalizedKeys),
);
case "typescript":
return composeLoaders(
createTextFileLoader(bucketPathPattern),
createFormatterLoader(
options.formatter,
"typescript",
bucketPathPattern,
),
createLockedPatternsLoader(lockedPatterns),
createTypescriptLoader(),
createFlatLoader(),
createEnsureKeyOrderLoader(),
createSyncLoader(),
createLockedKeysLoader(lockedKeys || []),
createIgnoredKeysLoader(ignoredKeys || []),
createUnlocalizableLoader(options.returnUnlocalizedKeys),
);
case "txt":
return composeLoaders(
createTextFileLoader(bucketPathPattern),
createLockedPatternsLoader(lockedPatterns),
createTxtLoader(),
createLockedKeysLoader(lockedKeys || []),
createIgnoredKeysLoader(ignoredKeys || []),
createSyncLoader(),
createUnlocalizableLoader(options.returnUnlocalizedKeys),
);
case "json-dictionary":
return composeLoaders(
createTextFileLoader(bucketPathPattern),
createFormatterLoader(options.formatter, "json", bucketPathPattern),
createLockedPatternsLoader(lockedPatterns),
createJsonLoader(),
createJsonKeysLoader(),
createEnsureKeyOrderLoader(),
createFlatLoader(),
createInjectLocaleLoader(options.injectLocale),
createLockedKeysLoader(lockedKeys || []),
createIgnoredKeysLoader(ignoredKeys || []),
createSyncLoader(),
createUnlocalizableLoader(options.returnUnlocalizedKeys),
);
}
}
```
--------------------------------------------------------------------------------
/packages/cli/src/cli/loaders/markdoc.spec.ts:
--------------------------------------------------------------------------------
```typescript
import { describe, it, expect } from "vitest";
import createMarkdocLoader from "./markdoc";
describe("markdoc loader", () => {
describe("block-level tag", () => {
it("should extract text content from block-level tag", async () => {
const loader = createMarkdocLoader();
loader.setDefaultLocale("en");
const input = `{% foo %}
This is content inside of a block-level tag
{% /foo %}`;
const output = await loader.pull("en", input);
// Should extract the text content with semantic keys
const contents = Object.values(output);
expect(contents).toContain("This is content inside of a block-level tag");
});
it("should preserve tag structure on push", async () => {
const loader = createMarkdocLoader();
loader.setDefaultLocale("en");
const input = `{% foo %}
This is content inside of a block-level tag
{% /foo %}`;
const pulled = await loader.pull("en", input);
const pushed = await loader.push("en", pulled);
expect(pushed.trim()).toBe(input.trim());
});
it("should apply translations on push", async () => {
const loader = createMarkdocLoader();
loader.setDefaultLocale("en");
const input = `{% example %}
This paragraph is nested within a Markdoc tag.
{% /example %}`;
const pulled = await loader.pull("en", input);
// Modify the content using semantic keys
const translated = { ...pulled };
const contentKey = Object.keys(translated).find(
(k) =>
translated[k] === "This paragraph is nested within a Markdoc tag.",
);
if (contentKey) {
translated[contentKey] =
"Este párrafo está anidado dentro de una etiqueta Markdoc.";
}
const pushed = await loader.push("es", translated);
expect(pushed).toContain(
"Este párrafo está anidado dentro de una etiqueta Markdoc.",
);
expect(pushed).toContain("{% example %}");
expect(pushed).toContain("{% /example %}");
});
});
describe("self-closing tag", () => {
it("should handle self-closing tag with no content", async () => {
const loader = createMarkdocLoader();
loader.setDefaultLocale("en");
const input = `{% example /%}`;
const output = await loader.pull("en", input);
// Should have the tag but no text content
expect(output).toBeDefined();
});
it("should preserve self-closing tag on push", async () => {
const loader = createMarkdocLoader();
loader.setDefaultLocale("en");
const input = `{% example /%}`;
const pulled = await loader.pull("en", input);
const pushed = await loader.push("en", pulled);
expect(pushed.trim()).toBe(input.trim());
});
});
describe("inline tag", () => {
it("should extract text from inline tag", async () => {
const loader = createMarkdocLoader();
loader.setDefaultLocale("en");
const input = `This is a paragraph {% foo %}that contains a tag{% /foo %}`;
const output = await loader.pull("en", input);
// Should extract both text segments
const contents = Object.values(output);
expect(contents).toContain("This is a paragraph ");
expect(contents).toContain("that contains a tag");
});
it("should preserve inline tag structure on push", async () => {
const loader = createMarkdocLoader();
loader.setDefaultLocale("en");
const input = `This is a paragraph {% foo %}that contains a tag{% /foo %}`;
const pulled = await loader.pull("en", input);
const pushed = await loader.push("en", pulled);
expect(pushed.trim()).toBe(input.trim());
});
it("should apply translations to inline tag content", async () => {
const loader = createMarkdocLoader();
loader.setDefaultLocale("en");
const input = `This is a paragraph {% foo %}that contains a tag{% /foo %}`;
const pulled = await loader.pull("en", input);
// Translate both text segments
const translated = { ...pulled };
Object.keys(translated).forEach((key) => {
if (translated[key] === "This is a paragraph ") {
translated[key] = "Este es un párrafo ";
} else if (translated[key] === "that contains a tag") {
translated[key] = "que contiene una etiqueta";
}
});
const pushed = await loader.push("es", translated);
expect(pushed).toContain("Este es un párrafo");
expect(pushed).toContain("que contiene una etiqueta");
expect(pushed).toContain("{% foo %}");
expect(pushed).toContain("{% /foo %}");
});
});
describe("inline tag only content", () => {
it("should handle inline tag as sole paragraph content", async () => {
const loader = createMarkdocLoader();
loader.setDefaultLocale("en");
const input = `{% foo %}This is content inside of an inline tag{% /foo %}`;
const output = await loader.pull("en", input);
const contents = Object.values(output);
expect(contents).toContain("This is content inside of an inline tag");
});
it("should preserve inline-only tag structure on push", async () => {
const loader = createMarkdocLoader();
loader.setDefaultLocale("en");
const input = `{% foo %}This is content inside of an inline tag{% /foo %}`;
const pulled = await loader.pull("en", input);
const pushed = await loader.push("en", pulled);
expect(pushed.trim()).toBe(input.trim());
});
});
describe("mixed content", () => {
it("should handle document with multiple tags and text", async () => {
const loader = createMarkdocLoader();
loader.setDefaultLocale("en");
const input = `# Heading
This is a paragraph.
{% note %}
Important information here.
{% /note %}
Another paragraph with {% inline %}inline content{% /inline %}.
{% self-closing /%}`;
const output = await loader.pull("en", input);
const pushed = await loader.push("en", output);
// Verify structure is preserved
expect(pushed).toContain("# Heading");
expect(pushed).toContain("{% note %}");
expect(pushed).toContain("{% /note %}");
expect(pushed).toContain("{% inline %}");
expect(pushed).toContain("{% /inline %}");
expect(pushed).toContain("{% self-closing /%}");
});
});
describe("nested tags", () => {
it("should handle nested tags", async () => {
const loader = createMarkdocLoader();
loader.setDefaultLocale("en");
const input = `{% outer %}
Outer content
{% inner %}
Inner content
{% /inner %}
More outer content
{% /outer %}`;
const output = await loader.pull("en", input);
const pushed = await loader.push("en", output);
expect(pushed).toContain("{% outer %}");
expect(pushed).toContain("{% inner %}");
expect(pushed).toContain("{% /inner %}");
expect(pushed).toContain("{% /outer %}");
});
});
describe("interpolation", () => {
it("should preserve variable interpolation", async () => {
const loader = createMarkdocLoader();
loader.setDefaultLocale("en");
const input = `Hello {% $username %}`;
const output = await loader.pull("en", input);
const pushed = await loader.push("en", output);
expect(pushed.trim()).toBe(input.trim());
});
it("should preserve function interpolation", async () => {
const loader = createMarkdocLoader();
loader.setDefaultLocale("en");
const input = `Result: {% calculateValue() %}`;
const output = await loader.pull("en", input);
const pushed = await loader.push("en", output);
expect(pushed.trim()).toBe(input.trim());
});
it("should preserve interpolation in middle of text", async () => {
const loader = createMarkdocLoader();
loader.setDefaultLocale("en");
const input = `This is {% $var %} some text.`;
const output = await loader.pull("en", input);
const pushed = await loader.push("en", output);
expect(pushed.trim()).toBe(input.trim());
});
it("should translate text around interpolation", async () => {
const loader = createMarkdocLoader();
loader.setDefaultLocale("en");
const input = `Hello {% $username %}, welcome!`;
const output = await loader.pull("en", input);
// Should extract text segments but not interpolation
const textContents = Object.values(output).filter(
(v) => typeof v === "string",
);
expect(textContents).toContain("Hello ");
expect(textContents).toContain(", welcome!");
// Translate the text segments
const translated = { ...output };
Object.keys(translated).forEach((key) => {
if (translated[key] === "Hello ") {
translated[key] = "Hola ";
} else if (translated[key] === ", welcome!") {
translated[key] = ", ¡bienvenido!";
}
});
const pushed = await loader.push("es", translated);
expect(pushed).toContain("Hola");
expect(pushed).toContain("¡bienvenido!");
expect(pushed).toContain("{% $username %}");
});
it("should handle interpolation in tags", async () => {
const loader = createMarkdocLoader();
loader.setDefaultLocale("en");
const input = `{% callout %}
The value is {% $value %} today.
{% /callout %}`;
const output = await loader.pull("en", input);
const pushed = await loader.push("en", output);
expect(pushed).toContain("{% callout %}");
expect(pushed).toContain("{% $value %}");
expect(pushed).toContain("{% /callout %}");
});
});
describe("annotations", () => {
it("should preserve annotations with shorthand class attribute", async () => {
const loader = createMarkdocLoader();
loader.setDefaultLocale("en");
const input = `# Heading {% .example %}`;
const output = await loader.pull("en", input);
const pushed = await loader.push("en", output);
expect(pushed).toContain("# Heading");
expect(pushed).toContain("{% .example %}");
});
it("should preserve annotations with shorthand id attribute", async () => {
const loader = createMarkdocLoader();
loader.setDefaultLocale("en");
const input = `# Heading {% #main-title %}`;
const output = await loader.pull("en", input);
const pushed = await loader.push("en", output);
expect(pushed).toContain("# Heading");
expect(pushed).toContain("{% #main-title %}");
});
it("should preserve annotations with multiple shorthand attributes", async () => {
const loader = createMarkdocLoader();
loader.setDefaultLocale("en");
const input = `# Heading {% #foo .bar .baz %}`;
const output = await loader.pull("en", input);
const pushed = await loader.push("en", output);
expect(pushed).toContain("# Heading");
expect(pushed).toContain("{% #foo .bar .baz %}");
});
it("should translate heading text with annotations", async () => {
const loader = createMarkdocLoader();
loader.setDefaultLocale("en");
const input = `# Welcome {% .hero-title %}`;
const output = await loader.pull("en", input);
// Find and translate the heading text (note: has trailing space)
const translated = { ...output };
Object.keys(translated).forEach((key) => {
if (translated[key] === "Welcome ") {
translated[key] = "Bienvenido ";
}
});
const pushed = await loader.push("es", translated);
expect(pushed).toContain("Bienvenido");
expect(pushed).toContain("{% .hero-title %}");
});
});
describe("tag attributes", () => {
it("should preserve tags with full attributes", async () => {
const loader = createMarkdocLoader();
loader.setDefaultLocale("en");
const input = `{% callout type="note" %}
This is important information.
{% /callout %}`;
const output = await loader.pull("en", input);
const pushed = await loader.push("en", output);
expect(pushed).toContain('{% callout type="note" %}');
expect(pushed).toContain("{% /callout %}");
});
it("should preserve tags with multiple attributes", async () => {
const loader = createMarkdocLoader();
loader.setDefaultLocale("en");
const input = `{% image src="logo.png" alt="Company Logo" width="200" /%}`;
const output = await loader.pull("en", input);
const pushed = await loader.push("en", output);
expect(pushed.trim()).toBe(input.trim());
});
it("should preserve tags with array attributes", async () => {
const loader = createMarkdocLoader();
loader.setDefaultLocale("en");
const input = `{% chart data=[1, 2, 3] /%}`;
const output = await loader.pull("en", input);
const pushed = await loader.push("en", output);
expect(pushed.trim()).toBe(input.trim());
});
it("should translate content in tags with attributes", async () => {
const loader = createMarkdocLoader();
loader.setDefaultLocale("en");
const input = `{% callout type="warning" %}
Please read carefully.
{% /callout %}`;
const output = await loader.pull("en", input);
// Translate the content
const translated = { ...output };
Object.keys(translated).forEach((key) => {
if (translated[key] === "Please read carefully.") {
translated[key] = "Por favor lea con atención.";
}
});
const pushed = await loader.push("es", translated);
expect(pushed).toContain("Por favor lea con atención.");
expect(pushed).toContain('{% callout type="warning" %}');
expect(pushed).toContain("{% /callout %}");
});
});
describe("primary attributes", () => {
it("should preserve tags with primary attribute", async () => {
const loader = createMarkdocLoader();
loader.setDefaultLocale("en");
const input = `{% if $showContent %}
Content is visible.
{% /if %}`;
const output = await loader.pull("en", input);
const pushed = await loader.push("en", output);
expect(pushed).toContain("{% if $showContent %}");
expect(pushed).toContain("{% /if %}");
});
it("should translate content in tags with primary attribute", async () => {
const loader = createMarkdocLoader();
loader.setDefaultLocale("en");
const input = `{% if $showContent %}
Content is visible.
{% /if %}`;
const output = await loader.pull("en", input);
// Translate the content
const translated = { ...output };
Object.keys(translated).forEach((key) => {
if (translated[key] === "Content is visible.") {
translated[key] = "El contenido es visible.";
}
});
const pushed = await loader.push("es", translated);
expect(pushed).toContain("El contenido es visible.");
expect(pushed).toContain("{% if $showContent %}");
});
});
describe("frontmatter", () => {
it("should extract frontmatter attributes", async () => {
const loader = createMarkdocLoader();
loader.setDefaultLocale("en");
const input = `---
title: My Document
description: A sample document
author: John Doe
---
# Heading
Content here.`;
const output = await loader.pull("en", input);
expect(output["fm-attr-title"]).toBe("My Document");
expect(output["fm-attr-description"]).toBe("A sample document");
expect(output["fm-attr-author"]).toBe("John Doe");
});
it("should preserve frontmatter on push", async () => {
const loader = createMarkdocLoader();
loader.setDefaultLocale("en");
const input = `---
title: My Document
description: A sample document
---
# Heading
Content here.`;
const pulled = await loader.pull("en", input);
const pushed = await loader.push("en", pulled);
expect(pushed).toContain("title: My Document");
expect(pushed).toContain("description: A sample document");
expect(pushed).toContain("# Heading");
expect(pushed).toContain("Content here.");
});
it("should translate frontmatter attributes", async () => {
const loader = createMarkdocLoader();
loader.setDefaultLocale("en");
const input = `---
title: Welcome
description: This is a guide
---
# Content
Some text.`;
const pulled = await loader.pull("en", input);
// Translate frontmatter
const translated = { ...pulled };
translated["fm-attr-title"] = "Bienvenido";
translated["fm-attr-description"] = "Esta es una guía";
const pushed = await loader.push("es", translated);
expect(pushed).toContain("title: Bienvenido");
expect(pushed).toContain("description: Esta es una guía");
});
it("should handle documents without frontmatter", async () => {
const loader = createMarkdocLoader();
loader.setDefaultLocale("en");
const input = `# Heading
Content without frontmatter.`;
const output = await loader.pull("en", input);
const pushed = await loader.push("en", output);
expect(pushed).not.toContain("---");
expect(pushed).toContain("# Heading");
expect(pushed).toContain("Content without frontmatter.");
});
});
});
```
--------------------------------------------------------------------------------
/packages/compiler/src/lib/lcp/api/index.ts:
--------------------------------------------------------------------------------
```typescript
import { createGroq } from "@ai-sdk/groq";
import { createGoogleGenerativeAI } from "@ai-sdk/google";
import { createOpenRouter } from "@openrouter/ai-sdk-provider";
import { createOllama } from "ollama-ai-provider";
import { createMistral } from "@ai-sdk/mistral";
import { generateText } from "ai";
import { LingoDotDevEngine } from "@lingo.dev/_sdk";
import { DictionarySchema } from "../schema";
import _ from "lodash";
import { getLocaleModel } from "../../../utils/locales";
import getSystemPrompt from "./prompt";
import { obj2xml, xml2obj } from "./xml2obj";
import shots from "./shots";
import {
getGroqKey,
getGroqKeyFromEnv,
getGoogleKey,
getGoogleKeyFromEnv,
getOpenRouterKey,
getOpenRouterKeyFromEnv,
getMistralKey,
getMistralKeyFromEnv,
getLingoDotDevKeyFromEnv,
getLingoDotDevKey,
} from "../../../utils/llm-api-key";
import dedent from "dedent";
import { isRunningInCIOrDocker } from "../../../utils/env";
import { LanguageModel } from "ai";
import { providerDetails } from "./provider-details";
export class LCPAPI {
static async translate(
models: "lingo.dev" | Record<string, string>,
sourceDictionary: DictionarySchema,
sourceLocale: string,
targetLocale: string,
prompt?: string | null,
): Promise<DictionarySchema> {
const timeLabel = `LCPAPI.translate: ${targetLocale}`;
console.time(timeLabel);
const chunks = this._chunkDictionary(sourceDictionary);
const translatedChunks = [];
for (const chunk of chunks) {
const translatedChunk = await this._translateChunk(
models,
chunk,
sourceLocale,
targetLocale,
prompt,
);
translatedChunks.push(translatedChunk);
}
const result = this._mergeDictionaries(translatedChunks);
console.timeEnd(timeLabel);
return result;
}
private static _chunkDictionary(
dictionary: DictionarySchema,
): DictionarySchema[] {
const MAX_ENTRIES_PER_CHUNK = 100;
const { files, ...rest } = dictionary;
const chunks: DictionarySchema[] = [];
let currentChunk: DictionarySchema = {
...rest,
files: {},
};
let currentEntryCount = 0;
Object.entries(files).forEach(([fileName, file]) => {
const entries = file.entries;
const entryPairs = Object.entries(entries);
let currentIndex = 0;
while (currentIndex < entryPairs.length) {
const remainingSpace = MAX_ENTRIES_PER_CHUNK - currentEntryCount;
const entriesToAdd = entryPairs.slice(
currentIndex,
currentIndex + remainingSpace,
);
if (entriesToAdd.length > 0) {
currentChunk.files[fileName] = currentChunk.files[fileName] || {
entries: {},
};
currentChunk.files[fileName].entries = {
...currentChunk.files[fileName].entries,
...Object.fromEntries(entriesToAdd),
};
currentEntryCount += entriesToAdd.length;
}
currentIndex += entriesToAdd.length;
if (
currentEntryCount >= MAX_ENTRIES_PER_CHUNK ||
(currentIndex < entryPairs.length &&
currentEntryCount + (entryPairs.length - currentIndex) >
MAX_ENTRIES_PER_CHUNK)
) {
chunks.push(currentChunk);
currentChunk = { ...rest, files: {} };
currentEntryCount = 0;
}
}
});
if (currentEntryCount > 0) {
chunks.push(currentChunk);
}
return chunks;
}
private static _mergeDictionaries(dictionaries: DictionarySchema[]) {
const fileNames = _.uniq(
_.flatMap(dictionaries, (dict) => Object.keys(dict.files)),
);
const files = _(fileNames)
.map((fileName) => {
const entries = dictionaries.reduce((entries, dict) => {
const file = dict.files[fileName];
if (file) {
entries = _.merge(entries, file.entries);
}
return entries;
}, {});
return [fileName, { entries }];
})
.fromPairs()
.value();
const dictionary = {
version: dictionaries[0].version,
locale: dictionaries[0].locale,
files,
};
return dictionary;
}
private static _createLingoDotDevEngine() {
// Specific check for CI/CD or Docker missing GROQ key
if (isRunningInCIOrDocker()) {
const apiKeyFromEnv = getLingoDotDevKeyFromEnv();
if (!apiKeyFromEnv) {
this._failMissingLLMKeyCi("lingo.dev");
}
}
const apiKey = getLingoDotDevKey();
if (!apiKey) {
throw new Error(
"⚠️ Lingo.dev API key not found. Please set LINGODOTDEV_API_KEY environment variable or configure it user-wide.",
);
}
console.log(`Creating Lingo.dev client`);
return new LingoDotDevEngine({
apiKey,
});
}
private static async _translateChunk(
models: "lingo.dev" | Record<string, string>,
sourceDictionary: DictionarySchema,
sourceLocale: string,
targetLocale: string,
prompt?: string | null,
): Promise<DictionarySchema> {
if (models === "lingo.dev") {
try {
const lingoDotDevEngine = this._createLingoDotDevEngine();
console.log(
`✨ Using Lingo.dev Engine to localize from "${sourceLocale}" to "${targetLocale}"`,
);
const result = await lingoDotDevEngine.localizeObject(
sourceDictionary,
{
sourceLocale: sourceLocale,
targetLocale: targetLocale,
},
);
return result as DictionarySchema;
} catch (error) {
this._failLLMFailureLocal(
"lingo.dev",
targetLocale,
error instanceof Error ? error.message : "Unknown error",
);
// This throw is unreachable because the failure method exits,
// but it helps satisfy the TypeScript compiler.
throw error;
}
} else {
const { provider, model } = getLocaleModel(
models,
sourceLocale,
targetLocale,
);
if (!provider || !model) {
throw new Error(
dedent`
🚫 Lingo.dev Localization Engine Not Configured!
The "models" parameter is missing or incomplete in your Lingo.dev configuration.
👉 To fix this, set the "models" parameter to either:
• "lingo.dev" (for the default engine)
• a map of locale-to-model, e.g. { "models": { "en:es": "openai:gpt-3.5-turbo" } }
Example:
{
// ...
"models": "lingo.dev"
}
For more details, see: https://lingo.dev/compiler
To get help, join our Discord: https://lingo.dev/go/discord
`,
);
}
try {
const aiModel = this._createAiModel(provider, model, targetLocale);
console.log(
`ℹ️ Using raw LLM API ("${provider}":"${model}") to translate from "${sourceLocale}" to "${targetLocale}"`,
);
const response = await generateText({
model: aiModel,
messages: [
{
role: "system",
content: getSystemPrompt({
sourceLocale,
targetLocale,
prompt: prompt ?? undefined,
}),
},
...shots.flatMap((shotsTuple) => [
{
role: "user" as const,
content: obj2xml(shotsTuple[0]),
},
{
role: "assistant" as const,
content: obj2xml(shotsTuple[1]),
},
]),
{
role: "user",
content: obj2xml(sourceDictionary),
},
],
});
console.log("Response text received for", targetLocale);
let responseText = response.text;
// Extract XML content
responseText = responseText.substring(
responseText.indexOf("<"),
responseText.lastIndexOf(">") + 1,
);
return xml2obj(responseText);
} catch (error) {
this._failLLMFailureLocal(
provider,
targetLocale,
error instanceof Error ? error.message : "Unknown error",
);
// This throw is unreachable because the failure method exits,
// but it helps satisfy the TypeScript compiler.
throw error;
}
}
}
/**
* Instantiates an AI model based on provider and model ID.
* Includes CI/CD API key checks.
* @param providerId The ID of the AI provider (e.g., "groq", "google").
* @param modelId The ID of the specific model (e.g., "llama3-8b-8192", "gemini-2.0-flash").
* @param targetLocale The target locale being translated to (for logging/error messages).
* @returns An instantiated AI LanguageModel.
* @throws Error if the provider is not supported or API key is missing in CI/CD.
*/
private static _createAiModel(
providerId: string,
modelId: string,
targetLocale: string,
): LanguageModel {
switch (providerId) {
case "groq": {
// Specific check for CI/CD or Docker missing GROQ key
if (isRunningInCIOrDocker()) {
const groqFromEnv = getGroqKeyFromEnv();
if (!groqFromEnv) {
this._failMissingLLMKeyCi(providerId);
}
}
const groqKey = getGroqKey();
if (!groqKey) {
throw new Error(
"⚠️ GROQ API key not found. Please set GROQ_API_KEY environment variable or configure it user-wide.",
);
}
console.log(
`Creating Groq client for ${targetLocale} using model ${modelId}`,
);
return createGroq({ apiKey: groqKey })(modelId);
}
case "google": {
// Specific check for CI/CD or Docker missing Google key
if (isRunningInCIOrDocker()) {
const googleFromEnv = getGoogleKeyFromEnv();
if (!googleFromEnv) {
this._failMissingLLMKeyCi(providerId);
}
}
const googleKey = getGoogleKey();
if (!googleKey) {
throw new Error(
"⚠️ Google API key not found. Please set GOOGLE_API_KEY environment variable or configure it user-wide.",
);
}
console.log(
`Creating Google Generative AI client for ${targetLocale} using model ${modelId}`,
);
return createGoogleGenerativeAI({ apiKey: googleKey })(modelId);
}
case "openrouter": {
// Specific check for CI/CD or Docker missing OpenRouter key
if (isRunningInCIOrDocker()) {
const openRouterFromEnv = getOpenRouterKeyFromEnv();
if (!openRouterFromEnv) {
this._failMissingLLMKeyCi(providerId);
}
}
const openRouterKey = getOpenRouterKey();
if (!openRouterKey) {
throw new Error(
"⚠️ OpenRouter API key not found. Please set OPENROUTER_API_KEY environment variable or configure it user-wide.",
);
}
console.log(
`Creating OpenRouter client for ${targetLocale} using model ${modelId}`,
);
return createOpenRouter({
apiKey: openRouterKey,
})(modelId);
}
case "ollama": {
// No API key check needed for Ollama
console.log(
`Creating Ollama client for ${targetLocale} using model ${modelId} at default Ollama address`,
);
return createOllama()(modelId);
}
case "mistral": {
// Specific check for CI/CD or Docker missing Mistral key
if (isRunningInCIOrDocker()) {
const mistralFromEnv = getMistralKeyFromEnv();
if (!mistralFromEnv) {
this._failMissingLLMKeyCi(providerId);
}
}
const mistralKey = getMistralKey();
if (!mistralKey) {
throw new Error(
"⚠️ Mistral API key not found. Please set MISTRAL_API_KEY environment variable or configure it user-wide.",
);
}
console.log(
`Creating Mistral client for ${targetLocale} using model ${modelId}`,
);
return createMistral({ apiKey: mistralKey })(modelId);
}
default: {
throw new Error(
`⚠️ Provider "${providerId}" for locale "${targetLocale}" is not supported. Only "groq", "google", "openrouter", "ollama", and "mistral" providers are supported at the moment.`,
);
}
}
}
/**
* Show an actionable error message and exit the process when the compiler
* is running in CI/CD without a required LLM API key.
* The message explains why this situation is unusual and how to fix it.
* @param providerId The ID of the LLM provider whose key is missing.
*/
private static _failMissingLLMKeyCi(providerId: string): never {
let details = providerDetails[providerId];
if (!details) {
// Fallback for unsupported provider in failure message logic
throw new Error(
`Internal Error: Missing details for provider "${providerId}" when reporting missing key in CI/CD. You might be using an unsupported provider.`,
);
}
const errorMessage = dedent`
💡 You're using Lingo.dev Localization Compiler, and it detected unlocalized components in your app.
The compiler needs a ${details.name} API key to translate missing strings, but ${details.apiKeyEnvVar} is not set in the environment.
This is unexpected: typically you run a full build locally, commit the generated translation files, and push them to CI/CD.
However, If you want CI/CD to translate the new strings, provide the key with:
• Session-wide: export ${details.apiKeyEnvVar}=<your-api-key>
• Project-wide / CI: add ${details.apiKeyEnvVar}=<your-api-key> to your pipeline environment variables
⭐️ Also:
1. If you don't yet have a ${details.name} API key, get one for free at ${details.getKeyLink}
2. If you want to use a different LLM, update your configuration. Refer to documentation for help: https://lingo.dev/compiler
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
`;
console.log(errorMessage);
throw new Error(`Missing ${details.name} API key in CI/CD environment.`);
}
/**
* Show an actionable error message and exit the process when an LLM API call
* fails during local compilation.
* @param providerId The ID of the LLM provider that failed.
* @param targetLocale The target locale being translated to.
* @param errorMessage The error message received from the API.
*/
private static _failLLMFailureLocal(
providerId: string,
targetLocale: string,
errorMessage: string,
): never {
const details = providerDetails[providerId];
if (!details) {
// Fallback
throw new Error(
`Internal Error: Missing details for provider "${providerId}" when reporting local failure. Original Error: ${errorMessage}`,
);
}
const isInvalidApiKey = errorMessage.match("Invalid API Key"); // TODO: This may change per-provider, so might update this later
if (isInvalidApiKey) {
const message = dedent`
⚠️ Lingo.dev Compiler requires a valid ${details.name} API key to translate your application.
It looks like you set ${details.name} API key but it is not valid. Please check your API key and try again.
Error details from ${details.name} API: ${errorMessage}
👉 You can set the API key in one of the following ways:
1. User-wide: Run npx lingo.dev@latest config set ${details.apiKeyConfigKey} <your-api-key>
2. Project-wide: Add ${details.apiKeyEnvVar}=<your-api-key> to .env file in every project that uses Lingo.dev Localization Compiler
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
⭐️ Also:
1. If you don't yet have a ${details.name} API key, get one for free at ${details.getKeyLink}
2. If you want to use a different LLM, raise an issue in our open-source repo: https://lingo.dev/go/gh
3. If you have questions, feature requests, or would like to contribute, join our Discord: https://lingo.dev/go/discord
`;
console.log(message);
throw new Error(`Invalid ${details.name} API key.`);
} else {
const message = dedent`
⚠️ Lingo.dev Compiler tried to translate your application to "${targetLocale}" locale via ${
details.name
} but it failed.
Error details from ${details.name} API: ${errorMessage}
This error comes from the ${
details.name
} API, please check their documentation for more details: ${
details.docsLink
}
⭐️ Also:
1. Did you set ${
details.apiKeyEnvVar
? `${details.apiKeyEnvVar}`
: "the provider API key"
} environment variable correctly ${
!details.apiKeyEnvVar ? "(if required)" : ""
}?
2. Did you reach any limits of your ${details.name} account?
3. If you have questions, feature requests, or would like to contribute, join our Discord: https://lingo.dev/go/discord
`;
console.log(message);
throw new Error(
`Translation failed for locale "${targetLocale}" using ${details.name}: ${errorMessage}`,
);
}
}
}
```
--------------------------------------------------------------------------------
/packages/cli/src/cli/loaders/ignored-keys-buckets.spec.ts:
--------------------------------------------------------------------------------
```typescript
import { describe, it, expect, beforeEach, vi } from "vitest";
import fs from "fs/promises";
import dedent from "dedent";
import createBucketLoader from "./index";
describe("ignored keys support across buckets", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.resetModules();
setupFileMocks();
});
it("android: should omit ignored keys on pull", async () => {
const input = `
<resources>
<string name="button.title">Submit</string>
<string name="button.description">Description</string>
</resources>
`.trim();
mockFileOperations(input);
const loader = createBucketLoader(
"android",
"values-[locale]/strings.xml",
{ defaultLocale: "en" },
[],
[],
["button.description"],
);
loader.setDefaultLocale("en");
const data = await loader.pull("en");
expect(data).toEqual({ "button.title": "Submit" });
});
it("csv: should omit ignored keys on pull", async () => {
const input = `id,en\nbutton.title,Submit\nbutton.description,Description`;
mockFileOperations(input);
const loader = createBucketLoader(
"csv",
"i18n.csv",
{ defaultLocale: "en" },
[],
[],
["button.description"],
);
loader.setDefaultLocale("en");
const data = await loader.pull("en");
expect(data).toEqual({ "button.title": "Submit" });
});
it("html: should omit ignored keys (by prefix) on pull", async () => {
const input = dedent`
<html>
<head>
<title>My Page</title>
<meta name="description" content="Page description" />
</head>
<body>
<h1>Hello</h1>
<p>Paragraph</p>
</body>
</html>
`;
mockFileOperations(input);
const loader = createBucketLoader(
"html",
"i18n/[locale].html",
{ defaultLocale: "en" },
[],
[],
["head"],
);
loader.setDefaultLocale("en");
const data = await loader.pull("en");
expect(Object.keys(data).some((k) => k.startsWith("head"))).toBe(false);
});
it("ejs: should omit ignored keys on pull", async () => {
const input = `<h1>Welcome</h1><p>Hello <%= name %></p>`;
mockFileOperations(input);
const loader = createBucketLoader(
"ejs",
"templates/[locale].ejs",
{ defaultLocale: "en" },
[],
[],
["text_*"],
);
loader.setDefaultLocale("en");
const data = await loader.pull("en");
expect(data).toEqual({});
});
it("json: should omit ignored keys on pull", async () => {
const input = JSON.stringify({ title: "Submit", description: "Desc" });
mockFileOperations(input);
const loader = createBucketLoader(
"json",
"i18n/[locale].json",
{ defaultLocale: "en" },
[],
[],
["description"],
);
loader.setDefaultLocale("en");
const data = await loader.pull("en");
expect(data).toEqual({ title: "Submit" });
});
it("json5: should omit ignored keys on pull", async () => {
const input = `{
// comment
title: "Submit",
description: "Desc"
}`;
mockFileOperations(input);
const loader = createBucketLoader(
"json5",
"i18n/[locale].json5",
{ defaultLocale: "en" },
[],
[],
["description"],
);
loader.setDefaultLocale("en");
const data = await loader.pull("en");
expect(data).toEqual({ title: "Submit" });
});
it("jsonc: should omit ignored keys on pull", async () => {
const input = `{
// comment
"title": "Submit",
"description": "Desc"
}`;
mockFileOperations(input);
const loader = createBucketLoader(
"jsonc",
"i18n/[locale].jsonc",
{ defaultLocale: "en" },
[],
[],
["description"],
);
loader.setDefaultLocale("en");
const data = await loader.pull("en");
expect(data).toEqual({ title: "Submit" });
});
it("markdown: should omit ignored keys (frontmatter) on pull", async () => {
const input = dedent`
---
title: Test Markdown
date: 2023-05-25
---
# Heading 1
Content.
`;
mockFileOperations(input);
const loader = createBucketLoader(
"markdown",
"i18n/[locale].md",
{ defaultLocale: "en" },
[],
[],
["fm-attr-title"],
);
loader.setDefaultLocale("en");
const data = await loader.pull("en");
expect(Object.keys(data)).not.toContain("fm-attr-title");
});
it("markdoc: should omit ignored keys by semantic prefix on pull", async () => {
const input = dedent`
---
title: My Page
---
# Heading 1
Hello world
`;
mockFileOperations(input);
const loader = createBucketLoader(
"markdoc",
"docs/[locale].md",
{ defaultLocale: "en" },
[],
[],
["heading"],
);
loader.setDefaultLocale("en");
const data = await loader.pull("en");
expect(Object.keys(data).some((k) => k.startsWith("heading"))).toBe(false);
});
it("mdx: should omit ignored section keys on pull", async () => {
const input = dedent`
---
title: Hello
---
# Title
Paragraph
`;
mockFileOperations(input);
const loader = createBucketLoader(
"mdx",
"i18n/[locale].mdx",
{ defaultLocale: "en", formatter: undefined },
[],
[],
["md-section-0"],
);
loader.setDefaultLocale("en");
const data = await loader.pull("en");
expect(Object.keys(data)).not.toContain("md-section-0");
});
it("po: should omit ignored keys on pull", async () => {
const input = dedent`
#: hello.py:1
msgid "Hello"
msgstr ""
`;
mockFileOperations(input);
const loader = createBucketLoader(
"po",
"i18n/[locale].po",
{ defaultLocale: "en" },
[],
[],
["Hello"],
);
loader.setDefaultLocale("en");
const data = await loader.pull("en");
expect(data).toEqual({});
});
it("properties: should omit ignored keys on pull", async () => {
const input = dedent`
welcome.message=Welcome
error.message=Error
`;
mockFileOperations(input);
const loader = createBucketLoader(
"properties",
"i18n/[locale].properties",
{ defaultLocale: "en" },
[],
[],
["error.message"],
);
loader.setDefaultLocale("en");
const data = await loader.pull("en");
expect(data).toEqual({ "welcome.message": "Welcome" });
});
it("xcode-strings: should omit ignored keys on pull", async () => {
const input = `"hello" = "Hello!";\n"bye" = "Bye!";`;
mockFileOperations(input);
const loader = createBucketLoader(
"xcode-strings",
"i18n/[locale].strings",
{ defaultLocale: "en" },
[],
[],
["bye"],
);
loader.setDefaultLocale("en");
const data = await loader.pull("en");
expect(data).toEqual({ hello: "Hello!" });
});
it("xcode-stringsdict: should omit ignored keys on pull", async () => {
const input = dedent`
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>greeting</key>
<string>Hello!</string>
<key>items_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@items@</string>
<key>items</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%d item</string>
<key>other</key>
<string>%d items</string>
</dict>
</dict>
</dict>
</plist>
`;
mockFileOperations(input);
const loader = createBucketLoader(
"xcode-stringsdict",
"i18n/[locale].stringsdict",
{ defaultLocale: "en" },
[],
[],
["items_count"],
);
loader.setDefaultLocale("en");
const data = await loader.pull("en");
expect(Object.keys(data)).toContain("greeting");
expect(Object.keys(data).some((k) => k.startsWith("items_count"))).toBe(
false,
);
});
it("xcode-xcstrings: should omit ignored keys on pull", async () => {
const input = dedent`
{
"sourceLanguage": "en",
"strings": {
"greeting": {
"extractionState": "manual",
"localizations": {
"en": { "stringUnit": { "state": "translated", "value": "Hello!" } }
}
},
"message": {
"extractionState": "manual",
"localizations": {
"en": { "stringUnit": { "state": "translated", "value": "Welcome" } }
}
}
}
}
`;
mockFileOperations(input);
const loader = createBucketLoader(
"xcode-xcstrings",
"i18n/[locale].xcstrings",
{ defaultLocale: "en" },
[],
[],
["message"],
);
loader.setDefaultLocale("en");
const data = await loader.pull("en");
expect(data).toEqual({ greeting: "Hello!" });
});
it("xcode-xcstrings-v2: should omit ignored string keys on pull", async () => {
const input = dedent`
{
"sourceLanguage": "en",
"strings": {
"hello": {
"extractionState": "manual",
"localizations": {
"en": { "stringUnit": { "state": "translated", "value": "Hello" } }
}
},
"world": {
"extractionState": "manual",
"localizations": {
"en": { "stringUnit": { "state": "translated", "value": "World" } }
}
}
}
}
`;
mockFileOperations(input);
const loader = createBucketLoader(
"xcode-xcstrings-v2",
"i18n/[locale].xcstrings",
{ defaultLocale: "en" },
[],
[],
["world"],
);
loader.setDefaultLocale("en");
const data = await loader.pull("en");
expect(Object.keys(data)).toContain("hello");
expect(Object.keys(data)).not.toContain("world");
});
it("yaml: should omit ignored keys on pull", async () => {
const input = dedent`
title: Submit
description: Desc
`;
mockFileOperations(input);
const loader = createBucketLoader(
"yaml",
"i18n/[locale].yml",
{ defaultLocale: "en" },
[],
[],
["description"],
);
loader.setDefaultLocale("en");
const data = await loader.pull("en");
expect(data).toEqual({ title: "Submit" });
});
it("yaml-root-key: should omit ignored keys on pull", async () => {
const input = dedent`
en:
title: Submit
description: Desc
`;
mockFileOperations(input);
const loader = createBucketLoader(
"yaml-root-key",
"i18n/[locale].yml",
{ defaultLocale: "en" },
[],
[],
["description"],
);
loader.setDefaultLocale("en");
const data = await loader.pull("en");
expect(data).toEqual({ title: "Submit" });
});
it("flutter: should omit ignored keys on pull", async () => {
const input = JSON.stringify(
{
"@@locale": "en",
greeting: "Hello, {name}!",
"@greeting": { description: "d" },
farewell: "Goodbye!",
},
null,
2,
);
mockFileOperations(input);
const loader = createBucketLoader(
"flutter",
"lib/l10n/app_[locale].arb",
{ defaultLocale: "en" },
[],
[],
["farewell"],
);
loader.setDefaultLocale("en");
const data = await loader.pull("en");
expect(Object.keys(data)).toContain("greeting");
expect(Object.keys(data)).not.toContain("farewell");
});
it("xliff: should omit ignored keys on pull", async () => {
const input = dedent`
<?xml version="1.0" encoding="utf-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
<file original="" source-language="en" datatype="plaintext">
<body>
<trans-unit id="greeting" resname="greeting"><source>Hello</source></trans-unit>
<trans-unit id="farewell" resname="farewell"><source>Goodbye</source></trans-unit>
</body>
</file>
</xliff>
`;
mockFileOperations(input);
const loader = createBucketLoader(
"xliff",
"i18n/[locale].xliff",
{ defaultLocale: "en" },
[],
[],
["farewell"],
);
loader.setDefaultLocale("en");
const data = await loader.pull("en");
expect(Object.keys(data)).toContain("greeting");
expect(Object.keys(data)).not.toContain("farewell");
});
it("xml: should omit ignored keys on pull", async () => {
const input = `<root><title>Hello</title><description>Desc</description></root>`;
mockFileOperations(input);
const loader = createBucketLoader(
"xml",
"i18n/[locale].xml",
{ defaultLocale: "en" },
[],
[],
["root/description"],
);
loader.setDefaultLocale("en");
const data = await loader.pull("en");
expect(Object.keys(data)).toContain("root/title");
expect(Object.keys(data)).not.toContain("root/description");
});
it("srt: should omit ignored keys on pull", async () => {
const input = dedent`
1
00:00:01,000 --> 00:00:04,000
Hello
2
00:00:05,000 --> 00:00:06,000
World
`;
mockFileOperations(input);
const loader = createBucketLoader(
"srt",
"i18n/[locale].srt",
{ defaultLocale: "en" },
[],
[],
["1#*"],
);
loader.setDefaultLocale("en");
const data = await loader.pull("en");
// Expect only entry 2 remains
const keys = Object.keys(data);
expect(keys.length).toBe(1);
expect(keys[0].startsWith("2#")).toBe(true);
});
it("vtt: should omit ignored keys on pull", async () => {
const input = dedent`
WEBVTT
00:00:00.000 --> 00:00:02.000
First
00:00:02.000 --> 00:00:04.000
Second
`;
mockFileOperations(input);
const loader = createBucketLoader(
"vtt",
"i18n/[locale].vtt",
{ defaultLocale: "en" },
[],
[],
["0#*"],
);
loader.setDefaultLocale("en");
const data = await loader.pull("en");
// One cue should be filtered
expect(Object.keys(data).length).toBe(1);
});
it("php: should omit ignored keys on pull", async () => {
const input = dedent`
<?php
return [
'title' => 'Submit',
'description' => 'Desc',
];
`;
mockFileOperations(input);
const loader = createBucketLoader(
"php",
"i18n/[locale].php",
{ defaultLocale: "en" },
[],
[],
["description"],
);
loader.setDefaultLocale("en");
const data = await loader.pull("en");
expect(data).toEqual({ title: "Submit" });
});
it("vue-json: should omit ignored keys on pull", async () => {
const input = dedent`
<template></template>
<i18n>
{"en": {"title": "Hello", "description": "Desc"}}
</i18n>
<script setup></script>
`;
mockFileOperations(input);
const loader = createBucketLoader(
"vue-json",
"i18n/App.vue",
{ defaultLocale: "en" },
[],
[],
["description"],
);
loader.setDefaultLocale("en");
const data = await loader.pull("en");
expect(data).toEqual({ title: "Hello" });
});
it("typescript: should omit ignored keys on pull", async () => {
const input = dedent`
export default {
title: "Submit",
description: "Desc"
};
`;
mockFileOperations(input);
const loader = createBucketLoader(
"typescript",
"i18n/[locale].ts",
{ defaultLocale: "en" },
[],
[],
["description"],
);
loader.setDefaultLocale("en");
const data = await loader.pull("en");
expect(data).toEqual({ title: "Submit" });
});
it("txt: should omit ignored keys on pull", async () => {
const input = dedent`
First line
Second line
`;
mockFileOperations(input);
const loader = createBucketLoader(
"txt",
"fastlane/metadata/[locale]/description.txt",
{ defaultLocale: "en" },
[],
[],
["1"],
);
loader.setDefaultLocale("en");
const data = await loader.pull("en");
expect(Object.keys(data)).toEqual(["2"]);
});
it("json-dictionary: should omit ignored keys on pull (wildcard)", async () => {
const input = JSON.stringify(
{
title: { en: "Title" },
pages: [
{
elements: [
{ title: { en: "E1" }, description: { en: "D1" } },
{ title: { en: "E2" }, description: { en: "D2" } },
],
},
],
},
null,
2,
);
mockFileOperations(input);
const loader = createBucketLoader(
"json-dictionary",
"i18n/[locale].json",
{ defaultLocale: "en" },
[],
[],
["pages/*/elements/*/description"],
);
loader.setDefaultLocale("en");
const data = await loader.pull("en");
const keys = Object.keys(data);
expect(keys).toContain("title");
expect(keys).toContain("pages/0/elements/0/title");
expect(keys.find((k) => k.includes("/description"))).toBeUndefined();
});
});
function setupFileMocks() {
vi.mock("fs/promises", () => ({
default: {
readFile: vi.fn(),
writeFile: vi.fn(),
mkdir: vi.fn(),
access: vi.fn(),
},
}));
vi.mock("path", () => ({
default: {
resolve: vi.fn((path) => path),
dirname: vi.fn((path) => path.split("/").slice(0, -1).join("/")),
},
}));
}
function mockFileOperations(input: string) {
(fs.access as any).mockImplementation(() => Promise.resolve());
(fs.readFile as any).mockImplementation(() => Promise.resolve(input));
(fs.writeFile as any).mockImplementation(() => Promise.resolve());
}
```
--------------------------------------------------------------------------------
/packages/cli/src/cli/loaders/xcode-xcstrings.spec.ts:
--------------------------------------------------------------------------------
```typescript
import { describe, it, expect } from "vitest";
import createXcodeXcstringsLoader, { _removeLocale } from "./xcode-xcstrings";
describe("loaders/xcode-xcstrings", () => {
const defaultLocale = "en";
const mockInput = {
sourceLanguage: "en",
strings: {
"app.title": {
localizations: {
en: {
stringUnit: {
state: "translated",
value: "My App",
},
},
es: {
stringUnit: {
state: "translated",
value: "Mi App",
},
},
},
},
"items.count": {
localizations: {
en: {
variations: {
plural: {
one: {
stringUnit: {
state: "translated",
value: "1 item",
},
},
other: {
stringUnit: {
state: "translated",
value: "%d items",
},
},
},
},
},
es: {
variations: {
plural: {
one: {
stringUnit: {
state: "translated",
value: "1 artículo",
},
},
other: {
stringUnit: {
state: "translated",
value: "%d artículos",
},
},
},
},
},
},
},
"key.no-translate": {
shouldTranslate: false,
localizations: {
en: {
stringUnit: {
state: "translated",
value: "Do not translate",
},
},
},
},
"key.source-only": {
localizations: {},
},
"key.missing-localization": {
localizations: {
es: {
stringUnit: {
state: "translated",
value: "solo español",
},
},
},
},
},
version: "1.0",
};
describe("pull", () => {
it("should pull simple string translations for a given locale", async () => {
const loader = createXcodeXcstringsLoader(defaultLocale);
loader.setDefaultLocale(defaultLocale);
await loader.pull(defaultLocale, mockInput);
const result = await loader.pull("es", mockInput);
expect(result).toEqual({
"app.title": "Mi App",
"items.count": {
one: "1 artículo",
other: "%d artículos",
},
"key.missing-localization": "solo español",
});
});
it("should pull plural translations for a given locale", async () => {
const loader = createXcodeXcstringsLoader(defaultLocale);
loader.setDefaultLocale(defaultLocale);
const result = await loader.pull("en", mockInput);
expect(result["items.count"]).toEqual({
one: "1 item",
other: "%d items",
});
});
it("should use the key as value for the source language if no translation is available", async () => {
const loader = createXcodeXcstringsLoader(defaultLocale);
loader.setDefaultLocale(defaultLocale);
const result = await loader.pull("en", mockInput);
expect(result["key.source-only"]).toBe("key.source-only");
expect(result["key.missing-localization"]).toBe(
"key.missing-localization",
);
});
it("should not use key as value if not source language", async () => {
const loader = createXcodeXcstringsLoader(defaultLocale);
loader.setDefaultLocale(defaultLocale);
await loader.pull(defaultLocale, mockInput);
const result = await loader.pull("es", mockInput);
expect(result["key.source-only"]).toBeUndefined();
});
it("should skip keys marked with shouldTranslate: false", async () => {
const loader = createXcodeXcstringsLoader(defaultLocale);
loader.setDefaultLocale(defaultLocale);
const result = await loader.pull("en", mockInput);
expect(result["key.no-translate"]).toBeUndefined();
});
it("should return an empty object for a locale with no translations", async () => {
const loader = createXcodeXcstringsLoader(defaultLocale);
loader.setDefaultLocale(defaultLocale);
await loader.pull(defaultLocale, mockInput);
const result = await loader.pull("fr", mockInput);
expect(result).toEqual({});
});
});
describe("push", () => {
it("should push simple string translations", async () => {
const loader = createXcodeXcstringsLoader(defaultLocale);
loader.setDefaultLocale(defaultLocale);
await loader.pull(defaultLocale, mockInput);
const payload = {
"app.title": "Mon App",
};
const result = await loader.push("fr", payload);
expect(result).not.toBeNull();
expect(result!.version).toBe("1.0");
expect(result!.strings["app.title"].localizations.fr).toEqual({
stringUnit: {
state: "translated",
value: "Mon App",
},
});
});
it("should push plural translations in plain object format", async () => {
const loader = createXcodeXcstringsLoader(defaultLocale);
loader.setDefaultLocale(defaultLocale);
await loader.pull(defaultLocale, mockInput);
const payload = {
"items.count": {
one: "1 article",
other: "%d articles",
},
};
const result = await loader.push("fr", payload);
expect(result).not.toBeNull();
expect(result!.strings["items.count"].localizations.fr).toEqual({
variations: {
plural: {
one: {
stringUnit: {
state: "translated",
value: "1 article",
},
},
other: {
stringUnit: {
state: "translated",
value: "%d articles",
},
},
},
},
});
});
it("should merge translations into existing input", async () => {
const loader = createXcodeXcstringsLoader(defaultLocale);
loader.setDefaultLocale(defaultLocale);
await loader.pull(defaultLocale, mockInput);
const payload = {
"app.title": "Mi App (actualizado)",
};
const result = await loader.push("es", payload);
expect(result).not.toBeNull();
// check new value
expect(
result!.strings["app.title"].localizations.es.stringUnit.value,
).toBe("Mi App (actualizado)");
// check existing value is untouched
expect(
result!.strings["app.title"].localizations.en.stringUnit.value,
).toBe("My App");
});
it("should preserve the shouldTranslate: false flag", async () => {
const loader = createXcodeXcstringsLoader(defaultLocale);
loader.setDefaultLocale(defaultLocale);
await loader.pull(defaultLocale, mockInput);
const payload = {
"key.no-translate": "Ne pas traduire",
};
const result = await loader.push("fr", payload);
expect(result).not.toBeNull();
expect(result!.strings["key.no-translate"].shouldTranslate).toBe(false);
expect(
result!.strings["key.no-translate"].localizations.fr.stringUnit.value,
).toBe("Ne pas traduire");
});
it("should handle pushing to a null or undefined originalInput", async () => {
const loader = createXcodeXcstringsLoader(defaultLocale);
loader.setDefaultLocale(defaultLocale);
await loader.pull(defaultLocale, { strings: {} });
const payload = {
greeting: "Hello",
};
const result = await loader.push("en", payload);
expect(result).toEqual({
strings: {
greeting: {
localizations: {
en: {
stringUnit: {
state: "translated",
value: "Hello",
},
},
},
},
},
});
});
it("should skip null and undefined values in payload", async () => {
const loader = createXcodeXcstringsLoader(defaultLocale);
loader.setDefaultLocale(defaultLocale);
await loader.pull(defaultLocale, mockInput);
const payload = {
"app.title": "new title",
"key.null": null,
"key.undefined": undefined,
};
const result = await loader.push("en", payload);
expect(result).not.toBeNull();
expect(Object.keys(result!.strings)).not.toContain("key.null");
expect(Object.keys(result!.strings)).not.toContain("key.undefined");
expect(
result!.strings["app.title"].localizations.en.stringUnit.value,
).toBe("new title");
});
it("should remove the pushed locale from original input", async () => {
const loader = createXcodeXcstringsLoader(defaultLocale);
loader.setDefaultLocale(defaultLocale);
await loader.pull(defaultLocale, mockInput);
const payload = {
"app.title": "new title",
};
const result = await loader.push("en", payload);
expect(result).not.toBeNull();
expect(result!.strings["app.title"].localizations.en.stringUnit).toEqual({
state: "translated",
value: "new title",
});
expect(result!.strings["items.count"].localizations.en).toBeUndefined();
expect(
result!.strings["key.no-translate"].localizations.en,
).toBeUndefined();
expect(
result!.strings["key.source-only"].localizations.en,
).toBeUndefined();
expect(
result!.strings["key.missing-localization"].localizations.en,
).toBeUndefined();
});
});
describe("_removeLocale", () => {
it("should remove the locale from the input", () => {
const input = {
sourceLanguage: "en",
strings: {
key1: {
localizations: {
en: { stringUnit: { state: "translated", value: "Hello" } },
es: { stringUnit: { state: "translated", value: "Hola" } },
},
},
key2: {
localizations: {
en: { stringUnit: { state: "translated", value: "World" } },
fr: { stringUnit: { state: "translated", value: "Monde" } },
},
},
key3: {
localizations: {
en: {
variations: {
plural: {
one: {
stringUnit: { state: "translated", value: "1 item" },
},
},
},
},
fr: {
variations: {
plural: {
one: {
stringUnit: { state: "translated", value: "1 article" },
},
},
},
},
},
},
},
};
const result = _removeLocale(input, "en");
expect(result).toEqual({
sourceLanguage: "en",
strings: {
key1: {
localizations: {
es: { stringUnit: { state: "translated", value: "Hola" } },
},
},
key2: {
localizations: {
fr: { stringUnit: { state: "translated", value: "Monde" } },
},
},
key3: {
localizations: {
fr: {
variations: {
plural: {
one: {
stringUnit: { state: "translated", value: "1 article" },
},
},
},
},
},
},
},
});
});
it("should do nothing if the locale does not exist", () => {
const input = {
sourceLanguage: "en",
strings: {
key1: {
localizations: {
en: { stringUnit: { state: "translated", value: "Hello" } },
es: { stringUnit: { state: "translated", value: "Hola" } },
},
},
},
};
const result = _removeLocale(input, "fr");
expect(result).toEqual({
sourceLanguage: "en",
strings: {
key1: {
localizations: {
en: { stringUnit: { state: "translated", value: "Hello" } },
es: { stringUnit: { state: "translated", value: "Hola" } },
},
},
},
});
});
it("should handle empty strings object", () => {
const input = {
sourceLanguage: "en",
strings: {},
};
const result = _removeLocale(input, "en");
expect(result).toEqual({
sourceLanguage: "en",
strings: {},
});
});
it("should handle keys with no localizations", () => {
const input = {
sourceLanguage: "en",
strings: {
key1: {
localizations: {},
},
},
};
const result = _removeLocale(input, "en");
expect(result).toEqual({
sourceLanguage: "en",
strings: {
key1: {
localizations: {},
},
},
});
});
});
describe("pullHints", () => {
it("should extract comments from xcstrings format", async () => {
const inputWithComments = {
sourceLanguage: "en",
strings: {
welcome_message: {
comment: "Greeting shown on the main screen",
extractionState: "manual",
localizations: {
en: {
stringUnit: {
state: "translated",
value: "Welcome!",
},
},
},
},
user_count: {
comment: "Number of active users",
extractionState: "manual",
localizations: {
en: {
variations: {
plural: {
one: {
stringUnit: {
state: "translated",
value: "1 user",
},
},
other: {
stringUnit: {
state: "translated",
value: "%d users",
},
},
},
},
},
},
},
no_comment_key: {
extractionState: "manual",
localizations: {
en: {
stringUnit: {
state: "translated",
value: "No comment",
},
},
},
},
},
};
const loader = createXcodeXcstringsLoader(defaultLocale);
loader.setDefaultLocale(defaultLocale);
await loader.pull(defaultLocale, inputWithComments);
const hints = await loader.pullHints(inputWithComments);
expect(hints).toEqual({
welcome_message: { hint: "Greeting shown on the main screen" },
user_count: { hint: "Number of active users" },
"user_count/one": { hint: "Number of active users" },
"user_count/other": { hint: "Number of active users" },
});
});
it("should handle empty input", async () => {
const loader = createXcodeXcstringsLoader(defaultLocale);
loader.setDefaultLocale(defaultLocale);
const hints1 = await loader.pullHints({});
expect(hints1).toEqual({});
const hints2 = await loader.pullHints(null as any);
expect(hints2).toEqual({});
const hints3 = await loader.pullHints(undefined as any);
expect(hints3).toEqual({});
});
it("should handle xcstrings without comments", async () => {
const loader = createXcodeXcstringsLoader(defaultLocale);
loader.setDefaultLocale(defaultLocale);
await loader.pull(defaultLocale, mockInput);
const hints = await loader.pullHints(mockInput);
expect(hints).toEqual({});
});
it("should handle strings with only some having comments", async () => {
const inputWithMixedComments = {
sourceLanguage: "en",
strings: {
with_comment: {
comment: "This has a comment",
localizations: {
en: {
stringUnit: {
state: "translated",
value: "Value with comment",
},
},
},
},
without_comment: {
localizations: {
en: {
stringUnit: {
state: "translated",
value: "Value without comment",
},
},
},
},
},
};
const loader = createXcodeXcstringsLoader(defaultLocale);
loader.setDefaultLocale(defaultLocale);
await loader.pull(defaultLocale, inputWithMixedComments);
const hints = await loader.pullHints(inputWithMixedComments);
expect(hints).toEqual({
with_comment: { hint: "This has a comment" },
});
});
it("should handle multiple locales with same comment", async () => {
const inputWithMultipleLocales = {
sourceLanguage: "en",
strings: {
multi_locale: {
comment: "Available in multiple languages",
localizations: {
en: {
stringUnit: {
state: "translated",
value: "English",
},
},
es: {
stringUnit: {
state: "translated",
value: "Español",
},
},
fr: {
variations: {
plural: {
one: {
stringUnit: {
state: "translated",
value: "1 français",
},
},
other: {
stringUnit: {
state: "translated",
value: "%d français",
},
},
},
},
},
},
},
},
};
const loader = createXcodeXcstringsLoader(defaultLocale);
loader.setDefaultLocale(defaultLocale);
await loader.pull(defaultLocale, inputWithMultipleLocales);
const hints = await loader.pullHints(inputWithMultipleLocales);
expect(hints).toEqual({
multi_locale: { hint: "Available in multiple languages" },
"multi_locale/one": { hint: "Available in multiple languages" },
"multi_locale/other": { hint: "Available in multiple languages" },
});
});
});
});
```