#
tokens: 48602/50000 14/626 files (page 10/20)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 10 of 20. Use http://codebase.md/lingodotdev/lingo.dev?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .changeset
│   ├── config.json
│   └── README.md
├── .claude
│   ├── agents
│   │   └── code-architect-reviewer.md
│   └── commands
│       ├── analyze-bucket-type.md
│       └── create-bucket-docs.md
├── .editorconfig
├── .github
│   ├── dependabot.yml
│   └── workflows
│       ├── docker.yml
│       ├── lingodotdev.yml
│       ├── pr-check.yml
│       ├── pr-lint.yml
│       └── release.yml
├── .gitignore
├── .husky
│   └── commit-msg
├── .npmrc
├── .prettierignore
├── .prettierrc
├── .vscode
│   ├── extensions.json
│   ├── launch.json
│   └── settings.json
├── action.yml
├── CLAUDE.md
├── CODE_OF_CONDUCT.md
├── commitlint.config.js
├── composer.json
├── content
│   ├── banner.compiler.png
│   ├── banner.dark.png
│   └── banner.launch.png
├── CONTRIBUTING.md
├── DEBUGGING.md
├── demo
│   ├── adonisjs
│   │   ├── .editorconfig
│   │   ├── .env.example
│   │   ├── .gitignore
│   │   ├── ace.js
│   │   ├── adonisrc.ts
│   │   ├── app
│   │   │   ├── exceptions
│   │   │   │   └── handler.ts
│   │   │   └── middleware
│   │   │       └── container_bindings_middleware.ts
│   │   ├── bin
│   │   │   ├── console.ts
│   │   │   ├── server.ts
│   │   │   └── test.ts
│   │   ├── CHANGELOG.md
│   │   ├── config
│   │   │   ├── app.ts
│   │   │   ├── bodyparser.ts
│   │   │   ├── cors.ts
│   │   │   ├── hash.ts
│   │   │   ├── inertia.ts
│   │   │   ├── logger.ts
│   │   │   ├── session.ts
│   │   │   ├── shield.ts
│   │   │   ├── static.ts
│   │   │   └── vite.ts
│   │   ├── eslint.config.js
│   │   ├── inertia
│   │   │   ├── app
│   │   │   │   ├── app.tsx
│   │   │   │   └── ssr.tsx
│   │   │   ├── css
│   │   │   │   └── app.css
│   │   │   ├── lingo
│   │   │   │   ├── dictionary.js
│   │   │   │   └── meta.json
│   │   │   ├── pages
│   │   │   │   ├── errors
│   │   │   │   │   ├── not_found.tsx
│   │   │   │   │   └── server_error.tsx
│   │   │   │   └── home.tsx
│   │   │   └── tsconfig.json
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── resources
│   │   │   └── views
│   │   │       └── inertia_layout.edge
│   │   ├── start
│   │   │   ├── env.ts
│   │   │   ├── kernel.ts
│   │   │   └── routes.ts
│   │   ├── tests
│   │   │   └── bootstrap.ts
│   │   ├── tsconfig.json
│   │   └── vite.config.ts
│   ├── next-app
│   │   ├── .gitignore
│   │   ├── CHANGELOG.md
│   │   ├── eslint.config.mjs
│   │   ├── next.config.ts
│   │   ├── package.json
│   │   ├── postcss.config.mjs
│   │   ├── public
│   │   │   ├── file.svg
│   │   │   ├── globe.svg
│   │   │   ├── next.svg
│   │   │   ├── vercel.svg
│   │   │   └── window.svg
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── app
│   │   │   │   ├── client-component.tsx
│   │   │   │   ├── favicon.ico
│   │   │   │   ├── globals.css
│   │   │   │   ├── layout.tsx
│   │   │   │   ├── lingo-dot-dev.tsx
│   │   │   │   ├── page.tsx
│   │   │   │   └── test
│   │   │   │       └── page.tsx
│   │   │   ├── components
│   │   │   │   ├── hero-actions.tsx
│   │   │   │   ├── hero-subtitle.tsx
│   │   │   │   ├── hero-title.tsx
│   │   │   │   └── index.ts
│   │   │   └── lingo
│   │   │       ├── dictionary.js
│   │   │       └── meta.json
│   │   └── tsconfig.json
│   ├── react-router-app
│   │   ├── .dockerignore
│   │   ├── .gitignore
│   │   ├── app
│   │   │   ├── app.css
│   │   │   ├── lingo
│   │   │   │   ├── dictionary.js
│   │   │   │   └── meta.json
│   │   │   ├── root.tsx
│   │   │   ├── routes
│   │   │   │   ├── home.tsx
│   │   │   │   └── test.tsx
│   │   │   ├── routes.ts
│   │   │   └── welcome
│   │   │       ├── lingo-dot-dev.tsx
│   │   │       ├── logo-dark.svg
│   │   │       ├── logo-light.svg
│   │   │       └── welcome.tsx
│   │   ├── Dockerfile
│   │   ├── package.json
│   │   ├── public
│   │   │   └── favicon.ico
│   │   ├── react-router.config.ts
│   │   ├── README.md
│   │   ├── tsconfig.json
│   │   └── vite.config.ts
│   └── vite-project
│       ├── .gitignore
│       ├── CHANGELOG.md
│       ├── eslint.config.js
│       ├── index.html
│       ├── package.json
│       ├── public
│       │   └── vite.svg
│       ├── README.md
│       ├── src
│       │   ├── App.css
│       │   ├── App.tsx
│       │   ├── assets
│       │   │   └── react.svg
│       │   ├── components
│       │   │   └── test.tsx
│       │   ├── index.css
│       │   ├── lingo
│       │   │   ├── dictionary.js
│       │   │   └── meta.json
│       │   ├── lingo-dot-dev.tsx
│       │   ├── main.tsx
│       │   └── vite-env.d.ts
│       ├── tsconfig.app.json
│       ├── tsconfig.json
│       ├── tsconfig.node.json
│       └── vite.config.ts
├── Dockerfile
├── i18n.json
├── i18n.lock
├── integrations
│   └── directus
│       ├── .gitignore
│       ├── CHANGELOG.md
│       ├── docker-compose.yml
│       ├── Dockerfile
│       ├── package.json
│       ├── README.md
│       ├── src
│       │   ├── api.ts
│       │   ├── app.ts
│       │   └── index.spec.ts
│       ├── tsconfig.json
│       ├── tsconfig.test.json
│       └── tsup.config.ts
├── ISSUE_TEMPLATE.md
├── legacy
│   ├── cli
│   │   ├── bin
│   │   │   └── cli.mjs
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   └── readme.md
│   └── sdk
│       ├── CHANGELOG.md
│       ├── index.d.ts
│       ├── index.js
│       ├── package.json
│       └── README.md
├── LICENSE.md
├── mcp.md
├── package.json
├── packages
│   ├── cli
│   │   ├── assets
│   │   │   ├── failure.mp3
│   │   │   └── success.mp3
│   │   ├── bin
│   │   │   └── cli.mjs
│   │   ├── CHANGELOG.md
│   │   ├── demo
│   │   │   ├── android
│   │   │   │   ├── en
│   │   │   │   │   └── example.xml
│   │   │   │   ├── es
│   │   │   │   │   └── example.xml
│   │   │   │   ├── i18n.json
│   │   │   │   └── i18n.lock
│   │   │   ├── csv
│   │   │   │   ├── example.csv
│   │   │   │   ├── i18n.json
│   │   │   │   └── i18n.lock
│   │   │   ├── demo.spec.ts
│   │   │   ├── ejs
│   │   │   │   ├── en
│   │   │   │   │   └── example.ejs
│   │   │   │   ├── es
│   │   │   │   │   └── example.ejs
│   │   │   │   ├── i18n.json
│   │   │   │   └── i18n.lock
│   │   │   ├── flutter
│   │   │   │   ├── en
│   │   │   │   │   └── example.arb
│   │   │   │   ├── es
│   │   │   │   │   └── example.arb
│   │   │   │   ├── i18n.json
│   │   │   │   └── i18n.lock
│   │   │   ├── html
│   │   │   │   ├── en
│   │   │   │   │   └── example.html
│   │   │   │   ├── es
│   │   │   │   │   └── example.html
│   │   │   │   ├── i18n.json
│   │   │   │   └── i18n.lock
│   │   │   ├── json
│   │   │   │   ├── en
│   │   │   │   │   └── example.json
│   │   │   │   ├── es
│   │   │   │   │   └── example.json
│   │   │   │   ├── i18n.json
│   │   │   │   └── i18n.lock
│   │   │   ├── json-dictionary
│   │   │   │   ├── example.json
│   │   │   │   ├── i18n.json
│   │   │   │   └── i18n.lock
│   │   │   ├── json5
│   │   │   │   ├── en
│   │   │   │   │   └── example.json5
│   │   │   │   ├── es
│   │   │   │   │   └── example.json5
│   │   │   │   ├── i18n.json
│   │   │   │   └── i18n.lock
│   │   │   ├── jsonc
│   │   │   │   ├── en
│   │   │   │   │   └── example.jsonc
│   │   │   │   ├── es
│   │   │   │   │   └── example.jsonc
│   │   │   │   ├── i18n.json
│   │   │   │   ├── i18n.lock
│   │   │   │   └── ru
│   │   │   │       └── example.jsonc
│   │   │   ├── markdoc
│   │   │   │   ├── en
│   │   │   │   │   └── example.markdoc
│   │   │   │   ├── es
│   │   │   │   │   └── example.markdoc
│   │   │   │   ├── i18n.json
│   │   │   │   └── i18n.lock
│   │   │   ├── markdown
│   │   │   │   ├── en
│   │   │   │   │   └── example.md
│   │   │   │   ├── es
│   │   │   │   │   └── example.md
│   │   │   │   ├── i18n.json
│   │   │   │   └── i18n.lock
│   │   │   ├── mdx
│   │   │   │   ├── en
│   │   │   │   │   └── example.mdx
│   │   │   │   ├── es
│   │   │   │   │   └── example.mdx
│   │   │   │   ├── i18n.json
│   │   │   │   └── i18n.lock
│   │   │   ├── php
│   │   │   │   ├── en
│   │   │   │   │   └── example.php
│   │   │   │   ├── es
│   │   │   │   │   └── example.php
│   │   │   │   ├── i18n.json
│   │   │   │   └── i18n.lock
│   │   │   ├── po
│   │   │   │   ├── en
│   │   │   │   │   └── example.po
│   │   │   │   ├── es
│   │   │   │   │   └── example.po
│   │   │   │   ├── i18n.json
│   │   │   │   └── i18n.lock
│   │   │   ├── properties
│   │   │   │   ├── en
│   │   │   │   │   └── example.properties
│   │   │   │   ├── es
│   │   │   │   │   └── example.properties
│   │   │   │   ├── i18n.json
│   │   │   │   └── i18n.lock
│   │   │   ├── run_i18n.sh
│   │   │   ├── srt
│   │   │   │   ├── en
│   │   │   │   │   └── example.srt
│   │   │   │   ├── es
│   │   │   │   │   └── example.srt
│   │   │   │   ├── i18n.json
│   │   │   │   └── i18n.lock
│   │   │   ├── txt
│   │   │   │   ├── en
│   │   │   │   │   └── example.txt
│   │   │   │   ├── es
│   │   │   │   │   └── example.txt
│   │   │   │   ├── i18n.json
│   │   │   │   └── i18n.lock
│   │   │   ├── typescript
│   │   │   │   ├── en
│   │   │   │   │   └── example.ts
│   │   │   │   ├── es
│   │   │   │   │   └── example.ts
│   │   │   │   ├── i18n.json
│   │   │   │   └── i18n.lock
│   │   │   ├── vtt
│   │   │   │   ├── en
│   │   │   │   │   └── example.vtt
│   │   │   │   ├── es
│   │   │   │   │   └── example.vtt
│   │   │   │   ├── i18n.json
│   │   │   │   └── i18n.lock
│   │   │   ├── vue-json
│   │   │   │   ├── example.vue
│   │   │   │   ├── i18n.json
│   │   │   │   └── i18n.lock
│   │   │   ├── xcode-strings
│   │   │   │   ├── en
│   │   │   │   │   └── example.strings
│   │   │   │   ├── es
│   │   │   │   │   └── example.strings
│   │   │   │   ├── i18n.json
│   │   │   │   └── i18n.lock
│   │   │   ├── xcode-stringsdict
│   │   │   │   ├── en
│   │   │   │   │   └── example.stringsdict
│   │   │   │   ├── es
│   │   │   │   │   └── example.stringsdict
│   │   │   │   ├── i18n.json
│   │   │   │   └── i18n.lock
│   │   │   ├── xcode-xcstrings
│   │   │   │   ├── example.xcstrings
│   │   │   │   ├── i18n.json
│   │   │   │   └── i18n.lock
│   │   │   ├── xcode-xcstrings-v2
│   │   │   │   ├── complex-example.xcstrings
│   │   │   │   ├── example.xcstrings
│   │   │   │   ├── i18n.json
│   │   │   │   └── i18n.lock
│   │   │   ├── xliff
│   │   │   │   ├── en
│   │   │   │   │   ├── example-v1.2.xliff
│   │   │   │   │   └── example-v2.xliff
│   │   │   │   ├── es
│   │   │   │   │   ├── example-v1.2.xliff
│   │   │   │   │   ├── example-v2.xliff
│   │   │   │   │   └── example.xliff
│   │   │   │   ├── i18n.json
│   │   │   │   └── i18n.lock
│   │   │   ├── xml
│   │   │   │   ├── en
│   │   │   │   │   └── example.xml
│   │   │   │   ├── es
│   │   │   │   │   └── example.xml
│   │   │   │   ├── i18n.json
│   │   │   │   └── i18n.lock
│   │   │   ├── yaml
│   │   │   │   ├── en
│   │   │   │   │   └── example.yml
│   │   │   │   ├── es
│   │   │   │   │   └── example.yml
│   │   │   │   ├── i18n.json
│   │   │   │   └── i18n.lock
│   │   │   └── yaml-root-key
│   │   │       ├── en
│   │   │       │   └── example.yml
│   │   │       ├── es
│   │   │       │   └── example.yml
│   │   │       ├── i18n.json
│   │   │       └── i18n.lock
│   │   ├── i18n.json
│   │   ├── i18n.lock
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── cli
│   │   │   │   ├── cmd
│   │   │   │   │   ├── auth.ts
│   │   │   │   │   ├── ci
│   │   │   │   │   │   ├── flows
│   │   │   │   │   │   │   ├── _base.ts
│   │   │   │   │   │   │   ├── in-branch.ts
│   │   │   │   │   │   │   └── pull-request.ts
│   │   │   │   │   │   ├── index.ts
│   │   │   │   │   │   └── platforms
│   │   │   │   │   │       ├── _base.ts
│   │   │   │   │   │       ├── bitbucket.ts
│   │   │   │   │   │       ├── github.ts
│   │   │   │   │   │       ├── gitlab.ts
│   │   │   │   │   │       └── index.ts
│   │   │   │   │   ├── cleanup.ts
│   │   │   │   │   ├── config
│   │   │   │   │   │   ├── get.ts
│   │   │   │   │   │   ├── index.ts
│   │   │   │   │   │   ├── set.ts
│   │   │   │   │   │   └── unset.ts
│   │   │   │   │   ├── i18n.ts
│   │   │   │   │   ├── init.ts
│   │   │   │   │   ├── lockfile.ts
│   │   │   │   │   ├── login.ts
│   │   │   │   │   ├── logout.ts
│   │   │   │   │   ├── may-the-fourth.ts
│   │   │   │   │   ├── mcp.ts
│   │   │   │   │   ├── purge.ts
│   │   │   │   │   ├── run
│   │   │   │   │   │   ├── _const.ts
│   │   │   │   │   │   ├── _types.ts
│   │   │   │   │   │   ├── _utils.ts
│   │   │   │   │   │   ├── execute.spec.ts
│   │   │   │   │   │   ├── execute.ts
│   │   │   │   │   │   ├── frozen.ts
│   │   │   │   │   │   ├── index.ts
│   │   │   │   │   │   ├── plan.ts
│   │   │   │   │   │   ├── setup.ts
│   │   │   │   │   │   └── watch.ts
│   │   │   │   │   ├── show
│   │   │   │   │   │   ├── _shared-key-command.ts
│   │   │   │   │   │   ├── config.ts
│   │   │   │   │   │   ├── files.ts
│   │   │   │   │   │   ├── ignored-keys.ts
│   │   │   │   │   │   ├── index.ts
│   │   │   │   │   │   ├── locale.ts
│   │   │   │   │   │   └── locked-keys.ts
│   │   │   │   │   └── status.ts
│   │   │   │   ├── constants.ts
│   │   │   │   ├── index.spec.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── loaders
│   │   │   │   │   ├── _types.ts
│   │   │   │   │   ├── _utils.ts
│   │   │   │   │   ├── android.spec.ts
│   │   │   │   │   ├── android.ts
│   │   │   │   │   ├── csv.spec.ts
│   │   │   │   │   ├── csv.ts
│   │   │   │   │   ├── dato
│   │   │   │   │   │   ├── _base.ts
│   │   │   │   │   │   ├── _utils.ts
│   │   │   │   │   │   ├── api.ts
│   │   │   │   │   │   ├── extract.ts
│   │   │   │   │   │   ├── filter.ts
│   │   │   │   │   │   └── index.ts
│   │   │   │   │   ├── ejs.spec.ts
│   │   │   │   │   ├── ejs.ts
│   │   │   │   │   ├── ensure-key-order.spec.ts
│   │   │   │   │   ├── ensure-key-order.ts
│   │   │   │   │   ├── flat.spec.ts
│   │   │   │   │   ├── flat.ts
│   │   │   │   │   ├── flutter.spec.ts
│   │   │   │   │   ├── flutter.ts
│   │   │   │   │   ├── formatters
│   │   │   │   │   │   ├── _base.ts
│   │   │   │   │   │   ├── biome.ts
│   │   │   │   │   │   ├── index.ts
│   │   │   │   │   │   └── prettier.ts
│   │   │   │   │   ├── html.ts
│   │   │   │   │   ├── icu-safety.spec.ts
│   │   │   │   │   ├── ignored-keys-buckets.spec.ts
│   │   │   │   │   ├── ignored-keys.spec.ts
│   │   │   │   │   ├── ignored-keys.ts
│   │   │   │   │   ├── index.spec.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── inject-locale.spec.ts
│   │   │   │   │   ├── inject-locale.ts
│   │   │   │   │   ├── json-dictionary.spec.ts
│   │   │   │   │   ├── json-dictionary.ts
│   │   │   │   │   ├── json-sorting.test.ts
│   │   │   │   │   ├── json-sorting.ts
│   │   │   │   │   ├── json.ts
│   │   │   │   │   ├── json5.spec.ts
│   │   │   │   │   ├── json5.ts
│   │   │   │   │   ├── jsonc.spec.ts
│   │   │   │   │   ├── jsonc.ts
│   │   │   │   │   ├── locked-keys.spec.ts
│   │   │   │   │   ├── locked-keys.ts
│   │   │   │   │   ├── locked-patterns.spec.ts
│   │   │   │   │   ├── locked-patterns.ts
│   │   │   │   │   ├── markdoc.spec.ts
│   │   │   │   │   ├── markdoc.ts
│   │   │   │   │   ├── markdown.ts
│   │   │   │   │   ├── mdx.spec.ts
│   │   │   │   │   ├── mdx.ts
│   │   │   │   │   ├── mdx2
│   │   │   │   │   │   ├── _types.ts
│   │   │   │   │   │   ├── _utils.ts
│   │   │   │   │   │   ├── code-placeholder.spec.ts
│   │   │   │   │   │   ├── code-placeholder.ts
│   │   │   │   │   │   ├── frontmatter-split.spec.ts
│   │   │   │   │   │   ├── frontmatter-split.ts
│   │   │   │   │   │   ├── localizable-document.spec.ts
│   │   │   │   │   │   ├── localizable-document.ts
│   │   │   │   │   │   ├── section-split.spec.ts
│   │   │   │   │   │   ├── section-split.ts
│   │   │   │   │   │   └── sections-split-2.ts
│   │   │   │   │   ├── passthrough.ts
│   │   │   │   │   ├── php.ts
│   │   │   │   │   ├── plutil-json-loader.ts
│   │   │   │   │   ├── po
│   │   │   │   │   │   ├── _types.ts
│   │   │   │   │   │   ├── index.spec.ts
│   │   │   │   │   │   └── index.ts
│   │   │   │   │   ├── properties.ts
│   │   │   │   │   ├── root-key.ts
│   │   │   │   │   ├── srt.ts
│   │   │   │   │   ├── sync.ts
│   │   │   │   │   ├── text-file.ts
│   │   │   │   │   ├── txt.ts
│   │   │   │   │   ├── typescript
│   │   │   │   │   │   ├── cjs-interop.ts
│   │   │   │   │   │   ├── index.spec.ts
│   │   │   │   │   │   └── index.ts
│   │   │   │   │   ├── unlocalizable.spec.ts
│   │   │   │   │   ├── unlocalizable.ts
│   │   │   │   │   ├── variable
│   │   │   │   │   │   ├── index.spec.ts
│   │   │   │   │   │   └── index.ts
│   │   │   │   │   ├── vtt.ts
│   │   │   │   │   ├── vue-json.ts
│   │   │   │   │   ├── xcode-strings
│   │   │   │   │   │   ├── escape.ts
│   │   │   │   │   │   ├── parser.ts
│   │   │   │   │   │   ├── tokenizer.ts
│   │   │   │   │   │   └── types.ts
│   │   │   │   │   ├── xcode-strings.spec.ts
│   │   │   │   │   ├── xcode-strings.ts
│   │   │   │   │   ├── xcode-stringsdict.ts
│   │   │   │   │   ├── xcode-xcstrings-icu.spec.ts
│   │   │   │   │   ├── xcode-xcstrings-icu.ts
│   │   │   │   │   ├── xcode-xcstrings-lock-compatibility.spec.ts
│   │   │   │   │   ├── xcode-xcstrings-v2-loader.ts
│   │   │   │   │   ├── xcode-xcstrings.spec.ts
│   │   │   │   │   ├── xcode-xcstrings.ts
│   │   │   │   │   ├── xliff.spec.ts
│   │   │   │   │   ├── xliff.ts
│   │   │   │   │   ├── xml.ts
│   │   │   │   │   └── yaml.ts
│   │   │   │   ├── localizer
│   │   │   │   │   ├── _types.ts
│   │   │   │   │   ├── explicit.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── lingodotdev.ts
│   │   │   │   ├── processor
│   │   │   │   │   ├── _base.ts
│   │   │   │   │   ├── basic.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── lingo.ts
│   │   │   │   └── utils
│   │   │   │       ├── auth.ts
│   │   │   │       ├── buckets.spec.ts
│   │   │   │       ├── buckets.ts
│   │   │   │       ├── cache.ts
│   │   │   │       ├── cloudflare-status.ts
│   │   │   │       ├── config.ts
│   │   │   │       ├── delta.spec.ts
│   │   │   │       ├── delta.ts
│   │   │   │       ├── ensure-patterns.ts
│   │   │   │       ├── errors.ts
│   │   │   │       ├── exec.spec.ts
│   │   │   │       ├── exec.ts
│   │   │   │       ├── exit-gracefully.spec.ts
│   │   │   │       ├── exit-gracefully.ts
│   │   │   │       ├── exp-backoff.ts
│   │   │   │       ├── find-locale-paths.spec.ts
│   │   │   │       ├── find-locale-paths.ts
│   │   │   │       ├── fs.ts
│   │   │   │       ├── init-ci-cd.ts
│   │   │   │       ├── key-matching.spec.ts
│   │   │   │       ├── key-matching.ts
│   │   │   │       ├── lockfile.ts
│   │   │   │       ├── md5.ts
│   │   │   │       ├── observability.ts
│   │   │   │       ├── plutil-formatter.spec.ts
│   │   │   │       ├── plutil-formatter.ts
│   │   │   │       ├── settings.ts
│   │   │   │       ├── ui.ts
│   │   │   │       └── update-gitignore.ts
│   │   │   ├── compiler
│   │   │   │   └── index.ts
│   │   │   ├── locale-codes
│   │   │   │   └── index.ts
│   │   │   ├── react
│   │   │   │   ├── client.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── react-router.ts
│   │   │   │   └── rsc.ts
│   │   │   ├── sdk
│   │   │   │   └── index.ts
│   │   │   └── spec
│   │   │       └── index.ts
│   │   ├── tests
│   │   │   └── mock-storage.ts
│   │   ├── troubleshooting.md
│   │   ├── tsconfig.json
│   │   ├── tsconfig.test.json
│   │   ├── tsup.config.ts
│   │   ├── types
│   │   │   ├── vtt.d.ts
│   │   │   └── xliff.d.ts
│   │   ├── vitest.config.ts
│   │   └── WATCH_MODE.md
│   ├── compiler
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── _base.ts
│   │   │   ├── _const.ts
│   │   │   ├── _loader-utils.spec.ts
│   │   │   ├── _loader-utils.ts
│   │   │   ├── _utils.spec.ts
│   │   │   ├── _utils.ts
│   │   │   ├── client-dictionary-loader.ts
│   │   │   ├── i18n-directive.spec.ts
│   │   │   ├── i18n-directive.ts
│   │   │   ├── index.spec.ts
│   │   │   ├── index.ts
│   │   │   ├── jsx-attribute-flag.spec.ts
│   │   │   ├── jsx-attribute-flag.ts
│   │   │   ├── jsx-attribute-scope-inject.spec.ts
│   │   │   ├── jsx-attribute-scope-inject.ts
│   │   │   ├── jsx-attribute-scopes-export.spec.ts
│   │   │   ├── jsx-attribute-scopes-export.ts
│   │   │   ├── jsx-attribute.spec.ts
│   │   │   ├── jsx-attribute.ts
│   │   │   ├── jsx-fragment.spec.ts
│   │   │   ├── jsx-fragment.ts
│   │   │   ├── jsx-html-lang.spec.ts
│   │   │   ├── jsx-html-lang.ts
│   │   │   ├── jsx-provider.spec.ts
│   │   │   ├── jsx-provider.ts
│   │   │   ├── jsx-remove-attributes.spec.ts
│   │   │   ├── jsx-remove-attributes.ts
│   │   │   ├── jsx-root-flag.spec.ts
│   │   │   ├── jsx-root-flag.ts
│   │   │   ├── jsx-scope-flag.spec.ts
│   │   │   ├── jsx-scope-flag.ts
│   │   │   ├── jsx-scope-inject.spec.ts
│   │   │   ├── jsx-scope-inject.ts
│   │   │   ├── jsx-scopes-export.spec.ts
│   │   │   ├── jsx-scopes-export.ts
│   │   │   ├── lib
│   │   │   │   └── lcp
│   │   │   │       ├── api
│   │   │   │       │   ├── index.ts
│   │   │   │       │   ├── prompt.spec.ts
│   │   │   │       │   ├── prompt.ts
│   │   │   │       │   ├── provider-details.spec.ts
│   │   │   │       │   ├── provider-details.ts
│   │   │   │       │   ├── shots.ts
│   │   │   │       │   ├── xml2obj.spec.ts
│   │   │   │       │   └── xml2obj.ts
│   │   │   │       ├── api.spec.ts
│   │   │   │       ├── cache.spec.ts
│   │   │   │       ├── cache.ts
│   │   │   │       ├── index.spec.ts
│   │   │   │       ├── index.ts
│   │   │   │       ├── schema.ts
│   │   │   │       ├── server.spec.ts
│   │   │   │       └── server.ts
│   │   │   ├── lingo-turbopack-loader.ts
│   │   │   ├── react-router-dictionary-loader.ts
│   │   │   ├── rsc-dictionary-loader.ts
│   │   │   └── utils
│   │   │       ├── ast-key.spec.ts
│   │   │       ├── ast-key.ts
│   │   │       ├── create-locale-import-map.spec.ts
│   │   │       ├── create-locale-import-map.ts
│   │   │       ├── env.spec.ts
│   │   │       ├── env.ts
│   │   │       ├── hash.spec.ts
│   │   │       ├── hash.ts
│   │   │       ├── index.spec.ts
│   │   │       ├── index.ts
│   │   │       ├── invokations.spec.ts
│   │   │       ├── invokations.ts
│   │   │       ├── jsx-attribute-scope.ts
│   │   │       ├── jsx-attribute.spec.ts
│   │   │       ├── jsx-attribute.ts
│   │   │       ├── jsx-content-whitespace.spec.ts
│   │   │       ├── jsx-content.spec.ts
│   │   │       ├── jsx-content.ts
│   │   │       ├── jsx-element.spec.ts
│   │   │       ├── jsx-element.ts
│   │   │       ├── jsx-expressions.test.ts
│   │   │       ├── jsx-expressions.ts
│   │   │       ├── jsx-functions.spec.ts
│   │   │       ├── jsx-functions.ts
│   │   │       ├── jsx-scope.spec.ts
│   │   │       ├── jsx-scope.ts
│   │   │       ├── jsx-variables.spec.ts
│   │   │       ├── jsx-variables.ts
│   │   │       ├── llm-api-key.ts
│   │   │       ├── llm-api-keys.spec.ts
│   │   │       ├── locales.spec.ts
│   │   │       ├── locales.ts
│   │   │       ├── module-params.spec.ts
│   │   │       ├── module-params.ts
│   │   │       ├── observability.spec.ts
│   │   │       ├── observability.ts
│   │   │       ├── rc.spec.ts
│   │   │       └── rc.ts
│   │   ├── tsconfig.json
│   │   ├── tsup.config.ts
│   │   └── vitest.config.ts
│   ├── locales
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── constants.ts
│   │   │   ├── index.ts
│   │   │   ├── names
│   │   │   │   ├── index.spec.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── integration.spec.ts
│   │   │   │   └── loader.ts
│   │   │   ├── parser.spec.ts
│   │   │   ├── parser.ts
│   │   │   ├── types.ts
│   │   │   ├── validation.spec.ts
│   │   │   └── validation.ts
│   │   ├── tsconfig.json
│   │   └── tsup.config.ts
│   ├── react
│   │   ├── build.config.ts
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── client
│   │   │   │   ├── attribute-component.spec.tsx
│   │   │   │   ├── attribute-component.tsx
│   │   │   │   ├── component.lingo-component.spec.tsx
│   │   │   │   ├── component.spec.tsx
│   │   │   │   ├── component.tsx
│   │   │   │   ├── context.spec.tsx
│   │   │   │   ├── context.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── loader.spec.ts
│   │   │   │   ├── loader.ts
│   │   │   │   ├── locale-switcher.spec.tsx
│   │   │   │   ├── locale-switcher.tsx
│   │   │   │   ├── locale.spec.ts
│   │   │   │   ├── locale.ts
│   │   │   │   ├── provider.spec.tsx
│   │   │   │   ├── provider.tsx
│   │   │   │   ├── utils.spec.ts
│   │   │   │   └── utils.ts
│   │   │   ├── core
│   │   │   │   ├── attribute-component.spec.tsx
│   │   │   │   ├── attribute-component.tsx
│   │   │   │   ├── component.spec.tsx
│   │   │   │   ├── component.tsx
│   │   │   │   ├── const.ts
│   │   │   │   ├── get-dictionary.spec.ts
│   │   │   │   ├── get-dictionary.ts
│   │   │   │   └── index.ts
│   │   │   ├── react-router
│   │   │   │   ├── index.ts
│   │   │   │   ├── loader.spec.ts
│   │   │   │   └── loader.ts
│   │   │   ├── rsc
│   │   │   │   ├── attribute-component.tsx
│   │   │   │   ├── component.lingo-component.spec.tsx
│   │   │   │   ├── component.spec.tsx
│   │   │   │   ├── component.tsx
│   │   │   │   ├── index.ts
│   │   │   │   ├── loader.spec.ts
│   │   │   │   ├── loader.ts
│   │   │   │   ├── provider.spec.tsx
│   │   │   │   ├── provider.tsx
│   │   │   │   ├── utils.spec.ts
│   │   │   │   └── utils.ts
│   │   │   └── test
│   │   │       └── setup.ts
│   │   ├── tsconfig.json
│   │   └── vitest.config.ts
│   ├── sdk
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── abort-controller.specs.ts
│   │   │   ├── index.spec.ts
│   │   │   └── index.ts
│   │   ├── tsconfig.json
│   │   ├── tsconfig.test.json
│   │   └── tsup.config.ts
│   └── spec
│       ├── CHANGELOG.md
│       ├── package.json
│       ├── README.md
│       ├── src
│       │   ├── config.spec.ts
│       │   ├── config.ts
│       │   ├── formats.ts
│       │   ├── index.spec.ts
│       │   ├── index.ts
│       │   ├── json-schema.ts
│       │   ├── locales.spec.ts
│       │   └── locales.ts
│       ├── tsconfig.json
│       ├── tsconfig.test.json
│       └── tsup.config.ts
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── readme
│   ├── ar.md
│   ├── bn.md
│   ├── de.md
│   ├── en.md
│   ├── es.md
│   ├── fa.md
│   ├── fr.md
│   ├── he.md
│   ├── hi.md
│   ├── it.md
│   ├── ja.md
│   ├── ko.md
│   ├── pl.md
│   ├── pt-BR.md
│   ├── ru.md
│   ├── tr.md
│   ├── uk-UA.md
│   └── zh-Hans.md
├── readme.md
├── scripts
│   ├── docs
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── generate-cli-docs.ts
│   │   │   ├── generate-config-docs.ts
│   │   │   ├── json-schema
│   │   │   │   ├── markdown-renderer.test.ts
│   │   │   │   ├── markdown-renderer.ts
│   │   │   │   ├── parser.test.ts
│   │   │   │   ├── parser.ts
│   │   │   │   └── types.ts
│   │   │   ├── utils.test.ts
│   │   │   └── utils.ts
│   │   ├── tsconfig.json
│   │   └── vitest.config.ts
│   └── packagist-publish.php
└── turbo.json
```

# Files

--------------------------------------------------------------------------------
/demo/vite-project/src/lingo-dot-dev.tsx:
--------------------------------------------------------------------------------

```typescript
 1 | import type { SVGProps } from "react";
 2 | 
 3 | export function LingoDotDev(props: SVGProps<SVGSVGElement>) {
 4 |   const { width, height, ...rest } = props;
 5 |   return (
 6 |     <svg
 7 |       width={width ? width : !height ? 300 : undefined}
 8 |       height={height ? height : !width ? 46 : undefined}
 9 |       viewBox="0 0 150 26"
10 |       fill="#000000"
11 |       xmlns="http://www.w3.org/2000/svg"
12 |       {...rest}
13 |     >
14 |       <path
15 |         d="M0.0528644 9.3081L9.51858 19.6347C9.55631 19.6754 9.60893 19.6992 9.66454 19.6992H25.1832C25.36 19.6992 25.4483 19.4857 25.3232 19.3606L18.5126 12.5501H13.1508V15.231L6.89523 8.97546H0.198827C0.026055 8.97546 -0.0643027 9.181 0.0528644 9.3081Z"
16 |         fill="#69E300"
17 |       ></path>
18 |       <path
19 |         d="M25.3593 12.2182L15.8926 1.89157C15.8548 1.85086 15.8022 1.82703 15.7466 1.82703H0.227916C0.0501791 1.82703 -0.0381927 2.04051 0.0869179 2.16562L6.8975 8.9762H12.2594V6.29526L18.5149 12.5508H25.2123C25.3851 12.5508 25.4754 12.3453 25.3583 12.2182H25.3593Z"
20 |         fill="#69E300"
21 |       ></path>
22 |       <path d="M104.582 19.7876H101.476V16.7095H104.582V19.7876Z"></path>
23 |       <path d="M109.677 19.1594C108.713 18.5597 107.951 17.7139 107.389 16.6255C106.827 15.537 106.543 14.2995 106.543 12.9199C106.543 11.5402 106.824 10.3097 107.389 9.22814C107.951 8.14662 108.717 7.30775 109.677 6.70807C110.641 6.10838 111.712 5.81027 112.894 5.81027C114.62 5.81027 116 6.46542 117.036 7.77225H117.171V0.711182H119.868V19.7868H117.553L117.279 18.0432H117.143C116.09 19.3882 114.672 20.0607 112.894 20.0607C111.712 20.0607 110.641 19.7626 109.677 19.1629V19.1594ZM115.195 16.979C115.802 16.5977 116.284 16.0535 116.641 15.3429C116.994 14.6357 117.171 13.8246 117.171 12.9164C117.171 12.0082 116.994 11.204 116.641 10.5038C116.287 9.80357 115.805 9.26281 115.195 8.8815C114.585 8.5002 113.927 8.30955 113.22 8.30955C112.512 8.30955 111.829 8.5002 111.23 8.8815C110.63 9.26281 110.152 9.80357 109.798 10.5038C109.445 11.204 109.268 12.0082 109.268 12.9164C109.268 13.8246 109.445 14.6565 109.798 15.3568C110.152 16.057 110.63 16.5977 111.23 16.979C111.829 17.3603 112.492 17.551 113.22 17.551C113.947 17.551 114.585 17.3603 115.195 16.979Z"></path>
24 |       <path d="M126.219 16.6632C127.051 17.2802 127.901 17.7135 128.972 17.7135C129.734 17.7135 130.407 17.5402 130.989 17.197C131.572 16.8504 132.005 16.4067 132.296 15.8625H135.104C134.684 17.0792 133.95 18.0844 132.896 18.8748C131.842 19.6651 130.535 20.0603 128.972 20.0603C127.7 20.0603 126.545 19.7552 125.512 19.1486C124.476 18.542 123.665 17.6962 123.072 16.6147C122.483 15.5332 122.188 14.3026 122.188 12.923C122.188 11.5433 122.483 10.3405 123.072 9.25896C123.661 8.17744 124.476 7.33511 125.512 6.72502C126.549 6.1184 127.7 5.81335 128.972 5.81335C130.244 5.81335 131.291 6.10453 132.282 6.68689C133.274 7.26925 134.053 8.06652 134.625 9.08564C135.197 10.1048 135.485 11.2487 135.485 12.5174C135.485 13.1517 135.44 13.6336 135.35 13.9629H124.874L127.121 11.8934H132.819C132.729 10.805 132.327 9.90371 131.62 9.19657C130.913 8.48942 130.029 8.13238 128.979 8.13238C127.928 8.13238 126.972 8.48595 126.226 9.19657C125.481 9.90371 125.037 10.805 124.892 11.8934C124.892 11.8934 124.316 15.2628 126.226 16.6736L126.219 16.6632Z"></path>
25 |       <path d="M144.556 19.7876H140.605L135.783 6.08142H138.618L142.324 16.7372H142.868L146.574 6.08142H149.409L144.56 19.7876H144.556Z"></path>
26 |       <path d="M53.4117 3.49132H50.4688V0.766724H53.4117V3.49132ZM53.2765 19.7869H50.5797V6.08073H53.2765V19.7869Z"></path>
27 |       <path d="M56.2451 19.7872V6.08105H58.5607L58.8345 7.49881H58.9697C59.3129 7.06204 59.8398 6.6738 60.5504 6.32716C61.2575 5.98052 62.0756 5.81067 63.0046 5.81067C64.0584 5.81067 64.9978 6.05332 65.8263 6.53168C66.6513 7.01351 67.2995 7.68946 67.7605 8.563C68.225 9.43653 68.4538 10.4349 68.4538 11.5614V19.7907H65.7569V11.7798C65.7569 10.7988 65.438 9.98076 64.8037 9.32561C64.1693 8.67045 63.379 8.34461 62.4327 8.34461C61.7983 8.34461 61.2125 8.5006 60.6752 8.80911C60.1379 9.11762 59.7185 9.53706 59.4065 10.064C59.098 10.5908 58.942 11.1628 58.942 11.7798V19.7907H56.2451V19.7872Z"></path>
28 |       <path d="M73.9972 24.772C73.0613 24.2832 72.3472 23.6697 71.8585 22.9313C71.3697 22.193 71.1062 21.4477 71.0681 20.6817H73.7927C73.8655 21.316 74.2121 21.8776 74.8292 22.3559C75.4462 22.8378 76.3024 23.0769 77.3908 23.0769C78.4793 23.0769 79.4152 22.758 80.0877 22.1237C80.7602 21.4859 81.0964 20.6435 81.0964 19.5897V17.4094H80.9612C80.4898 17.9363 79.897 18.3557 79.1899 18.6642C78.4828 18.9727 77.6993 19.1287 76.8466 19.1287C75.6646 19.1287 74.6004 18.8479 73.6436 18.2829C72.6904 17.7213 71.9416 16.931 71.3974 15.9119C70.8532 14.8962 70.5793 13.7419 70.5793 12.4524C70.5793 11.1629 70.8532 10.0121 71.3974 9.00679C71.9416 7.99807 72.6904 7.21466 73.6436 6.64964C74.5969 6.08808 75.6646 5.80383 76.8466 5.80383C77.7201 5.80383 78.5209 5.97715 79.2592 6.32033C79.9941 6.66697 80.5903 7.128 81.0444 7.71036H81.1796L81.4535 6.07421H83.7968V19.6175C83.7968 20.7787 83.5437 21.8048 83.0341 22.6956C82.5246 23.5865 81.7862 24.2763 80.8122 24.7651C79.8416 25.2573 78.7011 25.5 77.3908 25.5C76.0805 25.5 74.9332 25.2538 73.9972 24.7651V24.772ZM79.1726 16.1476C79.7619 15.794 80.2298 15.2949 80.5765 14.6501C80.9231 14.0054 81.093 13.2739 81.093 12.4559C81.093 11.6378 80.9196 10.9133 80.5765 10.2755C80.2298 9.64115 79.7619 9.14545 79.1726 8.79188C78.5833 8.4383 77.9247 8.26152 77.1967 8.26152C76.0701 8.26152 75.1411 8.65322 74.4028 9.43316C73.6679 10.2131 73.3005 11.2218 73.3005 12.4593C73.3005 13.6968 73.6679 14.7298 74.4028 15.5098C75.1377 16.2897 76.0701 16.6814 77.1967 16.6814C77.9247 16.6814 78.5833 16.5046 79.1726 16.1511V16.1476Z"></path>
29 |       <path d="M89.2696 19.1459C88.2331 18.5393 87.422 17.6935 86.8292 16.612C86.24 15.5305 85.9453 14.2999 85.9453 12.9203C85.9453 11.5406 86.24 10.3378 86.8292 9.25628C87.4185 8.17476 88.2331 7.33242 89.2696 6.72233C90.3061 6.11571 91.4569 5.81067 92.7291 5.81067C94.0012 5.81067 95.1486 6.11571 96.1747 6.72233C97.2007 7.33242 98.0084 8.17476 98.6012 9.25628C99.1905 10.3378 99.4851 11.558 99.4851 12.9203C99.4851 14.2826 99.1905 15.5305 98.6012 16.612C98.0119 17.6935 97.2007 18.5393 96.1747 19.1459C95.1486 19.756 93.9978 20.0576 92.7291 20.0576C91.4604 20.0576 90.3026 19.7526 89.2696 19.1459ZM94.7846 16.9794C95.4121 16.5981 95.9078 16.0539 96.2683 15.3433C96.6322 14.6361 96.8125 13.825 96.8125 12.9168C96.8125 12.0086 96.6322 11.2044 96.2683 10.5042C95.9043 9.80397 95.4086 9.26321 94.7846 8.88191C94.1572 8.5006 93.4709 8.30995 92.7291 8.30995C91.9873 8.30995 91.2974 8.5006 90.6735 8.88191C90.0461 9.26321 89.5504 9.80397 89.1899 10.5042C88.8259 11.2044 88.6456 12.0086 88.6456 12.9168C88.6456 13.825 88.8259 14.6327 89.1899 15.3433C89.5538 16.0504 90.0495 16.5981 90.6735 16.9794C91.3009 17.3607 91.9873 17.5514 92.7291 17.5514C93.4709 17.5514 94.1607 17.3607 94.7846 16.9794Z"></path>
30 |       <path d="M48.8417 19.5756H36.7439V0.5H39.6037V16.851L39.6557 19.5756L42.6541 16.851H48.8417V19.5756Z"></path>
31 |     </svg>
32 |   );
33 | }
34 | 
```

--------------------------------------------------------------------------------
/demo/next-app/src/app/lingo-dot-dev.tsx:
--------------------------------------------------------------------------------

```typescript
 1 | import type { SVGProps } from "react";
 2 | 
 3 | export function LingoDotDev(props: SVGProps<SVGSVGElement>) {
 4 |   return (
 5 |     <svg
 6 |       width={props.width ? props.width : !props.height ? 150 : undefined}
 7 |       height={props.height ? props.height : !props.width ? 26 : undefined}
 8 |       viewBox="0 0 150 26"
 9 |       fill="#000000"
10 |       xmlns="http://www.w3.org/2000/svg"
11 |       className={props.className}
12 |     >
13 |       <path
14 |         d="M0.0528644 9.3081L9.51858 19.6347C9.55631 19.6754 9.60893 19.6992 9.66454 19.6992H25.1832C25.36 19.6992 25.4483 19.4857 25.3232 19.3606L18.5126 12.5501H13.1508V15.231L6.89523 8.97546H0.198827C0.026055 8.97546 -0.0643027 9.181 0.0528644 9.3081Z"
15 |         fill="#69E300"
16 |       ></path>
17 |       <path
18 |         d="M25.3593 12.2182L15.8926 1.89157C15.8548 1.85086 15.8022 1.82703 15.7466 1.82703H0.227916C0.0501791 1.82703 -0.0381927 2.04051 0.0869179 2.16562L6.8975 8.9762H12.2594V6.29526L18.5149 12.5508H25.2123C25.3851 12.5508 25.4754 12.3453 25.3583 12.2182H25.3593Z"
19 |         fill="#69E300"
20 |       ></path>
21 |       <path d="M104.582 19.7876H101.476V16.7095H104.582V19.7876Z"></path>
22 |       <path d="M109.677 19.1594C108.713 18.5597 107.951 17.7139 107.389 16.6255C106.827 15.537 106.543 14.2995 106.543 12.9199C106.543 11.5402 106.824 10.3097 107.389 9.22814C107.951 8.14662 108.717 7.30775 109.677 6.70807C110.641 6.10838 111.712 5.81027 112.894 5.81027C114.62 5.81027 116 6.46542 117.036 7.77225H117.171V0.711182H119.868V19.7868H117.553L117.279 18.0432H117.143C116.09 19.3882 114.672 20.0607 112.894 20.0607C111.712 20.0607 110.641 19.7626 109.677 19.1629V19.1594ZM115.195 16.979C115.802 16.5977 116.284 16.0535 116.641 15.3429C116.994 14.6357 117.171 13.8246 117.171 12.9164C117.171 12.0082 116.994 11.204 116.641 10.5038C116.287 9.80357 115.805 9.26281 115.195 8.8815C114.585 8.5002 113.927 8.30955 113.22 8.30955C112.512 8.30955 111.829 8.5002 111.23 8.8815C110.63 9.26281 110.152 9.80357 109.798 10.5038C109.445 11.204 109.268 12.0082 109.268 12.9164C109.268 13.8246 109.445 14.6565 109.798 15.3568C110.152 16.057 110.63 16.5977 111.23 16.979C111.829 17.3603 112.492 17.551 113.22 17.551C113.947 17.551 114.585 17.3603 115.195 16.979Z"></path>
23 |       <path d="M126.219 16.6632C127.051 17.2802 127.901 17.7135 128.972 17.7135C129.734 17.7135 130.407 17.5402 130.989 17.197C131.572 16.8504 132.005 16.4067 132.296 15.8625H135.104C134.684 17.0792 133.95 18.0844 132.896 18.8748C131.842 19.6651 130.535 20.0603 128.972 20.0603C127.7 20.0603 126.545 19.7552 125.512 19.1486C124.476 18.542 123.665 17.6962 123.072 16.6147C122.483 15.5332 122.188 14.3026 122.188 12.923C122.188 11.5433 122.483 10.3405 123.072 9.25896C123.661 8.17744 124.476 7.33511 125.512 6.72502C126.549 6.1184 127.7 5.81335 128.972 5.81335C130.244 5.81335 131.291 6.10453 132.282 6.68689C133.274 7.26925 134.053 8.06652 134.625 9.08564C135.197 10.1048 135.485 11.2487 135.485 12.5174C135.485 13.1517 135.44 13.6336 135.35 13.9629H124.874L127.121 11.8934H132.819C132.729 10.805 132.327 9.90371 131.62 9.19657C130.913 8.48942 130.029 8.13238 128.979 8.13238C127.928 8.13238 126.972 8.48595 126.226 9.19657C125.481 9.90371 125.037 10.805 124.892 11.8934C124.892 11.8934 124.316 15.2628 126.226 16.6736L126.219 16.6632Z"></path>
24 |       <path d="M144.556 19.7876H140.605L135.783 6.08142H138.618L142.324 16.7372H142.868L146.574 6.08142H149.409L144.56 19.7876H144.556Z"></path>
25 |       <path d="M53.4117 3.49132H50.4688V0.766724H53.4117V3.49132ZM53.2765 19.7869H50.5797V6.08073H53.2765V19.7869Z"></path>
26 |       <path d="M56.2451 19.7872V6.08105H58.5607L58.8345 7.49881H58.9697C59.3129 7.06204 59.8398 6.6738 60.5504 6.32716C61.2575 5.98052 62.0756 5.81067 63.0046 5.81067C64.0584 5.81067 64.9978 6.05332 65.8263 6.53168C66.6513 7.01351 67.2995 7.68946 67.7605 8.563C68.225 9.43653 68.4538 10.4349 68.4538 11.5614V19.7907H65.7569V11.7798C65.7569 10.7988 65.438 9.98076 64.8037 9.32561C64.1693 8.67045 63.379 8.34461 62.4327 8.34461C61.7983 8.34461 61.2125 8.5006 60.6752 8.80911C60.1379 9.11762 59.7185 9.53706 59.4065 10.064C59.098 10.5908 58.942 11.1628 58.942 11.7798V19.7907H56.2451V19.7872Z"></path>
27 |       <path d="M73.9972 24.772C73.0613 24.2832 72.3472 23.6697 71.8585 22.9313C71.3697 22.193 71.1062 21.4477 71.0681 20.6817H73.7927C73.8655 21.316 74.2121 21.8776 74.8292 22.3559C75.4462 22.8378 76.3024 23.0769 77.3908 23.0769C78.4793 23.0769 79.4152 22.758 80.0877 22.1237C80.7602 21.4859 81.0964 20.6435 81.0964 19.5897V17.4094H80.9612C80.4898 17.9363 79.897 18.3557 79.1899 18.6642C78.4828 18.9727 77.6993 19.1287 76.8466 19.1287C75.6646 19.1287 74.6004 18.8479 73.6436 18.2829C72.6904 17.7213 71.9416 16.931 71.3974 15.9119C70.8532 14.8962 70.5793 13.7419 70.5793 12.4524C70.5793 11.1629 70.8532 10.0121 71.3974 9.00679C71.9416 7.99807 72.6904 7.21466 73.6436 6.64964C74.5969 6.08808 75.6646 5.80383 76.8466 5.80383C77.7201 5.80383 78.5209 5.97715 79.2592 6.32033C79.9941 6.66697 80.5903 7.128 81.0444 7.71036H81.1796L81.4535 6.07421H83.7968V19.6175C83.7968 20.7787 83.5437 21.8048 83.0341 22.6956C82.5246 23.5865 81.7862 24.2763 80.8122 24.7651C79.8416 25.2573 78.7011 25.5 77.3908 25.5C76.0805 25.5 74.9332 25.2538 73.9972 24.7651V24.772ZM79.1726 16.1476C79.7619 15.794 80.2298 15.2949 80.5765 14.6501C80.9231 14.0054 81.093 13.2739 81.093 12.4559C81.093 11.6378 80.9196 10.9133 80.5765 10.2755C80.2298 9.64115 79.7619 9.14545 79.1726 8.79188C78.5833 8.4383 77.9247 8.26152 77.1967 8.26152C76.0701 8.26152 75.1411 8.65322 74.4028 9.43316C73.6679 10.2131 73.3005 11.2218 73.3005 12.4593C73.3005 13.6968 73.6679 14.7298 74.4028 15.5098C75.1377 16.2897 76.0701 16.6814 77.1967 16.6814C77.9247 16.6814 78.5833 16.5046 79.1726 16.1511V16.1476Z"></path>
28 |       <path d="M89.2696 19.1459C88.2331 18.5393 87.422 17.6935 86.8292 16.612C86.24 15.5305 85.9453 14.2999 85.9453 12.9203C85.9453 11.5406 86.24 10.3378 86.8292 9.25628C87.4185 8.17476 88.2331 7.33242 89.2696 6.72233C90.3061 6.11571 91.4569 5.81067 92.7291 5.81067C94.0012 5.81067 95.1486 6.11571 96.1747 6.72233C97.2007 7.33242 98.0084 8.17476 98.6012 9.25628C99.1905 10.3378 99.4851 11.558 99.4851 12.9203C99.4851 14.2826 99.1905 15.5305 98.6012 16.612C98.0119 17.6935 97.2007 18.5393 96.1747 19.1459C95.1486 19.756 93.9978 20.0576 92.7291 20.0576C91.4604 20.0576 90.3026 19.7526 89.2696 19.1459ZM94.7846 16.9794C95.4121 16.5981 95.9078 16.0539 96.2683 15.3433C96.6322 14.6361 96.8125 13.825 96.8125 12.9168C96.8125 12.0086 96.6322 11.2044 96.2683 10.5042C95.9043 9.80397 95.4086 9.26321 94.7846 8.88191C94.1572 8.5006 93.4709 8.30995 92.7291 8.30995C91.9873 8.30995 91.2974 8.5006 90.6735 8.88191C90.0461 9.26321 89.5504 9.80397 89.1899 10.5042C88.8259 11.2044 88.6456 12.0086 88.6456 12.9168C88.6456 13.825 88.8259 14.6327 89.1899 15.3433C89.5538 16.0504 90.0495 16.5981 90.6735 16.9794C91.3009 17.3607 91.9873 17.5514 92.7291 17.5514C93.4709 17.5514 94.1607 17.3607 94.7846 16.9794Z"></path>
29 |       <path d="M48.8417 19.5756H36.7439V0.5H39.6037V16.851L39.6557 19.5756L42.6541 16.851H48.8417V19.5756Z"></path>
30 |     </svg>
31 |   );
32 | }
33 | 
```

--------------------------------------------------------------------------------
/demo/react-router-app/app/welcome/lingo-dot-dev.tsx:
--------------------------------------------------------------------------------

```typescript
 1 | import type { SVGProps } from "react";
 2 | 
 3 | export function LingoDotDev(props: SVGProps<SVGSVGElement>) {
 4 |   return (
 5 |     <svg
 6 |       width={props.width ? props.width : !props.height ? 150 : undefined}
 7 |       height={props.height ? props.height : !props.width ? 26 : undefined}
 8 |       viewBox="0 0 150 26"
 9 |       fill="#000000"
10 |       xmlns="http://www.w3.org/2000/svg"
11 |       className={props.className}
12 |     >
13 |       <path
14 |         d="M0.0528644 9.3081L9.51858 19.6347C9.55631 19.6754 9.60893 19.6992 9.66454 19.6992H25.1832C25.36 19.6992 25.4483 19.4857 25.3232 19.3606L18.5126 12.5501H13.1508V15.231L6.89523 8.97546H0.198827C0.026055 8.97546 -0.0643027 9.181 0.0528644 9.3081Z"
15 |         fill="#69E300"
16 |       ></path>
17 |       <path
18 |         d="M25.3593 12.2182L15.8926 1.89157C15.8548 1.85086 15.8022 1.82703 15.7466 1.82703H0.227916C0.0501791 1.82703 -0.0381927 2.04051 0.0869179 2.16562L6.8975 8.9762H12.2594V6.29526L18.5149 12.5508H25.2123C25.3851 12.5508 25.4754 12.3453 25.3583 12.2182H25.3593Z"
19 |         fill="#69E300"
20 |       ></path>
21 |       <path d="M104.582 19.7876H101.476V16.7095H104.582V19.7876Z"></path>
22 |       <path d="M109.677 19.1594C108.713 18.5597 107.951 17.7139 107.389 16.6255C106.827 15.537 106.543 14.2995 106.543 12.9199C106.543 11.5402 106.824 10.3097 107.389 9.22814C107.951 8.14662 108.717 7.30775 109.677 6.70807C110.641 6.10838 111.712 5.81027 112.894 5.81027C114.62 5.81027 116 6.46542 117.036 7.77225H117.171V0.711182H119.868V19.7868H117.553L117.279 18.0432H117.143C116.09 19.3882 114.672 20.0607 112.894 20.0607C111.712 20.0607 110.641 19.7626 109.677 19.1629V19.1594ZM115.195 16.979C115.802 16.5977 116.284 16.0535 116.641 15.3429C116.994 14.6357 117.171 13.8246 117.171 12.9164C117.171 12.0082 116.994 11.204 116.641 10.5038C116.287 9.80357 115.805 9.26281 115.195 8.8815C114.585 8.5002 113.927 8.30955 113.22 8.30955C112.512 8.30955 111.829 8.5002 111.23 8.8815C110.63 9.26281 110.152 9.80357 109.798 10.5038C109.445 11.204 109.268 12.0082 109.268 12.9164C109.268 13.8246 109.445 14.6565 109.798 15.3568C110.152 16.057 110.63 16.5977 111.23 16.979C111.829 17.3603 112.492 17.551 113.22 17.551C113.947 17.551 114.585 17.3603 115.195 16.979Z"></path>
23 |       <path d="M126.219 16.6632C127.051 17.2802 127.901 17.7135 128.972 17.7135C129.734 17.7135 130.407 17.5402 130.989 17.197C131.572 16.8504 132.005 16.4067 132.296 15.8625H135.104C134.684 17.0792 133.95 18.0844 132.896 18.8748C131.842 19.6651 130.535 20.0603 128.972 20.0603C127.7 20.0603 126.545 19.7552 125.512 19.1486C124.476 18.542 123.665 17.6962 123.072 16.6147C122.483 15.5332 122.188 14.3026 122.188 12.923C122.188 11.5433 122.483 10.3405 123.072 9.25896C123.661 8.17744 124.476 7.33511 125.512 6.72502C126.549 6.1184 127.7 5.81335 128.972 5.81335C130.244 5.81335 131.291 6.10453 132.282 6.68689C133.274 7.26925 134.053 8.06652 134.625 9.08564C135.197 10.1048 135.485 11.2487 135.485 12.5174C135.485 13.1517 135.44 13.6336 135.35 13.9629H124.874L127.121 11.8934H132.819C132.729 10.805 132.327 9.90371 131.62 9.19657C130.913 8.48942 130.029 8.13238 128.979 8.13238C127.928 8.13238 126.972 8.48595 126.226 9.19657C125.481 9.90371 125.037 10.805 124.892 11.8934C124.892 11.8934 124.316 15.2628 126.226 16.6736L126.219 16.6632Z"></path>
24 |       <path d="M144.556 19.7876H140.605L135.783 6.08142H138.618L142.324 16.7372H142.868L146.574 6.08142H149.409L144.56 19.7876H144.556Z"></path>
25 |       <path d="M53.4117 3.49132H50.4688V0.766724H53.4117V3.49132ZM53.2765 19.7869H50.5797V6.08073H53.2765V19.7869Z"></path>
26 |       <path d="M56.2451 19.7872V6.08105H58.5607L58.8345 7.49881H58.9697C59.3129 7.06204 59.8398 6.6738 60.5504 6.32716C61.2575 5.98052 62.0756 5.81067 63.0046 5.81067C64.0584 5.81067 64.9978 6.05332 65.8263 6.53168C66.6513 7.01351 67.2995 7.68946 67.7605 8.563C68.225 9.43653 68.4538 10.4349 68.4538 11.5614V19.7907H65.7569V11.7798C65.7569 10.7988 65.438 9.98076 64.8037 9.32561C64.1693 8.67045 63.379 8.34461 62.4327 8.34461C61.7983 8.34461 61.2125 8.5006 60.6752 8.80911C60.1379 9.11762 59.7185 9.53706 59.4065 10.064C59.098 10.5908 58.942 11.1628 58.942 11.7798V19.7907H56.2451V19.7872Z"></path>
27 |       <path d="M73.9972 24.772C73.0613 24.2832 72.3472 23.6697 71.8585 22.9313C71.3697 22.193 71.1062 21.4477 71.0681 20.6817H73.7927C73.8655 21.316 74.2121 21.8776 74.8292 22.3559C75.4462 22.8378 76.3024 23.0769 77.3908 23.0769C78.4793 23.0769 79.4152 22.758 80.0877 22.1237C80.7602 21.4859 81.0964 20.6435 81.0964 19.5897V17.4094H80.9612C80.4898 17.9363 79.897 18.3557 79.1899 18.6642C78.4828 18.9727 77.6993 19.1287 76.8466 19.1287C75.6646 19.1287 74.6004 18.8479 73.6436 18.2829C72.6904 17.7213 71.9416 16.931 71.3974 15.9119C70.8532 14.8962 70.5793 13.7419 70.5793 12.4524C70.5793 11.1629 70.8532 10.0121 71.3974 9.00679C71.9416 7.99807 72.6904 7.21466 73.6436 6.64964C74.5969 6.08808 75.6646 5.80383 76.8466 5.80383C77.7201 5.80383 78.5209 5.97715 79.2592 6.32033C79.9941 6.66697 80.5903 7.128 81.0444 7.71036H81.1796L81.4535 6.07421H83.7968V19.6175C83.7968 20.7787 83.5437 21.8048 83.0341 22.6956C82.5246 23.5865 81.7862 24.2763 80.8122 24.7651C79.8416 25.2573 78.7011 25.5 77.3908 25.5C76.0805 25.5 74.9332 25.2538 73.9972 24.7651V24.772ZM79.1726 16.1476C79.7619 15.794 80.2298 15.2949 80.5765 14.6501C80.9231 14.0054 81.093 13.2739 81.093 12.4559C81.093 11.6378 80.9196 10.9133 80.5765 10.2755C80.2298 9.64115 79.7619 9.14545 79.1726 8.79188C78.5833 8.4383 77.9247 8.26152 77.1967 8.26152C76.0701 8.26152 75.1411 8.65322 74.4028 9.43316C73.6679 10.2131 73.3005 11.2218 73.3005 12.4593C73.3005 13.6968 73.6679 14.7298 74.4028 15.5098C75.1377 16.2897 76.0701 16.6814 77.1967 16.6814C77.9247 16.6814 78.5833 16.5046 79.1726 16.1511V16.1476Z"></path>
28 |       <path d="M89.2696 19.1459C88.2331 18.5393 87.422 17.6935 86.8292 16.612C86.24 15.5305 85.9453 14.2999 85.9453 12.9203C85.9453 11.5406 86.24 10.3378 86.8292 9.25628C87.4185 8.17476 88.2331 7.33242 89.2696 6.72233C90.3061 6.11571 91.4569 5.81067 92.7291 5.81067C94.0012 5.81067 95.1486 6.11571 96.1747 6.72233C97.2007 7.33242 98.0084 8.17476 98.6012 9.25628C99.1905 10.3378 99.4851 11.558 99.4851 12.9203C99.4851 14.2826 99.1905 15.5305 98.6012 16.612C98.0119 17.6935 97.2007 18.5393 96.1747 19.1459C95.1486 19.756 93.9978 20.0576 92.7291 20.0576C91.4604 20.0576 90.3026 19.7526 89.2696 19.1459ZM94.7846 16.9794C95.4121 16.5981 95.9078 16.0539 96.2683 15.3433C96.6322 14.6361 96.8125 13.825 96.8125 12.9168C96.8125 12.0086 96.6322 11.2044 96.2683 10.5042C95.9043 9.80397 95.4086 9.26321 94.7846 8.88191C94.1572 8.5006 93.4709 8.30995 92.7291 8.30995C91.9873 8.30995 91.2974 8.5006 90.6735 8.88191C90.0461 9.26321 89.5504 9.80397 89.1899 10.5042C88.8259 11.2044 88.6456 12.0086 88.6456 12.9168C88.6456 13.825 88.8259 14.6327 89.1899 15.3433C89.5538 16.0504 90.0495 16.5981 90.6735 16.9794C91.3009 17.3607 91.9873 17.5514 92.7291 17.5514C93.4709 17.5514 94.1607 17.3607 94.7846 16.9794Z"></path>
29 |       <path d="M48.8417 19.5756H36.7439V0.5H39.6037V16.851L39.6557 19.5756L42.6541 16.851H48.8417V19.5756Z"></path>
30 |     </svg>
31 |   );
32 | }
33 | 
```

--------------------------------------------------------------------------------
/packages/cli/src/cli/loaders/markdoc.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import Markdoc from "@markdoc/markdoc";
  2 | import YAML from "yaml";
  3 | import { ILoader } from "./_types";
  4 | import { createLoader } from "./_utils";
  5 | 
  6 | type MarkdocNode = {
  7 |   $$mdtype?: string;
  8 |   type: string;
  9 |   tag?: string;
 10 |   attributes?: Record<string, any>;
 11 |   children?: MarkdocNode[];
 12 |   [key: string]: any;
 13 | };
 14 | 
 15 | type NodeCounter = {
 16 |   [nodeType: string]: number;
 17 | };
 18 | 
 19 | type NodePathMap = {
 20 |   [semanticKey: string]: string; // maps semantic key to AST path
 21 | };
 22 | 
 23 | const FM_ATTR_PREFIX = "fm-attr-";
 24 | 
 25 | export default function createMarkdocLoader(): ILoader<
 26 |   string,
 27 |   Record<string, string>
 28 | > {
 29 |   return createLoader({
 30 |     async pull(locale, input) {
 31 |       const ast = Markdoc.parse(input) as unknown as MarkdocNode;
 32 |       const result: Record<string, string> = {};
 33 |       const counters: NodeCounter = {};
 34 | 
 35 |       // Traverse the AST and extract text content with semantic keys
 36 |       traverseAndExtract(ast, "", result, counters);
 37 | 
 38 |       // Extract frontmatter if present
 39 |       if (ast.attributes?.frontmatter) {
 40 |         const frontmatter = YAML.parse(ast.attributes.frontmatter);
 41 |         Object.entries(frontmatter).forEach(([key, value]) => {
 42 |           if (typeof value === "string") {
 43 |             result[`${FM_ATTR_PREFIX}${key}`] = value;
 44 |           }
 45 |         });
 46 |       }
 47 | 
 48 |       return result;
 49 |     },
 50 | 
 51 |     async push(locale, data, originalInput) {
 52 |       if (!originalInput) {
 53 |         throw new Error("Original input is required for push");
 54 |       }
 55 | 
 56 |       const ast = Markdoc.parse(originalInput) as unknown as MarkdocNode;
 57 |       const counters: NodeCounter = {};
 58 |       const pathMap: NodePathMap = {};
 59 | 
 60 |       // Build path map from semantic keys to AST paths
 61 |       buildPathMap(ast, "", counters, pathMap);
 62 | 
 63 |       // Extract frontmatter from data
 64 |       const frontmatterEntries = Object.entries(data)
 65 |         .filter(([key]) => key.startsWith(FM_ATTR_PREFIX))
 66 |         .map(([key, value]) => [key.replace(FM_ATTR_PREFIX, ""), value]);
 67 | 
 68 |       // Update frontmatter in AST if present
 69 |       if (frontmatterEntries.length > 0 && ast.attributes) {
 70 |         const frontmatter = Object.fromEntries(frontmatterEntries);
 71 |         ast.attributes.frontmatter = YAML.stringify(frontmatter, {
 72 |           defaultStringType: "PLAIN",
 73 |         }).trim();
 74 |       }
 75 | 
 76 |       // Filter out frontmatter keys from translation data
 77 |       const contentData = Object.fromEntries(
 78 |         Object.entries(data).filter(([key]) => !key.startsWith(FM_ATTR_PREFIX)),
 79 |       );
 80 | 
 81 |       // Apply translations using the path map
 82 |       applyTranslations(ast, "", contentData, pathMap);
 83 | 
 84 |       // Format back to string
 85 |       return Markdoc.format(ast);
 86 |     },
 87 |   });
 88 | }
 89 | 
 90 | function getSemanticNodeType(node: MarkdocNode): string | null {
 91 |   // For custom tags, use the tag name instead of "tag"
 92 |   if (node.type === "tag") return node.tag || "tag";
 93 |   return node.type;
 94 | }
 95 | 
 96 | function traverseAndExtract(
 97 |   node: MarkdocNode,
 98 |   path: string,
 99 |   result: Record<string, string>,
100 |   counters: NodeCounter,
101 |   parentType?: string,
102 | ) {
103 |   if (!node || typeof node !== "object") {
104 |     return;
105 |   }
106 | 
107 |   // Determine the semantic type for this node
108 |   let semanticType = parentType;
109 |   const nodeSemanticType = getSemanticNodeType(node);
110 | 
111 |   // Use node's own semantic type for structural elements
112 |   if (
113 |     nodeSemanticType &&
114 |     !["text", "strong", "em", "inline", "link"].includes(nodeSemanticType)
115 |   ) {
116 |     semanticType = nodeSemanticType;
117 |   }
118 | 
119 |   // If this is a text node, extract its content only if it's a string
120 |   // Skip interpolation nodes (where content is a Variable or Function object)
121 |   if (node.type === "text" && node.attributes?.content) {
122 |     const content = node.attributes.content;
123 | 
124 |     // Only extract if content is a string (not interpolation)
125 |     if (typeof content === "string" && content.trim()) {
126 |       if (semanticType) {
127 |         const index = counters[semanticType] || 0;
128 |         counters[semanticType] = index + 1;
129 |         const semanticKey = `${semanticType}-${index}`;
130 |         result[semanticKey] = content;
131 |       }
132 |     }
133 |   }
134 | 
135 |   // If the node has children, traverse them
136 |   if (Array.isArray(node.children)) {
137 |     node.children.forEach((child, index) => {
138 |       const childPath = path
139 |         ? `${path}/children/${index}`
140 |         : `children/${index}`;
141 |       traverseAndExtract(child, childPath, result, counters, semanticType);
142 |     });
143 |   }
144 | }
145 | 
146 | function buildPathMap(
147 |   node: MarkdocNode,
148 |   path: string,
149 |   counters: NodeCounter,
150 |   pathMap: NodePathMap,
151 |   parentType?: string,
152 | ) {
153 |   if (!node || typeof node !== "object") {
154 |     return;
155 |   }
156 | 
157 |   // Determine the semantic type for this node
158 |   let semanticType = parentType;
159 |   const nodeSemanticType = getSemanticNodeType(node);
160 | 
161 |   // Use node's own semantic type for structural elements
162 |   if (
163 |     nodeSemanticType &&
164 |     !["text", "strong", "em", "inline", "link"].includes(nodeSemanticType)
165 |   ) {
166 |     semanticType = nodeSemanticType;
167 |   }
168 | 
169 |   // Build the map from semantic keys to AST paths
170 |   if (node.type === "text" && node.attributes?.content) {
171 |     const content = node.attributes.content;
172 | 
173 |     if (typeof content === "string" && content.trim()) {
174 |       if (semanticType) {
175 |         const index = counters[semanticType] || 0;
176 |         counters[semanticType] = index + 1;
177 |         const semanticKey = `${semanticType}-${index}`;
178 |         const contentPath = path
179 |           ? `${path}/attributes/content`
180 |           : "attributes/content";
181 |         pathMap[semanticKey] = contentPath;
182 |       }
183 |     }
184 |   }
185 | 
186 |   // Recursively build map for children
187 |   if (Array.isArray(node.children)) {
188 |     node.children.forEach((child, index) => {
189 |       const childPath = path
190 |         ? `${path}/children/${index}`
191 |         : `children/${index}`;
192 |       buildPathMap(child, childPath, counters, pathMap, semanticType);
193 |     });
194 |   }
195 | }
196 | 
197 | function applyTranslations(
198 |   node: MarkdocNode,
199 |   path: string,
200 |   data: Record<string, string>,
201 |   pathMap: NodePathMap,
202 | ) {
203 |   if (!node || typeof node !== "object") {
204 |     return;
205 |   }
206 | 
207 |   // Check if we have a translation for this node's text content
208 |   // Only apply translations to string content (not interpolation)
209 |   if (node.type === "text" && node.attributes?.content) {
210 |     const content = node.attributes.content;
211 | 
212 |     // Only apply translation if content is currently a string
213 |     if (typeof content === "string") {
214 |       const contentPath = path
215 |         ? `${path}/attributes/content`
216 |         : "attributes/content";
217 | 
218 |       // Find the semantic key for this path
219 |       const semanticKey = Object.keys(pathMap).find(
220 |         (key) => pathMap[key] === contentPath,
221 |       );
222 | 
223 |       if (semanticKey && data[semanticKey] !== undefined) {
224 |         node.attributes.content = data[semanticKey];
225 |       }
226 |     }
227 |     // If content is an object (Variable/Function), leave it unchanged
228 |   }
229 | 
230 |   // Recursively apply translations to children
231 |   if (Array.isArray(node.children)) {
232 |     node.children.forEach((child, index) => {
233 |       const childPath = path
234 |         ? `${path}/children/${index}`
235 |         : `children/${index}`;
236 |       applyTranslations(child, childPath, data, pathMap);
237 |     });
238 |   }
239 | }
240 | 
```

--------------------------------------------------------------------------------
/packages/compiler/src/utils/jsx-content.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { NodePath } from "@babel/traverse";
  2 | import * as t from "@babel/types";
  3 | import { getJsxElementName } from "./jsx-element";
  4 | import _ from "lodash";
  5 | 
  6 | const WHITESPACE_PLACEHOLDER = "[lingo-whitespace-placeholder]";
  7 | 
  8 | export function extractJsxContent(
  9 |   nodePath: NodePath<t.JSXElement>,
 10 |   replaceWhitespacePlaceholders = true, // do not replace when called recursively
 11 | ) {
 12 |   const chunks: string[] = [];
 13 | 
 14 |   nodePath.traverse({
 15 |     JSXElement(path) {
 16 |       if (path.parent === nodePath.node) {
 17 |         const content = extractJsxContent(path, false);
 18 |         const name = getJsxElementName(path);
 19 |         chunks.push(`<element:${name}>${content}</element:${name}>`);
 20 |         path.skip();
 21 |       }
 22 |     },
 23 |     JSXText(path) {
 24 |       chunks.push(path.node.value);
 25 |     },
 26 |     JSXExpressionContainer(path) {
 27 |       if (path.parent !== nodePath.node) {
 28 |         return;
 29 |       }
 30 | 
 31 |       const expr = path.node.expression;
 32 |       if (t.isCallExpression(expr)) {
 33 |         let key = "";
 34 |         if (t.isIdentifier(expr.callee)) {
 35 |           key = `${expr.callee.name}`;
 36 |         } else if (t.isMemberExpression(expr.callee)) {
 37 |           let firstCallee: t.Expression | t.V8IntrinsicIdentifier = expr.callee;
 38 |           while (
 39 |             t.isMemberExpression(firstCallee) &&
 40 |             t.isCallExpression(firstCallee.object)
 41 |           ) {
 42 |             firstCallee = firstCallee.object.callee;
 43 |           }
 44 | 
 45 |           let current: t.Expression | t.V8IntrinsicIdentifier = firstCallee;
 46 |           const parts: string[] = [];
 47 | 
 48 |           while (t.isMemberExpression(current)) {
 49 |             if (t.isIdentifier(current.property)) {
 50 |               parts.unshift(current.property.name);
 51 |             }
 52 |             current = current.object;
 53 |           }
 54 | 
 55 |           if (t.isIdentifier(current)) {
 56 |             parts.unshift(current.name);
 57 |           }
 58 | 
 59 |           if (
 60 |             t.isMemberExpression(firstCallee) &&
 61 |             t.isNewExpression(firstCallee.object) &&
 62 |             t.isIdentifier(firstCallee.object.callee)
 63 |           ) {
 64 |             parts.unshift(firstCallee.object.callee.name);
 65 |           }
 66 | 
 67 |           key = parts.join(".");
 68 |         }
 69 | 
 70 |         chunks.push(`<function:${key}/>`);
 71 |       } else if (t.isIdentifier(expr)) {
 72 |         chunks.push(`{${expr.name}}`);
 73 |       } else if (t.isMemberExpression(expr)) {
 74 |         let current: t.MemberExpression | t.Identifier = expr;
 75 |         const parts = [];
 76 | 
 77 |         while (t.isMemberExpression(current)) {
 78 |           if (t.isIdentifier(current.property)) {
 79 |             if (current.computed) {
 80 |               parts.unshift(`[${current.property.name}]`);
 81 |             } else {
 82 |               parts.unshift(current.property.name);
 83 |             }
 84 |           }
 85 |           current = current.object as t.MemberExpression | t.Identifier;
 86 |         }
 87 | 
 88 |         if (t.isIdentifier(current)) {
 89 |           parts.unshift(current.name);
 90 |           chunks.push(`{${parts.join(".").replaceAll(".[", "[")}}`);
 91 |         }
 92 |       } else if (isWhitespace(path)) {
 93 |         chunks.push(WHITESPACE_PLACEHOLDER);
 94 |       } else if (isExpression(path)) {
 95 |         chunks.push("<expression/>");
 96 |       }
 97 |       path.skip();
 98 |     },
 99 |   });
100 | 
101 |   const result = chunks.join("");
102 |   const normalized = normalizeJsxWhitespace(result);
103 | 
104 |   if (replaceWhitespacePlaceholders) {
105 |     return normalized.replaceAll(WHITESPACE_PLACEHOLDER, " ");
106 |   }
107 |   return normalized;
108 | }
109 | 
110 | const compilerProps = ["data-jsx-attribute-scope", "data-jsx-scope"];
111 | 
112 | function isExpression(nodePath: NodePath<t.JSXExpressionContainer>) {
113 |   const isCompilerExpression =
114 |     !_.isArray(nodePath.container) &&
115 |     t.isJSXAttribute(nodePath.container) &&
116 |     t.isJSXIdentifier(nodePath.container.name) &&
117 |     compilerProps.includes(nodePath.container.name.name);
118 |   return (
119 |     !isCompilerExpression && !t.isJSXEmptyExpression(nodePath.node.expression)
120 |   );
121 | }
122 | 
123 | function isWhitespace(nodePath: NodePath<t.JSXExpressionContainer>) {
124 |   const expr = nodePath.node.expression;
125 |   return t.isStringLiteral(expr) && expr.value === " ";
126 | }
127 | 
128 | function normalizeJsxWhitespace(input: string) {
129 |   // Handle single-line content
130 |   if (!input.includes("\n")) {
131 |     // For single-line content, only trim if it appears to be formatting whitespace
132 |     // (e.g., "   hello world   " should be trimmed to "hello world")
133 |     // But preserve meaningful leading/trailing spaces (e.g., " hello" should stay " hello")
134 | 
135 |     // If the content is mostly whitespace with some text, it's likely formatting
136 |     const trimmed = input.trim();
137 |     if (trimmed.length === 0) return "";
138 | 
139 |     // Check if we have excessive whitespace (more than 1 space on each side)
140 |     const leadingMatch = input.match(/^\s*/);
141 |     const trailingMatch = input.match(/\s*$/);
142 |     const leadingSpaces = leadingMatch ? leadingMatch[0].length : 0;
143 |     const trailingSpaces = trailingMatch ? trailingMatch[0].length : 0;
144 | 
145 |     if (leadingSpaces > 1 || trailingSpaces > 1) {
146 |       // This looks like formatting whitespace, collapse it
147 |       return input.replace(/\s+/g, " ").trim();
148 |     } else {
149 |       // This looks like meaningful whitespace, preserve it but collapse internal spaces
150 |       return input.replace(/\s{2,}/g, " ");
151 |     }
152 |   }
153 | 
154 |   // Handle multi-line content
155 |   const lines = input.split("\n");
156 |   let result = "";
157 | 
158 |   for (let i = 0; i < lines.length; i++) {
159 |     const line = lines[i];
160 |     const trimmedLine = line.trim();
161 | 
162 |     // Skip empty lines
163 |     if (trimmedLine === "") continue;
164 | 
165 |     // Check if this line contains a placeholder (explicit whitespace)
166 |     if (trimmedLine.includes(WHITESPACE_PLACEHOLDER)) {
167 |       // For lines with placeholders, preserve the original spacing
168 |       result += trimmedLine;
169 |     } else if (
170 |       trimmedLine.startsWith("<element:") ||
171 |       trimmedLine.startsWith("<function:") ||
172 |       trimmedLine.startsWith("{") ||
173 |       trimmedLine.startsWith("<expression/>")
174 |     ) {
175 |       // When we encounter an element/function/expression
176 |       // Add space only when:
177 |       // 1. We have existing content AND
178 |       // 2. Result doesn't already end with space or placeholder AND
179 |       // 3. The result ends with a word character (indicating text) AND
180 |       // 4. The element content starts with a space (indicating word continuation)
181 |       const shouldAddSpace =
182 |         result &&
183 |         !result.endsWith(" ") &&
184 |         !result.endsWith(WHITESPACE_PLACEHOLDER) &&
185 |         /\w$/.test(result) &&
186 |         // Check if element content starts with space by looking for "> " pattern
187 |         trimmedLine.includes("> ");
188 | 
189 |       if (shouldAddSpace) {
190 |         result += " ";
191 |       }
192 |       result += trimmedLine;
193 |     } else {
194 |       // For regular text content, ensure proper spacing
195 |       // Only add space if the result doesn't already end with a space or placeholder
196 |       if (
197 |         result &&
198 |         !result.endsWith(" ") &&
199 |         !result.endsWith(WHITESPACE_PLACEHOLDER)
200 |       ) {
201 |         result += " ";
202 |       }
203 |       result += trimmedLine;
204 |     }
205 |   }
206 | 
207 |   // Collapse multiple spaces but preserve single spaces around placeholders
208 |   result = result.replace(/\s{2,}/g, " ");
209 |   return result.trim();
210 | }
211 | 
```

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

```typescript
  1 | import { describe, it, expect, vi } from "vitest";
  2 | import { LingoDotDevEngine } from "./index";
  3 | 
  4 | describe("ReplexicaEngine", () => {
  5 |   it("should pass", () => {
  6 |     expect(1).toBe(1);
  7 |   });
  8 | 
  9 |   describe("localizeHtml", () => {
 10 |     it("should correctly extract, localize, and reconstruct HTML content", async () => {
 11 |       // Setup test HTML with various edge cases
 12 |       const inputHtml = `
 13 | <!DOCTYPE html>
 14 | <html>
 15 |   <head>
 16 |     <title>Test Page</title>
 17 |     <meta name="description" content="Page description">
 18 |   </head>
 19 |   <body>
 20 |     standalone text
 21 |     <div>
 22 |       <h1>Hello World</h1>
 23 |       <p>
 24 |         This is a paragraph with
 25 |         <a href="/test" title="Link title">a link</a>
 26 |         and an
 27 |         <img src="/test.jpg" alt="Test image">
 28 |         and some <b>bold <i>and italic</i></b> text.
 29 |       </p>
 30 |       <script>
 31 |         const doNotTranslate = "this text should be ignored";
 32 |       </script>
 33 |       <input type="text" placeholder="Enter text">
 34 |     </div>
 35 |   </body>
 36 | </html>`.trim();
 37 | 
 38 |       // Mock the internal localization method
 39 |       const engine = new LingoDotDevEngine({ apiKey: "test" });
 40 |       const mockLocalizeRaw = vi.spyOn(engine as any, "_localizeRaw");
 41 |       mockLocalizeRaw.mockImplementation(async (content: any) => {
 42 |         // Simulate translation by adding 'ES:' prefix to all strings
 43 |         return Object.fromEntries(
 44 |           Object.entries(content).map(([key, value]) => [key, `ES:${value}`]),
 45 |         );
 46 |       });
 47 | 
 48 |       // Execute the localization
 49 |       const result = await engine.localizeHtml(inputHtml, {
 50 |         sourceLocale: "en",
 51 |         targetLocale: "es",
 52 |       });
 53 | 
 54 |       // Verify the extracted content passed to _localizeRaw
 55 |       expect(mockLocalizeRaw).toHaveBeenCalledWith(
 56 |         {
 57 |           "head/0/0": "Test Page",
 58 |           "head/1#content": "Page description",
 59 |           "body/0": "standalone text",
 60 |           "body/1/0/0": "Hello World",
 61 |           "body/1/1/0": "This is a paragraph with",
 62 |           "body/1/1/1#title": "Link title",
 63 |           "body/1/1/1/0": "a link",
 64 |           "body/1/1/2": "and an",
 65 |           "body/1/1/3#alt": "Test image",
 66 |           "body/1/1/4": "and some",
 67 |           "body/1/1/5/0": "bold",
 68 |           "body/1/1/5/1/0": "and italic",
 69 |           "body/1/1/6": "text.",
 70 |           "body/1/3#placeholder": "Enter text",
 71 |         },
 72 |         {
 73 |           sourceLocale: "en",
 74 |           targetLocale: "es",
 75 |         },
 76 |         undefined,
 77 |         undefined, // AbortSignal
 78 |       );
 79 | 
 80 |       // Verify the final HTML structure
 81 |       expect(result).toContain('<html lang="es">');
 82 |       expect(result).toContain("<title>ES:Test Page</title>");
 83 |       expect(result).toContain('content="ES:Page description"');
 84 |       expect(result).toContain(">ES:standalone text<");
 85 |       expect(result).toContain("<h1>ES:Hello World</h1>");
 86 |       expect(result).toContain('title="ES:Link title"');
 87 |       expect(result).toContain('alt="ES:Test image"');
 88 |       expect(result).toContain('placeholder="ES:Enter text"');
 89 |       expect(result).toContain(
 90 |         'const doNotTranslate = "this text should be ignored"',
 91 |       );
 92 |     });
 93 |   });
 94 | 
 95 |   describe("localizeStringArray", () => {
 96 |     it("should localize an array of strings and maintain order", async () => {
 97 |       const engine = new LingoDotDevEngine({ apiKey: "test" });
 98 |       const mockLocalizeObject = vi.spyOn(engine, "localizeObject");
 99 |       mockLocalizeObject.mockImplementation(async (obj: any) => {
100 |         // Simulate translation by adding 'ES:' prefix to all string values
101 |         return Object.fromEntries(
102 |           Object.entries(obj).map(([key, value]) => [key, `ES:${value}`]),
103 |         );
104 |       });
105 | 
106 |       const inputArray = ["Hello", "Goodbye", "How are you?"];
107 | 
108 |       const result = await engine.localizeStringArray(inputArray, {
109 |         sourceLocale: "en",
110 |         targetLocale: "es",
111 |       });
112 | 
113 |       // Verify the mapped object was passed to localizeObject
114 |       expect(mockLocalizeObject).toHaveBeenCalledWith(
115 |         {
116 |           item_0: "Hello",
117 |           item_1: "Goodbye",
118 |           item_2: "How are you?",
119 |         },
120 |         {
121 |           sourceLocale: "en",
122 |           targetLocale: "es",
123 |         },
124 |       );
125 | 
126 |       // Verify the result maintains the original order
127 |       expect(result).toEqual(["ES:Hello", "ES:Goodbye", "ES:How are you?"]);
128 |       expect(result).toHaveLength(3);
129 |     });
130 | 
131 |     it("should handle empty array", async () => {
132 |       const engine = new LingoDotDevEngine({ apiKey: "test" });
133 |       const mockLocalizeObject = vi.spyOn(engine, "localizeObject");
134 |       mockLocalizeObject.mockImplementation(async () => ({}));
135 | 
136 |       const result = await engine.localizeStringArray([], {
137 |         sourceLocale: "en",
138 |         targetLocale: "es",
139 |       });
140 | 
141 |       expect(mockLocalizeObject).toHaveBeenCalledWith(
142 |         {},
143 |         {
144 |           sourceLocale: "en",
145 |           targetLocale: "es",
146 |         },
147 |       );
148 | 
149 |       expect(result).toEqual([]);
150 |     });
151 |   });
152 | 
153 |   describe("hints support", () => {
154 |     it("should send hints to the backend API", async () => {
155 |       // Mock global fetch
156 |       const mockFetch = vi.fn();
157 |       global.fetch = mockFetch as any;
158 | 
159 |       mockFetch.mockResolvedValue({
160 |         ok: true,
161 |         json: async () => ({
162 |           data: {
163 |             "brand-name": "Optimum",
164 |             "team-label": "Equipo de la NHL",
165 |           },
166 |         }),
167 |       });
168 | 
169 |       const engine = new LingoDotDevEngine({
170 |         apiKey: "test-api-key",
171 |         apiUrl: "https://test.api.url",
172 |       });
173 | 
174 |       const hints = {
175 |         "brand-name": ["This is a brand name and should not be translated"],
176 |         "team-label": ["NHL stands for National Hockey League"],
177 |       };
178 | 
179 |       await engine.localizeObject(
180 |         {
181 |           "brand-name": "Optimum",
182 |           "team-label": "NHL Team",
183 |         },
184 |         {
185 |           sourceLocale: "en",
186 |           targetLocale: "es",
187 |           hints,
188 |         },
189 |       );
190 | 
191 |       // Verify fetch was called with correct parameters
192 |       expect(mockFetch).toHaveBeenCalledTimes(1);
193 |       const fetchCall = mockFetch.mock.calls[0];
194 |       expect(fetchCall[0]).toBe("https://test.api.url/i18n");
195 | 
196 |       // Parse the request body to verify hints are included
197 |       const requestBody = JSON.parse(fetchCall[1].body);
198 |       expect(requestBody.hints).toEqual(hints);
199 |       expect(requestBody.data).toEqual({
200 |         "brand-name": "Optimum",
201 |         "team-label": "NHL Team",
202 |       });
203 |       expect(requestBody.locale).toEqual({
204 |         source: "en",
205 |         target: "es",
206 |       });
207 |     });
208 | 
209 |     it("should handle localizeObject without hints", async () => {
210 |       const mockFetch = vi.fn();
211 |       global.fetch = mockFetch as any;
212 | 
213 |       mockFetch.mockResolvedValue({
214 |         ok: true,
215 |         json: async () => ({
216 |           data: {
217 |             greeting: "Hola",
218 |           },
219 |         }),
220 |       });
221 | 
222 |       const engine = new LingoDotDevEngine({
223 |         apiKey: "test-api-key",
224 |         apiUrl: "https://test.api.url",
225 |       });
226 | 
227 |       await engine.localizeObject(
228 |         {
229 |           greeting: "Hello",
230 |         },
231 |         {
232 |           sourceLocale: "en",
233 |           targetLocale: "es",
234 |         },
235 |       );
236 | 
237 |       expect(mockFetch).toHaveBeenCalledTimes(1);
238 |       const requestBody = JSON.parse(mockFetch.mock.calls[0][1].body);
239 |       expect(requestBody.hints).toBeUndefined();
240 |     });
241 |   });
242 | });
243 | 
```

--------------------------------------------------------------------------------
/packages/cli/src/cli/localizer/explicit.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { createAnthropic } from "@ai-sdk/anthropic";
  2 | import { createGoogleGenerativeAI } from "@ai-sdk/google";
  3 | import { createOpenAI } from "@ai-sdk/openai";
  4 | import { createOpenRouter } from "@openrouter/ai-sdk-provider";
  5 | import { createMistral } from "@ai-sdk/mistral";
  6 | import { I18nConfig } from "@lingo.dev/_spec";
  7 | import chalk from "chalk";
  8 | import dedent from "dedent";
  9 | import { ILocalizer, LocalizerData } from "./_types";
 10 | import { LanguageModel, Message, generateText } from "ai";
 11 | import { colors } from "../constants";
 12 | import { jsonrepair } from "jsonrepair";
 13 | import { createOllama } from "ollama-ai-provider";
 14 | 
 15 | export default function createExplicitLocalizer(
 16 |   provider: NonNullable<I18nConfig["provider"]>,
 17 | ): ILocalizer {
 18 |   const settings = provider.settings || {};
 19 | 
 20 |   switch (provider.id) {
 21 |     default:
 22 |       throw new Error(
 23 |         dedent`
 24 |           You're trying to use unsupported provider: ${chalk.dim(provider.id)}.
 25 | 
 26 |           To fix this issue:
 27 |           1. Switch to one of the supported providers, or
 28 |           2. Remove the ${chalk.italic(
 29 |             "provider",
 30 |           )} node from your i18n.json configuration to switch to ${chalk.hex(
 31 |             colors.green,
 32 |           )("Lingo.dev")}
 33 | 
 34 |           ${chalk.hex(colors.blue)("Docs: https://lingo.dev/go/docs")}
 35 |         `,
 36 |       );
 37 |     case "openai":
 38 |       return createAiSdkLocalizer({
 39 |         factory: (params) => createOpenAI(params).languageModel(provider.model),
 40 |         id: provider.id,
 41 |         prompt: provider.prompt,
 42 |         apiKeyName: "OPENAI_API_KEY",
 43 |         baseUrl: provider.baseUrl,
 44 |         settings,
 45 |       });
 46 |     case "anthropic":
 47 |       return createAiSdkLocalizer({
 48 |         factory: (params) =>
 49 |           createAnthropic(params).languageModel(provider.model),
 50 |         id: provider.id,
 51 |         prompt: provider.prompt,
 52 |         apiKeyName: "ANTHROPIC_API_KEY",
 53 |         baseUrl: provider.baseUrl,
 54 |         settings,
 55 |       });
 56 |     case "google":
 57 |       return createAiSdkLocalizer({
 58 |         factory: (params) =>
 59 |           createGoogleGenerativeAI(params).languageModel(provider.model),
 60 |         id: provider.id,
 61 |         prompt: provider.prompt,
 62 |         apiKeyName: "GOOGLE_API_KEY",
 63 |         baseUrl: provider.baseUrl,
 64 |         settings,
 65 |       });
 66 |     case "openrouter":
 67 |       return createAiSdkLocalizer({
 68 |         factory: (params) =>
 69 |           createOpenRouter(params).languageModel(provider.model),
 70 |         id: provider.id,
 71 |         prompt: provider.prompt,
 72 |         apiKeyName: "OPENROUTER_API_KEY",
 73 |         baseUrl: provider.baseUrl,
 74 |         settings,
 75 |       });
 76 |     case "ollama":
 77 |       return createAiSdkLocalizer({
 78 |         factory: (_params) => createOllama().languageModel(provider.model),
 79 |         id: provider.id,
 80 |         prompt: provider.prompt,
 81 |         skipAuth: true,
 82 |         settings,
 83 |       });
 84 |     case "mistral":
 85 |       return createAiSdkLocalizer({
 86 |         factory: (params) =>
 87 |           createMistral(params).languageModel(provider.model),
 88 |         id: provider.id,
 89 |         prompt: provider.prompt,
 90 |         apiKeyName: "MISTRAL_API_KEY",
 91 |         baseUrl: provider.baseUrl,
 92 |         settings,
 93 |       });
 94 |   }
 95 | }
 96 | 
 97 | function createAiSdkLocalizer(params: {
 98 |   factory: (params: { apiKey?: string; baseUrl?: string }) => LanguageModel;
 99 |   id: NonNullable<I18nConfig["provider"]>["id"];
100 |   prompt: string;
101 |   apiKeyName?: string;
102 |   baseUrl?: string;
103 |   skipAuth?: boolean;
104 |   settings?: { temperature?: number };
105 | }): ILocalizer {
106 |   const skipAuth = params.skipAuth === true;
107 | 
108 |   const apiKey = process.env[params?.apiKeyName ?? ""];
109 |   if ((!skipAuth && !apiKey) || !params.apiKeyName) {
110 |     throw new Error(
111 |       dedent`
112 |         You're trying to use raw ${chalk.dim(params.id)} API for translation. ${
113 |           params.apiKeyName
114 |             ? `However, ${chalk.dim(
115 |                 params.apiKeyName,
116 |               )} environment variable is not set.`
117 |             : "However, that provider is unavailable."
118 |         }
119 | 
120 |         To fix this issue:
121 |         1. ${
122 |           params.apiKeyName
123 |             ? `Set ${chalk.dim(
124 |                 params.apiKeyName,
125 |               )} in your environment variables`
126 |             : "Set the environment variable for your provider (if required)"
127 |         }, or
128 |         2. Remove the ${chalk.italic(
129 |           "provider",
130 |         )} node from your i18n.json configuration to switch to ${chalk.hex(
131 |           colors.green,
132 |         )("Lingo.dev")}
133 | 
134 |         ${chalk.hex(colors.blue)("Docs: https://lingo.dev/go/docs")}
135 |       `,
136 |     );
137 |   }
138 | 
139 |   const model = params.factory(
140 |     skipAuth ? {} : { apiKey, baseUrl: params.baseUrl },
141 |   );
142 | 
143 |   return {
144 |     id: params.id,
145 |     checkAuth: async () => {
146 |       // For BYOK providers, auth check is not meaningful
147 |       // Configuration validation happens in validateSettings
148 |       return { authenticated: true, username: "anonymous" };
149 |     },
150 |     validateSettings: async () => {
151 |       try {
152 |         await generateText({
153 |           model,
154 |           ...params.settings,
155 |           messages: [
156 |             { role: "system", content: "You are an echo server" },
157 |             { role: "user", content: "OK" },
158 |             { role: "assistant", content: "OK" },
159 |             { role: "user", content: "OK" },
160 |           ],
161 |         });
162 | 
163 |         return { valid: true };
164 |       } catch (error) {
165 |         const errorMessage =
166 |           error instanceof Error ? error.message : String(error);
167 |         return { valid: false, error: errorMessage };
168 |       }
169 |     },
170 |     localize: async (input: LocalizerData) => {
171 |       const systemPrompt = params.prompt
172 |         .replaceAll("{source}", input.sourceLocale)
173 |         .replaceAll("{target}", input.targetLocale);
174 |       const shots = [
175 |         [
176 |           {
177 |             sourceLocale: "en",
178 |             targetLocale: "es",
179 |             data: {
180 |               message: "Hello, world!",
181 |             },
182 |           },
183 |           {
184 |             sourceLocale: "en",
185 |             targetLocale: "es",
186 |             data: {
187 |               message: "Hola, mundo!",
188 |             },
189 |           },
190 |         ],
191 |       ];
192 | 
193 |       const payload = {
194 |         sourceLocale: input.sourceLocale,
195 |         targetLocale: input.targetLocale,
196 |         data: input.processableData,
197 |       };
198 | 
199 |       const response = await generateText({
200 |         model,
201 |         ...params.settings,
202 |         messages: [
203 |           { role: "system", content: systemPrompt },
204 |           { role: "user", content: "OK" },
205 |           ...shots.flatMap(
206 |             ([userShot, assistantShot]) =>
207 |               [
208 |                 { role: "user", content: JSON.stringify(userShot) },
209 |                 { role: "assistant", content: JSON.stringify(assistantShot) },
210 |               ] as Message[],
211 |           ),
212 |           { role: "user", content: JSON.stringify(payload) },
213 |         ],
214 |       });
215 | 
216 |       const result = JSON.parse(response.text);
217 | 
218 |       // Handle both object and string responses
219 |       if (typeof result.data === "object" && result.data !== null) {
220 |         return result.data;
221 |       }
222 | 
223 |       // Handle string responses - extract and repair JSON
224 |       const index = result.data.indexOf("{");
225 |       const lastIndex = result.data.lastIndexOf("}");
226 |       const trimmed = result.data.slice(index, lastIndex + 1);
227 |       const repaired = jsonrepair(trimmed);
228 |       const finalResult = JSON.parse(repaired);
229 | 
230 |       return finalResult.data;
231 |     },
232 |   };
233 | }
234 | 
```

--------------------------------------------------------------------------------
/packages/cli/src/cli/loaders/po/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import _ from "lodash";
  2 | import gettextParser from "gettext-parser";
  3 | import { GetTextTranslations } from "gettext-parser";
  4 | import { ILoader } from "../_types";
  5 | import { composeLoaders, createLoader } from "../_utils";
  6 | 
  7 | export type PoTranslationEntry = GetTextTranslations["translations"][""];
  8 | export type PoTranslationValue = { singular: string; plural: string | null };
  9 | 
 10 | export type PoLoaderParams = {
 11 |   multiline: boolean;
 12 | };
 13 | 
 14 | export default function createPoLoader(
 15 |   params: PoLoaderParams = { multiline: false },
 16 | ): ILoader<string, Record<string, PoTranslationValue>> {
 17 |   return composeLoaders(createPoDataLoader(params), createPoContentLoader());
 18 | }
 19 | 
 20 | export function createPoDataLoader(
 21 |   params: PoLoaderParams,
 22 | ): ILoader<string, PoTranslationEntry> {
 23 |   return createLoader({
 24 |     async pull(locale, input) {
 25 |       const parsedPo = gettextParser.po.parse(input);
 26 |       const result: PoTranslationEntry = {};
 27 |       const sections = input.split("\n\n").filter(Boolean);
 28 |       for (const section of sections) {
 29 |         const sectionPo = gettextParser.po.parse(section);
 30 |         // skip section with no translations (some sections might have only obsolete entries)
 31 |         if (Object.keys(sectionPo.translations).length === 0) {
 32 |           continue;
 33 |         }
 34 | 
 35 |         const contextKey = _.keys(sectionPo.translations)[0];
 36 |         const entries = sectionPo.translations[contextKey];
 37 |         Object.entries(entries).forEach(([msgid, entry]) => {
 38 |           if (msgid && entry.msgid) {
 39 |             const context = entry.msgctxt || "";
 40 |             const fullEntry = parsedPo.translations[context]?.[msgid];
 41 |             if (fullEntry) {
 42 |               result[msgid] = fullEntry;
 43 |             }
 44 |           }
 45 |         });
 46 |       }
 47 |       return result;
 48 |     },
 49 | 
 50 |     async push(locale, data, originalInput, originalLocale, pullInput) {
 51 |       // Parse each section to maintain structure
 52 |       const currentSections = pullInput?.split("\n\n").filter(Boolean) || [];
 53 |       const originalSections =
 54 |         originalInput?.split("\n\n").filter(Boolean) || [];
 55 |       const result = originalSections
 56 |         .map((section) => {
 57 |           const sectionPo = gettextParser.po.parse(section);
 58 |           // skip section with no translations (some sections might have only obsolete entries)
 59 |           if (Object.keys(sectionPo.translations).length === 0) {
 60 |             return null;
 61 |           }
 62 | 
 63 |           const contextKey = _.keys(sectionPo.translations)[0];
 64 |           const entries = sectionPo.translations[contextKey];
 65 |           const msgid = Object.keys(entries).find((key) => entries[key].msgid);
 66 |           if (!msgid) {
 67 |             // If the section is empty, try to find it in the current sections
 68 |             const currentSection = currentSections.find((cs) => {
 69 |               const csPo = gettextParser.po.parse(cs);
 70 |               const csContextKey = _.keys(csPo.translations)[0];
 71 |               const csEntries = csPo.translations[csContextKey];
 72 |               const csMsgid = Object.keys(csEntries).find(
 73 |                 (key) => csEntries[key].msgid,
 74 |               );
 75 |               return csMsgid === msgid;
 76 |             });
 77 | 
 78 |             if (currentSection) {
 79 |               return currentSection;
 80 |             }
 81 |             return section;
 82 |           }
 83 |           if (data[msgid]) {
 84 |             const updatedPo = _.merge({}, sectionPo, {
 85 |               translations: {
 86 |                 [contextKey]: {
 87 |                   [msgid]: {
 88 |                     msgstr: data[msgid].msgstr,
 89 |                   },
 90 |                 },
 91 |               },
 92 |             });
 93 |             const updatedSection = gettextParser.po
 94 |               .compile(updatedPo, { foldLength: params.multiline ? 76 : false })
 95 |               .toString()
 96 |               .replace(
 97 |                 [`msgid ""`, `msgstr "Content-Type: text/plain\\n"`].join("\n"),
 98 |                 "",
 99 |               )
100 |               .trim();
101 |             return preserveCommentOrder(updatedSection, section);
102 |           }
103 |           return section.trim();
104 |         })
105 |         .filter(Boolean)
106 |         .join("\n\n");
107 |       return result;
108 |     },
109 |   });
110 | }
111 | 
112 | export function createPoContentLoader(): ILoader<
113 |   PoTranslationEntry,
114 |   Record<string, PoTranslationEntry>
115 | > {
116 |   return createLoader({
117 |     async pull(locale, input, initCtx, originalLocale) {
118 |       const result = _.chain(input)
119 |         .entries()
120 |         .filter(([, entry]) => !!entry.msgid)
121 |         .map(([, entry]) => {
122 |           const singularFallback =
123 |             locale === originalLocale ? entry.msgid : null;
124 |           const pluralFallback =
125 |             locale === originalLocale
126 |               ? entry.msgid_plural || entry.msgid
127 |               : null;
128 |           const hasPlural = entry.msgstr.length > 1;
129 |           return [
130 |             entry.msgid,
131 |             {
132 |               singular: entry.msgstr[0] || singularFallback,
133 |               plural: hasPlural
134 |                 ? ((entry.msgstr[1] || pluralFallback) as string | null)
135 |                 : null,
136 |             },
137 |           ];
138 |         })
139 |         .fromPairs()
140 |         .value();
141 |       return result;
142 |     },
143 |     async push(locale, data, originalInput) {
144 |       const result = _.chain(originalInput)
145 |         .entries()
146 |         .map(([, entry]) => [
147 |           entry.msgid,
148 |           {
149 |             ...entry,
150 |             msgstr: [
151 |               data[entry.msgid]?.singular,
152 |               data[entry.msgid]?.plural || null,
153 |             ].filter(Boolean),
154 |           },
155 |         ])
156 |         .fromPairs()
157 |         .value();
158 | 
159 |       return result;
160 |     },
161 |   });
162 | }
163 | 
164 | function preserveCommentOrder(section: string, originalSection: string) {
165 |   // Split both sections into lines
166 |   const sectionLines = section.split(/\r?\n/);
167 |   const originalLines = originalSection.split(/\r?\n/);
168 | 
169 |   // Helper: is a comment line
170 |   const isComment = (line: string) => line.trim().startsWith("#");
171 | 
172 |   // Extract comment lines and their indices
173 |   const sectionComments = sectionLines.filter(isComment);
174 |   const nonCommentLines = sectionLines.filter((line) => !isComment(line));
175 | 
176 |   // If there are no comments in the section, return the section as is
177 |   if (sectionComments.length <= 1) {
178 |     return section;
179 |   }
180 | 
181 |   // Extract the order of comment lines from the original section
182 |   const originalCommentOrder = originalLines.filter(isComment);
183 | 
184 |   // Build a map from comment content (trimmed) to the actual comment line in the new section
185 |   const commentMap = new Map<string, string>();
186 |   for (const line of sectionComments) {
187 |     commentMap.set(line.trim(), line);
188 |   }
189 | 
190 |   // Reorder comments to match the original order, using the new section's comment lines
191 |   const reorderedComments: string[] = [];
192 |   for (const orig of originalCommentOrder) {
193 |     const trimmed = orig.trim();
194 |     if (commentMap.has(trimmed)) {
195 |       reorderedComments.push(commentMap.get(trimmed)!);
196 |       commentMap.delete(trimmed);
197 |     }
198 |   }
199 |   // Add any new comments from the new section that weren't in the original, preserving their order
200 |   for (const line of sectionComments) {
201 |     if (!originalCommentOrder.some((orig) => orig.trim() === line.trim())) {
202 |       reorderedComments.push(line);
203 |     }
204 |   }
205 | 
206 |   // Reconstruct the section: comments (in order) + non-comment lines (in order)
207 |   return [...reorderedComments, ...nonCommentLines]
208 |     .join("\n")
209 |     .replace(/\n{3,}/g, "\n\n")
210 |     .trim();
211 | }
212 | 
```

--------------------------------------------------------------------------------
/packages/cli/src/cli/cmd/ci/flows/pull-request.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { execSync } from "child_process";
  2 | import { InBranchFlow } from "./in-branch";
  3 | import { IIntegrationFlowOptions } from "./_base";
  4 | 
  5 | export class PullRequestFlow extends InBranchFlow {
  6 |   async preRun() {
  7 |     const canContinue = await super.preRun?.();
  8 |     if (!canContinue) {
  9 |       return false;
 10 |     }
 11 | 
 12 |     this.ora.start("Calculating automated branch name");
 13 |     this.i18nBranchName = this.calculatePrBranchName();
 14 |     this.ora.succeed(
 15 |       `Automated branch name calculated: ${this.i18nBranchName}`,
 16 |     );
 17 | 
 18 |     this.ora.start("Checking if branch exists");
 19 |     const branchExists = await this.checkBranchExistance(this.i18nBranchName);
 20 |     this.ora.succeed(branchExists ? "Branch exists" : "Branch does not exist");
 21 | 
 22 |     if (branchExists) {
 23 |       this.ora.start(`Checking out branch ${this.i18nBranchName}`);
 24 |       this.checkoutI18nBranch(this.i18nBranchName);
 25 |       this.ora.succeed(`Checked out branch ${this.i18nBranchName}`);
 26 | 
 27 |       this.ora.start(
 28 |         `Syncing with ${this.platformKit.platformConfig.baseBranchName}`,
 29 |       );
 30 |       this.syncI18nBranch();
 31 |       this.ora.succeed(`Checked out and synced branch ${this.i18nBranchName}`);
 32 |     } else {
 33 |       this.ora.start(`Creating branch ${this.i18nBranchName}`);
 34 |       this.createI18nBranch(this.i18nBranchName);
 35 |       this.ora.succeed(`Created branch ${this.i18nBranchName}`);
 36 |     }
 37 | 
 38 |     return true;
 39 |   }
 40 | 
 41 |   override async run(options: IIntegrationFlowOptions) {
 42 |     return super.run({
 43 |       force: true,
 44 |       ...options,
 45 |     });
 46 |   }
 47 | 
 48 |   async postRun() {
 49 |     if (!this.i18nBranchName) {
 50 |       throw new Error(
 51 |         "i18nBranchName is not set. Did you forget to call preRun?",
 52 |       );
 53 |     }
 54 | 
 55 |     this.ora.start("Checking if PR already exists");
 56 |     const pullRequestNumber = await this.ensureFreshPr(this.i18nBranchName);
 57 |     // await this.createLabelIfNotExists(pullRequestNumber, 'lingo.dev/i18n', false);
 58 |     this.ora.succeed(
 59 |       `Pull request ready: ${this.platformKit.buildPullRequestUrl(
 60 |         pullRequestNumber,
 61 |       )}`,
 62 |     );
 63 |   }
 64 | 
 65 |   private calculatePrBranchName(): string {
 66 |     return `lingo.dev/${this.platformKit.platformConfig.baseBranchName}`;
 67 |   }
 68 | 
 69 |   private async checkBranchExistance(prBranchName: string) {
 70 |     return this.platformKit.branchExists({
 71 |       branch: prBranchName,
 72 |     });
 73 |   }
 74 | 
 75 |   private async ensureFreshPr(i18nBranchName: string) {
 76 |     // Check if PR exists
 77 |     this.ora.start(
 78 |       `Checking for existing PR with head ${i18nBranchName} and base ${this.platformKit.platformConfig.baseBranchName}`,
 79 |     );
 80 |     let prNumber = await this.platformKit.getOpenPullRequestNumber({
 81 |       branch: i18nBranchName,
 82 |     });
 83 | 
 84 |     if (prNumber) {
 85 |       this.ora.succeed(`Existing PR found: #${prNumber}`);
 86 |     } else {
 87 |       // Create new PR
 88 |       this.ora.start(`Creating new PR`);
 89 |       prNumber = await this.platformKit.createPullRequest({
 90 |         head: i18nBranchName,
 91 |         title: this.platformKit.config.pullRequestTitle,
 92 |         body: this.getPrBodyContent(),
 93 |       });
 94 |       this.ora.succeed(`Created new PR: #${prNumber}`);
 95 |     }
 96 | 
 97 |     return prNumber;
 98 |   }
 99 | 
100 |   private checkoutI18nBranch(i18nBranchName: string) {
101 |     execSync(`git fetch origin ${i18nBranchName}`, { stdio: "inherit" });
102 |     execSync(`git checkout -b ${i18nBranchName}`, {
103 |       stdio: "inherit",
104 |     });
105 |   }
106 | 
107 |   private createI18nBranch(i18nBranchName: string) {
108 |     try {
109 |       execSync(
110 |         `git fetch origin ${this.platformKit.platformConfig.baseBranchName}`,
111 |         { stdio: "inherit" },
112 |       );
113 |       execSync(
114 |         `git checkout -b ${i18nBranchName} origin/${this.platformKit.platformConfig.baseBranchName}`,
115 |         {
116 |           stdio: "inherit",
117 |         },
118 |       );
119 |     } catch (error) {
120 |       const errorMessage =
121 |         error instanceof Error ? error.message : "Unknown error occurred";
122 |       this.ora.fail(`Failed to create branch: ${errorMessage}`);
123 |       this.ora.info(`
124 |       Troubleshooting tips:
125 |       1. Make sure you have permission to create branches
126 |       2. Check if the branch already exists locally (try 'git branch -a')
127 |       3. Verify connectivity to remote repository
128 |     `);
129 |       throw new Error(`Branch creation failed: ${errorMessage}`);
130 |     }
131 |   }
132 | 
133 |   private syncI18nBranch() {
134 |     if (!this.i18nBranchName) {
135 |       throw new Error("i18nBranchName is not set");
136 |     }
137 | 
138 |     this.ora.start(
139 |       `Fetching latest changes from ${this.platformKit.platformConfig.baseBranchName}`,
140 |     );
141 |     execSync(
142 |       `git fetch origin ${this.platformKit.platformConfig.baseBranchName}`,
143 |       { stdio: "inherit" },
144 |     );
145 |     this.ora.succeed(
146 |       `Fetched latest changes from ${this.platformKit.platformConfig.baseBranchName}`,
147 |     );
148 | 
149 |     try {
150 |       this.ora.start("Attempting to rebase branch");
151 |       execSync(
152 |         `git rebase origin/${this.platformKit.platformConfig.baseBranchName}`,
153 |         { stdio: "inherit" },
154 |       );
155 |       this.ora.succeed("Successfully rebased branch");
156 |     } catch (error) {
157 |       this.ora.warn("Rebase failed, falling back to alternative sync method");
158 | 
159 |       this.ora.start("Aborting failed rebase");
160 |       execSync("git rebase --abort", { stdio: "inherit" });
161 |       this.ora.succeed("Aborted failed rebase");
162 | 
163 |       this.ora.start(
164 |         `Resetting to ${this.platformKit.platformConfig.baseBranchName}`,
165 |       );
166 |       execSync(
167 |         `git reset --hard origin/${this.platformKit.platformConfig.baseBranchName}`,
168 |         { stdio: "inherit" },
169 |       );
170 |       this.ora.succeed(
171 |         `Reset to ${this.platformKit.platformConfig.baseBranchName}`,
172 |       );
173 | 
174 |       this.ora.start("Restoring target files");
175 |       const targetFiles = ["i18n.lock"];
176 |       const targetFileNames = execSync(
177 |         `npx lingo.dev@latest show files --target ${this.platformKit.platformConfig.baseBranchName}`,
178 |         { encoding: "utf8" },
179 |       )
180 |         .split("\n")
181 |         .filter(Boolean);
182 |       targetFiles.push(...targetFileNames);
183 |       execSync(`git fetch origin ${this.i18nBranchName}`, { stdio: "inherit" });
184 |       for (const file of targetFiles) {
185 |         try {
186 |           // bring all files to the i18n branch's state
187 |           execSync(`git checkout FETCH_HEAD -- ${file}`, { stdio: "inherit" });
188 |         } catch (error) {
189 |           // If file doesn't exist in FETCH_HEAD, that's okay - just skip it
190 |           this.ora.warn(`Skipping non-existent file: ${file}`);
191 |           continue;
192 |         }
193 |       }
194 |       this.ora.succeed("Restored target files");
195 |     }
196 | 
197 |     this.ora.start("Checking for changes to commit");
198 |     const hasChanges = this.checkCommitableChanges();
199 |     if (hasChanges) {
200 |       execSync("git add .", { stdio: "inherit" });
201 |       execSync(
202 |         `git commit -m "chore: sync with ${this.platformKit.platformConfig.baseBranchName}" --no-verify`,
203 |         {
204 |           stdio: "inherit",
205 |         },
206 |       );
207 |       this.ora.succeed("Committed additional changes");
208 |     } else {
209 |       this.ora.succeed("No changes to commit");
210 |     }
211 |   }
212 | 
213 |   private getPrBodyContent(): string {
214 |     return `
215 | Hey team,
216 | 
217 | [**Lingo.dev**](https://lingo.dev) here with fresh translations!
218 | 
219 | ### In this update
220 | 
221 | - Added missing translations
222 | - Performed brand voice, context and glossary checks
223 | - Enhanced translations using Lingo.dev Localization Engine
224 | 
225 | ### Next Steps
226 | 
227 | - [ ] Review the changes
228 | - [ ] Merge when ready
229 |     `.trim();
230 |   }
231 | }
232 | 
```

--------------------------------------------------------------------------------
/packages/cli/src/cli/loaders/locked-patterns.spec.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect } from "vitest";
  2 | import createLockedPatternsLoader from "./locked-patterns";
  3 | import dedent from "dedent";
  4 | 
  5 | describe("Locked Patterns Loader", () => {
  6 |   describe("Basic functionality", () => {
  7 |     it("should do nothing when no patterns are provided", async () => {
  8 |       const loader = createLockedPatternsLoader();
  9 |       loader.setDefaultLocale("en");
 10 | 
 11 |       const md = dedent`
 12 |         # Title
 13 |         
 14 |         Some content.
 15 |         
 16 |         !params
 17 |         
 18 |         !! parameter_name
 19 |         
 20 |         !type string
 21 |       `;
 22 | 
 23 |       const result = await loader.pull("en", md);
 24 | 
 25 |       const placeholderRegex = /---LOCKED-PATTERN-[0-9a-f]+---/g;
 26 |       const placeholders = result.match(placeholderRegex) || [];
 27 |       expect(placeholders.length).toBe(0); // No patterns should be replaced
 28 | 
 29 |       expect(result).toBe(md);
 30 | 
 31 |       const pushed = await loader.push("es", result);
 32 |       expect(pushed).toBe(md);
 33 |     });
 34 | 
 35 |     it("should preserve content matching patterns", async () => {
 36 |       const loader = createLockedPatternsLoader([
 37 |         "!params",
 38 |         "!! [\\w_]+",
 39 |         "!type [\\w<>\\[\\]\"',]+",
 40 |       ]);
 41 |       loader.setDefaultLocale("en");
 42 | 
 43 |       const md = dedent`
 44 |         # Title
 45 |         
 46 |         Some content.
 47 |         
 48 |         !params
 49 |         
 50 |         !! parameter_name
 51 |         
 52 |         !type string
 53 |       `;
 54 | 
 55 |       const result = await loader.pull("en", md);
 56 | 
 57 |       const placeholderRegex = /---LOCKED-PATTERN-[0-9a-f]+---/g;
 58 |       const placeholders = result.match(placeholderRegex) || [];
 59 |       expect(placeholders.length).toBe(3); // Three patterns should be replaced
 60 | 
 61 |       const sanitizedContent = result.replace(
 62 |         placeholderRegex,
 63 |         "---PLACEHOLDER---",
 64 |       );
 65 | 
 66 |       const expectedSanitized = dedent`
 67 |         # Title
 68 |         
 69 |         Some content.
 70 |         
 71 |         ---PLACEHOLDER---
 72 |         
 73 |         ---PLACEHOLDER---
 74 |         
 75 |         ---PLACEHOLDER---
 76 |       `;
 77 | 
 78 |       expect(sanitizedContent).toBe(expectedSanitized);
 79 | 
 80 |       const translated = result
 81 |         .replace("# Title", "# Título")
 82 |         .replace("Some content.", "Algún contenido.");
 83 | 
 84 |       const pushed = await loader.push("es", translated);
 85 | 
 86 |       const expectedPushed = dedent`
 87 |         # Título
 88 |         
 89 |         Algún contenido.
 90 |         
 91 |         !params
 92 |         
 93 |         !! parameter_name
 94 |         
 95 |         !type string
 96 |       `;
 97 | 
 98 |       expect(pushed).toBe(expectedPushed);
 99 |     });
100 |   });
101 | 
102 |   describe("Real-world patterns", () => {
103 |     it("should handle !hover syntax in code blocks", async () => {
104 |       const loader = createLockedPatternsLoader([
105 |         "// !hover[\\s\\S]*?(?=\\n|$)",
106 |         "// !hover\\([\\d:]+\\)[\\s\\S]*?(?=\\n|$)",
107 |       ]);
108 |       loader.setDefaultLocale("en");
109 | 
110 |       const md = dedent`
111 |         \`\`\`js
112 |         const x = 1;
113 |         const pubkey = "vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg";
114 |         \`\`\`
115 |       `;
116 | 
117 |       const result = await loader.pull("en", md);
118 | 
119 |       const placeholderRegex = /---LOCKED-PATTERN-[0-9a-f]+---/g;
120 |       const placeholders = result.match(placeholderRegex) || [];
121 |       expect(placeholders.length).toBe(0); // No patterns should be replaced
122 | 
123 |       const pushed = await loader.push("es", result);
124 | 
125 |       expect(pushed).toBe(md);
126 |     });
127 | 
128 |     it("should handle !! parameter headings", async () => {
129 |       const loader = createLockedPatternsLoader(["!! [\\w_]+"]);
130 |       loader.setDefaultLocale("en");
131 | 
132 |       const md = dedent`
133 |         # Parameters
134 |         
135 |         !! pubkey
136 |         
137 |         The public key of the account to query.
138 |         
139 |         !! encoding
140 |         
141 |         Encoding format for the returned Account data.
142 |       `;
143 | 
144 |       const result = await loader.pull("en", md);
145 | 
146 |       const placeholderRegex = /---LOCKED-PATTERN-[0-9a-f]+---/g;
147 |       const placeholders = result.match(placeholderRegex) || [];
148 |       expect(placeholders.length).toBe(2); // Two patterns should be replaced
149 | 
150 |       const sanitizedContent = result.replace(
151 |         placeholderRegex,
152 |         "---PLACEHOLDER---",
153 |       );
154 | 
155 |       const expectedSanitized = dedent`
156 |         # Parameters
157 |         
158 |         ---PLACEHOLDER---
159 |         
160 |         The public key of the account to query.
161 |         
162 |         ---PLACEHOLDER---
163 |         
164 |         Encoding format for the returned Account data.
165 |       `;
166 | 
167 |       expect(sanitizedContent).toBe(expectedSanitized);
168 | 
169 |       const translated = result
170 |         .replace("# Parameters", "# Parámetros")
171 |         .replace(
172 |           "The public key of the account to query.",
173 |           "La clave pública de la cuenta a consultar.",
174 |         )
175 |         .replace(
176 |           "Encoding format for the returned Account data.",
177 |           "Formato de codificación para los datos de la cuenta devueltos.",
178 |         );
179 | 
180 |       const pushed = await loader.push("es", translated);
181 | 
182 |       const expectedPushed = dedent`
183 |         # Parámetros
184 |         
185 |         !! pubkey
186 |         
187 |         La clave pública de la cuenta a consultar.
188 |         
189 |         !! encoding
190 |         
191 |         Formato de codificación para los datos de la cuenta devueltos.
192 |       `;
193 | 
194 |       expect(pushed).toBe(expectedPushed);
195 |     });
196 | 
197 |     it("should handle !type, !required, and !values declarations", async () => {
198 |       const loader = createLockedPatternsLoader([
199 |         "!! [\\w_]+",
200 |         "!type [\\w<>\\[\\]\"',]+",
201 |         "!required",
202 |         "!values [\\s\\S]*?(?=\\n\\n|$)",
203 |       ]);
204 |       loader.setDefaultLocale("en");
205 | 
206 |       const md = dedent`
207 |         !! pubkey
208 |         
209 |         !type string
210 |         !required
211 |         
212 |         The public key of the account to query.
213 |         
214 |         !! encoding
215 |         
216 |         !type string
217 |         !values "base58" (default), "base64", "jsonParsed"
218 |         
219 |         Encoding format for the returned Account data.
220 |       `;
221 | 
222 |       const result = await loader.pull("en", md);
223 | 
224 |       const placeholderRegex = /---LOCKED-PATTERN-[0-9a-f]+---/g;
225 |       const placeholders = result.match(placeholderRegex) || [];
226 |       expect(placeholders.length).toBe(6); // Six patterns should be replaced
227 | 
228 |       const sanitizedContent = result.replace(
229 |         placeholderRegex,
230 |         "---PLACEHOLDER---",
231 |       );
232 | 
233 |       const expectedSanitized = dedent`
234 |         ---PLACEHOLDER---
235 |         
236 |         ---PLACEHOLDER---
237 |         ---PLACEHOLDER---
238 |         
239 |         The public key of the account to query.
240 |         
241 |         ---PLACEHOLDER---
242 |         
243 |         ---PLACEHOLDER---
244 |         ---PLACEHOLDER---
245 |         
246 |         Encoding format for the returned Account data.
247 |       `;
248 | 
249 |       expect(sanitizedContent).toBe(expectedSanitized);
250 | 
251 |       const translated = result
252 |         .replace(
253 |           "The public key of the account to query.",
254 |           "La clave pública de la cuenta a consultar.",
255 |         )
256 |         .replace(
257 |           "Encoding format for the returned Account data.",
258 |           "Formato de codificación para los datos de la cuenta devueltos.",
259 |         );
260 | 
261 |       const pushed = await loader.push("es", translated);
262 | 
263 |       const expectedPushed = dedent`
264 |         !! pubkey
265 |         
266 |         !type string
267 |         !required
268 |         
269 |         La clave pública de la cuenta a consultar.
270 |         
271 |         !! encoding
272 |         
273 |         !type string
274 |         !values "base58" (default), "base64", "jsonParsed"
275 |         
276 |         Formato de codificación para los datos de la cuenta devueltos.
277 |       `;
278 | 
279 |       expect(pushed).toBe(expectedPushed);
280 |     });
281 |   });
282 | });
283 | 
```

--------------------------------------------------------------------------------
/packages/cli/src/cli/loaders/dato/api.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import _ from "lodash";
  2 | import { ILoader } from "../_types";
  3 | import { createLoader } from "../_utils";
  4 | import createDatoClient, { DatoClient } from "./_utils";
  5 | import { SimpleSchemaTypes } from "@datocms/cma-client-node";
  6 | import { DatoConfig } from "./_base";
  7 | import inquirer from "inquirer";
  8 | 
  9 | export type DatoApiLoaderOutput = {
 10 |   [modelId: string]: {
 11 |     fields: SimpleSchemaTypes.Field[];
 12 |     records: SimpleSchemaTypes.Item[];
 13 |   };
 14 | };
 15 | 
 16 | export type DatoApiLoaderCtx = {
 17 |   models: {
 18 |     [modelId: string]: {
 19 |       fields: SimpleSchemaTypes.Field[];
 20 |       records: SimpleSchemaTypes.Item[];
 21 |     };
 22 |   };
 23 | };
 24 | 
 25 | export default function createDatoApiLoader(
 26 |   config: DatoConfig,
 27 |   onConfigUpdate: (config: DatoConfig) => void,
 28 | ): ILoader<void, DatoApiLoaderOutput, DatoApiLoaderCtx> {
 29 |   const dato = createDatoClient({
 30 |     apiKey: process.env.DATO_API_TOKEN || "",
 31 |     projectId: config.project,
 32 |   });
 33 |   return createLoader({
 34 |     init: async () => {
 35 |       const result: DatoApiLoaderCtx = {
 36 |         models: {},
 37 |       };
 38 |       const updatedConfig = _.cloneDeep(config);
 39 |       console.log(`Initializing DatoCMS loader...`);
 40 | 
 41 |       const project = await dato.findProject();
 42 |       const modelChoices = await getModelChoices(dato, config);
 43 |       const selectedModels = await promptModelSelection(modelChoices);
 44 | 
 45 |       for (const modelId of selectedModels) {
 46 |         if (!updatedConfig.models[modelId]) {
 47 |           updatedConfig.models[modelId] = {
 48 |             fields: [],
 49 |             records: [],
 50 |           };
 51 |         }
 52 |       }
 53 | 
 54 |       for (const modelId of Object.keys(updatedConfig.models)) {
 55 |         if (!selectedModels.includes(modelId)) {
 56 |           delete updatedConfig.models[modelId];
 57 |         }
 58 |       }
 59 | 
 60 |       for (const modelId of _.keys(updatedConfig.models)) {
 61 |         const { modelName, fields } = await getModelFields(dato, modelId);
 62 | 
 63 |         if (fields.length > 0) {
 64 |           result.models[modelId] = { fields: [], records: [] };
 65 | 
 66 |           const fieldInfos = await getFieldDetails(dato, fields);
 67 |           const fieldChoices = createFieldChoices(fieldInfos);
 68 |           const selectedFields = await promptFieldSelection(
 69 |             modelName,
 70 |             fieldChoices,
 71 |           );
 72 | 
 73 |           for (const fieldInfo of fieldInfos) {
 74 |             const isLocalized = await updateFieldLocalization(
 75 |               dato,
 76 |               fieldInfo,
 77 |               selectedFields.includes(fieldInfo.id),
 78 |             );
 79 |             if (isLocalized) {
 80 |               result.models[modelId].fields.push(fieldInfo);
 81 |               updatedConfig.models[modelId].fields = _.uniq([
 82 |                 ...(updatedConfig.models[modelId].fields || []),
 83 |                 fieldInfo.api_key,
 84 |               ]);
 85 |             }
 86 |           }
 87 | 
 88 |           const records = await dato.findRecordsForModel(modelId);
 89 |           const recordChoices = createRecordChoices(
 90 |             records,
 91 |             config.models[modelId]?.records || [],
 92 |             project,
 93 |           );
 94 |           const selectedRecords = await promptRecordSelection(
 95 |             modelName,
 96 |             recordChoices,
 97 |           );
 98 | 
 99 |           result.models[modelId].records = records.filter((record) =>
100 |             selectedRecords.includes(record.id),
101 |           );
102 |           updatedConfig.models[modelId].records = selectedRecords;
103 |         }
104 |       }
105 |       console.log(`DatoCMS loader initialized.`);
106 |       onConfigUpdate(updatedConfig);
107 |       return result;
108 |     },
109 |     async pull(locale, input, initCtx) {
110 |       const result: DatoApiLoaderOutput = {};
111 | 
112 |       for (const modelId of _.keys(initCtx?.models || {})) {
113 |         let records = initCtx?.models[modelId].records || [];
114 |         const recordIds = records.map((record) => record.id);
115 |         records = await dato.findRecords(recordIds);
116 |         console.log(`Fetched ${records.length} records for model ${modelId}`);
117 | 
118 |         if (records.length > 0) {
119 |           result[modelId] = {
120 |             fields: initCtx?.models?.[modelId]?.fields || [],
121 |             records: records,
122 |           };
123 |         }
124 |       }
125 |       return result;
126 |     },
127 |     async push(locale, data, originalInput) {
128 |       for (const modelId of _.keys(data)) {
129 |         for (let i = 0; i < data[modelId].records.length; i++) {
130 |           const record = data[modelId].records[i];
131 |           console.log(
132 |             `Updating record ${i + 1}/${
133 |               data[modelId].records.length
134 |             } for model ${modelId}...`,
135 |           );
136 |           await dato.updateRecord(record.id, record);
137 |         }
138 |       }
139 |     },
140 |   });
141 | }
142 | 
143 | export async function getModelFields(dato: any, modelId: string) {
144 |   const modelInfo = await dato.findModel(modelId);
145 |   return {
146 |     modelName: modelInfo.name,
147 |     fields: _.filter(modelInfo.fields, (field) => field.type === "field"),
148 |   };
149 | }
150 | 
151 | export async function getFieldDetails(
152 |   dato: DatoClient,
153 |   fields: SimpleSchemaTypes.Field[],
154 | ) {
155 |   return Promise.all(fields.map((field) => dato.findField(field.id)));
156 | }
157 | 
158 | export function createFieldChoices(fieldInfos: SimpleSchemaTypes.Field[]) {
159 |   return fieldInfos.map((field) => ({
160 |     name: field.label,
161 |     value: field.id,
162 |     checked: field.localized,
163 |   }));
164 | }
165 | 
166 | export async function promptFieldSelection(modelName: string, choices: any[]) {
167 |   const { selectedFields } = await inquirer.prompt([
168 |     {
169 |       type: "checkbox",
170 |       name: "selectedFields",
171 |       message: `Select fields to enable localization for model "${modelName}":`,
172 |       choices,
173 |       pageSize: process.stdout.rows - 4, // Subtract some rows for prompt text and margins
174 |     },
175 |   ]);
176 |   return selectedFields;
177 | }
178 | 
179 | export async function updateFieldLocalization(
180 |   dato: any,
181 |   fieldInfo: SimpleSchemaTypes.Field,
182 |   shouldBeLocalized: boolean,
183 | ) {
184 |   if (shouldBeLocalized !== fieldInfo.localized) {
185 |     console.log(
186 |       `${shouldBeLocalized ? "Enabling" : "Disabling"} localization for ${
187 |         fieldInfo.label
188 |       }...`,
189 |     );
190 |     await dato.updateField(fieldInfo.id, { localized: shouldBeLocalized });
191 |   }
192 |   return shouldBeLocalized;
193 | }
194 | 
195 | export function createRecordChoices(
196 |   records: SimpleSchemaTypes.Item[],
197 |   selectedIds: string[] = [],
198 |   project: SimpleSchemaTypes.Site,
199 | ) {
200 |   return records.map((record) => ({
201 |     name: `${record.id} - https://${project.internal_domain}/editor/item_types/${record.item_type.id}/items/${record.id}`,
202 |     value: record.id,
203 |     checked: selectedIds?.includes(record.id),
204 |   }));
205 | }
206 | 
207 | export async function promptRecordSelection(modelName: string, choices: any[]) {
208 |   const { selectedRecords } = await inquirer.prompt([
209 |     {
210 |       type: "checkbox",
211 |       name: "selectedRecords",
212 |       message: `Select records to include for model "${modelName}":`,
213 |       choices,
214 |       pageSize: process.stdout.rows - 4, // Subtract some rows for prompt text and margins
215 |     },
216 |   ]);
217 |   return selectedRecords;
218 | }
219 | 
220 | export async function getModelChoices(dato: DatoClient, config: DatoConfig) {
221 |   const models = await dato.findModels();
222 |   return models.map((model) => ({
223 |     name: `${model.name} (${model.api_key})`,
224 |     value: model.id,
225 |     checked: config.models[model.id] !== undefined,
226 |     pageSize: process.stdout.rows - 4, // Subtract some rows for prompt text and margins
227 |   }));
228 | }
229 | 
230 | export async function promptModelSelection(choices: any[]) {
231 |   const { selectedModels } = await inquirer.prompt([
232 |     {
233 |       type: "checkbox",
234 |       name: "selectedModels",
235 |       message: "Select models to include:",
236 |       choices,
237 |       pageSize: process.stdout.rows - 4, // Subtract some rows for prompt text and margins
238 |     },
239 |   ]);
240 |   return selectedModels;
241 | }
242 | 
```

--------------------------------------------------------------------------------
/packages/react/src/core/component.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import {
  2 |   createElement,
  3 |   ReactNode,
  4 |   FunctionComponent,
  5 |   ReactElement,
  6 | } from "react";
  7 | import _ from "lodash";
  8 | import React, { useMemo } from "react";
  9 | 
 10 | export type LingoComponentProps = {
 11 |   [key: string]: any;
 12 |   $dictionary: any;
 13 |   $as: any;
 14 |   $fileKey: string;
 15 |   $entryKey: string;
 16 |   $values?: Record<string, ReactNode>;
 17 |   $elements?: Array<FunctionComponent<any>>;
 18 |   $functions?: Record<string, ReactNode[]>;
 19 |   $expressions?: ReactNode[];
 20 | };
 21 | 
 22 | export const LingoComponent = React.forwardRef(
 23 |   (props: Omit<LingoComponentProps, "ref">, ref: React.Ref<any>) => {
 24 |     const {
 25 |       $dictionary,
 26 |       $as,
 27 |       $fileKey,
 28 |       $entryKey,
 29 |       $variables,
 30 |       $elements,
 31 |       $functions,
 32 |       $expressions,
 33 |       ...rest
 34 |     } = props;
 35 |     const maybeValue = $dictionary?.files?.[$fileKey]?.entries?.[$entryKey];
 36 | 
 37 |     const children = useMemo(() => {
 38 |       return _.flow([
 39 |         (nodes) => ifNotEmpty(replaceElements, $elements, nodes),
 40 |         (nodes) => ifNotEmpty(replaceVariables, $variables, nodes),
 41 |         (nodes) => ifNotEmpty(replaceFunctions, $functions, nodes),
 42 |         (nodes) => ifNotEmpty(replaceExpressions, $expressions, nodes),
 43 |       ])([maybeValue ?? $entryKey]);
 44 |     }, [
 45 |       maybeValue,
 46 |       $entryKey,
 47 |       $elements,
 48 |       $variables,
 49 |       $functions,
 50 |       $expressions,
 51 |     ]);
 52 | 
 53 |     const isFragment = $as.toString() === "Symbol(react.fragment)";
 54 |     const isLingoComponent =
 55 |       typeof $as === "function" &&
 56 |       ($as.name === "LingoComponent" || $as.name === "LingoAttributeComponent");
 57 | 
 58 |     const elementProps = {
 59 |       ...rest,
 60 |       ...(isLingoComponent ? { $fileKey } : {}),
 61 |       ...(isFragment ? {} : { ref }),
 62 |     };
 63 | 
 64 |     return createElement($as, elementProps, ...children);
 65 |   },
 66 | );
 67 | 
 68 | // testValue needs to be cloned before passing to the callback for the first time only
 69 | // it can not be cloned inside the callback because it is called recursively
 70 | function ifNotEmpty<T>(
 71 |   callback: (nodes: ReactNode[], value: T) => ReactNode[],
 72 |   testValue: T,
 73 |   nodes: ReactNode[],
 74 | ): ReactNode[] {
 75 |   return callback(nodes, _.clone(testValue));
 76 | }
 77 | 
 78 | function replaceVariables(
 79 |   nodes: ReactNode[],
 80 |   variables: Record<string, ReactNode>,
 81 | ): ReactNode[] {
 82 |   if (_.isEmpty(variables)) {
 83 |     return nodes;
 84 |   }
 85 |   const segments = nodes.map((node) => {
 86 |     if (typeof node === "string") {
 87 |       const segments: ReactNode[] = [];
 88 |       let lastIndex = 0;
 89 |       const variableRegex = /{([\w\.\[\]]+)}/g;
 90 |       let match;
 91 | 
 92 |       while ((match = variableRegex.exec(node)) !== null) {
 93 |         if (match.index > lastIndex) {
 94 |           segments.push(node.slice(lastIndex, match.index));
 95 |         }
 96 | 
 97 |         const [fullMatch, name] = match;
 98 |         const value = variables[name];
 99 |         segments.push(value ?? fullMatch);
100 | 
101 |         lastIndex = match.index + fullMatch.length;
102 |       }
103 | 
104 |       if (lastIndex < node.length) {
105 |         segments.push(node.slice(lastIndex));
106 |       }
107 | 
108 |       return segments;
109 |     } else if (isReactElement(node)) {
110 |       const props = node.props as { children?: ReactNode };
111 |       return createElement(
112 |         node.type,
113 |         { ...props },
114 |         ...replaceVariables(_.castArray(props.children || []), variables),
115 |       );
116 |     }
117 |     return node;
118 |   });
119 | 
120 |   return _.flatMap(segments);
121 | }
122 | 
123 | function isReactElement(node: ReactNode): node is ReactElement {
124 |   return (
125 |     node !== null &&
126 |     typeof node === "object" &&
127 |     "type" in node &&
128 |     "props" in node
129 |   );
130 | }
131 | 
132 | function replaceElements(
133 |   nodes: ReactNode[],
134 |   elements?: Array<FunctionComponent>,
135 |   elementIndex: { current: number } = { current: 0 },
136 | ): ReactNode[] {
137 |   const ELEMENT_PATTERN = /<element:([\w.]+)>(.*?)<\/element:\1>/gs;
138 | 
139 |   if (_.isEmpty(elements)) {
140 |     return nodes.map((node) => {
141 |       if (typeof node !== "string") return node;
142 | 
143 |       return node.replace(ELEMENT_PATTERN, (match, elementName, content) => {
144 |         return content;
145 |       });
146 |     });
147 |   }
148 | 
149 |   return nodes
150 |     .map((node) => {
151 |       if (typeof node !== "string") return node;
152 | 
153 |       const segments: ReactNode[] = [];
154 |       let lastIndex = 0;
155 | 
156 |       let match;
157 | 
158 |       while ((match = ELEMENT_PATTERN.exec(node)) !== null) {
159 |         if (match.index > lastIndex) {
160 |           segments.push(node.slice(lastIndex, match.index));
161 |         }
162 | 
163 |         const [fullMatch, elementName, content] = match;
164 |         const Element = elements?.[elementIndex.current];
165 |         elementIndex.current++;
166 | 
167 |         const innerContent = replaceElements([content], elements, elementIndex);
168 |         if (Element) {
169 |           segments.push(createElement(Element, {}, ...innerContent));
170 |         } else {
171 |           segments.push(...innerContent);
172 |         }
173 | 
174 |         lastIndex = match.index + fullMatch.length;
175 |       }
176 | 
177 |       if (lastIndex < node.length) {
178 |         segments.push(node.slice(lastIndex));
179 |       }
180 | 
181 |       return segments;
182 |     })
183 |     .flat();
184 | }
185 | 
186 | function replaceFunctions(
187 |   nodes: ReactNode[],
188 |   functions: Record<string, ReactNode[]>,
189 | ): ReactNode[] {
190 |   if (_.isEmpty(functions)) {
191 |     return nodes;
192 |   }
193 | 
194 |   const functionIndices: Record<string, number> = {};
195 | 
196 |   return nodes
197 |     .map((node) => {
198 |       if (typeof node === "string") {
199 |         const segments: ReactNode[] = [];
200 |         let lastIndex = 0;
201 |         const functionRegex = /<function:([\w\.]+)\/>/g;
202 |         let match;
203 | 
204 |         while ((match = functionRegex.exec(node)) !== null) {
205 |           if (match.index > lastIndex) {
206 |             segments.push(node.slice(lastIndex, match.index));
207 |           }
208 | 
209 |           const [fullMatch, name] = match;
210 |           if (!functionIndices[name]) {
211 |             functionIndices[name] = 0;
212 |           }
213 |           const value = functions[name]?.[functionIndices[name]++];
214 |           segments.push(value ?? fullMatch);
215 | 
216 |           lastIndex = match.index + fullMatch.length;
217 |         }
218 | 
219 |         if (lastIndex < node.length) {
220 |           segments.push(node.slice(lastIndex));
221 |         }
222 | 
223 |         return segments;
224 |       } else if (isReactElement(node)) {
225 |         const props = node.props as { children?: ReactNode };
226 |         return createElement(
227 |           node.type,
228 |           { ...props },
229 |           ...replaceFunctions(_.castArray(props.children || []), functions),
230 |         );
231 |       }
232 |       return node;
233 |     })
234 |     .flat();
235 | }
236 | 
237 | function replaceExpressions(
238 |   nodes: ReactNode[],
239 |   expressions: ReactNode[],
240 | ): ReactNode[] {
241 |   if (_.isEmpty(expressions)) {
242 |     return nodes;
243 |   }
244 | 
245 |   let expressionIndex = 0;
246 | 
247 |   function processWithIndex(nodeList: ReactNode[]): ReactNode[] {
248 |     return nodeList
249 |       .map((node) => {
250 |         if (typeof node === "string") {
251 |           const segments: ReactNode[] = [];
252 |           let lastIndex = 0;
253 |           const expressionRegex = /<expression\/>/g;
254 |           let match;
255 | 
256 |           while ((match = expressionRegex.exec(node)) !== null) {
257 |             if (match.index > lastIndex) {
258 |               segments.push(node.slice(lastIndex, match.index));
259 |             }
260 | 
261 |             const value = expressions[expressionIndex++];
262 |             segments.push(value ?? match[0]);
263 | 
264 |             lastIndex = match.index + match[0].length;
265 |           }
266 | 
267 |           if (lastIndex < node.length) {
268 |             segments.push(node.slice(lastIndex));
269 |           }
270 | 
271 |           return segments;
272 |         } else if (isReactElement(node)) {
273 |           const props = node.props as { children?: ReactNode };
274 |           return createElement(
275 |             node.type,
276 |             { ...props },
277 |             ...processWithIndex(_.castArray(props.children || [])),
278 |           );
279 |         }
280 |         return node;
281 |       })
282 |       .flat();
283 |   }
284 | 
285 |   return processWithIndex(nodes);
286 | }
287 | 
```

--------------------------------------------------------------------------------
/packages/cli/src/cli/loaders/icu-safety.spec.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect } from "vitest";
  2 | import { isICUPluralObject, isPluralFormsObject } from "./xcode-xcstrings-icu";
  3 | 
  4 | /**
  5 |  * Safety tests to ensure ICU type guards don't falsely match normal data
  6 |  * from other bucket types (android, json, yaml, etc.)
  7 |  */
  8 | describe("ICU type guards - Safety for other bucket types", () => {
  9 |   describe("isICUPluralObject", () => {
 10 |     it("should return false for regular strings", () => {
 11 |       expect(isICUPluralObject("Hello world")).toBe(false);
 12 |       expect(isICUPluralObject("")).toBe(false);
 13 |       expect(isICUPluralObject("a string with {braces}")).toBe(false);
 14 |     });
 15 | 
 16 |     it("should return false for numbers", () => {
 17 |       expect(isICUPluralObject(42)).toBe(false);
 18 |       expect(isICUPluralObject(0)).toBe(false);
 19 |       expect(isICUPluralObject(-1)).toBe(false);
 20 |     });
 21 | 
 22 |     it("should return false for arrays", () => {
 23 |       expect(isICUPluralObject([])).toBe(false);
 24 |       expect(isICUPluralObject(["one", "two"])).toBe(false);
 25 |       expect(isICUPluralObject([{ icu: "fake" }])).toBe(false);
 26 |     });
 27 | 
 28 |     it("should return false for null/undefined", () => {
 29 |       expect(isICUPluralObject(null)).toBe(false);
 30 |       expect(isICUPluralObject(undefined)).toBe(false);
 31 |     });
 32 | 
 33 |     it("should return false for plain objects (json, yaml data)", () => {
 34 |       expect(isICUPluralObject({ name: "John", age: 30 })).toBe(false);
 35 |       expect(isICUPluralObject({ key: "value" })).toBe(false);
 36 |       expect(isICUPluralObject({ nested: { data: "here" } })).toBe(false);
 37 |     });
 38 | 
 39 |     it("should return false for objects with 'icu' property but wrong format", () => {
 40 |       // Must have valid ICU MessageFormat pattern
 41 |       expect(isICUPluralObject({ icu: "not valid icu" })).toBe(false);
 42 |       expect(isICUPluralObject({ icu: "{just braces}" })).toBe(false);
 43 |       expect(isICUPluralObject({ icu: "plain text" })).toBe(false);
 44 |     });
 45 | 
 46 |     it("should return false for android plurals format", () => {
 47 |       // Android uses different structure
 48 |       expect(
 49 |         isICUPluralObject({
 50 |           quantity: {
 51 |             one: "1 item",
 52 |             other: "%d items",
 53 |           },
 54 |         }),
 55 |       ).toBe(false);
 56 |     });
 57 | 
 58 |     it("should return false for stringsdict format", () => {
 59 |       // iOS stringsdict uses different structure
 60 |       expect(
 61 |         isICUPluralObject({
 62 |           NSStringFormatSpecTypeKey: "NSStringPluralRuleType",
 63 |           NSStringFormatValueTypeKey: "d",
 64 |         }),
 65 |       ).toBe(false);
 66 |     });
 67 | 
 68 |     it("should return TRUE only for valid ICU plural objects", () => {
 69 |       // Valid ICU object
 70 |       expect(
 71 |         isICUPluralObject({
 72 |           icu: "{count, plural, one {1 item} other {# items}}",
 73 |           _meta: {
 74 |             variables: {
 75 |               count: {
 76 |                 format: "%d",
 77 |                 role: "plural",
 78 |               },
 79 |             },
 80 |           },
 81 |         }),
 82 |       ).toBe(true);
 83 | 
 84 |       // Valid ICU object without metadata
 85 |       expect(
 86 |         isICUPluralObject({
 87 |           icu: "{count, plural, one {1 item} other {# items}}",
 88 |         }),
 89 |       ).toBe(true);
 90 |     });
 91 |   });
 92 | 
 93 |   describe("isPluralFormsObject", () => {
 94 |     it("should return false for regular strings", () => {
 95 |       expect(isPluralFormsObject("Hello world")).toBe(false);
 96 |       expect(isPluralFormsObject("")).toBe(false);
 97 |     });
 98 | 
 99 |     it("should return false for numbers", () => {
100 |       expect(isPluralFormsObject(42)).toBe(false);
101 |       expect(isPluralFormsObject(0)).toBe(false);
102 |     });
103 | 
104 |     it("should return false for arrays", () => {
105 |       expect(isPluralFormsObject([])).toBe(false);
106 |       expect(isPluralFormsObject(["one", "two"])).toBe(false);
107 |     });
108 | 
109 |     it("should return false for null/undefined", () => {
110 |       expect(isPluralFormsObject(null)).toBe(false);
111 |       expect(isPluralFormsObject(undefined)).toBe(false);
112 |     });
113 | 
114 |     it("should return false for plain objects (json, yaml data)", () => {
115 |       expect(isPluralFormsObject({ name: "John", age: 30 })).toBe(false);
116 |       expect(isPluralFormsObject({ key: "value" })).toBe(false);
117 |       expect(isPluralFormsObject({ nested: { data: "here" } })).toBe(false);
118 |     });
119 | 
120 |     it("should return false for objects with non-CLDR keys", () => {
121 |       expect(isPluralFormsObject({ quantity: "one" })).toBe(false);
122 |       expect(isPluralFormsObject({ count: "1", total: "10" })).toBe(false);
123 |       expect(isPluralFormsObject({ first: "a", second: "b" })).toBe(false);
124 |     });
125 | 
126 |     it("should return false for objects with CLDR keys but non-string values", () => {
127 |       expect(isPluralFormsObject({ one: 1, other: 2 })).toBe(false);
128 |       expect(isPluralFormsObject({ one: { nested: "obj" } })).toBe(false);
129 |       expect(isPluralFormsObject({ one: ["array"] })).toBe(false);
130 |     });
131 | 
132 |     it("should return false for objects missing 'other' form", () => {
133 |       // 'other' is required in all locales per CLDR
134 |       expect(isPluralFormsObject({ one: "1 item" })).toBe(false);
135 |       expect(isPluralFormsObject({ zero: "0 items", one: "1 item" })).toBe(
136 |         false,
137 |       );
138 |     });
139 | 
140 |     it("should return TRUE only for valid CLDR plural objects", () => {
141 |       // Valid with required 'other' form
142 |       expect(
143 |         isPluralFormsObject({
144 |           one: "1 item",
145 |           other: "# items",
146 |         }),
147 |       ).toBe(true);
148 | 
149 |       // Valid with multiple CLDR forms
150 |       expect(
151 |         isPluralFormsObject({
152 |           zero: "No items",
153 |           one: "1 item",
154 |           few: "A few items",
155 |           many: "Many items",
156 |           other: "# items",
157 |         }),
158 |       ).toBe(true);
159 |     });
160 |   });
161 | 
162 |   describe("Real-world bucket type data", () => {
163 |     it("JSON bucket - should not match ICU guards", () => {
164 |       const jsonData = {
165 |         welcome: "Welcome!",
166 |         user: {
167 |           name: "John",
168 |           greeting: "Hello {name}",
169 |         },
170 |         count: 42,
171 |       };
172 | 
173 |       expect(isICUPluralObject(jsonData)).toBe(false);
174 |       expect(isICUPluralObject(jsonData.user)).toBe(false);
175 |       expect(isPluralFormsObject(jsonData)).toBe(false);
176 |       expect(isPluralFormsObject(jsonData.user)).toBe(false);
177 |     });
178 | 
179 |     it("YAML bucket - should not match ICU guards", () => {
180 |       const yamlData = {
181 |         app: {
182 |           title: "My App",
183 |           description: "An awesome app",
184 |         },
185 |         messages: {
186 |           error: "Something went wrong",
187 |           success: "Operation completed",
188 |         },
189 |       };
190 | 
191 |       expect(isICUPluralObject(yamlData.app)).toBe(false);
192 |       expect(isICUPluralObject(yamlData.messages)).toBe(false);
193 |       expect(isPluralFormsObject(yamlData.app)).toBe(false);
194 |       expect(isPluralFormsObject(yamlData.messages)).toBe(false);
195 |     });
196 | 
197 |     it("Android bucket - should not match ICU guards", () => {
198 |       const androidData = {
199 |         app_name: "MyApp",
200 |         welcome_message: "Welcome %s!",
201 |         item_count: {
202 |           // Android format, not CLDR
203 |           "@quantity": "plural",
204 |           one: "1 item",
205 |           other: "%d items",
206 |         },
207 |       };
208 | 
209 |       expect(isICUPluralObject(androidData["item_count"])).toBe(false);
210 |       // This might match isPluralFormsObject if it has 'other' - that's intentional
211 |       // Android plurals ARE CLDR plural forms
212 |     });
213 | 
214 |     it("Properties bucket - should not match ICU guards", () => {
215 |       const propertiesData = {
216 |         "app.title": "My Application",
217 |         "app.version": "1.0.0",
218 |         "user.greeting": "Hello {0}",
219 |       };
220 | 
221 |       for (const value of Object.values(propertiesData)) {
222 |         expect(isICUPluralObject(value)).toBe(false);
223 |         expect(isPluralFormsObject(value)).toBe(false);
224 |       }
225 |     });
226 |   });
227 | });
228 | 
```

--------------------------------------------------------------------------------
/packages/sdk/src/abort-controller.specs.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, vi, beforeEach } from "vitest";
  2 | import { LingoDotDevEngine } from "../src/index.js";
  3 | 
  4 | // Mock fetch globally
  5 | global.fetch = vi.fn();
  6 | 
  7 | describe("AbortController Support", () => {
  8 |   let engine: LingoDotDevEngine;
  9 | 
 10 |   beforeEach(() => {
 11 |     engine = new LingoDotDevEngine({
 12 |       apiKey: "test-key",
 13 |       apiUrl: "https://test.api.com",
 14 |     });
 15 |     vi.clearAllMocks();
 16 |   });
 17 | 
 18 |   describe("localizeText", () => {
 19 |     it("should pass AbortSignal to fetch", async () => {
 20 |       const controller = new AbortController();
 21 |       const mockResponse = {
 22 |         ok: true,
 23 |         json: vi.fn().mockResolvedValue({ data: { text: "Hola" } }),
 24 |       };
 25 |       (global.fetch as any).mockResolvedValue(mockResponse);
 26 | 
 27 |       await engine.localizeText(
 28 |         "Hello",
 29 |         { sourceLocale: "en", targetLocale: "es" },
 30 |         undefined,
 31 |         controller.signal,
 32 |       );
 33 | 
 34 |       expect(global.fetch).toHaveBeenCalledWith(
 35 |         "https://test.api.com/i18n",
 36 |         expect.objectContaining({
 37 |           signal: controller.signal,
 38 |         }),
 39 |       );
 40 |     });
 41 | 
 42 |     it("should throw error when operation is aborted", async () => {
 43 |       const controller = new AbortController();
 44 |       controller.abort();
 45 | 
 46 |       await expect(
 47 |         engine.localizeText(
 48 |           "Hello",
 49 |           { sourceLocale: "en", targetLocale: "es" },
 50 |           undefined,
 51 |           controller.signal,
 52 |         ),
 53 |       ).rejects.toThrow("Operation was aborted");
 54 |     });
 55 |   });
 56 | 
 57 |   describe("localizeObject", () => {
 58 |     it("should pass AbortSignal to internal method", async () => {
 59 |       const controller = new AbortController();
 60 |       const mockResponse = {
 61 |         ok: true,
 62 |         json: vi.fn().mockResolvedValue({ data: { key: "valor" } }),
 63 |       };
 64 |       (global.fetch as any).mockResolvedValue(mockResponse);
 65 | 
 66 |       await engine.localizeObject(
 67 |         { key: "value" },
 68 |         { sourceLocale: "en", targetLocale: "es" },
 69 |         undefined,
 70 |         controller.signal,
 71 |       );
 72 | 
 73 |       expect(global.fetch).toHaveBeenCalledWith(
 74 |         "https://test.api.com/i18n",
 75 |         expect.objectContaining({
 76 |           signal: controller.signal,
 77 |         }),
 78 |       );
 79 |     });
 80 |   });
 81 | 
 82 |   describe("localizeHtml", () => {
 83 |     it("should pass AbortSignal to internal method", async () => {
 84 |       const controller = new AbortController();
 85 |       const mockResponse = {
 86 |         ok: true,
 87 |         json: vi.fn().mockResolvedValue({ data: { "body/0": "Hola" } }),
 88 |       };
 89 |       (global.fetch as any).mockResolvedValue(mockResponse);
 90 | 
 91 |       // Mock JSDOM
 92 |       const mockJSDOM = {
 93 |         JSDOM: vi.fn().mockImplementation(() => ({
 94 |           window: {
 95 |             document: {
 96 |               documentElement: {
 97 |                 setAttribute: vi.fn(),
 98 |               },
 99 |               head: {
100 |                 childNodes: [],
101 |               },
102 |               body: {
103 |                 childNodes: [
104 |                   {
105 |                     nodeType: 3,
106 |                     textContent: "Hello",
107 |                     parentElement: null,
108 |                   },
109 |                 ],
110 |               },
111 |             },
112 |           },
113 |           serialize: vi.fn().mockReturnValue("<html><body>Hola</body></html>"),
114 |         })),
115 |       };
116 | 
117 |       // Mock dynamic import
118 |       vi.doMock("jsdom", () => mockJSDOM);
119 | 
120 |       await engine.localizeHtml(
121 |         "<html><body>Hello</body></html>",
122 |         { sourceLocale: "en", targetLocale: "es" },
123 |         undefined,
124 |         controller.signal,
125 |       );
126 | 
127 |       expect(global.fetch).toHaveBeenCalledWith(
128 |         "https://test.api.com/i18n",
129 |         expect.objectContaining({
130 |           signal: controller.signal,
131 |         }),
132 |       );
133 |     });
134 |   });
135 | 
136 |   describe("localizeChat", () => {
137 |     it("should pass AbortSignal to internal method", async () => {
138 |       const controller = new AbortController();
139 |       const mockResponse = {
140 |         ok: true,
141 |         json: vi.fn().mockResolvedValue({ data: { chat_0: "Hola" } }),
142 |       };
143 |       (global.fetch as any).mockResolvedValue(mockResponse);
144 | 
145 |       await engine.localizeChat(
146 |         [{ name: "User", text: "Hello" }],
147 |         { sourceLocale: "en", targetLocale: "es" },
148 |         undefined,
149 |         controller.signal,
150 |       );
151 | 
152 |       expect(global.fetch).toHaveBeenCalledWith(
153 |         "https://test.api.com/i18n",
154 |         expect.objectContaining({
155 |           signal: controller.signal,
156 |         }),
157 |       );
158 |     });
159 |   });
160 | 
161 |   describe("batchLocalizeText", () => {
162 |     it("should pass AbortSignal to individual localizeText calls", async () => {
163 |       const controller = new AbortController();
164 |       const mockResponse = {
165 |         ok: true,
166 |         json: vi.fn().mockResolvedValue({ data: { text: "Hola" } }),
167 |       };
168 |       (global.fetch as any).mockResolvedValue(mockResponse);
169 | 
170 |       await engine.batchLocalizeText(
171 |         "Hello",
172 |         {
173 |           sourceLocale: "en",
174 |           targetLocales: ["es", "fr"],
175 |         },
176 |         controller.signal,
177 |       );
178 | 
179 |       expect(global.fetch).toHaveBeenCalledTimes(2);
180 |       expect(global.fetch).toHaveBeenCalledWith(
181 |         "https://test.api.com/i18n",
182 |         expect.objectContaining({
183 |           signal: controller.signal,
184 |         }),
185 |       );
186 |     });
187 |   });
188 | 
189 |   describe("recognizeLocale", () => {
190 |     it("should pass AbortSignal to fetch", async () => {
191 |       const controller = new AbortController();
192 |       const mockResponse = {
193 |         ok: true,
194 |         json: vi.fn().mockResolvedValue({ locale: "en" }),
195 |       };
196 |       (global.fetch as any).mockResolvedValue(mockResponse);
197 | 
198 |       await engine.recognizeLocale("Hello world", controller.signal);
199 | 
200 |       expect(global.fetch).toHaveBeenCalledWith(
201 |         "https://test.api.com/recognize",
202 |         expect.objectContaining({
203 |           signal: controller.signal,
204 |         }),
205 |       );
206 |     });
207 |   });
208 | 
209 |   describe("whoami", () => {
210 |     it("should pass AbortSignal to fetch", async () => {
211 |       const controller = new AbortController();
212 |       const mockResponse = {
213 |         ok: true,
214 |         json: vi
215 |           .fn()
216 |           .mockResolvedValue({ email: "[email protected]", id: "123" }),
217 |       };
218 |       (global.fetch as any).mockResolvedValue(mockResponse);
219 | 
220 |       await engine.whoami(controller.signal);
221 | 
222 |       expect(global.fetch).toHaveBeenCalledWith(
223 |         "https://test.api.com/whoami",
224 |         expect.objectContaining({
225 |           signal: controller.signal,
226 |         }),
227 |       );
228 |     });
229 |   });
230 | 
231 |   describe("Batch operations abortion", () => {
232 |     it("should abort between chunks in _localizeRaw", async () => {
233 |       const controller = new AbortController();
234 | 
235 |       // Create a large payload that will be split into multiple chunks
236 |       const largePayload: Record<string, string> = {};
237 |       for (let i = 0; i < 100; i++) {
238 |         largePayload[`key${i}`] = `value${i}`.repeat(50); // Make values long enough
239 |       }
240 | 
241 |       const mockResponse = {
242 |         ok: true,
243 |         json: vi.fn().mockResolvedValue({ data: { key0: "processed" } }),
244 |       };
245 | 
246 |       // Mock fetch to abort the controller after the first call
247 |       let callCount = 0;
248 |       (global.fetch as any).mockImplementation(async () => {
249 |         callCount++;
250 |         if (callCount === 1) {
251 |           // Abort immediately after first call starts
252 |           controller.abort();
253 |         }
254 |         return mockResponse;
255 |       });
256 | 
257 |       await expect(
258 |         engine._localizeRaw(
259 |           largePayload,
260 |           { sourceLocale: "en", targetLocale: "es" },
261 |           undefined,
262 |           controller.signal,
263 |         ),
264 |       ).rejects.toThrow("Operation was aborted");
265 | 
266 |       // Should have made at least one call
267 |       expect(callCount).toBeGreaterThan(0);
268 |     });
269 |   });
270 | });
271 | 
```
Page 10/20FirstPrevNextLast