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

# Directory Structure

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

# Files

--------------------------------------------------------------------------------
/packages/cli/src/cli/utils/delta.spec.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
  2 | import { createDeltaProcessor } from "./delta";
  3 | import * as path from "path";
  4 | import { tryReadFile, writeFile, checkIfFileExists } from "../utils/fs";
  5 | import YAML from "yaml";
  6 | 
  7 | // Setup mocks before importing the module
  8 | vi.mock("object-hash", () => ({
  9 |   MD5: vi.fn().mockImplementation((value) => `mocked-hash-${value}`),
 10 | }));
 11 | 
 12 | // Mock dependencies
 13 | vi.mock("path", () => ({
 14 |   join: vi.fn(() => "/mocked/path/i18n.lock"),
 15 | }));
 16 | 
 17 | vi.mock("../utils/fs", () => ({
 18 |   tryReadFile: vi.fn(),
 19 |   writeFile: vi.fn(),
 20 |   checkIfFileExists: vi.fn(),
 21 | }));
 22 | 
 23 | // Import MD5 after mocking
 24 | import { MD5 } from "object-hash";
 25 | 
 26 | describe("createDeltaProcessor", () => {
 27 |   const mockFileKey = "test-file-key";
 28 |   let mockProcessor;
 29 | 
 30 |   beforeEach(() => {
 31 |     vi.clearAllMocks();
 32 |     // Reset the mock implementation for MD5
 33 |     (MD5 as any).mockImplementation((value) => `mocked-hash-${value}`);
 34 |     // Create a new processor instance for each test
 35 |     mockProcessor = createDeltaProcessor(mockFileKey);
 36 |   });
 37 | 
 38 |   describe("checkIfLockExists", () => {
 39 |     it("should call checkIfFileExists with the correct path", async () => {
 40 |       (checkIfFileExists as any).mockResolvedValue(true);
 41 | 
 42 |       const result = await mockProcessor.checkIfLockExists();
 43 | 
 44 |       expect(path.join).toHaveBeenCalledWith(process.cwd(), "i18n.lock");
 45 |       expect(checkIfFileExists).toHaveBeenCalledWith("/mocked/path/i18n.lock");
 46 |       expect(result).toBe(true);
 47 |     });
 48 |   });
 49 | 
 50 |   describe("calculateDelta", () => {
 51 |     it("should correctly identify added keys", async () => {
 52 |       const sourceData = { key1: "value1", key2: "value2" };
 53 |       const targetData = { key1: "value1" };
 54 |       const checksums = { key1: "checksum1" };
 55 | 
 56 |       const result = await mockProcessor.calculateDelta({
 57 |         sourceData,
 58 |         targetData,
 59 |         checksums,
 60 |       });
 61 | 
 62 |       expect(result.added).toEqual(["key2"]);
 63 |       expect(result.hasChanges).toBe(true);
 64 |     });
 65 | 
 66 |     it("should correctly identify removed keys", async () => {
 67 |       const sourceData = { key1: "value1" };
 68 |       const targetData = { key1: "value1", key2: "value2" };
 69 |       const checksums = { key1: "checksum1", key2: "checksum2" };
 70 | 
 71 |       const result = await mockProcessor.calculateDelta({
 72 |         sourceData,
 73 |         targetData,
 74 |         checksums,
 75 |       });
 76 | 
 77 |       expect(result.removed).toEqual(["key2"]);
 78 |       expect(result.hasChanges).toBe(true);
 79 |     });
 80 | 
 81 |     it("should correctly identify updated keys", async () => {
 82 |       const sourceData = { key1: "new-value1" };
 83 |       const targetData = { key1: "value1" };
 84 |       const checksums = { key1: "old-checksum" }; // Different from MD5(new-value1)
 85 | 
 86 |       const result = await mockProcessor.calculateDelta({
 87 |         sourceData,
 88 |         targetData,
 89 |         checksums,
 90 |       });
 91 | 
 92 |       expect(result.updated).toContain("key1");
 93 |       expect(result.hasChanges).toBe(true);
 94 |     });
 95 | 
 96 |     it("should correctly identify renamed keys", async () => {
 97 |       // Mock to simulate a renamed key (same hash but different key name)
 98 |       (MD5 as any).mockImplementation((value) =>
 99 |         value === "value1" ? "same-hash" : "other-hash",
100 |       );
101 | 
102 |       const sourceData = { newKey: "value1" };
103 |       const targetData = { oldKey: "something" };
104 |       const checksums = { oldKey: "same-hash" };
105 | 
106 |       const result = await mockProcessor.calculateDelta({
107 |         sourceData,
108 |         targetData,
109 |         checksums,
110 |       });
111 | 
112 |       expect(result.renamed).toEqual([["oldKey", "newKey"]]);
113 |       expect(result.added).toEqual([]);
114 |       expect(result.removed).toEqual([]);
115 |       expect(result.hasChanges).toBe(true);
116 |     });
117 | 
118 |     it("should return hasChanges=false when there are no changes", async () => {
119 |       const sourceData = { key1: "value1" };
120 |       const targetData = { key1: "value1" };
121 | 
122 |       // Mock to simulate matching checksums
123 |       (MD5 as any).mockImplementation((value) => "matching-hash");
124 |       const checksums = { key1: "matching-hash" };
125 | 
126 |       const result = await mockProcessor.calculateDelta({
127 |         sourceData,
128 |         targetData,
129 |         checksums,
130 |       });
131 | 
132 |       expect(result.added).toEqual([]);
133 |       expect(result.removed).toEqual([]);
134 |       expect(result.updated).toEqual([]);
135 |       expect(result.renamed).toEqual([]);
136 |       expect(result.hasChanges).toBe(false);
137 |     });
138 |   });
139 | 
140 |   describe("loadLock", () => {
141 |     it("should return default lock data when no file exists", async () => {
142 |       (tryReadFile as any).mockReturnValue(null);
143 | 
144 |       const result = await mockProcessor.loadLock();
145 | 
146 |       expect(result).toEqual({
147 |         version: 1,
148 |         checksums: {},
149 |       });
150 |     });
151 | 
152 |     it("should parse and return lock file data when it exists", async () => {
153 |       const mockYaml = "version: 1\nchecksums:\n  fileId:\n    key1: checksum1";
154 |       (tryReadFile as any).mockReturnValue(mockYaml);
155 | 
156 |       const result = await mockProcessor.loadLock();
157 | 
158 |       expect(result).toEqual({
159 |         version: 1,
160 |         checksums: {
161 |           fileId: {
162 |             key1: "checksum1",
163 |           },
164 |         },
165 |       });
166 |     });
167 |   });
168 | 
169 |   describe("saveLock", () => {
170 |     it("should stringify and save lock data", async () => {
171 |       const lockData = {
172 |         version: 1 as const,
173 |         checksums: {
174 |           fileId: {
175 |             key1: "checksum1",
176 |           },
177 |         },
178 |       };
179 | 
180 |       await mockProcessor.saveLock(lockData);
181 | 
182 |       expect(writeFile).toHaveBeenCalledWith(
183 |         "/mocked/path/i18n.lock",
184 |         expect.any(String),
185 |       );
186 | 
187 |       // Verify the YAML conversion is correct
188 |       const yamlArg = (writeFile as any).mock.calls[0][1];
189 |       const parsedBack = YAML.parse(yamlArg);
190 |       expect(parsedBack).toEqual(lockData);
191 |     });
192 |   });
193 | 
194 |   describe("loadChecksums and saveChecksums", () => {
195 |     it("should load checksums for the specific file key", async () => {
196 |       // Reset MD5 implementation for fileKey hash
197 |       (MD5 as any).mockImplementation((value) => "mocked-hash");
198 | 
199 |       // Mock the loadLock to return specific data
200 |       const mockLockData = {
201 |         version: 1 as const,
202 |         checksums: {
203 |           "mocked-hash": {
204 |             key1: "checksum1",
205 |           },
206 |         },
207 |       };
208 | 
209 |       vi.spyOn(mockProcessor, "loadLock").mockResolvedValue(mockLockData);
210 | 
211 |       const result = await mockProcessor.loadChecksums();
212 | 
213 |       expect(result).toEqual({
214 |         key1: "checksum1",
215 |       });
216 |     });
217 | 
218 |     it("should save checksums for the specific file key", async () => {
219 |       const checksums = { key1: "checksum1" };
220 | 
221 |       // Reset MD5 implementation for fileKey hash
222 |       (MD5 as any).mockImplementation((value) => "mocked-hash");
223 | 
224 |       // Mock loadLock and saveLock
225 |       const mockLockData = {
226 |         version: 1 as const,
227 |         checksums: {},
228 |       };
229 |       vi.spyOn(mockProcessor, "loadLock").mockResolvedValue(mockLockData);
230 |       const saveLockSpy = vi
231 |         .spyOn(mockProcessor, "saveLock")
232 |         .mockResolvedValue(void 0);
233 | 
234 |       await mockProcessor.saveChecksums(checksums);
235 | 
236 |       expect(saveLockSpy).toHaveBeenCalledWith({
237 |         version: 1,
238 |         checksums: {
239 |           "mocked-hash": checksums,
240 |         },
241 |       });
242 |     });
243 |   });
244 | 
245 |   describe("createChecksums", () => {
246 |     it("should create checksums from source data", async () => {
247 |       const sourceData = {
248 |         key1: "value1",
249 |         key2: "value2",
250 |       };
251 | 
252 |       // Setup counter for mock
253 |       let counter = 0;
254 |       (MD5 as any).mockImplementation((value) => `mock-hash-${++counter}`);
255 | 
256 |       const result = await mockProcessor.createChecksums(sourceData);
257 | 
258 |       expect(result).toEqual({
259 |         key1: "mock-hash-1",
260 |         key2: "mock-hash-2",
261 |       });
262 |     });
263 |   });
264 | });
265 | 
```

--------------------------------------------------------------------------------
/legacy/sdk/CHANGELOG.md:
--------------------------------------------------------------------------------

```markdown
  1 | # @replexica/sdk
  2 | 
  3 | ## 0.7.11
  4 | 
  5 | ### Patch Changes
  6 | 
  7 | - [`5dee9ee`](https://github.com/lingodotdev/lingo.dev/commit/5dee9ee743fbef489fbe342597a768ebd59e5f67) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add proxies to legacy packages
  8 | 
  9 | - [`63eb57b`](https://github.com/lingodotdev/lingo.dev/commit/63eb57b8f4cc37605be196085fafbbfdab71cce5) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add deprecation message to legacy package jsons
 10 | 
 11 | - [`bbf7760`](https://github.com/lingodotdev/lingo.dev/commit/bbf7760580f1631805d68612053ebcd4601bb02b) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add deprecation warning to the legacy package proxies
 12 | 
 13 | - Updated dependencies [[`b4c7f1e`](https://github.com/lingodotdev/lingo.dev/commit/b4c7f1e86334d229bee62219c26f30d0b523926d)]:
 14 |   - [email protected]
 15 | 
 16 | ## 0.7.10
 17 | 
 18 | ### Patch Changes
 19 | 
 20 | - Updated dependencies [[`003344f`](https://github.com/lingodotdev/lingo.dev/commit/003344ffcca98a391a298707f18476971c4d4c2b)]:
 21 |   - @replexica/[email protected]
 22 | 
 23 | ## 0.7.9
 24 | 
 25 | ### Patch Changes
 26 | 
 27 | - Updated dependencies [[`a2ada16`](https://github.com/lingodotdev/lingo.dev/commit/a2ada16ecf4cd559d3486f0e4756d58808194f7e)]:
 28 |   - @replexica/[email protected]
 29 | 
 30 | ## 0.7.8
 31 | 
 32 | ### Patch Changes
 33 | 
 34 | - Updated dependencies [[`e6521b8`](https://github.com/lingodotdev/lingo.dev/commit/e6521b86637c254c011aba89a3558802c04ab3ca)]:
 35 |   - @replexica/[email protected]
 36 | 
 37 | ## 0.7.7
 38 | 
 39 | ### Patch Changes
 40 | 
 41 | - Updated dependencies [[`cff3c4e`](https://github.com/lingodotdev/lingo.dev/commit/cff3c4eb1a40f82a9c4c095e49cfd9fce053b048)]:
 42 |   - @replexica/[email protected]
 43 | 
 44 | ## 0.7.6
 45 | 
 46 | ### Patch Changes
 47 | 
 48 | - Updated dependencies [[`58d7b35`](https://github.com/lingodotdev/lingo.dev/commit/58d7b3567e51cc3ef0fad0288c13451381b95a98)]:
 49 |   - @replexica/[email protected]
 50 | 
 51 | ## 0.7.5
 52 | 
 53 | ### Patch Changes
 54 | 
 55 | - Updated dependencies [[`9cf5299`](https://github.com/lingodotdev/lingo.dev/commit/9cf5299f7efbef70fd83f95177eac49b4d8f8007), [`3ab5de6`](https://github.com/lingodotdev/lingo.dev/commit/3ab5de66d8a913297b46095c2e73823124cc8c5b)]:
 56 |   - @replexica/[email protected]
 57 | 
 58 | ## 0.7.4
 59 | 
 60 | ### Patch Changes
 61 | 
 62 | - Updated dependencies [[`1556977`](https://github.com/lingodotdev/lingo.dev/commit/1556977332a6f949100283bfa8c9a9ff5e74b156)]:
 63 |   - @replexica/[email protected]
 64 | 
 65 | ## 0.7.3
 66 | 
 67 | ### Patch Changes
 68 | 
 69 | - [`cbef8f3`](https://github.com/lingodotdev/lingo.dev/commit/cbef8f3cafdc955d61053ce885d98e425acb668d) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - moved jsdom import into the html handler function
 70 | 
 71 | ## 0.7.2
 72 | 
 73 | ### Patch Changes
 74 | 
 75 | - Updated dependencies [[`5cb3c93`](https://github.com/lingodotdev/lingo.dev/commit/5cb3c930fff6e30cff5cc2266b794f75a0db646d)]:
 76 |   - @replexica/[email protected]
 77 | 
 78 | ## 0.7.1
 79 | 
 80 | ### Patch Changes
 81 | 
 82 | - [`db819a4`](https://github.com/lingodotdev/lingo.dev/commit/db819a42412ceb67fedbe729b7d018952686d60b) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - reduce default batch size to avoid hitting rate limits
 83 | 
 84 | - [`2c5cbcf`](https://github.com/lingodotdev/lingo.dev/commit/2c5cbcfbf6feb28440255cdea0818c8cefa61d91) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - filter out non extistent keys
 85 | 
 86 | ## 0.7.0
 87 | 
 88 | ### Minor Changes
 89 | 
 90 | - [`c42dc2d`](https://github.com/lingodotdev/lingo.dev/commit/c42dc2d5b4efe95e804b5a7e7f6d354cf8622dc7) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add `batchLocalizeText` to sdk
 91 | 
 92 | ## 0.6.0
 93 | 
 94 | ### Minor Changes
 95 | 
 96 | - [`a71a88e`](https://github.com/lingodotdev/lingo.dev/commit/a71a88e5c8bd6601b0838c381433a87763142801) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - fast mode
 97 | 
 98 | ### Patch Changes
 99 | 
100 | - [`f0a77ad`](https://github.com/lingodotdev/lingo.dev/commit/f0a77ad774a01c30e7e9bc5a0253638176332fd2) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - updated default batch size limits in the SDK
101 | 
102 | ## 0.5.0
103 | 
104 | ### Minor Changes
105 | 
106 | - [`ebf44cb`](https://github.com/lingodotdev/lingo.dev/commit/ebf44cbb462516abfe660c295c04627796c5a3a7) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - implement recognize locale
107 | 
108 | - [`42d0a5a`](https://github.com/lingodotdev/lingo.dev/commit/42d0a5a7a53e296192a31e8f1d67c126793ea280) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - added .localizeHtml implementation to SDK
109 | 
110 | ### Patch Changes
111 | 
112 | - Updated dependencies [[`a6b22a3`](https://github.com/lingodotdev/lingo.dev/commit/a6b22a3237f574455d8119f914d82b0b247b4151)]:
113 |   - @replexica/[email protected]
114 | 
115 | ## 0.4.3
116 | 
117 | ### Patch Changes
118 | 
119 | - Updated dependencies [[`091ee35`](https://github.com/lingodotdev/lingo.dev/commit/091ee353081795bf8f61c7d41483bc309c7b62ef)]:
120 |   - @replexica/[email protected]
121 | 
122 | ## 0.4.2
123 | 
124 | ### Patch Changes
125 | 
126 | - Updated dependencies [[`5e282d7`](https://github.com/lingodotdev/lingo.dev/commit/5e282d7ffa5ca9494aa7046a090bb7c327085a86)]:
127 |   - @replexica/[email protected]
128 | 
129 | ## 0.4.1
130 | 
131 | ### Patch Changes
132 | 
133 | - Updated dependencies [[`0071cd6`](https://github.com/lingodotdev/lingo.dev/commit/0071cd66b1c868ad3898fc368390a628c5a67767)]:
134 |   - @replexica/[email protected]
135 | 
136 | ## 0.4.0
137 | 
138 | ### Minor Changes
139 | 
140 | - [#264](https://github.com/lingodotdev/lingo.dev/pull/264) [`cdef5b7`](https://github.com/lingodotdev/lingo.dev/commit/cdef5b7bfbee4670c6de62cf4b4f3e0315748e25) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - added format specific methods to `@replexica/sdk`
141 | 
142 | ## 0.3.4
143 | 
144 | ### Patch Changes
145 | 
146 | - Updated dependencies [[`2859938`](https://github.com/lingodotdev/lingo.dev/commit/28599388a91bf80cea3813bb4b8999bb4df302c9)]:
147 |   - @replexica/[email protected]
148 | 
149 | ## 0.3.3
150 | 
151 | ### Patch Changes
152 | 
153 | - Updated dependencies [[`ca9e20e`](https://github.com/lingodotdev/lingo.dev/commit/ca9e20eef9047e20d39ccf9dff74d2f6069d4676), [`2aedf3b`](https://github.com/lingodotdev/lingo.dev/commit/2aedf3bec2d9dffc7b43fc10dea0cab5742d44af), [`626082a`](https://github.com/lingodotdev/lingo.dev/commit/626082a64b88fb3b589acd950afeafe417ce5ddc)]:
154 |   - @replexica/[email protected]
155 | 
156 | ## 0.3.2
157 | 
158 | ### Patch Changes
159 | 
160 | - Updated dependencies [[`1601f70`](https://github.com/lingodotdev/lingo.dev/commit/1601f708bdf0ff1786d3bf9b19265ac5b567f740)]:
161 |   - @replexica/[email protected]
162 | 
163 | ## 0.3.1
164 | 
165 | ### Patch Changes
166 | 
167 | - Updated dependencies [[`bc5a28c`](https://github.com/lingodotdev/lingo.dev/commit/bc5a28c3c98b619872924b5f913229ac01387524)]:
168 |   - @replexica/[email protected]
169 | 
170 | ## 0.3.0
171 | 
172 | ### Minor Changes
173 | 
174 | - [#165](https://github.com/lingodotdev/lingo.dev/pull/165) [`5c2ca37`](https://github.com/lingodotdev/lingo.dev/commit/5c2ca37114663eaeb529a027e33949ef3839549b) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Update locale code resolution logic
175 | 
176 | ### Patch Changes
177 | 
178 | - Updated dependencies [[`5c2ca37`](https://github.com/lingodotdev/lingo.dev/commit/5c2ca37114663eaeb529a027e33949ef3839549b)]:
179 |   - @replexica/[email protected]
180 | 
181 | ## 0.2.1
182 | 
183 | ### Patch Changes
184 | 
185 | - Updated dependencies [[`6870fc7`](https://github.com/lingodotdev/lingo.dev/commit/6870fc758dae9d1adb641576befbd8cda61cd5ea)]:
186 |   - @replexica/[email protected]
187 | 
188 | ## 0.2.0
189 | 
190 | ### Minor Changes
191 | 
192 | - [`d6e6d5c`](https://github.com/lingodotdev/lingo.dev/commit/d6e6d5c24b266de3769e95545f74632e7d75c697) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Add support for multisource localization to the CLI
193 | 
194 | ### Patch Changes
195 | 
196 | - Updated dependencies [[`d6e6d5c`](https://github.com/lingodotdev/lingo.dev/commit/d6e6d5c24b266de3769e95545f74632e7d75c697)]:
197 |   - @replexica/[email protected]
198 | 
199 | ## 0.1.1
200 | 
201 | ### Patch Changes
202 | 
203 | - Updated dependencies [[`73c9250`](https://github.com/lingodotdev/lingo.dev/commit/73c925084989ccea120cae1617ec87776c88e83e)]:
204 |   - @replexica/[email protected]
205 | 
206 | ## 0.1.0
207 | 
208 | ### Minor Changes
209 | 
210 | - [#142](https://github.com/lingodotdev/lingo.dev/pull/142) [`d9b0e51`](https://github.com/lingodotdev/lingo.dev/commit/d9b0e512196329cc781a4d33346f8ca0f3a81e7e) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - Extract API calling into SDK package
211 | 
```

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

```typescript
  1 | import { parse, ParseError } from "jsonc-parser";
  2 | import { ILoader } from "./_types";
  3 | import { createLoader } from "./_utils";
  4 | 
  5 | interface CommentInfo {
  6 |   hint?: string;
  7 |   [key: string]: any;
  8 | }
  9 | 
 10 | function extractCommentsFromJsonc(jsoncString: string): Record<string, any> {
 11 |   const lines = jsoncString.split("\n");
 12 |   const comments: Record<string, any> = {};
 13 | 
 14 |   // Parse to validate structure
 15 |   const errors: ParseError[] = [];
 16 |   const result = parse(jsoncString, errors, {
 17 |     allowTrailingComma: true,
 18 |     disallowComments: false,
 19 |     allowEmptyContent: true,
 20 |   });
 21 | 
 22 |   if (errors.length > 0) {
 23 |     return {};
 24 |   }
 25 | 
 26 |   // Track nesting context
 27 |   const contextStack: Array<{ key: string; isArray: boolean }> = [];
 28 | 
 29 |   for (let i = 0; i < lines.length; i++) {
 30 |     const line = lines[i];
 31 |     const trimmedLine = line.trim();
 32 | 
 33 |     if (!trimmedLine) continue;
 34 | 
 35 |     // Handle different comment types
 36 |     const commentData = extractCommentFromLine(line, lines, i);
 37 |     if (commentData.hint) {
 38 |       let keyInfo;
 39 | 
 40 |       if (commentData.isInline) {
 41 |         // For inline comments, extract key from the same line
 42 |         const keyMatch = line.match(/^\s*["']?([^"':,\s]+)["']?\s*:/);
 43 |         if (keyMatch) {
 44 |           const key = keyMatch[1];
 45 |           const path = contextStack.map((ctx) => ctx.key).filter(Boolean);
 46 |           keyInfo = { key, path };
 47 |         }
 48 |       } else {
 49 |         // For standalone comments, find the next key
 50 |         keyInfo = findAssociatedKey(lines, commentData.lineIndex, contextStack);
 51 |       }
 52 | 
 53 |       if (keyInfo && keyInfo.key) {
 54 |         setCommentAtPath(comments, keyInfo.path, keyInfo.key, commentData.hint);
 55 |       }
 56 | 
 57 |       // Skip processed lines for multi-line comments
 58 |       i = commentData.endIndex;
 59 |       continue;
 60 |     }
 61 | 
 62 |     // Update context for object/array nesting
 63 |     updateContext(contextStack, line, result);
 64 |   }
 65 | 
 66 |   return comments;
 67 | }
 68 | 
 69 | function extractCommentFromLine(
 70 |   line: string,
 71 |   lines: string[],
 72 |   lineIndex: number,
 73 | ): {
 74 |   hint: string | null;
 75 |   lineIndex: number;
 76 |   endIndex: number;
 77 |   isInline: boolean;
 78 | } {
 79 |   const trimmed = line.trim();
 80 | 
 81 |   // Single-line comment (standalone)
 82 |   if (trimmed.startsWith("//")) {
 83 |     const hint = trimmed.replace(/^\/\/\s*/, "").trim();
 84 |     return { hint, lineIndex, endIndex: lineIndex, isInline: false };
 85 |   }
 86 | 
 87 |   // Block comment (standalone or multi-line)
 88 |   if (trimmed.startsWith("/*")) {
 89 |     const blockResult = extractBlockComment(lines, lineIndex);
 90 |     return { ...blockResult, isInline: false };
 91 |   }
 92 | 
 93 |   // Inline comments (after JSON content)
 94 |   // Handle single-line inline comments
 95 |   const singleInlineMatch = line.match(/^(.+?)\s*\/\/\s*(.+)$/);
 96 |   if (singleInlineMatch && singleInlineMatch[1].includes(":")) {
 97 |     const hint = singleInlineMatch[2].trim();
 98 |     return { hint, lineIndex, endIndex: lineIndex, isInline: true };
 99 |   }
100 | 
101 |   // Handle block inline comments
102 |   const blockInlineMatch = line.match(/^(.+?)\s*\/\*\s*(.*?)\s*\*\/.*$/);
103 |   if (blockInlineMatch && blockInlineMatch[1].includes(":")) {
104 |     const hint = blockInlineMatch[2].trim();
105 |     return { hint, lineIndex, endIndex: lineIndex, isInline: true };
106 |   }
107 | 
108 |   return { hint: null, lineIndex, endIndex: lineIndex, isInline: false };
109 | }
110 | 
111 | function extractBlockComment(
112 |   lines: string[],
113 |   startIndex: number,
114 | ): { hint: string | null; lineIndex: number; endIndex: number } {
115 |   const startLine = lines[startIndex];
116 | 
117 |   // Single-line block comment
118 |   const singleMatch = startLine.match(/\/\*\s*(.*?)\s*\*\//);
119 |   if (singleMatch) {
120 |     return {
121 |       hint: singleMatch[1].trim(),
122 |       lineIndex: startIndex,
123 |       endIndex: startIndex,
124 |     };
125 |   }
126 | 
127 |   // Multi-line block comment
128 |   const commentParts: string[] = [];
129 |   let endIndex = startIndex;
130 | 
131 |   // Extract content from first line
132 |   const firstContent = startLine.replace(/.*?\/\*\s*/, "").trim();
133 |   if (firstContent && !firstContent.includes("*/")) {
134 |     commentParts.push(firstContent);
135 |   }
136 | 
137 |   // Process subsequent lines
138 |   for (let i = startIndex + 1; i < lines.length; i++) {
139 |     const line = lines[i];
140 |     endIndex = i;
141 | 
142 |     if (line.includes("*/")) {
143 |       const lastContent = line
144 |         .replace(/\*\/.*$/, "")
145 |         .replace(/^\s*\*?\s*/, "")
146 |         .trim();
147 |       if (lastContent) {
148 |         commentParts.push(lastContent);
149 |       }
150 |       break;
151 |     } else {
152 |       const content = line.replace(/^\s*\*?\s*/, "").trim();
153 |       if (content) {
154 |         commentParts.push(content);
155 |       }
156 |     }
157 |   }
158 | 
159 |   return {
160 |     hint: commentParts.join(" ").trim() || null,
161 |     lineIndex: startIndex,
162 |     endIndex,
163 |   };
164 | }
165 | 
166 | function findAssociatedKey(
167 |   lines: string[],
168 |   commentLineIndex: number,
169 |   contextStack: Array<{ key: string; isArray: boolean }>,
170 | ): { key: string | null; path: string[] } {
171 |   // Look for the next key after the comment
172 |   for (let i = commentLineIndex + 1; i < lines.length; i++) {
173 |     const line = lines[i].trim();
174 | 
175 |     if (
176 |       !line ||
177 |       line.startsWith("//") ||
178 |       line.startsWith("/*") ||
179 |       line === "{" ||
180 |       line === "}"
181 |     ) {
182 |       continue;
183 |     }
184 | 
185 |     // Extract key from line
186 |     const keyMatch = line.match(/^\s*["']?([^"':,\s]+)["']?\s*:/);
187 |     if (keyMatch) {
188 |       const key = keyMatch[1];
189 |       const path = contextStack.map((ctx) => ctx.key).filter(Boolean);
190 |       return { key, path };
191 |     }
192 |   }
193 | 
194 |   return { key: null, path: [] };
195 | }
196 | 
197 | function updateContext(
198 |   contextStack: Array<{ key: string; isArray: boolean }>,
199 |   line: string,
200 |   parsedJson: any,
201 | ): void {
202 |   // This is a simplified context tracking - in a full implementation,
203 |   // you'd want more sophisticated AST-based tracking
204 |   const openBraces = (line.match(/\{/g) || []).length;
205 |   const closeBraces = (line.match(/\}/g) || []).length;
206 | 
207 |   if (openBraces > closeBraces) {
208 |     // Extract the key that's opening this object
209 |     const keyMatch = line.match(/^\s*["']?([^"':,\s]+)["']?\s*:\s*\{/);
210 |     if (keyMatch) {
211 |       contextStack.push({ key: keyMatch[1], isArray: false });
212 |     }
213 |   } else if (closeBraces > openBraces) {
214 |     // Pop context when closing braces
215 |     for (let i = 0; i < closeBraces - openBraces; i++) {
216 |       contextStack.pop();
217 |     }
218 |   }
219 | }
220 | 
221 | function setCommentAtPath(
222 |   comments: Record<string, any>,
223 |   path: string[],
224 |   key: string,
225 |   hint: string,
226 | ): void {
227 |   let current = comments;
228 | 
229 |   // Navigate to the correct nested location
230 |   for (const pathKey of path) {
231 |     if (!current[pathKey]) {
232 |       current[pathKey] = {};
233 |     }
234 |     current = current[pathKey];
235 |   }
236 | 
237 |   // Set the hint for the key
238 |   if (!current[key]) {
239 |     current[key] = {};
240 |   }
241 | 
242 |   if (typeof current[key] === "object" && current[key] !== null) {
243 |     current[key].hint = hint;
244 |   } else {
245 |     current[key] = { hint };
246 |   }
247 | }
248 | 
249 | export default function createJsoncLoader(): ILoader<
250 |   string,
251 |   Record<string, any>
252 | > {
253 |   return createLoader({
254 |     pull: async (locale, input) => {
255 |       const jsoncString = input || "{}";
256 |       const errors: ParseError[] = [];
257 |       const result = parse(jsoncString, errors, {
258 |         allowTrailingComma: true,
259 |         disallowComments: false,
260 |         allowEmptyContent: true,
261 |       });
262 | 
263 |       if (errors.length > 0) {
264 |         throw new Error(`Failed to parse JSONC: ${errors[0].error}`);
265 |       }
266 | 
267 |       return result || {};
268 |     },
269 |     push: async (locale, data) => {
270 |       // JSONC parser's stringify preserves formatting but doesn't add comments
271 |       // We'll use standard JSON.stringify with pretty formatting for output
272 |       const serializedData = JSON.stringify(data, null, 2);
273 |       return serializedData;
274 |     },
275 |     pullHints: async (input) => {
276 |       if (!input || typeof input !== "string") {
277 |         return {};
278 |       }
279 | 
280 |       try {
281 |         return extractCommentsFromJsonc(input);
282 |       } catch (error) {
283 |         console.warn("Failed to extract comments from JSONC:", error);
284 |         return {};
285 |       }
286 |     },
287 |   });
288 | }
289 | 
```

--------------------------------------------------------------------------------
/packages/locales/src/names/integration.spec.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, vi, beforeEach } from "vitest";
  2 | import { getCountryName, getLanguageName, getScriptName } from "./index";
  3 | 
  4 | // Mock the loader functions to return predictable data
  5 | vi.mock("./loader", () => ({
  6 |   loadTerritoryNames: vi.fn(),
  7 |   loadLanguageNames: vi.fn(),
  8 |   loadScriptNames: vi.fn(),
  9 | }));
 10 | 
 11 | import {
 12 |   loadTerritoryNames,
 13 |   loadLanguageNames,
 14 |   loadScriptNames,
 15 | } from "./loader";
 16 | 
 17 | const mockLoadTerritoryNames = loadTerritoryNames as ReturnType<typeof vi.fn>;
 18 | const mockLoadLanguageNames = loadLanguageNames as ReturnType<typeof vi.fn>;
 19 | const mockLoadScriptNames = loadScriptNames as ReturnType<typeof vi.fn>;
 20 | 
 21 | describe("Integration Tests", () => {
 22 |   beforeEach(() => {
 23 |     vi.clearAllMocks();
 24 |   });
 25 | 
 26 |   describe("getCountryName", () => {
 27 |     it("should get country names in different languages", async () => {
 28 |       // Mock data for different languages
 29 |       mockLoadTerritoryNames
 30 |         .mockResolvedValueOnce({ US: "United States", CN: "China" }) // en
 31 |         .mockResolvedValueOnce({ US: "Estados Unidos", CN: "China" }) // es
 32 |         .mockResolvedValueOnce({ US: "États-Unis", CN: "Chine" }); // fr
 33 | 
 34 |       const result1 = await getCountryName("US", "en");
 35 |       const result2 = await getCountryName("US", "es");
 36 |       const result3 = await getCountryName("US", "fr");
 37 | 
 38 |       expect(result1).toBe("United States");
 39 |       expect(result2).toBe("Estados Unidos");
 40 |       expect(result3).toBe("États-Unis");
 41 | 
 42 |       expect(mockLoadTerritoryNames).toHaveBeenCalledTimes(3);
 43 |       expect(mockLoadTerritoryNames).toHaveBeenNthCalledWith(1, "en");
 44 |       expect(mockLoadTerritoryNames).toHaveBeenNthCalledWith(2, "es");
 45 |       expect(mockLoadTerritoryNames).toHaveBeenNthCalledWith(3, "fr");
 46 |     });
 47 | 
 48 |     it("should normalize country codes to uppercase", async () => {
 49 |       mockLoadTerritoryNames.mockResolvedValue({ US: "United States" });
 50 | 
 51 |       const result = await getCountryName("us");
 52 |       expect(result).toBe("United States");
 53 |     });
 54 | 
 55 |     it("should handle loader errors gracefully", async () => {
 56 |       mockLoadTerritoryNames.mockRejectedValue(new Error("Network error"));
 57 | 
 58 |       await expect(getCountryName("US")).rejects.toThrow("Network error");
 59 |     });
 60 |   });
 61 | 
 62 |   describe("getLanguageName", () => {
 63 |     it("should get language names in different languages", async () => {
 64 |       mockLoadLanguageNames
 65 |         .mockResolvedValueOnce({ en: "English", es: "Spanish" }) // en
 66 |         .mockResolvedValueOnce({ en: "inglés", es: "español" }) // es
 67 |         .mockResolvedValueOnce({ en: "anglais", es: "espagnol" }); // fr
 68 | 
 69 |       const result1 = await getLanguageName("en", "en");
 70 |       const result2 = await getLanguageName("en", "es");
 71 |       const result3 = await getLanguageName("en", "fr");
 72 | 
 73 |       expect(result1).toBe("English");
 74 |       expect(result2).toBe("inglés");
 75 |       expect(result3).toBe("anglais");
 76 |     });
 77 | 
 78 |     it("should normalize language codes to lowercase", async () => {
 79 |       mockLoadLanguageNames.mockResolvedValue({ en: "English" });
 80 | 
 81 |       const result = await getLanguageName("EN");
 82 |       expect(result).toBe("English");
 83 |     });
 84 |   });
 85 | 
 86 |   describe("getScriptName", () => {
 87 |     it("should get script names in different languages", async () => {
 88 |       mockLoadScriptNames
 89 |         .mockResolvedValueOnce({
 90 |           Hans: "Simplified Han",
 91 |           Hant: "Traditional Han",
 92 |         }) // en
 93 |         .mockResolvedValueOnce({
 94 |           Hans: "han simplificado",
 95 |           Hant: "han tradicional",
 96 |         }) // es
 97 |         .mockResolvedValueOnce({
 98 |           Hans: "han simplifié",
 99 |           Hant: "han traditionnel",
100 |         }); // fr
101 | 
102 |       const result1 = await getScriptName("Hans", "en");
103 |       const result2 = await getScriptName("Hans", "es");
104 |       const result3 = await getScriptName("Hans", "fr");
105 | 
106 |       expect(result1).toBe("Simplified Han");
107 |       expect(result2).toBe("han simplificado");
108 |       expect(result3).toBe("han simplifié");
109 |     });
110 | 
111 |     it("should preserve script code case", async () => {
112 |       mockLoadScriptNames.mockResolvedValue({
113 |         Latn: "Latin",
114 |         CYRL: "Cyrillic",
115 |         hans: "Simplified Han",
116 |       });
117 | 
118 |       const result1 = await getScriptName("Latn");
119 |       const result2 = await getScriptName("CYRL");
120 |       const result3 = await getScriptName("hans");
121 | 
122 |       expect(result1).toBe("Latin");
123 |       expect(result2).toBe("Cyrillic");
124 |       expect(result3).toBe("Simplified Han");
125 |     });
126 |   });
127 | 
128 |   describe("Error handling", () => {
129 |     it("should throw for empty inputs", async () => {
130 |       await expect(getCountryName("")).rejects.toThrow(
131 |         "Country code is required",
132 |       );
133 |       await expect(getLanguageName("")).rejects.toThrow(
134 |         "Language code is required",
135 |       );
136 |       await expect(getScriptName("")).rejects.toThrow(
137 |         "Script code is required",
138 |       );
139 |     });
140 | 
141 |     it("should throw for null/undefined inputs", async () => {
142 |       await expect(getCountryName(null as any)).rejects.toThrow(
143 |         "Country code is required",
144 |       );
145 |       await expect(getLanguageName(undefined as any)).rejects.toThrow(
146 |         "Language code is required",
147 |       );
148 |       await expect(getScriptName(null as any)).rejects.toThrow(
149 |         "Script code is required",
150 |       );
151 |     });
152 | 
153 |     it("should throw for unknown codes", async () => {
154 |       mockLoadTerritoryNames.mockResolvedValue({ US: "United States" });
155 |       mockLoadLanguageNames.mockResolvedValue({ en: "English" });
156 |       mockLoadScriptNames.mockResolvedValue({ Latn: "Latin" });
157 | 
158 |       await expect(getCountryName("XX")).rejects.toThrow(
159 |         'Country code "XX" not found',
160 |       );
161 |       await expect(getLanguageName("xx")).rejects.toThrow(
162 |         'Language code "xx" not found',
163 |       );
164 |       await expect(getScriptName("Xxxx")).rejects.toThrow(
165 |         'Script code "Xxxx" not found',
166 |       );
167 |     });
168 |   });
169 | 
170 |   describe("Real-world scenarios", () => {
171 |     it("should handle Chinese locale names", async () => {
172 |       mockLoadLanguageNames.mockResolvedValue({
173 |         en: "英语",
174 |         es: "西班牙语",
175 |         fr: "法语",
176 |         de: "德语",
177 |       });
178 | 
179 |       const result1 = await getLanguageName("en", "zh");
180 |       const result2 = await getLanguageName("es", "zh");
181 |       const result3 = await getLanguageName("fr", "zh");
182 |       const result4 = await getLanguageName("de", "zh");
183 | 
184 |       expect(result1).toBe("英语");
185 |       expect(result2).toBe("西班牙语");
186 |       expect(result3).toBe("法语");
187 |       expect(result4).toBe("德语");
188 |     });
189 | 
190 |     it("should handle Arabic locale names", async () => {
191 |       mockLoadTerritoryNames.mockResolvedValue({
192 |         US: "الولايات المتحدة",
193 |         GB: "المملكة المتحدة",
194 |         FR: "فرنسا",
195 |       });
196 | 
197 |       const result1 = await getCountryName("US", "ar");
198 |       const result2 = await getCountryName("GB", "ar");
199 |       const result3 = await getCountryName("FR", "ar");
200 | 
201 |       expect(result1).toBe("الولايات المتحدة");
202 |       expect(result2).toBe("المملكة المتحدة");
203 |       expect(result3).toBe("فرنسا");
204 |     });
205 | 
206 |     it("should handle script variants", async () => {
207 |       mockLoadScriptNames.mockResolvedValue({
208 |         Hans: "Simplified Han",
209 |         Hant: "Traditional Han",
210 |         Latn: "Latin",
211 |         Cyrl: "Cyrillic",
212 |         Arab: "Arabic",
213 |         Deva: "Devanagari",
214 |       });
215 | 
216 |       const result1 = await getScriptName("Hans");
217 |       const result2 = await getScriptName("Hant");
218 |       const result3 = await getScriptName("Latn");
219 |       const result4 = await getScriptName("Cyrl");
220 |       const result5 = await getScriptName("Arab");
221 |       const result6 = await getScriptName("Deva");
222 | 
223 |       expect(result1).toBe("Simplified Han");
224 |       expect(result2).toBe("Traditional Han");
225 |       expect(result3).toBe("Latin");
226 |       expect(result4).toBe("Cyrillic");
227 |       expect(result5).toBe("Arabic");
228 |       expect(result6).toBe("Devanagari");
229 |     });
230 |   });
231 | });
232 | 
```

--------------------------------------------------------------------------------
/packages/cli/src/cli/cmd/init.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { InteractiveCommand, InteractiveOption } from "interactive-commander";
  2 | import Ora from "ora";
  3 | import { getConfig, saveConfig } from "../utils/config";
  4 | import {
  5 |   defaultConfig,
  6 |   LocaleCode,
  7 |   resolveLocaleCode,
  8 |   bucketTypes,
  9 | } from "@lingo.dev/_spec";
 10 | import fs from "fs";
 11 | import path from "path";
 12 | import _ from "lodash";
 13 | import { checkbox, confirm, input } from "@inquirer/prompts";
 14 | import { login } from "./login";
 15 | import { getSettings, saveSettings } from "../utils/settings";
 16 | import { createAuthenticator } from "../utils/auth";
 17 | import findLocaleFiles from "../utils/find-locale-paths";
 18 | import { ensurePatterns } from "../utils/ensure-patterns";
 19 | import updateGitignore from "../utils/update-gitignore";
 20 | import initCICD from "../utils/init-ci-cd";
 21 | import open from "open";
 22 | 
 23 | const openUrl = (path: string) => {
 24 |   const settings = getSettings(undefined);
 25 |   open(`${settings.auth.webUrl}${path}`, { wait: false });
 26 | };
 27 | 
 28 | const throwHelpError = (option: string, value: string) => {
 29 |   if (value === "help") {
 30 |     openUrl("/go/call");
 31 |   }
 32 |   throw new Error(
 33 |     `Invalid ${option}: ${value}\n\nDo you need support for ${value} ${option}? Type "help" and we will.`,
 34 |   );
 35 | };
 36 | 
 37 | export default new InteractiveCommand()
 38 |   .command("init")
 39 |   .description("Create i18n.json configuration file for a new project")
 40 |   .helpOption("-h, --help", "Show help")
 41 |   .addOption(
 42 |     new InteractiveOption(
 43 |       "-f --force",
 44 |       "Overwrite existing Lingo.dev configuration instead of aborting initialization (destructive operation)",
 45 |     )
 46 |       .prompt(undefined)
 47 |       .default(false),
 48 |   )
 49 |   .addOption(
 50 |     new InteractiveOption(
 51 |       "-s --source <locale>",
 52 |       "Primary language of your application that content will be translated from. Defaults to 'en'",
 53 |     )
 54 |       .argParser((value) => {
 55 |         try {
 56 |           resolveLocaleCode(value as LocaleCode);
 57 |         } catch (e) {
 58 |           throwHelpError("locale", value);
 59 |         }
 60 |         return value;
 61 |       })
 62 |       .default("en"),
 63 |   )
 64 |   .addOption(
 65 |     new InteractiveOption(
 66 |       "-t --targets <locale...>",
 67 |       "Target languages to translate to. Accepts locale codes like 'es', 'fr', 'de-AT' separated by commas or spaces. Defaults to 'es'",
 68 |     )
 69 |       .argParser((value) => {
 70 |         const values = (
 71 |           value.includes(",") ? value.split(",") : value.split(" ")
 72 |         ) as LocaleCode[];
 73 |         values.forEach((value) => {
 74 |           try {
 75 |             resolveLocaleCode(value);
 76 |           } catch (e) {
 77 |             throwHelpError("locale", value);
 78 |           }
 79 |         });
 80 |         return values;
 81 |       })
 82 |       .default("es"),
 83 |   )
 84 |   .addOption(
 85 |     new InteractiveOption(
 86 |       "-b, --bucket <type>",
 87 |       "File format for your translation files. Must match a supported type such as json, yaml, or android",
 88 |     )
 89 |       .argParser((value) => {
 90 |         if (!bucketTypes.includes(value as (typeof bucketTypes)[number])) {
 91 |           throwHelpError("bucket format", value);
 92 |         }
 93 |         return value;
 94 |       })
 95 |       .default("json"),
 96 |   )
 97 |   .addOption(
 98 |     new InteractiveOption(
 99 |       "-p, --paths [path...]",
100 |       "File paths containing translations when using --no-interactive mode. Specify paths with [locale] placeholder, separated by commas or spaces",
101 |     )
102 |       .argParser((value) => {
103 |         if (!value || value.length === 0) return [];
104 |         const values = value.includes(",")
105 |           ? value.split(",")
106 |           : value.split(" ");
107 | 
108 |         for (const p of values) {
109 |           try {
110 |             const dirPath = path.dirname(p);
111 |             const stats = fs.statSync(dirPath);
112 |             if (!stats.isDirectory()) {
113 |               throw new Error(`${dirPath} is not a directory`);
114 |             }
115 |           } catch (err) {
116 |             throw new Error(`Invalid path: ${p}`);
117 |           }
118 |         }
119 | 
120 |         return values;
121 |       })
122 |       .prompt(undefined) // make non-interactive
123 |       .default([]),
124 |   )
125 |   .action(async (options) => {
126 |     const settings = getSettings(undefined);
127 |     const isInteractive = options.interactive;
128 | 
129 |     const spinner = Ora().start("Initializing Lingo.dev project");
130 | 
131 |     let existingConfig = await getConfig(false);
132 |     if (existingConfig && !options.force) {
133 |       spinner.fail("Lingo.dev project already initialized");
134 |       return process.exit(1);
135 |     }
136 | 
137 |     const newConfig = _.cloneDeep(defaultConfig);
138 | 
139 |     newConfig.locale.source = options.source;
140 |     newConfig.locale.targets = options.targets;
141 | 
142 |     if (!isInteractive) {
143 |       newConfig.buckets = {
144 |         [options.bucket]: {
145 |           include: options.paths || [],
146 |         },
147 |       };
148 |     } else {
149 |       let selectedPatterns: string[] = [];
150 |       const localeFiles = findLocaleFiles(options.bucket);
151 | 
152 |       if (!localeFiles) {
153 |         spinner.warn(
154 |           `Bucket type "${options.bucket}" does not supported automatic initialization. Add paths to "i18n.json" manually.`,
155 |         );
156 |         newConfig.buckets = {
157 |           [options.bucket]: {
158 |             include: options.paths || [],
159 |           },
160 |         };
161 |       } else {
162 |         const { patterns, defaultPatterns } = localeFiles;
163 | 
164 |         if (patterns.length > 0) {
165 |           spinner.succeed("Found existing locale files:");
166 | 
167 |           selectedPatterns = await checkbox({
168 |             message: "Select the paths to use",
169 |             choices: patterns.map((value) => ({
170 |               value,
171 |             })),
172 |           });
173 |         } else {
174 |           spinner.succeed("No existing locale files found.");
175 |         }
176 | 
177 |         if (selectedPatterns.length === 0) {
178 |           const useDefault = await confirm({
179 |             message: `Use (and create) default path ${defaultPatterns.join(
180 |               ", ",
181 |             )}?`,
182 |           });
183 |           if (useDefault) {
184 |             ensurePatterns(defaultPatterns, options.source);
185 |             selectedPatterns = defaultPatterns;
186 |           }
187 |         }
188 | 
189 |         if (selectedPatterns.length === 0) {
190 |           const customPaths = await input({
191 |             message: "Enter paths to use",
192 |           });
193 |           selectedPatterns = customPaths.includes(",")
194 |             ? customPaths.split(",")
195 |             : customPaths.split(" ");
196 |         }
197 | 
198 |         newConfig.buckets = {
199 |           [options.bucket]: {
200 |             include: selectedPatterns || [],
201 |           },
202 |         };
203 |       }
204 |     }
205 | 
206 |     await saveConfig(newConfig);
207 | 
208 |     spinner.succeed("Lingo.dev project initialized");
209 | 
210 |     if (isInteractive) {
211 |       await initCICD(spinner);
212 | 
213 |       const openDocs = await confirm({
214 |         message: "Would you like to see our docs?",
215 |       });
216 |       if (openDocs) {
217 |         openUrl("/go/docs");
218 |       }
219 |     }
220 | 
221 |     const authenticator = createAuthenticator({
222 |       apiKey: settings.auth.apiKey,
223 |       apiUrl: settings.auth.apiUrl,
224 |     });
225 |     const auth = await authenticator.whoami();
226 |     if (!auth) {
227 |       if (isInteractive) {
228 |         const doAuth = await confirm({
229 |           message: "It looks like you are not logged into the CLI. Login now?",
230 |         });
231 |         if (doAuth) {
232 |           const apiKey = await login(settings.auth.webUrl);
233 |           settings.auth.apiKey = apiKey;
234 |           await saveSettings(settings);
235 | 
236 |           const newAuthenticator = createAuthenticator({
237 |             apiKey: settings.auth.apiKey,
238 |             apiUrl: settings.auth.apiUrl,
239 |           });
240 |           const auth = await newAuthenticator.whoami();
241 |           if (auth) {
242 |             Ora().succeed(`Authenticated as ${auth?.email}`);
243 |           } else {
244 |             Ora().fail("Authentication failed.");
245 |           }
246 |         }
247 |       } else {
248 |         Ora().warn(
249 |           "You are not logged in. Run `npx lingo.dev@latest login` to login.",
250 |         );
251 |       }
252 |     } else {
253 |       Ora().succeed(`Authenticated as ${auth.email}`);
254 |     }
255 | 
256 |     updateGitignore();
257 | 
258 |     if (!isInteractive) {
259 |       Ora().info("Please see https://lingo.dev/cli");
260 |     }
261 |   });
262 | 
```

--------------------------------------------------------------------------------
/.claude/commands/create-bucket-docs.md:
--------------------------------------------------------------------------------

```markdown
  1 | ---
  2 | argument-hint: <analysis-output>
  3 | description: Create documentation for a Lingo.dev bucket type using analysis output
  4 | ---
  5 | 
  6 | Using the bucket analysis output provided at the end of this prompt, create documentation for the specified bucket type in Lingo.dev CLI.
  7 | 
  8 | ## Template Structure
  9 | 
 10 | ````markdown
 11 | ---
 12 | title: "[BUCKET_TYPE in title case]"
 13 | subtitle: "Translate [BUCKET_TYPE] files with Lingo.dev CLI"
 14 | ---
 15 | 
 16 | ## Introduction
 17 | 
 18 | [BUCKET_TYPE in title case] files are [BRIEF DESCRIPTION OF THE FILE FORMAT, ITS PURPOSE AND PRIMARY USE CASE]. [ONE SENTENCE ABOUT STRUCTURE OR KEY CHARACTERISTICS].
 19 | 
 20 | **Lingo.dev CLI** uses LLMs to translate your [BUCKET_TYPE] files across multiple locales. This guide shows you how to set up and run translations for [BUCKET_TYPE] files.
 21 | 
 22 | ## Quickstart
 23 | 
 24 | ### Step 1: Install Lingo.dev CLI
 25 | 
 26 | ```bash
 27 | # Install globally
 28 | npm install -g lingo.dev@latest
 29 | 
 30 | # Or run directly with npx
 31 | npx lingo.dev@latest --version
 32 | ```
 33 | 
 34 | ### Step 2: Authenticate
 35 | 
 36 | Log in to your Lingo.dev account:
 37 | 
 38 | ```bash
 39 | npx lingo.dev@latest login
 40 | ```
 41 | 
 42 | This opens your browser for authentication. Your API key is stored locally for future use.
 43 | 
 44 | ### Step 3: Initialize Project
 45 | 
 46 | Create your base configuration:
 47 | 
 48 | ```bash
 49 | npx lingo.dev@latest init
 50 | ```
 51 | 
 52 | This generates an `i18n.json` file with default settings.
 53 | 
 54 | ### Step 4: Configure [BUCKET_TYPE] Bucket
 55 | 
 56 | Update your `i18n.json` to add [BUCKET_TYPE] support:
 57 | 
 58 | ```json
 59 | {
 60 |   "$schema": "https://lingo.dev/schema/i18n.json",
 61 |   "version": "1.10",
 62 |   "locale": {
 63 |     "source": "en",
 64 |     "targets": ["es"]
 65 |   },
 66 |   "buckets": {
 67 |     "[BUCKET_TYPE]": {
 68 |       "include": ["[PATH_PATTERN]"]
 69 |     }
 70 |   }
 71 | }
 72 | ```
 73 | 
 74 | [IF separate-files: **Note**: Keep `[locale]` as-is in the config — it's replaced with actual locale codes at runtime.]
 75 | [IF in-place: DO NOT include any note about [locale]]
 76 | 
 77 | ### Step 5: Create File Structure
 78 | 
 79 | [FOR separate-files:]
 80 | Organize your [BUCKET_TYPE] files by locale:
 81 | 
 82 | ```
 83 | [directory]/
 84 | ├── en/
 85 | │   └── [filename]      # Source file
 86 | └── es/                 # Target directory (empty initially)
 87 | ```
 88 | 
 89 | Place your source [BUCKET_TYPE] files in the `en/` directory. The `es/` directory can be empty — translated files will be created there automatically.
 90 | 
 91 | [FOR in-place:]
 92 | Place your [BUCKET_TYPE] file in your project:
 93 | 
 94 | ```
 95 | [directory]/
 96 | └── [filename]          # Contains all locales
 97 | ```
 98 | 
 99 | This single file will contain translations for all configured locales.
100 | 
101 | ### Step 6: Run Translation
102 | 
103 | Execute the translation command:
104 | 
105 | ```bash
106 | npx lingo.dev@latest i18n
107 | ```
108 | 
109 | The CLI will:
110 | 
111 | - Read [BUCKET_TYPE] files from your source locale
112 | - Translate content to target locales using LLMs
113 | - [FOR separate-files: Create new files in target directories (e.g., `es/[filename]`)]
114 | - [FOR in-place: Update the file with translations for all configured locales]
115 | 
116 | [FOR separate-files: **Note**: Unlike some bucket types that modify files in place, the [BUCKET_TYPE] bucket creates separate files for each locale. Your source files remain unchanged.]
117 | [FOR in-place: **Note**: The [BUCKET_TYPE] bucket modifies the source file directly, adding translations for all target locales to the same file.]
118 | 
119 | ### Step 7: Verify Results
120 | 
121 | Check the translation status:
122 | 
123 | ```bash
124 | npx lingo.dev@latest status
125 | ```
126 | 
127 | [FOR separate-files: Review generated files in your target locale directory (`es/`).]
128 | [FOR in-place: Review the updated [filename] file which now contains all locales.]
129 | 
130 | ## [Feature Sections - ONLY include supported features]
131 | 
132 | [IF Locked Keys = YES:]
133 | 
134 | ## Locked Content
135 | 
136 | The [BUCKET_TYPE] bucket supports locking specific keys to prevent translation:
137 | 
138 | ```json
139 | "[BUCKET_TYPE]": {
140 |   "include": ["[PATH_PATTERN]"],
141 |   "lockedKeys": ["key1", "key2", "nested/key3"]
142 | }
143 | ```
144 | 
145 | This feature is available for [BUCKET_TYPE] and other structured format buckets where specific keys need to remain untranslated.
146 | 
147 | [IF Ignored Keys = YES:]
148 | 
149 | ## Ignored Keys
150 | 
151 | The [BUCKET_TYPE] bucket supports ignoring keys entirely during processing:
152 | 
153 | ```json
154 | "[BUCKET_TYPE]": {
155 |   "include": ["[PATH_PATTERN]"],
156 |   "ignoredKeys": ["debug", "internal/*"]
157 | }
158 | ```
159 | 
160 | Unlike locked keys which preserve content, ignored keys are completely skipped during the translation process.
161 | 
162 | [IF Inject Locale = YES:]
163 | 
164 | ## Inject Locale
165 | 
166 | The [BUCKET_TYPE] bucket supports automatically injecting locale codes into specific keys:
167 | 
168 | ```json
169 | "[BUCKET_TYPE]": {
170 |   "include": ["[PATH_PATTERN]"],
171 |   "injectLocale": ["settings/language", "config/locale"]
172 | }
173 | ```
174 | 
175 | These keys will automatically have their values replaced with the current locale code during translation.
176 | 
177 | [IF Translator Notes = YES:]
178 | 
179 | ## Translator Notes
180 | 
181 | The [BUCKET_TYPE] bucket supports providing context hints to improve translation quality. [Describe how translator notes/hints work for this specific bucket type]
182 | 
183 | ```[format]
184 | [Show example of how to add translator notes in this format]
185 | ```
186 | 
187 | ## Example
188 | 
189 | **Configuration** (`i18n.json`):
190 | 
191 | ```json
192 | {
193 |   "$schema": "https://lingo.dev/schema/i18n.json",
194 |   "version": "1.10",
195 |   "locale": {
196 |     "source": "en",
197 |     "targets": ["es"]
198 |   },
199 |   "buckets": {
200 |     "[BUCKET_TYPE]": {
201 |       "include": ["[REALISTIC_PATH]"]
202 |     }
203 |   }
204 | }
205 | ```
206 | 
207 | [FOR separate-files:]
208 | **Input** (`[path]/en/[filename]`):
209 | 
210 | ```[format]
211 | [Source content in appropriate format]
212 | ```
213 | 
214 | **Output** (`[path]/es/[filename]`):
215 | 
216 | ```[format]
217 | [Translated content in appropriate format]
218 | ```
219 | 
220 | [FOR in-place:]
221 | **Before translation** (`[path]/[filename]`):
222 | 
223 | ```[format]
224 | [Source content showing only English]
225 | ```
226 | 
227 | **After translation** (`[path]/[filename]`):
228 | 
229 | ```[format]
230 | [Same file now containing both English and Spanish]
231 | ```
232 | ````
233 | 
234 | ## Critical Adaptation Rules
235 | 
236 | ### For Separate-Files Buckets
237 | 
238 | 1. **Always use `[locale]` placeholder** in paths
239 | 2. Step 5: Show source (`en/`) and target (`es/`) directories
240 | 3. Step 6: Explain "creates new files"
241 | 4. Include the [locale] note in Step 4
242 | 5. Example: Show input as `path/en/file.ext` and output as `path/es/file.ext`
243 | 
244 | ### For In-Place Buckets
245 | 
246 | 1. **Never use `[locale]` placeholder** anywhere in the document
247 | 2. **Never include the [locale] note** in Step 4
248 | 3. Step 5: Show single file path
249 | 4. Step 6: Explain "modifies the file directly"
250 | 5. Example: Use "Before translation" and "After translation" labels
251 | 6. Example: Show the same file path for both states
252 | 
253 | ### Feature Sections
254 | 
255 | - Only include sections for features marked YES
256 | - Locked Keys: Content is preserved unchanged
257 | - Ignored Keys: Keys are skipped entirely during processing
258 | - Inject Locale: Keys automatically get the locale code as their value
259 | - Translator Notes: Format varies significantly by bucket type
260 | 
261 | ### Path Conventions
262 | 
263 | Choose realistic paths for the bucket type:
264 | 
265 | - iOS: `ios/Resources/`, `[AppName]/`
266 | - Android: `app/src/main/res/values-[locale]/`
267 | - Web: `locales/`, `i18n/`, `translations/`
268 | - Flutter: `lib/l10n/`
269 | - Java: `src/main/resources/`
270 | 
271 | ### Writing Rules
272 | 
273 | - Match the concise, direct tone of the template
274 | - No marketing language or unnecessary adjectives
275 | - Don't document what specifically gets translated
276 | - Don't include generic features (exclude patterns, multiple directories)
277 | - Focus only on bucket-specific behavior
278 | - Use only `en` → `es` in all examples
279 | - Keep examples minimal but representative
280 | 
281 | ## Instructions
282 | 
283 | 1. Parse the bucket analysis output provided in the arguments to determine:
284 | 
285 |    - Bucket type name
286 |    - File organization (separate-files if uses [locale] placeholder, in-place if not)
287 |    - Supported features (lockedKeys, ignoredKeys, injectLocale, hints/notes)
288 |    - Typical file extension and paths
289 | 
290 | 2. Based on the analysis, fill in the template with appropriate:
291 | 
292 |    - Description of the file format
293 |    - Realistic path patterns
294 |    - Only the features that are actually supported
295 |    - Appropriate examples for the format
296 | 
297 | 3. Generate the complete Markdown documentation following the specifications exactly.
298 | 
299 | ---
300 | 
301 | ## Bucket Analysis Output
302 | 
303 | $ARGUMENTS
304 | 
```

--------------------------------------------------------------------------------
/packages/cli/src/cli/cmd/purge.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { Command } from "interactive-commander";
  2 | import _ from "lodash";
  3 | import Ora from "ora";
  4 | import { getConfig } from "../utils/config";
  5 | import { getBuckets } from "../utils/buckets";
  6 | import { resolveOverriddenLocale } from "@lingo.dev/_spec";
  7 | import createBucketLoader from "../loaders";
  8 | import { minimatch } from "minimatch";
  9 | import { confirm } from "@inquirer/prompts";
 10 | 
 11 | interface PurgeOptions {
 12 |   bucket?: string[];
 13 |   file?: string[];
 14 |   key?: string;
 15 |   locale?: string[];
 16 |   yesReally?: boolean;
 17 | }
 18 | 
 19 | export default new Command()
 20 |   .command("purge")
 21 |   .description(
 22 |     "WARNING: Permanently delete translation entries from bucket path patterns defined in i18n.json. This is a destructive operation that cannot be undone. Without any filters, ALL managed keys will be removed from EVERY target locale.",
 23 |   )
 24 |   .helpOption("-h, --help", "Show help")
 25 |   .option(
 26 |     "--bucket <bucket>",
 27 |     "Limit the purge to specific bucket types defined under `buckets` in i18n.json. Repeat the flag to include multiple bucket types. Defaults to all buckets",
 28 |     (val: string, prev: string[]) => (prev ? [...prev, val] : [val]),
 29 |   )
 30 |   .option(
 31 |     "--file [files...]",
 32 |     "Filter which file paths to purge by matching against path patterns. Only paths containing any of these values will be processed. Examples: --file messages.json --file admin/",
 33 |   )
 34 |   .option(
 35 |     "--key <key>",
 36 |     "Filter which keys to delete using prefix matching on dot-separated key paths. Example: 'auth.login' matches all keys starting with auth.login. Omit this option to delete ALL keys. Keys marked as locked or ignored in i18n.json are automatically skipped",
 37 |     (val: string) => encodeURIComponent(val),
 38 |   )
 39 |   .option(
 40 |     "--locale <locale>",
 41 |     "Limit purging to specific target locale codes from i18n.json. Repeat the flag to include multiple locales. Defaults to all configured target locales. Warning: Including the source locale will delete content from it as well.",
 42 |     (val: string, prev: string[]) => (prev ? [...prev, val] : [val]),
 43 |   )
 44 |   .option(
 45 |     "--yes-really",
 46 |     "Bypass safety confirmations for destructive operations. Use with extreme caution - this will delete translation keys without asking for confirmation. Intended for automated scripts and CI environments only.",
 47 |   )
 48 |   .action(async function (options: PurgeOptions) {
 49 |     const ora = Ora();
 50 |     try {
 51 |       ora.start("Loading configuration...");
 52 |       const i18nConfig = getConfig();
 53 |       if (!i18nConfig) {
 54 |         throw new Error("i18n.json not found. Please run `lingo.dev init`.");
 55 |       }
 56 |       ora.succeed("Configuration loaded");
 57 | 
 58 |       let buckets = getBuckets(i18nConfig);
 59 |       if (options.bucket && options.bucket.length) {
 60 |         buckets = buckets.filter((bucket) =>
 61 |           options.bucket!.includes(bucket.type),
 62 |         );
 63 |       }
 64 |       if (options.file && options.file.length) {
 65 |         buckets = buckets
 66 |           .map((bucket) => {
 67 |             const paths = bucket.paths.filter((bucketPath) =>
 68 |               options.file?.some((f) => bucketPath.pathPattern.includes(f)),
 69 |             );
 70 |             return { ...bucket, paths };
 71 |           })
 72 |           .filter((bucket) => bucket.paths.length > 0);
 73 |         if (buckets.length === 0) {
 74 |           ora.fail("All files were filtered out by --file option.");
 75 |           process.exit(1);
 76 |         }
 77 |       }
 78 |       const sourceLocale = i18nConfig.locale.source;
 79 |       const targetLocales =
 80 |         options.locale && options.locale.length
 81 |           ? options.locale
 82 |           : i18nConfig.locale.targets;
 83 |       let removedAny = false;
 84 |       for (const bucket of buckets) {
 85 |         console.log();
 86 |         ora.info(`Processing bucket: ${bucket.type}`);
 87 |         for (const bucketPath of bucket.paths) {
 88 |           for (const _targetLocale of targetLocales) {
 89 |             const targetLocale = resolveOverriddenLocale(
 90 |               _targetLocale,
 91 |               bucketPath.delimiter,
 92 |             );
 93 |             const bucketOra = Ora({ indent: 2 }).start(
 94 |               `Processing path: ${bucketPath.pathPattern} [${targetLocale}]`,
 95 |             );
 96 |             try {
 97 |               const bucketLoader = createBucketLoader(
 98 |                 bucket.type,
 99 |                 bucketPath.pathPattern,
100 |                 {
101 |                   defaultLocale: sourceLocale,
102 |                   injectLocale: bucket.injectLocale,
103 |                   formatter: i18nConfig!.formatter,
104 |                 },
105 |                 bucket.lockedKeys,
106 |                 bucket.lockedPatterns,
107 |                 bucket.ignoredKeys,
108 |               );
109 |               await bucketLoader.init();
110 |               bucketLoader.setDefaultLocale(sourceLocale);
111 |               await bucketLoader.pull(sourceLocale);
112 |               let targetData = await bucketLoader.pull(targetLocale);
113 |               if (!targetData || Object.keys(targetData).length === 0) {
114 |                 bucketOra.info(
115 |                   `No translations found for ${bucketPath.pathPattern} [${targetLocale}]`,
116 |                 );
117 |                 continue;
118 |               }
119 |               let newData = { ...targetData };
120 |               let keysToRemove: string[] = [];
121 |               if (options.key) {
122 |                 // minimatch for key patterns
123 |                 keysToRemove = Object.keys(newData).filter((k) =>
124 |                   minimatch(k, options.key!),
125 |                 );
126 |               } else {
127 |                 // No key specified: remove all keys
128 |                 keysToRemove = Object.keys(newData);
129 |               }
130 |               if (keysToRemove.length > 0) {
131 |                 // Show what will be deleted
132 |                 if (options.key) {
133 |                   bucketOra.info(
134 |                     `About to delete ${keysToRemove.length} key(s) matching '${options.key}' from ${bucketPath.pathPattern} [${targetLocale}]:\n  ${keysToRemove.slice(0, 10).join(", ")}${keysToRemove.length > 10 ? ", ..." : ""}`,
135 |                   );
136 |                 } else {
137 |                   bucketOra.info(
138 |                     `About to delete all (${keysToRemove.length}) keys from ${bucketPath.pathPattern} [${targetLocale}]`,
139 |                   );
140 |                 }
141 | 
142 |                 if (!options.yesReally) {
143 |                   bucketOra.warn(
144 |                     "This is a destructive operation. If you are sure, type 'y' to continue. (Use --yes-really to skip this check.)",
145 |                   );
146 |                   const confirmed = await confirm({
147 |                     message: `Delete these keys from ${bucketPath.pathPattern} [${targetLocale}]?`,
148 |                     default: false,
149 |                   });
150 |                   if (!confirmed) {
151 |                     bucketOra.info("Skipped by user.");
152 |                     continue;
153 |                   }
154 |                 }
155 |                 for (const key of keysToRemove) {
156 |                   delete newData[key];
157 |                 }
158 |                 removedAny = true;
159 |                 await bucketLoader.push(targetLocale, newData);
160 |                 if (options.key) {
161 |                   bucketOra.succeed(
162 |                     `Removed ${keysToRemove.length} key(s) matching '${options.key}' from ${bucketPath.pathPattern} [${targetLocale}]`,
163 |                   );
164 |                 } else {
165 |                   bucketOra.succeed(
166 |                     `Removed all keys (${keysToRemove.length}) from ${bucketPath.pathPattern} [${targetLocale}]`,
167 |                   );
168 |                 }
169 |               } else if (options.key) {
170 |                 bucketOra.info(
171 |                   `No keys matching '${options.key}' found in ${bucketPath.pathPattern} [${targetLocale}]`,
172 |                 );
173 |               } else {
174 |                 bucketOra.info("No keys to remove.");
175 |               }
176 |             } catch (error) {
177 |               const err = error as Error;
178 |               bucketOra.fail(`Failed: ${err.message}`);
179 |             }
180 |           }
181 |         }
182 |       }
183 |       if (!removedAny) {
184 |         ora.info("No keys were removed.");
185 |       } else {
186 |         ora.succeed("Purge completed.");
187 |       }
188 |     } catch (error) {
189 |       const err = error as Error;
190 |       ora.fail(err.message);
191 |       process.exit(1);
192 |     }
193 |   });
194 | 
```

--------------------------------------------------------------------------------
/packages/spec/src/locales.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import Z from "zod";
  2 | 
  3 | const localeMap = {
  4 |   // Urdu (Pakistan)
  5 |   ur: ["ur-PK"],
  6 |   // Vietnamese (Vietnam)
  7 |   vi: ["vi-VN"],
  8 |   // Turkish (Turkey)
  9 |   tr: ["tr-TR"],
 10 |   // Tamil (India)
 11 |   ta: [
 12 |     "ta-IN", // India
 13 |     "ta-SG", // Singapore
 14 |   ],
 15 |   // Serbian
 16 |   sr: [
 17 |     "sr-RS", // Serbian (Latin)
 18 |     "sr-Latn-RS", // Serbian (Latin)
 19 |     "sr-Cyrl-RS", // Serbian (Cyrillic)
 20 |   ],
 21 |   // Hungarian (Hungary)
 22 |   hu: ["hu-HU"],
 23 |   // Hebrew (Israel)
 24 |   he: ["he-IL"],
 25 |   // Estonian (Estonia)
 26 |   et: ["et-EE"],
 27 |   // Greek
 28 |   el: [
 29 |     "el-GR", // Greece
 30 |     "el-CY", // Cyprus
 31 |   ],
 32 |   // Danish (Denmark)
 33 |   da: ["da-DK"],
 34 |   // Azerbaijani (Azerbaijan)
 35 |   az: ["az-AZ"],
 36 |   // Thai (Thailand)
 37 |   th: ["th-TH"],
 38 |   // Swedish (Sweden)
 39 |   sv: ["sv-SE"],
 40 |   // English
 41 |   en: [
 42 |     "en-US", // United States
 43 |     "en-GB", // United Kingdom
 44 |     "en-AU", // Australia
 45 |     "en-CA", // Canada
 46 |     "en-SG", // Singapore
 47 |     "en-IE", // Ireland
 48 |   ],
 49 |   // Spanish
 50 |   es: [
 51 |     "es-ES", // Spain
 52 |     "es-419", // Latin America
 53 |     "es-MX", // Mexico
 54 |     "es-AR", // Argentina
 55 |   ],
 56 |   // French
 57 |   fr: [
 58 |     "fr-FR", // France
 59 |     "fr-CA", // Canada
 60 |     "fr-BE", // Belgium
 61 |     "fr-LU", // Luxembourg
 62 |   ],
 63 |   // Catalan (Spain)
 64 |   ca: ["ca-ES"],
 65 |   // Japanese (Japan)
 66 |   ja: ["ja-JP"],
 67 |   // Kazakh (Kazakhstan)
 68 |   kk: ["kk-KZ"],
 69 |   // German
 70 |   de: [
 71 |     "de-DE", // Germany
 72 |     "de-AT", // Austria
 73 |     "de-CH", // Switzerland
 74 |   ],
 75 |   // Portuguese
 76 |   pt: [
 77 |     "pt-PT", // Portugal
 78 |     "pt-BR", // Brazil
 79 |   ],
 80 |   // Italian
 81 |   it: [
 82 |     "it-IT", // Italy
 83 |     "it-CH", // Switzerland
 84 |   ],
 85 |   // Russian
 86 |   ru: [
 87 |     "ru-RU", // Russia
 88 |     "ru-BY", // Belarus
 89 |   ],
 90 |   // Ukrainian (Ukraine)
 91 |   uk: ["uk-UA"],
 92 |   // Belarusian (Belarus)
 93 |   be: ["be-BY"],
 94 |   // Hindi (India)
 95 |   hi: ["hi-IN"],
 96 |   // Chinese
 97 |   zh: [
 98 |     "zh-CN", // Simplified Chinese (China)
 99 |     "zh-TW", // Traditional Chinese (Taiwan)
100 |     "zh-HK", // Traditional Chinese (Hong Kong)
101 |     "zh-SG", // Simplified Chinese (Singapore)
102 |     "zh-Hans", // Simplified Chinese
103 |     "zh-Hant", // Traditional Chinese
104 |     "zh-Hant-HK", // Traditional Chinese (Hong Kong)
105 |     "zh-Hant-TW", // Traditional Chinese (Taiwan)
106 |     "zh-Hant-CN", // Traditional Chinese (China)
107 |     "zh-Hans-HK", // Simplified Chinese (Hong Kong)
108 |     "zh-Hans-TW", // Simplified Chinese (China)
109 |     "zh-Hans-CN", // Simplified Chinese (China)
110 |   ],
111 |   // Korean (South Korea)
112 |   ko: ["ko-KR"],
113 |   // Arabic
114 |   ar: [
115 |     "ar-EG", // Egypt
116 |     "ar-SA", // Saudi Arabia
117 |     "ar-AE", // United Arab Emirates
118 |     "ar-MA", // Morocco
119 |   ],
120 |   // Bulgarian (Bulgaria)
121 |   bg: ["bg-BG"],
122 |   // Czech (Czech Republic)
123 |   cs: ["cs-CZ"],
124 |   // Welsh (Wales)
125 |   cy: ["cy-GB"],
126 |   // Dutch
127 |   nl: [
128 |     "nl-NL", // Netherlands
129 |     "nl-BE", // Belgium
130 |   ],
131 |   // Polish (Poland)
132 |   pl: ["pl-PL"],
133 |   // Indonesian (Indonesia)
134 |   id: ["id-ID"],
135 |   is: ["is-IS"],
136 |   // Malay (Malaysia)
137 |   ms: ["ms-MY"],
138 |   // Finnish (Finland)
139 |   fi: ["fi-FI"],
140 |   // Basque (Spain)
141 |   eu: ["eu-ES"],
142 |   // Croatian (Croatia)
143 |   hr: ["hr-HR"],
144 |   // Hebrew (Israel) - alternative code
145 |   iw: ["iw-IL"],
146 |   // Khmer (Cambodia)
147 |   km: ["km-KH"],
148 |   // Latvian (Latvia)
149 |   lv: ["lv-LV"],
150 |   // Lithuanian (Lithuania)
151 |   lt: ["lt-LT"],
152 |   // Norwegian
153 |   no: [
154 |     "no-NO", // Norway (legacy)
155 |     "nb-NO", // Norwegian Bokmål
156 |     "nn-NO", // Norwegian Nynorsk
157 |   ],
158 |   // Romanian (Romania)
159 |   ro: ["ro-RO"],
160 |   // Slovak (Slovakia)
161 |   sk: ["sk-SK"],
162 |   // Swahili
163 |   sw: [
164 |     "sw-TZ", // Tanzania
165 |     "sw-KE", // Kenya
166 |     "sw-UG", // Uganda
167 |     "sw-CD", // Democratic Republic of Congo
168 |     "sw-RW", // Rwanda
169 |   ],
170 |   // Persian (Iran)
171 |   fa: ["fa-IR"],
172 |   // Filipino (Philippines)
173 |   fil: ["fil-PH"],
174 |   // Punjabi
175 |   pa: [
176 |     "pa-IN", // India
177 |     "pa-PK", // Pakistan
178 |   ],
179 |   // Bengali
180 |   bn: [
181 |     "bn-BD", // Bangladesh
182 |     "bn-IN", // India
183 |   ],
184 |   // Irish (Ireland)
185 |   ga: ["ga-IE"],
186 |   // Galician (Spain)
187 |   gl: ["gl-ES"],
188 |   // Maltese (Malta)
189 |   mt: ["mt-MT"],
190 |   // Slovenian (Slovenia)
191 |   sl: ["sl-SI"],
192 |   // Albanian (Albania)
193 |   sq: ["sq-AL"],
194 |   // Bavarian (Germany)
195 |   bar: ["bar-DE"],
196 |   // Neapolitan (Italy)
197 |   nap: ["nap-IT"],
198 |   // Afrikaans (South Africa)
199 |   af: ["af-ZA"],
200 |   // Uzbek (Latin)
201 |   uz: ["uz-Latn"],
202 |   // Somali (Somalia)
203 |   so: ["so-SO"],
204 |   // Tigrinya (Ethiopia)
205 |   ti: ["ti-ET"],
206 |   // Standard Moroccan Tamazight (Morocco)
207 |   zgh: ["zgh-MA"],
208 |   // Tagalog (Philippines)
209 |   tl: ["tl-PH"],
210 |   // Telugu (India)
211 |   te: ["te-IN"],
212 |   // Kinyarwanda (Rwanda)
213 |   rw: ["rw-RW"],
214 |   // Georgian (Georgia)
215 |   ka: ["ka-GE"],
216 |   // Malayalam (India)
217 |   ml: ["ml-IN"],
218 |   // Armenian (Armenia)
219 |   hy: ["hy-AM"],
220 |   // Macedonian (Macedonia)
221 |   mk: ["mk-MK"],
222 | } as const;
223 | 
224 | export type LocaleCodeShort = keyof typeof localeMap;
225 | export type LocaleCodeFull = (typeof localeMap)[LocaleCodeShort][number];
226 | export type LocaleCode = LocaleCodeShort | LocaleCodeFull;
227 | export type LocaleDelimiter = "-" | "_" | null;
228 | 
229 | export const localeCodesShort = Object.keys(localeMap) as LocaleCodeShort[];
230 | export const localeCodesFull = Object.values(
231 |   localeMap,
232 | ).flat() as LocaleCodeFull[];
233 | export const localeCodesFullUnderscore = localeCodesFull.map((value) =>
234 |   value.replace("-", "_"),
235 | );
236 | export const localeCodesFullExplicitRegion = localeCodesFull.map((value) => {
237 |   const chunks = value.split("-");
238 |   const result = [chunks[0], "-r", chunks.slice(1).join("-")].join("");
239 |   return result;
240 | });
241 | export const localeCodes = [
242 |   ...localeCodesShort,
243 |   ...localeCodesFull,
244 |   ...localeCodesFullUnderscore,
245 |   ...localeCodesFullExplicitRegion,
246 | ] as LocaleCode[];
247 | 
248 | export const localeCodeSchema = Z.string().refine(
249 |   (value) => localeCodes.includes(value as any),
250 |   {
251 |     message: "Invalid locale code",
252 |   },
253 | );
254 | 
255 | /**
256 |  * Resolves a locale code to its full locale representation.
257 |  *
258 |  *  If the provided locale code is already a full locale code, it returns as is.
259 |  *  If the provided locale code is a short locale code, it returns the first corresponding full locale.
260 |  *  If the locale code is not found, it throws an error.
261 |  *
262 |  * @param {localeCodes} value - The locale code to resolve (either short or full)
263 |  * @return {LocaleCodeFull} The resolved full locale code
264 |  * @throws {Error} If the provided locale code is invalid.
265 |  */
266 | export const resolveLocaleCode = (value: string): LocaleCodeFull => {
267 |   const existingFullLocaleCode = Object.values(localeMap)
268 |     .flat()
269 |     .includes(value as any);
270 |   if (existingFullLocaleCode) {
271 |     return value as LocaleCodeFull;
272 |   }
273 | 
274 |   const existingShortLocaleCode = Object.keys(localeMap).includes(value);
275 |   if (existingShortLocaleCode) {
276 |     const correspondingFullLocales = localeMap[value as LocaleCodeShort];
277 |     const fallbackFullLocale = correspondingFullLocales[0];
278 |     return fallbackFullLocale;
279 |   }
280 | 
281 |   throw new Error(`Invalid locale code: ${value}`);
282 | };
283 | 
284 | /**
285 |  * Determines the delimiter used in a locale code
286 |  *
287 |  * @param {string} locale - the locale string (e.g.,"en_US","en-GB")
288 |  * @return { string | null} - The delimiter ("_" or "-") if found, otherwise `null`.
289 |  */
290 | 
291 | export const getLocaleCodeDelimiter = (locale: string): LocaleDelimiter => {
292 |   if (locale.includes("_")) {
293 |     return "_";
294 |   } else if (locale.includes("-")) {
295 |     return "-";
296 |   } else {
297 |     return null;
298 |   }
299 | };
300 | 
301 | /**
302 |  * Replaces the delimiter in a locale string with the specified delimiter.
303 |  *
304 |  * @param {string}locale - The locale string (e.g.,"en_US", "en-GB").
305 |  * @param {"-" | "_" | null} [delimiter] - The new delimiter to replace the existing one.
306 |  * @returns {string} The locale string with the replaced delimiter, or the original locale if no delimiter is provided.
307 |  */
308 | 
309 | export const resolveOverriddenLocale = (
310 |   locale: string,
311 |   delimiter?: LocaleDelimiter,
312 | ): string => {
313 |   if (!delimiter) {
314 |     return locale;
315 |   }
316 | 
317 |   const currentDelimiter = getLocaleCodeDelimiter(locale);
318 |   if (!currentDelimiter) {
319 |     return locale;
320 |   }
321 | 
322 |   return locale.replace(currentDelimiter, delimiter);
323 | };
324 | 
325 | /**
326 |  * Normalizes a locale string by replacing underscores with hyphens
327 |  * and removing the "r" in certain regional codes (e.g., "fr-rCA" → "fr-CA")
328 |  *
329 |  * @param {string} locale - The locale string (e.g.,"en_US", "en-GB").
330 |  * @return {string} The normalized locale string.
331 |  */
332 | 
333 | export function normalizeLocale(locale: string): string {
334 |   return locale.replaceAll("_", "-").replace(/([a-z]{2,3}-)r/, "$1");
335 | }
336 | 
```

--------------------------------------------------------------------------------
/packages/cli/src/cli/loaders/typescript/index.spec.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, expect, it } from "vitest";
  2 | import createTypescriptLoader from "./index";
  3 | import dedent from "dedent";
  4 | 
  5 | describe("typescript loader", () => {
  6 |   it("should extract string literals from default export object", async () => {
  7 |     const input = `
  8 |       export default {
  9 |         greeting: "Hello, world!",
 10 |         farewell: "Goodbye!",
 11 |         number: 42,
 12 |         boolean: true
 13 |       };
 14 |     `;
 15 | 
 16 |     const loader = createTypescriptLoader().setDefaultLocale("en");
 17 |     const result = await loader.pull("en", input);
 18 | 
 19 |     expect(result).toEqual({
 20 |       greeting: "Hello, world!",
 21 |       farewell: "Goodbye!",
 22 |     });
 23 |   });
 24 | 
 25 |   it("should extract string literals from exported variable", async () => {
 26 |     const input = `
 27 |       const messages = {
 28 |         welcome: "Welcome to our app",
 29 |         error: "Something went wrong",
 30 |         count: 5
 31 |       };
 32 |       export default messages;
 33 |     `;
 34 | 
 35 |     const loader = createTypescriptLoader().setDefaultLocale("en");
 36 |     const result = await loader.pull("en", input);
 37 | 
 38 |     expect(result).toEqual({
 39 |       welcome: "Welcome to our app",
 40 |       error: "Something went wrong",
 41 |     });
 42 |   });
 43 | 
 44 |   it("should handle empty or invalid input", async () => {
 45 |     const loader = createTypescriptLoader().setDefaultLocale("en");
 46 | 
 47 |     let result = await loader.pull("en", "");
 48 |     expect(result).toEqual({});
 49 | 
 50 |     result = await loader.pull("en", "const x = 5;");
 51 |     expect(result).toEqual({});
 52 |   });
 53 | 
 54 |   it("should update string literals in default export object", async () => {
 55 |     const input = `
 56 |       export default {
 57 |         greeting: "Hello, world!",
 58 |         farewell: "Goodbye!",
 59 |         number: 42
 60 |       };
 61 |     `;
 62 | 
 63 |     const loader = createTypescriptLoader().setDefaultLocale("en");
 64 | 
 65 |     await loader.pull("en", input);
 66 | 
 67 |     const data = {
 68 |       greeting: "Hola, mundo!",
 69 |       farewell: "Adiós!",
 70 |     };
 71 | 
 72 |     const result = await loader.push("es", data);
 73 | 
 74 |     expect(result).toBe(dedent`
 75 |       export default {
 76 |         greeting: "Hola, mundo!",
 77 |         farewell: "Adiós!",
 78 |         number: 42
 79 |       };
 80 |       `);
 81 |   });
 82 | 
 83 |   it("should extract string literals from nested objects", async () => {
 84 |     const input = `
 85 |       export default {
 86 |         messages: {
 87 |           welcome: "Welcome to our app",
 88 |           error: "Something went wrong",
 89 |           count: 5
 90 |         },
 91 |         settings: {
 92 |           theme: {
 93 |             name: "Dark Mode",
 94 |             colors: {
 95 |               primary: "blue",
 96 |               secondary: "gray"
 97 |             }
 98 |           }
 99 |         }
100 |       };
101 |     `;
102 | 
103 |     const loader = createTypescriptLoader().setDefaultLocale("en");
104 |     const result = await loader.pull("en", input);
105 | 
106 |     expect(result).toEqual({
107 |       messages: {
108 |         welcome: "Welcome to our app",
109 |         error: "Something went wrong",
110 |       },
111 |       settings: {
112 |         theme: {
113 |           name: "Dark Mode",
114 |           colors: {
115 |             primary: "blue",
116 |             secondary: "gray",
117 |           },
118 |         },
119 |       },
120 |     });
121 |   });
122 | 
123 |   it("should extract string literals from arrays", async () => {
124 |     const input = `
125 |       export default {
126 |         greetings: ["Hello", "Hi", "Hey"],
127 |         categories: [
128 |           { name: "Electronics", description: "Electronic devices" },
129 |           { name: "Books", description: "Reading materials" }
130 |         ]
131 |       };
132 |     `;
133 | 
134 |     const loader = createTypescriptLoader().setDefaultLocale("en");
135 |     const result = await loader.pull("en", input);
136 | 
137 |     expect(result).toEqual({
138 |       greetings: ["Hello", "Hi", "Hey"],
139 |       categories: [
140 |         { name: "Electronics", description: "Electronic devices" },
141 |         { name: "Books", description: "Reading materials" },
142 |       ],
143 |     });
144 |   });
145 | 
146 |   it("should update string literals in nested objects", async () => {
147 |     const input = dedent`
148 |       export default {
149 |         messages: {
150 |           welcome: "Welcome to our app",
151 |           error: "Something went wrong"
152 |         },
153 |         settings: {
154 |           theme: {
155 |             name: "Dark Mode",
156 |             colors: {
157 |               primary: "blue"
158 |             }
159 |           }
160 |         }
161 |       };
162 |     `;
163 | 
164 |     const loader = createTypescriptLoader().setDefaultLocale("en");
165 | 
166 |     let data = await loader.pull("en", input);
167 | 
168 |     data.settings.theme.colors.primary = "red";
169 | 
170 |     const result = await loader.push("es", data);
171 | 
172 |     expect(result).toBe(dedent`
173 |       export default {
174 |         messages: {
175 |           welcome: "Welcome to our app",
176 |           error: "Something went wrong"
177 |         },
178 |         settings: {
179 |           theme: {
180 |             name: "Dark Mode",
181 |             colors: {
182 |               primary: "red"
183 |             }
184 |           }
185 |         }
186 |       };
187 |       `);
188 |   });
189 | 
190 |   it("should update string literals in arrays", async () => {
191 |     const input = `
192 |       export default {
193 |         greetings: ["Hello", "Hi", "Hey"],
194 |       };
195 |     `;
196 | 
197 |     const loader = createTypescriptLoader().setDefaultLocale("en");
198 | 
199 |     let data = await loader.pull("en", input);
200 | 
201 |     data.greetings[0] = "Hola";
202 |     data.greetings[1] = "Hola";
203 |     data.greetings[2] = "Oye";
204 | 
205 |     const result = await loader.push("es", data);
206 | 
207 |     expect(result).toBe(dedent`
208 |       export default {
209 |         greetings: ["Hola", "Hola", "Oye"]
210 |       };
211 |       `);
212 |   });
213 | 
214 |   it("should handle mixed nested structures", async () => {
215 |     const input = `
216 |       export default {
217 |         app: {
218 |           name: "My App",
219 |           version: "1.0.0",
220 |           features: ["Login", "Dashboard", "Settings"],
221 |           pages: [
222 |             { 
223 |               title: "Home", 
224 |               sections: [
225 |                 { heading: "Welcome", content: "Welcome to our app" },
226 |                 { heading: "Features", content: "Check out our features" }
227 |               ]
228 |             },
229 |             { 
230 |               title: "About", 
231 |               sections: [
232 |                 { heading: "Our Story", content: "We started in 2020" }
233 |               ]
234 |             }
235 |           ]
236 |         }
237 |       };
238 |     `;
239 | 
240 |     const loader = createTypescriptLoader().setDefaultLocale("en");
241 |     const result = await loader.pull("en", input);
242 | 
243 |     expect(result).toEqual({
244 |       app: {
245 |         name: "My App",
246 |         version: "1.0.0",
247 |         features: ["Login", "Dashboard", "Settings"],
248 |         pages: [
249 |           {
250 |             title: "Home",
251 |             sections: [
252 |               { heading: "Welcome", content: "Welcome to our app" },
253 |               { heading: "Features", content: "Check out our features" },
254 |             ],
255 |           },
256 |           {
257 |             title: "About",
258 |             sections: [{ heading: "Our Story", content: "We started in 2020" }],
259 |           },
260 |         ],
261 |       },
262 |     });
263 |   });
264 | 
265 |   it("should extract string literals when default export has 'as const'", async () => {
266 |     const input = `
267 |       export default {
268 |         greeting: "Hello, world!",
269 |         farewell: "Goodbye!"
270 |       } as const;
271 |     `;
272 | 
273 |     const loader = createTypescriptLoader().setDefaultLocale("en");
274 |     const result = await loader.pull("en", input);
275 | 
276 |     expect(result).toEqual({
277 |       greeting: "Hello, world!",
278 |       farewell: "Goodbye!",
279 |     });
280 |   });
281 | 
282 |   it("should extract and update string literals including multiline template literals, URLs, and numeric keys", async () => {
283 |     const input = dedent`
284 |       export default {
285 |         multilineContent: \`Multiline test
286 | 
287 |   Super content
288 | 
289 |   Includes also "test"\`,
290 |         testUrl: 'https://someurl.com',
291 |         6: '6. Class',
292 |         9: '9. Class',
293 |       };
294 |     `;
295 | 
296 |     const loader = createTypescriptLoader().setDefaultLocale("en");
297 | 
298 |     // Pull phase – ensure the loader extracts all expected strings
299 |     const pulled = await loader.pull("en", input);
300 | 
301 |     expect(pulled).toEqual({
302 |       multilineContent: dedent`
303 |       Multiline test
304 | 
305 |       Super content
306 | 
307 |       Includes also "test"`,
308 |       testUrl: "https://someurl.com",
309 |       6: "6. Class",
310 |       9: "9. Class",
311 |     });
312 | 
313 |     // Push phase – modify some values and ensure they are written back
314 |     const updatedData = {
315 |       ...pulled,
316 |       multilineContent: dedent`
317 |       Prueba multilínea
318 | 
319 |       Contenido superior
320 | 
321 |       Incluye también "prueba"`,
322 |       testUrl: "https://algunaurl.com",
323 |       6: "6. Clase",
324 |       9: "9. Clase",
325 |     } as any;
326 | 
327 |     const result = await loader.push("es", updatedData);
328 | 
329 |     expect(result).toBe(
330 |       `
331 | export default {
332 |   multilineContent: \`Prueba multilínea
333 | 
334 | Contenido superior
335 | 
336 | Incluye también "prueba"\`,
337 |   testUrl: "https://algunaurl.com",
338 |   6: "6. Clase",
339 |   9: "9. Clase"
340 | };
341 |       `.trim(),
342 |     );
343 |   });
344 | 
345 |   // TODO
346 | });
347 | 
```

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

```typescript
  1 | import { describe, it, expect } from "vitest";
  2 | import createPoLoader, { PoLoaderParams } from "./index";
  3 | 
  4 | describe("createPoDataLoader", () => {
  5 |   it("pull the correct data", async () => {
  6 |     const loader = createLoader();
  7 |     const input = `
  8 |   #: hello.py:1
  9 |   msgid "Hello world"
 10 |   msgstr ""
 11 |       `.trim();
 12 | 
 13 |     const data = await loader.pull("en", input);
 14 |     expect(data).toEqual({
 15 |       "Hello world": {
 16 |         singular: "Hello world",
 17 |         plural: null,
 18 |       },
 19 |     });
 20 |   });
 21 | 
 22 |   it("pull entries with context", async () => {
 23 |     const loader = createLoader();
 24 |     const input = `
 25 | #: hello.py:1
 26 | msgctxt "role of the user in the workspace"
 27 | msgid "Role"
 28 | msgstr ""
 29 |     `.trim();
 30 | 
 31 |     const data = await loader.pull("en", input);
 32 |     expect(data).toEqual({
 33 |       Role: {
 34 |         singular: "Role",
 35 |         plural: null,
 36 |       },
 37 |     });
 38 |   });
 39 | 
 40 |   it("push entries with context preserving the original context value", async () => {
 41 |     const loader = createLoader();
 42 |     const input = `
 43 | #: hello.py:1
 44 | msgctxt "role of the user in the workspace"
 45 | msgid "Role"
 46 | msgstr ""
 47 | 
 48 | #: hello.py:2
 49 | msgctxt "role of the user in the workspace"
 50 | msgid "Admin"
 51 | msgstr ""
 52 |     `.trim();
 53 | 
 54 |     const update = {
 55 |       Admin: {
 56 |         singular: "[upd] Admin",
 57 |         plural: null,
 58 |       },
 59 |     };
 60 | 
 61 |     const updatedInput = `
 62 | #: hello.py:1
 63 | msgctxt "role of the user in the workspace"
 64 | msgid "Role"
 65 | msgstr ""
 66 | 
 67 | #: hello.py:2
 68 | msgctxt "role of the user in the workspace"
 69 | msgid "Admin"
 70 | msgstr "[upd] Admin"
 71 |     `.trim();
 72 | 
 73 |     await loader.pull("en", input);
 74 |     const result = await loader.push("en-upd", update);
 75 |     expect(result).toEqual(updatedInput);
 76 |   });
 77 | 
 78 |   it("avoid pulling metadata", async () => {
 79 |     const loader = createLoader();
 80 |     const input = `
 81 |   # SOME DESCRIPTIVE TITLE.
 82 |   # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
 83 |   # This file is distributed under the same license as the PACKAGE package.
 84 |   # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
 85 |   #
 86 |   #, fuzzy
 87 |   msgid ""
 88 |   msgstr ""
 89 |   "Project-Id-Version: PACKAGE VERSION\n"
 90 |   "Report-Msgid-Bugs-To: \n"
 91 |   "POT-Creation-Date: 2025-01-22 13:15+0000\n"
 92 |   "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 93 |   "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 94 |   "Language-Team: LANGUAGE <[email protected]>\n"
 95 |   "Language: \n"
 96 |   "MIME-Version: 1.0\n"
 97 |   "Content-Type: text/plain; charset=UTF-8\n"
 98 |   "Content-Transfer-Encoding: 8bit\n"
 99 |   "Plural-Forms: nplurals=2; plural=(n != 1);\n"
100 | 
101 |   #: hello.py:1
102 |   msgid "Hello world"
103 |   msgstr ""
104 |       `.trim();
105 | 
106 |     const data = await loader.pull("en", input);
107 |     expect(data).toEqual({
108 |       "Hello world": {
109 |         singular: "Hello world",
110 |         plural: null,
111 |       },
112 |     });
113 |   });
114 | 
115 |   it("update data when pushed", async () => {
116 |     const loader = createLoader();
117 |     const input = `
118 | #: hello.py:1
119 | msgid "Hello world"
120 | msgstr ""
121 |       `.trim();
122 |     const updatedData = {
123 |       "Hello world": {
124 |         singular: "Hello world!",
125 |         plural: null,
126 |       },
127 |     };
128 |     const updatedInput = `
129 | #: hello.py:1
130 | msgid "Hello world"
131 | msgstr "Hello world!"
132 |       `.trim();
133 | 
134 |     await loader.pull("en", input);
135 |     const result = await loader.push("en", updatedData);
136 | 
137 |     expect(result).toEqual(updatedInput);
138 |   });
139 | 
140 |   it("avoid pushing default metadata if it's missing", async () => {
141 |     const loader = createLoader();
142 |     const input = `
143 | #: hello.py:1
144 | msgid "Hello world"
145 | msgstr ""
146 |     `.trim();
147 |     const updatedInput = `
148 | #: hello.py:1
149 | msgid "Hello world"
150 | msgstr ""
151 |       `.trim();
152 | 
153 |     await loader.pull("en", input);
154 |     const result = await loader.push("en", {});
155 |     expect(result).toEqual(updatedInput);
156 |   });
157 | 
158 |   it("split long lines when told to do so", async () => {
159 |     const loader = createLoader({ multiline: true });
160 |     const input = `
161 | #: hello.py:1
162 | msgid ""
163 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod "
164 | "tempor incididunt ut labore et dolore magna aliqua."
165 | msgstr ""
166 |       `.trim();
167 | 
168 |     await loader.pull("en", input);
169 |     const result = await loader.push("en", {});
170 |     expect(result).toEqual(input);
171 |   });
172 | 
173 |   it("dont't split long lines by default", async () => {
174 |     const loader = createLoader();
175 |     const input = `
176 | #: hello.py:1
177 | msgid ""
178 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod "
179 | "tempor incididunt ut labore et dolore magna aliqua."
180 | msgstr ""
181 |       `.trim();
182 | 
183 |     const updatedInput = `
184 | #: hello.py:1
185 | msgid "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
186 | msgstr ""
187 |       `.trim();
188 | 
189 |     await loader.pull("en", input);
190 |     const result = await loader.push("en", {});
191 |     expect(result).toEqual(updatedInput);
192 |   });
193 | 
194 |   it("pull entries with context", async () => {
195 |     const loader = createLoader();
196 |     const input = `
197 | #: hello.py:1
198 | msgctxt "role of the user in the workspace"
199 | msgid "Role"
200 | msgstr ""
201 |     `.trim();
202 | 
203 |     const data = await loader.pull("en", input);
204 |     expect(data).toEqual({
205 |       Role: {
206 |         singular: "Role",
207 |         plural: null,
208 |       },
209 |     });
210 |   });
211 | 
212 |   it("push entries with context preserving the original context value", async () => {
213 |     const loader = createLoader();
214 |     const input = `
215 | #: hello.py:1
216 | msgctxt "role of the user in the workspace"
217 | msgid "Role"
218 | msgstr ""
219 |     `.trim();
220 |     const payload = {
221 |       Role: {
222 |         singular: "[upd] Role",
223 |         plural: null,
224 |       },
225 |     };
226 |     const updatedInput = `
227 | #: hello.py:1
228 | msgctxt "role of the user in the workspace"
229 | msgid "Role"
230 | msgstr "[upd] Role"
231 |     `.trim();
232 | 
233 |     await loader.pull("en", input);
234 |     const result = await loader.push("en-upd", payload);
235 |     expect(result).toEqual(updatedInput);
236 |   });
237 | 
238 |   it("fallbacks to msgid when single msgstr value is empty", async () => {
239 |     const loader = createLoader();
240 |     const input = `
241 | #: hello.py:1
242 | msgid "File"
243 | msgstr ""
244 |     `.trim();
245 | 
246 |     const data = await loader.pull("en", input);
247 |     expect(data).toEqual({
248 |       File: {
249 |         singular: "File",
250 |         plural: null,
251 |       },
252 |     });
253 |   });
254 | 
255 |   it("fallbacks to msgid when msgstr values are empty", async () => {
256 |     const loader = createLoader();
257 |     const input = `
258 | #: hello.py:1
259 | msgid "File"
260 | msgstr[0] ""
261 | msgstr[1] ""
262 |     `.trim();
263 | 
264 |     const data = await loader.pull("en", input);
265 |     expect(data).toEqual({
266 |       File: {
267 |         singular: "File",
268 |         plural: "File",
269 |       },
270 |     });
271 |   });
272 | 
273 |   it("does not fallback to msgid for non-source locale when single msgstr value is empty", async () => {
274 |     const loader = createLoader();
275 |     const input = `
276 | #: hello.py:1
277 | msgid "File"
278 | msgstr ""
279 |     `.trim();
280 | 
281 |     // First, pull default locale to satisfy loader invariants
282 |     await loader.pull("en", input);
283 | 
284 |     // Pull a different locale with the same content
285 |     const data = await loader.pull("fr", input);
286 | 
287 |     expect(data).toEqual({
288 |       File: {
289 |         singular: null,
290 |         plural: null,
291 |       },
292 |     });
293 |   });
294 | 
295 |   it("does not fallback to msgid for non-source locale when msgstr values are empty", async () => {
296 |     const loader = createLoader();
297 |     const input = `
298 | #: hello.py:1
299 | msgid "File"
300 | msgstr[0] ""
301 | msgstr[1] ""
302 |     `.trim();
303 | 
304 |     // Pull default locale first
305 |     await loader.pull("en", input);
306 | 
307 |     // Pull a different locale
308 |     const data = await loader.pull("fr", input);
309 | 
310 |     expect(data).toEqual({
311 |       File: {
312 |         singular: null,
313 |         plural: null,
314 |       },
315 |     });
316 |   });
317 | 
318 |   it("should preserve order of comments (file and line number, translator notes)", async () => {
319 |     const loader = createLoader();
320 |     const input = `
321 | # My animal
322 | #, animal
323 | #. This is an animal
324 | #: hello.py:1
325 | # I like animals
326 | #| foobar
327 | msgid "Zebra"
328 | msgstr ""
329 | 
330 | #. This is a bird
331 | #: hello.py:2
332 | msgid "Parrot"
333 | msgstr ""
334 | 
335 | #. Food
336 | msgid "Apple"
337 | msgstr ""
338 |     `.trim();
339 | 
340 |     const data = await loader.pull("en", input);
341 | 
342 |     const updatedData = {
343 |       Zebra: { singular: "[upd] Zebra", plural: null },
344 |       Parrot: { singular: "[upd] Parrot", plural: null },
345 |       Apple: { singular: "[upd] Apple", plural: null },
346 |     };
347 |     const expectedOutput = `
348 | # My animal
349 | #, animal
350 | #. This is an animal
351 | #: hello.py:1
352 | # I like animals
353 | #| foobar
354 | msgid "Zebra"
355 | msgstr "[upd] Zebra"
356 | 
357 | #. This is a bird
358 | #: hello.py:2
359 | msgid "Parrot"
360 | msgstr "[upd] Parrot"
361 | 
362 | #. Food
363 | msgid "Apple"
364 | msgstr "[upd] Apple"
365 |     `.trim();
366 | 
367 |     const result = await loader.push("en", updatedData);
368 |     expect(result).toEqual(expectedOutput);
369 |   });
370 | });
371 | 
372 | function createLoader(params: PoLoaderParams = { multiline: false }) {
373 |   return createPoLoader(params).setDefaultLocale("en");
374 | }
375 | 
```

--------------------------------------------------------------------------------
/packages/cli/src/cli/loaders/flat.spec.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, expect, it } from "vitest";
  2 | import { flatten } from "flat";
  3 | import createFlatLoader, {
  4 |   buildDenormalizedKeysMap,
  5 |   denormalizeObjectKeys,
  6 |   mapDenormalizedKeys,
  7 |   normalizeObjectKeys,
  8 |   OBJECT_NUMERIC_KEY_PREFIX,
  9 | } from "./flat";
 10 | 
 11 | describe("flat loader", () => {
 12 |   describe("createFlatLoader", () => {
 13 |     it("loads numeric object and array and preserves state", async () => {
 14 |       const loader = createFlatLoader();
 15 |       loader.setDefaultLocale("en");
 16 |       await loader.pull("en", {
 17 |         messages: { "1": "foo", "2": "bar" },
 18 |         years: ["January 13, 2025", "February 14, 2025"],
 19 |       });
 20 |       await loader.pull("es", {}); // run again to ensure state is preserved
 21 |       const output = await loader.push("en", {
 22 |         "messages/1": "foo",
 23 |         "messages/2": "bar",
 24 |         "years/0": "January 13, 2025",
 25 |         "years/1": "February 14, 2025",
 26 |       });
 27 |       expect(output).toEqual({
 28 |         messages: { "1": "foo", "2": "bar" },
 29 |         years: ["January 13, 2025", "February 14, 2025"],
 30 |       });
 31 |     });
 32 | 
 33 |     it("handles date objects correctly", async () => {
 34 |       const loader = createFlatLoader();
 35 |       loader.setDefaultLocale("en");
 36 |       const date = new Date("2023-01-01T00:00:00Z");
 37 |       await loader.pull("en", {
 38 |         publishedAt: date,
 39 |         metadata: { createdAt: date },
 40 |       });
 41 |       const output = await loader.push("en", {
 42 |         publishedAt: date.toISOString(),
 43 |         "metadata/createdAt": date.toISOString(),
 44 |       });
 45 |       expect(output).toEqual({
 46 |         publishedAt: date.toISOString(),
 47 |         metadata: { createdAt: date.toISOString() },
 48 |       });
 49 |     });
 50 |   });
 51 | 
 52 |   describe("helper functions", () => {
 53 |     const inputObj = {
 54 |       messages: {
 55 |         "1": "a",
 56 |         "2": "b",
 57 |       },
 58 |     };
 59 |     const inputArray = {
 60 |       messages: ["a", "b", "c"],
 61 |     };
 62 | 
 63 |     describe("denormalizeObjectKeys", () => {
 64 |       it("should denormalize object keys", () => {
 65 |         const output = denormalizeObjectKeys(inputObj);
 66 |         expect(output).toEqual({
 67 |           messages: {
 68 |             [`${OBJECT_NUMERIC_KEY_PREFIX}1`]: "a",
 69 |             [`${OBJECT_NUMERIC_KEY_PREFIX}2`]: "b",
 70 |           },
 71 |         });
 72 |       });
 73 | 
 74 |       it("should preserve array", () => {
 75 |         const output = denormalizeObjectKeys(inputArray);
 76 |         expect(output).toEqual({
 77 |           messages: ["a", "b", "c"],
 78 |         });
 79 |       });
 80 | 
 81 |       it("should preserve date objects", () => {
 82 |         const date = new Date();
 83 |         const input = { createdAt: date };
 84 |         const output = denormalizeObjectKeys(input);
 85 |         expect(output).toEqual({ createdAt: date });
 86 |       });
 87 |     });
 88 | 
 89 |     describe("buildDenormalizedKeysMap", () => {
 90 |       it("should build normalized keys map", () => {
 91 |         const denormalized: Record<string, string> = flatten(
 92 |           denormalizeObjectKeys(inputObj),
 93 |           { delimiter: "/" },
 94 |         );
 95 |         const output = buildDenormalizedKeysMap(denormalized);
 96 |         expect(output).toEqual({
 97 |           "messages/1": `messages/${OBJECT_NUMERIC_KEY_PREFIX}1`,
 98 |           "messages/2": `messages/${OBJECT_NUMERIC_KEY_PREFIX}2`,
 99 |         });
100 |       });
101 | 
102 |       it("should build keys map array", () => {
103 |         const denormalized: Record<string, string> = flatten(
104 |           denormalizeObjectKeys(inputArray),
105 |           { delimiter: "/" },
106 |         );
107 |         const output = buildDenormalizedKeysMap(denormalized);
108 |         expect(output).toEqual({
109 |           "messages/0": "messages/0",
110 |           "messages/1": "messages/1",
111 |           "messages/2": "messages/2",
112 |         });
113 |       });
114 |     });
115 | 
116 |     describe("normalizeObjectKeys", () => {
117 |       it("should normalize denormalized object keys", () => {
118 |         const output = normalizeObjectKeys(denormalizeObjectKeys(inputObj));
119 |         expect(output).toEqual(inputObj);
120 |       });
121 | 
122 |       it("should process array keys", () => {
123 |         const output = normalizeObjectKeys(denormalizeObjectKeys(inputArray));
124 |         expect(output).toEqual(inputArray);
125 |       });
126 | 
127 |       it("should preserve date objects", () => {
128 |         const date = new Date();
129 |         const input = { createdAt: date };
130 |         const output = normalizeObjectKeys(input);
131 |         expect(output).toEqual({ createdAt: date });
132 |       });
133 |     });
134 | 
135 |     describe("mapDeormalizedKeys", () => {
136 |       it("should map normalized keys", () => {
137 |         const denormalized: Record<string, string> = flatten(
138 |           denormalizeObjectKeys(inputObj),
139 |           { delimiter: "/" },
140 |         );
141 |         const keyMap = buildDenormalizedKeysMap(denormalized);
142 |         const flattened: Record<string, string> = flatten(inputObj, {
143 |           delimiter: "/",
144 |         });
145 |         const mapped = mapDenormalizedKeys(flattened, keyMap);
146 |         expect(mapped).toEqual(denormalized);
147 |       });
148 | 
149 |       it("should map array", () => {
150 |         const denormalized: Record<string, string> = flatten(
151 |           denormalizeObjectKeys(inputArray),
152 |           { delimiter: "/" },
153 |         );
154 |         const keyMap = buildDenormalizedKeysMap(denormalized);
155 |         const flattened: Record<string, string> = flatten(inputArray, {
156 |           delimiter: "/",
157 |         });
158 |         const mapped = mapDenormalizedKeys(flattened, keyMap);
159 |         expect(mapped).toEqual(denormalized);
160 |       });
161 |     });
162 |   });
163 | 
164 |   describe("pullHints", () => {
165 |     it("should flatten comments from nested structure", async () => {
166 |       const loader = createFlatLoader();
167 |       loader.setDefaultLocale("en");
168 | 
169 |       const input = {
170 |         key1: { hint: "This is a comment for key1" },
171 |         key2: { hint: "This is a comment for key2" },
172 |         key3: { hint: "This is a comment for key3" },
173 |         key4: { hint: "This is a block comment for key4" },
174 |         key5: { hint: "This is a comment for key5" },
175 |         key6: {
176 |           hint: "This is a comment for key6",
177 |           key7: { hint: "This is a comment for key7" },
178 |         },
179 |       };
180 | 
181 |       const comments = await loader.pullHints(input);
182 | 
183 |       expect(comments).toEqual({
184 |         key1: ["This is a comment for key1"],
185 |         key2: ["This is a comment for key2"],
186 |         key3: ["This is a comment for key3"],
187 |         key4: ["This is a block comment for key4"],
188 |         key5: ["This is a comment for key5"],
189 |         "key6/key7": [
190 |           "This is a comment for key6",
191 |           "This is a comment for key7",
192 |         ],
193 |       });
194 |     });
195 | 
196 |     it("should handle empty input", async () => {
197 |       const loader = createFlatLoader();
198 |       loader.setDefaultLocale("en");
199 | 
200 |       const comments = await loader.pullHints({});
201 |       expect(comments).toEqual({});
202 |     });
203 | 
204 |     it("should handle null/undefined input", async () => {
205 |       const loader = createFlatLoader();
206 |       loader.setDefaultLocale("en");
207 | 
208 |       const comments1 = await loader.pullHints(null as any);
209 |       expect(comments1).toEqual({});
210 | 
211 |       const comments2 = await loader.pullHints(undefined as any);
212 |       expect(comments2).toEqual({});
213 |     });
214 | 
215 |     it("should handle deeply nested structure", async () => {
216 |       const loader = createFlatLoader();
217 |       loader.setDefaultLocale("en");
218 | 
219 |       const input = {
220 |         level1: {
221 |           hint: "Level 1 hint",
222 |           level2: {
223 |             hint: "Level 2 hint",
224 |             level3: {
225 |               hint: "Level 3 hint",
226 |             },
227 |           },
228 |         },
229 |       };
230 | 
231 |       const comments = await loader.pullHints(input);
232 | 
233 |       expect(comments).toEqual({
234 |         "level1/level2/level3": [
235 |           "Level 1 hint",
236 |           "Level 2 hint",
237 |           "Level 3 hint",
238 |         ],
239 |       });
240 |     });
241 | 
242 |     it("should handle objects without hints", async () => {
243 |       const loader = createFlatLoader();
244 |       loader.setDefaultLocale("en");
245 | 
246 |       const input = {
247 |         key1: { hint: "Has hint" },
248 |         key2: {
249 |           key3: { hint: "Nested hint" },
250 |         },
251 |       };
252 | 
253 |       const comments = await loader.pullHints(input);
254 | 
255 |       expect(comments).toEqual({
256 |         key1: ["Has hint"],
257 |         "key2/key3": ["Nested hint"],
258 |       });
259 |     });
260 | 
261 |     it("should handle mixed structures", async () => {
262 |       const loader = createFlatLoader();
263 |       loader.setDefaultLocale("en");
264 | 
265 |       const input = {
266 |         simple: { hint: "Simple hint" },
267 |         parent: {
268 |           hint: "Parent hint",
269 |           child1: { hint: "Child 1 hint" },
270 |           child2: {
271 |             grandchild: { hint: "Grandchild hint" },
272 |           },
273 |         },
274 |       };
275 | 
276 |       const comments = await loader.pullHints(input);
277 | 
278 |       expect(comments).toEqual({
279 |         simple: ["Simple hint"],
280 |         "parent/child1": ["Parent hint", "Child 1 hint"],
281 |         "parent/child2/grandchild": ["Parent hint", "Grandchild hint"],
282 |       });
283 |     });
284 |   });
285 | });
286 | 
```

--------------------------------------------------------------------------------
/demo/react-router-app/app/lingo/dictionary.js:
--------------------------------------------------------------------------------

```javascript
  1 | export default {
  2 |   version: 0.1,
  3 |   files: {
  4 |     "root.tsx": {
  5 |       entries: {
  6 |         "9/declaration/body/1/argument/1/1/3-content": {
  7 |           content: {
  8 |             de: "width=device-width, initial-scale=1",
  9 |             en: "width=device-width, initial-scale=1",
 10 |             es: "width=device-width, initial-scale=1",
 11 |             fr: "width=device-width, initial-scale=1",
 12 |           },
 13 |           hash: "d94b318cb327f61f1aea44a6cb1fdcad",
 14 |         },
 15 |       },
 16 |     },
 17 |     "routes/test.tsx": {
 18 |       entries: {
 19 |         "3/declaration/body/0/argument/1/1": {
 20 |           content: {
 21 |             de: "Zurück nach Hause",
 22 |             en: "Go back home",
 23 |             es: "Volver a inicio",
 24 |             fr: "Retourner à l'accueil",
 25 |           },
 26 |           hash: "a0ac69aec348674378faaf92ce476f64",
 27 |         },
 28 |         "3/declaration/body/0/argument/1/3": {
 29 |           content: {
 30 |             de: "Dies ist eine Testseite",
 31 |             en: "This is a test page",
 32 |             es: "Esta es una página de prueba",
 33 |             fr: "Ceci est une page de test",
 34 |           },
 35 |           hash: "51eb13586d30537dfa934742439cc7ee",
 36 |         },
 37 |         "3/declaration/body/0/argument/1/5": {
 38 |           content: {
 39 |             de: "Willkommen auf der nicht-interaktiven Testseite.",
 40 |             en: "Welcome to non-interactive testing page.",
 41 |             es: "Bienvenido a la página de prueba no interactiva.",
 42 |             fr: "Bienvenue sur la page de test non interactive.",
 43 |           },
 44 |           hash: "792a8d0c1ca71a88ab7d887075e69b1d",
 45 |         },
 46 |         "3/declaration/body/0/argument/1/7": {
 47 |           content: {
 48 |             de: "Bitte versuchen Sie nicht, mit dieser Seite zu interagieren, um Ihre eigene Sicherheit zu gewährleisten.",
 49 |             en: "Please do not try to interact with this page for your own safety.",
 50 |             es: "Por favor, no intentes interactuar con esta página por tu propia seguridad.",
 51 |             fr: "Veuillez ne pas essayer d'interagir avec cette page pour votre propre sécurité.",
 52 |           },
 53 |           hash: "31ab29a98c0bb54378cb5a2390d07e57",
 54 |         },
 55 |       },
 56 |     },
 57 |     "welcome/welcome.tsx": {
 58 |       entries: {
 59 |         "3/declaration/body/0/argument/1/1/1/1-alt": {
 60 |           content: {
 61 |             de: "React Router",
 62 |             en: "React Router",
 63 |             es: "Enrutador de React",
 64 |             fr: "React Router",
 65 |           },
 66 |           hash: "68ae50c1603f87d51e788a96b419f2ee",
 67 |         },
 68 |         "3/declaration/body/0/argument/1/1/1/3-alt": {
 69 |           content: {
 70 |             de: "React Router",
 71 |             en: "React Router",
 72 |             es: "Enrutador de React",
 73 |             fr: "React Router",
 74 |           },
 75 |           hash: "68ae50c1603f87d51e788a96b419f2ee",
 76 |         },
 77 |         "3/declaration/body/0/argument/1/1/3/1-alt": {
 78 |           content: {
 79 |             de: "React Router",
 80 |             en: "React Router",
 81 |             es: "Enrutador de React",
 82 |             fr: "React Router",
 83 |           },
 84 |           hash: "68ae50c1603f87d51e788a96b419f2ee",
 85 |         },
 86 |         "3/declaration/body/0/argument/1/1/3/3-alt": {
 87 |           content: {
 88 |             de: "React Router",
 89 |             en: "React Router",
 90 |             es: "Enrutador de React",
 91 |             fr: "React Router",
 92 |           },
 93 |           hash: "68ae50c1603f87d51e788a96b419f2ee",
 94 |         },
 95 |         "3/declaration/body/0/argument/1/3": {
 96 |           content: {
 97 |             de: "Test",
 98 |             en: "Test",
 99 |             es: "Prueba",
100 |             fr: "Test",
101 |           },
102 |           hash: "4938894bf1608cee94696ec86f5d059a",
103 |         },
104 |         "3/declaration/body/0/argument/1/5/1/1": {
105 |           content: {
106 |             de: "Was kommt als nächstes?",
107 |             en: "What's next?",
108 |             es: "¿Qué sigue?",
109 |             fr: "Qu'en est-il ensuite ?",
110 |           },
111 |           hash: "e0d9d29b9e761346e506557eb7b7e798",
112 |         },
113 |         "4/declaration/body/0/argument/1/1": {
114 |           content: {
115 |             de: "<element:LingoDotDev></element:LingoDotDev> 💚<element:div><element:img></element:img><element:img></element:img></element:div>",
116 |             en: "<element:LingoDotDev></element:LingoDotDev> 💚<element:div><element:img></element:img><element:img></element:img></element:div>",
117 |             es: "<element:LingoDotDev></element:LingoDotDev> 💚<element:div><element:img></element:img><element:img></element:img></element:div>",
118 |             fr: "<element:LingoDotDev></element:LingoDotDev> 💚<element:div><element:img></element:img><element:img></element:img></element:div>",
119 |           },
120 |           hash: "201cf15cf0830aaaf478e49a9665d096",
121 |         },
122 |         "4/declaration/body/0/argument/1/1/3": {
123 |           content: {
124 |             de: "💚",
125 |             en: "💚",
126 |             es: "💚",
127 |             fr: "💚",
128 |           },
129 |           hash: "0ecc986bbbb51a93878f2d11bb45c04a",
130 |         },
131 |         "4/declaration/body/0/argument/1/1/3/1-alt": {
132 |           content: {
133 |             de: "React Router",
134 |             en: "React Router",
135 |             es: "Enrutador de React",
136 |             fr: "React Router",
137 |           },
138 |           hash: "68ae50c1603f87d51e788a96b419f2ee",
139 |         },
140 |         "4/declaration/body/0/argument/1/1/3/3-alt": {
141 |           content: {
142 |             de: "React Router",
143 |             en: "React Router",
144 |             es: "Enrutador de React",
145 |             fr: "React Router",
146 |           },
147 |           hash: "68ae50c1603f87d51e788a96b419f2ee",
148 |         },
149 |         "4/declaration/body/0/argument/1/1/5/1-alt": {
150 |           content: {
151 |             de: "React Router",
152 |             en: "React Router",
153 |             es: "Enrutador de React",
154 |             fr: "React Router",
155 |           },
156 |           hash: "68ae50c1603f87d51e788a96b419f2ee",
157 |         },
158 |         "4/declaration/body/0/argument/1/1/5/1/1-alt": {
159 |           content: {
160 |             de: "React Router",
161 |             en: "React Router",
162 |             es: "Enrutador de React",
163 |             fr: "React Router",
164 |           },
165 |           hash: "68ae50c1603f87d51e788a96b419f2ee",
166 |         },
167 |         "4/declaration/body/0/argument/1/1/5/1/3-alt": {
168 |           content: {
169 |             de: "React Router",
170 |             en: "React Router",
171 |             es: "Enrutador de React",
172 |             fr: "React Router",
173 |           },
174 |           hash: "68ae50c1603f87d51e788a96b419f2ee",
175 |         },
176 |         "4/declaration/body/0/argument/1/1/5/3-alt": {
177 |           content: {
178 |             de: "React Router",
179 |             en: "React Router",
180 |             es: "Enrutador de React",
181 |             fr: "React Router",
182 |           },
183 |           hash: "68ae50c1603f87d51e788a96b419f2ee",
184 |         },
185 |         "4/declaration/body/0/argument/1/1/5/5-alt": {
186 |           content: {
187 |             de: "React Router",
188 |             en: "React Router",
189 |             es: "Enrutador de React",
190 |             fr: "React Router",
191 |           },
192 |           hash: "68ae50c1603f87d51e788a96b419f2ee",
193 |         },
194 |         "4/declaration/body/0/argument/1/3": {
195 |           content: {
196 |             de: "Testseite öffnen",
197 |             en: "Open test page",
198 |             es: "Abrir página de prueba",
199 |             fr: "Ouvrir la page de test",
200 |           },
201 |           hash: "4e5098c50297642cf07ce303398bad59",
202 |         },
203 |         "4/declaration/body/0/argument/1/5": {
204 |           content: {
205 |             de: "Willkommen zu Ihrer neuen React Router Anwendung! Dieses Starter-Template enthält alles, was Sie benötigen, um mit React Router und Lingo.dev für die Internationalisierung zu beginnen.",
206 |             en: "Welcome to your new React Router application! This starter template includes everything you need to get started with React Router and Lingo.dev for internationalization.",
207 |             es: "¡Bienvenido a tu nueva aplicación de React Router! Esta plantilla inicial incluye todo lo que necesitas para empezar con React Router y Lingo.dev para la internacionalización.",
208 |             fr: "Bienvenue dans votre nouvelle application React Router ! Ce modèle de départ inclut tout ce dont vous avez besoin pour commencer avec React Router et Lingo.dev pour l'internationalisation.",
209 |           },
210 |           hash: "a90f2300128bce36346e0debd0b6092b",
211 |         },
212 |         "4/declaration/body/0/argument/1/5/1/1": {
213 |           content: {
214 |             de: "Was kommt als nächstes?",
215 |             en: "What's next?",
216 |             es: "¿Qué sigue?",
217 |             fr: "Qu'en est-il ensuite ?",
218 |           },
219 |           hash: "e0d9d29b9e761346e506557eb7b7e798",
220 |         },
221 |         "4/declaration/body/0/argument/1/7/1/1": {
222 |           content: {
223 |             de: "Was kommt als nächstes?",
224 |             en: "What's next?",
225 |             es: "¿Qué sigue?",
226 |             fr: "Qu'en est-il ensuite ?",
227 |           },
228 |           hash: "e0d9d29b9e761346e506557eb7b7e798",
229 |         },
230 |       },
231 |     },
232 |   },
233 | };
234 | 
```

--------------------------------------------------------------------------------
/packages/compiler/src/lib/lcp/server.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import {
  2 |   DictionaryFile,
  3 |   DictionarySchema,
  4 |   LCPSchema,
  5 |   LCPScope,
  6 | } from "./schema";
  7 | import _ from "lodash";
  8 | import { LCPCache } from "./cache";
  9 | import { LCPAPI } from "./api";
 10 | 
 11 | type LCPServerBaseParams = {
 12 |   lcp: LCPSchema;
 13 |   sourceLocale: string;
 14 |   sourceRoot: string;
 15 |   lingoDir: string;
 16 |   models: "lingo.dev" | Record<string, string>;
 17 |   prompt?: string | null;
 18 | };
 19 | 
 20 | export type LCPServerParams = LCPServerBaseParams & {
 21 |   targetLocales: string[];
 22 | };
 23 | 
 24 | export type LCPServerParamsForLocale = LCPServerBaseParams & {
 25 |   targetLocale: string;
 26 | };
 27 | 
 28 | export class LCPServer {
 29 |   private static inFlightPromise: Promise<
 30 |     Record<string, DictionarySchema>
 31 |   > | null = null;
 32 | 
 33 |   static async loadDictionaries(
 34 |     params: LCPServerParams,
 35 |   ): Promise<Record<string, DictionarySchema>> {
 36 |     // If a load is already in progress, await it
 37 |     if (this.inFlightPromise) {
 38 |       return this.inFlightPromise;
 39 |     }
 40 | 
 41 |     // Otherwise start a new load restricted by the limiter
 42 |     this.inFlightPromise = (async () => {
 43 |       try {
 44 |         const targetLocales = _.uniq([
 45 |           ...params.targetLocales,
 46 |           params.sourceLocale,
 47 |         ]);
 48 | 
 49 |         const dictionaries = await Promise.all(
 50 |           targetLocales.map((targetLocale) =>
 51 |             this.loadDictionaryForLocale({ ...params, targetLocale }),
 52 |           ),
 53 |         );
 54 | 
 55 |         const result = _.fromPairs(
 56 |           targetLocales.map((targetLocale, index) => [
 57 |             targetLocale,
 58 |             dictionaries[index],
 59 |           ]),
 60 |         );
 61 | 
 62 |         return result;
 63 |       } finally {
 64 |         // Clear inFlightPromise regardless of success/failure
 65 |         this.inFlightPromise = null;
 66 |       }
 67 |     })();
 68 | 
 69 |     return this.inFlightPromise;
 70 |   }
 71 | 
 72 |   static async loadDictionaryForLocale(
 73 |     params: LCPServerParamsForLocale,
 74 |   ): Promise<DictionarySchema> {
 75 |     const sourceDictionary = this._extractSourceDictionary(
 76 |       params.lcp,
 77 |       params.sourceLocale,
 78 |       params.targetLocale,
 79 |     );
 80 | 
 81 |     const cacheParams = {
 82 |       lcp: params.lcp,
 83 |       sourceLocale: params.sourceLocale,
 84 |       lingoDir: params.lingoDir,
 85 |       sourceRoot: params.sourceRoot,
 86 |     };
 87 | 
 88 |     if (this._countDictionaryEntries(sourceDictionary) === 0) {
 89 |       console.log(
 90 |         "Source dictionary is empty, returning empty dictionary for target locale",
 91 |       );
 92 |       return { ...sourceDictionary, locale: params.targetLocale };
 93 |     }
 94 | 
 95 |     const cache = LCPCache.readLocaleDictionary(
 96 |       params.targetLocale,
 97 |       cacheParams,
 98 |     );
 99 | 
100 |     const uncachedSourceDictionary = this._getDictionaryDiff(
101 |       sourceDictionary,
102 |       cache,
103 |     );
104 |     let targetDictionary: DictionarySchema;
105 |     let newTranslations: DictionarySchema | undefined;
106 |     if (this._countDictionaryEntries(uncachedSourceDictionary) === 0) {
107 |       targetDictionary = cache;
108 |     } else if (params.targetLocale === params.sourceLocale) {
109 |       console.log(
110 |         "ℹ️  Lingo.dev returns source dictionary - source and target locales are the same",
111 |       );
112 |       // cache source dictionary for convenience when editing the dictionary.js file
113 |       await LCPCache.writeLocaleDictionary(sourceDictionary, cacheParams);
114 |       return sourceDictionary;
115 |     } else {
116 |       newTranslations = await LCPAPI.translate(
117 |         params.models,
118 |         uncachedSourceDictionary,
119 |         params.sourceLocale,
120 |         params.targetLocale,
121 |         params.prompt,
122 |       );
123 | 
124 |       // we merge new translations with cache, so that we can cache empty strings
125 |       targetDictionary = this._mergeDictionaries(newTranslations, cache);
126 |       // ensure the locale metadata reflects the target locale
127 |       targetDictionary = {
128 |         ...targetDictionary,
129 |         locale: params.targetLocale,
130 |       };
131 |       await LCPCache.writeLocaleDictionary(targetDictionary, cacheParams);
132 |     }
133 | 
134 |     const targetDictionaryWithFallback = this._mergeDictionaries(
135 |       targetDictionary,
136 |       sourceDictionary,
137 |       true,
138 |     );
139 | 
140 |     const result = this._addOverridesToDictionary(
141 |       targetDictionaryWithFallback,
142 |       params.lcp,
143 |       params.targetLocale,
144 |     );
145 | 
146 |     if (newTranslations) {
147 |       console.log(
148 |         `ℹ️  Lingo.dev dictionary for ${params.targetLocale}:\n- %d entries\n- %d cached\n- %d uncached\n- %d translated\n- %d overrides`,
149 |         this._countDictionaryEntries(result),
150 |         this._countDictionaryEntries(cache),
151 |         this._countDictionaryEntries(uncachedSourceDictionary),
152 |         newTranslations ? this._countDictionaryEntries(newTranslations) : 0,
153 |         this._countDictionaryEntries(result) -
154 |           this._countDictionaryEntries(targetDictionary),
155 |       );
156 |     }
157 | 
158 |     // console.log("Generated object", JSON.stringify(result, null, 2));
159 |     return result;
160 |   }
161 | 
162 |   private static _extractSourceDictionary(
163 |     lcp: LCPSchema,
164 |     sourceLocale: string,
165 |     targetLocale: string,
166 |   ): DictionarySchema {
167 |     const dictionary: DictionarySchema = {
168 |       version: 0.1,
169 |       locale: sourceLocale,
170 |       files: {},
171 |     };
172 | 
173 |     for (const [fileKey, fileData] of Object.entries(lcp.files || {})) {
174 |       for (const [scopeKey, scopeData] of Object.entries(
175 |         fileData.scopes || {},
176 |       )) {
177 |         if (scopeData.skip) {
178 |           continue;
179 |         }
180 |         if (this._getScopeLocaleOverride(scopeData, targetLocale)) {
181 |           continue;
182 |         }
183 | 
184 |         _.set(
185 |           dictionary,
186 |           [
187 |             "files" satisfies keyof DictionarySchema,
188 |             fileKey,
189 |             "entries" satisfies keyof DictionaryFile,
190 |             scopeKey,
191 |           ],
192 |           scopeData.content,
193 |         );
194 |       }
195 |     }
196 | 
197 |     return dictionary;
198 |   }
199 | 
200 |   private static _addOverridesToDictionary(
201 |     dictionary: DictionarySchema,
202 |     lcp: LCPSchema,
203 |     targetLocale: string,
204 |   ) {
205 |     for (const [fileKey, fileData] of Object.entries(lcp.files || {})) {
206 |       for (const [scopeKey, scopeData] of Object.entries(
207 |         fileData.scopes || {},
208 |       )) {
209 |         const override = this._getScopeLocaleOverride(scopeData, targetLocale);
210 |         if (!override) {
211 |           continue;
212 |         }
213 |         _.set(
214 |           dictionary,
215 |           [
216 |             "files" satisfies keyof DictionarySchema,
217 |             fileKey,
218 |             "entries" satisfies keyof DictionaryFile,
219 |             scopeKey,
220 |           ],
221 |           override,
222 |         );
223 |       }
224 |     }
225 |     return dictionary;
226 |   }
227 | 
228 |   private static _getScopeLocaleOverride(scopeData: LCPScope, locale: string) {
229 |     return _.get(scopeData.overrides, locale) ?? null;
230 |   }
231 | 
232 |   private static _getDictionaryDiff(
233 |     sourceDictionary: DictionarySchema,
234 |     targetDictionary: DictionarySchema,
235 |   ) {
236 |     if (this._countDictionaryEntries(targetDictionary) === 0) {
237 |       return sourceDictionary;
238 |     }
239 | 
240 |     const files = _(sourceDictionary.files)
241 |       .mapValues((file, fileName) => ({
242 |         ...file,
243 |         entries: _(file.entries)
244 |           .mapValues((entry, entryName) => {
245 |             const targetEntry = _.get(targetDictionary.files, [
246 |               fileName,
247 |               "entries",
248 |               entryName,
249 |             ]);
250 |             if (targetEntry !== undefined) {
251 |               return undefined;
252 |             }
253 |             return entry;
254 |           })
255 |           .pickBy((value) => value !== undefined)
256 |           .value(),
257 |       }))
258 |       .pickBy((value) => Object.keys(value.entries).length > 0)
259 |       .value();
260 |     const dictionary = {
261 |       version: sourceDictionary.version,
262 |       locale: sourceDictionary.locale,
263 |       files,
264 |     };
265 |     return dictionary;
266 |   }
267 | 
268 |   private static _mergeDictionaries(
269 |     sourceDictionary: DictionarySchema,
270 |     targetDictionary: DictionarySchema,
271 |     removeEmptyEntries = false,
272 |   ) {
273 |     const fileNames = _.uniq([
274 |       ...Object.keys(sourceDictionary.files),
275 |       ...Object.keys(targetDictionary.files),
276 |     ]);
277 |     const files = _(fileNames)
278 |       .map((fileName) => {
279 |         const sourceFile = _.get(sourceDictionary.files, fileName);
280 |         const targetFile = _.get(targetDictionary.files, fileName);
281 |         const entries = removeEmptyEntries
282 |           ? _.pickBy(
283 |               sourceFile?.entries || {},
284 |               (value) => String(value || "")?.trim?.()?.length > 0,
285 |             )
286 |           : sourceFile?.entries || {};
287 |         return [
288 |           fileName,
289 |           {
290 |             ...targetFile,
291 |             entries: _.merge({}, targetFile?.entries || {}, entries),
292 |           },
293 |         ];
294 |       })
295 |       .fromPairs()
296 |       .value();
297 |     const dictionary = {
298 |       version: sourceDictionary.version,
299 |       locale: sourceDictionary.locale,
300 |       files,
301 |     };
302 |     return dictionary;
303 |   }
304 | 
305 |   private static _countDictionaryEntries(dict: DictionarySchema) {
306 |     return Object.values(dict.files).reduce(
307 |       (sum, file) => sum + Object.keys(file.entries).length,
308 |       0,
309 |     );
310 |   }
311 | }
312 | 
```
Page 11/20FirstPrevNextLast