#
tokens: 49031/50000 1/626 files (page 20/20)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 20 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/loaders/index.spec.ts:
--------------------------------------------------------------------------------

```typescript
   1 | import { describe, it, expect, vi, beforeEach } from "vitest";
   2 | import dedent from "dedent";
   3 | import _ from "lodash";
   4 | import fs from "fs/promises";
   5 | import createBucketLoader from "./index";
   6 | import createTextFileLoader from "./text-file";
   7 | 
   8 | describe("bucket loaders", () => {
   9 |   beforeEach(() => {
  10 |     vi.clearAllMocks();
  11 |     vi.resetModules();
  12 |   });
  13 | 
  14 |   describe("android bucket loader", () => {
  15 |     it("should load android data", async () => {
  16 |       setupFileMocks();
  17 | 
  18 |       const input = `
  19 |         <resources>
  20 |           <string name="button.title">Submit</string>
  21 |         </resources>
  22 |       `.trim();
  23 |       const expectedOutput = { "button.title": "Submit" };
  24 | 
  25 |       mockFileOperations(input);
  26 | 
  27 |       const androidLoader = createBucketLoader(
  28 |         "android",
  29 |         "values-[locale]/strings.xml",
  30 |         {
  31 |           defaultLocale: "en",
  32 |         },
  33 |       );
  34 |       androidLoader.setDefaultLocale("en");
  35 |       const data = await androidLoader.pull("en");
  36 | 
  37 |       expect(data).toEqual(expectedOutput);
  38 |     });
  39 | 
  40 |     it("should skip non-translatable strings", async () => {
  41 |       setupFileMocks();
  42 | 
  43 |       const input = `
  44 |         <resources>
  45 |           <string name="app_name" translatable="false">MyApp</string>
  46 |           <string name="button.title">Submit</string>
  47 |           <string name="version" translatable="false">1.0.0</string>
  48 |         </resources>
  49 |       `.trim();
  50 |       const expectedOutput = { "button.title": "Submit" };
  51 | 
  52 |       mockFileOperations(input);
  53 | 
  54 |       const androidLoader = createBucketLoader(
  55 |         "android",
  56 |         "values-[locale]/strings.xml",
  57 |         {
  58 |           defaultLocale: "en",
  59 |         },
  60 |       );
  61 |       androidLoader.setDefaultLocale("en");
  62 |       const data = await androidLoader.pull("en");
  63 | 
  64 |       expect(data).toEqual(expectedOutput);
  65 |     });
  66 | 
  67 |     it("should save android data", async () => {
  68 |       setupFileMocks();
  69 | 
  70 |       // Use proper Android Studio format: XML declaration + 4-space indentation
  71 |       const input = `<?xml version="1.0" encoding="utf-8"?>
  72 | <resources>
  73 |     <string name="button.title">Submit</string>
  74 | </resources>`;
  75 |       const payload = { "button.title": "Enviar" };
  76 |       // Output preserves XML declaration and uses 4-space indentation (Android standard)
  77 |       const expectedOutput = `<?xml version="1.0" encoding="utf-8"?>
  78 | <resources>
  79 |     <string name="button.title">Enviar</string>
  80 | </resources>`;
  81 | 
  82 |       mockFileOperations(input);
  83 | 
  84 |       const androidLoader = createBucketLoader(
  85 |         "android",
  86 |         "values-[locale]/strings.xml",
  87 |         {
  88 |           defaultLocale: "en",
  89 |         },
  90 |       );
  91 |       androidLoader.setDefaultLocale("en");
  92 |       await androidLoader.pull("en");
  93 | 
  94 |       await androidLoader.push("es", payload);
  95 | 
  96 |       expect(fs.writeFile).toHaveBeenCalledWith(
  97 |         "values-es/strings.xml",
  98 |         expectedOutput,
  99 |         {
 100 |           encoding: "utf-8",
 101 |           flag: "w",
 102 |         },
 103 |       );
 104 |     });
 105 | 
 106 |     it("should respect locked keys (pull)", async () => {
 107 |       setupFileMocks();
 108 | 
 109 |       const input = `<?xml version="1.0" encoding="utf-8"?>
 110 | <resources>
 111 |     <string name="locked_key">Original</string>
 112 |     <string name="unlocked_key">Hello</string>
 113 | </resources>`;
 114 | 
 115 |       mockFileOperations(input);
 116 | 
 117 |       const androidLoader = createBucketLoader(
 118 |         "android",
 119 |         "values-[locale]/strings.xml",
 120 |         { defaultLocale: "en" },
 121 |         ["locked_key"],
 122 |       );
 123 |       androidLoader.setDefaultLocale("en");
 124 |       const data = await androidLoader.pull("en");
 125 | 
 126 |       expect(data).toEqual({ unlocked_key: "Hello" });
 127 |     });
 128 |   });
 129 | 
 130 |   describe("csv bucket loader", () => {
 131 |     it("should load csv data ('KEY' as key, from automatic fallback", async () => {
 132 |       setupFileMocks();
 133 | 
 134 |       const input = ` ,KEY,en\n,button.title,Submit`;
 135 |       const expectedOutput = { "button.title": "Submit" };
 136 | 
 137 |       mockFileOperations(input);
 138 | 
 139 |       const csvLoader = createBucketLoader("csv", "i18n.csv", {
 140 |         defaultLocale: "en",
 141 |       });
 142 |       csvLoader.setDefaultLocale("en");
 143 |       const data = await csvLoader.pull("en");
 144 | 
 145 |       expect(data).toEqual(expectedOutput);
 146 |     });
 147 | 
 148 |     it("should load csv data ('id' as key, first cell)", async () => {
 149 |       setupFileMocks();
 150 | 
 151 |       const input = `id,en\nbutton.title,Submit`;
 152 |       const expectedOutput = { "button.title": "Submit" };
 153 | 
 154 |       mockFileOperations(input);
 155 | 
 156 |       const csvLoader = createBucketLoader("csv", "i18n.csv", {
 157 |         defaultLocale: "en",
 158 |       });
 159 |       csvLoader.setDefaultLocale("en");
 160 |       const data = await csvLoader.pull("en");
 161 | 
 162 |       expect(data).toEqual(expectedOutput);
 163 |     });
 164 | 
 165 |     it("should save csv data", async () => {
 166 |       setupFileMocks();
 167 | 
 168 |       const input = `id,en,es\nbutton.title,Submit,`;
 169 |       const payload = { "button.title": "Enviar" };
 170 |       const expectedOutput = `id,en,es\nbutton.title,Submit,Enviar`;
 171 | 
 172 |       mockFileOperations(input);
 173 | 
 174 |       const csvLoader = createBucketLoader("csv", "i18n.csv", {
 175 |         defaultLocale: "en",
 176 |       });
 177 |       csvLoader.setDefaultLocale("en");
 178 |       await csvLoader.pull("en");
 179 | 
 180 |       await csvLoader.push("es", payload);
 181 | 
 182 |       expect(fs.writeFile).toHaveBeenCalledWith("i18n.csv", expectedOutput, {
 183 |         encoding: "utf-8",
 184 |         flag: "w",
 185 |       });
 186 |     });
 187 | 
 188 |     it("should respect locked keys (pull)", async () => {
 189 |       setupFileMocks();
 190 | 
 191 |       const input = `id,en\nlocked_key,Original\nunlocked_key,Hello`;
 192 | 
 193 |       mockFileOperations(input);
 194 | 
 195 |       const csvLoader = createBucketLoader(
 196 |         "csv",
 197 |         "i18n.csv",
 198 |         {
 199 |           defaultLocale: "en",
 200 |         },
 201 |         ["locked_key"],
 202 |       );
 203 |       csvLoader.setDefaultLocale("en");
 204 |       const data = await csvLoader.pull("en");
 205 | 
 206 |       expect(data).toEqual({ unlocked_key: "Hello" });
 207 |     });
 208 |   });
 209 | 
 210 |   describe("flutter bucket loader", () => {
 211 |     it("should load flutter data", async () => {
 212 |       setupFileMocks();
 213 | 
 214 |       const input = `{
 215 |         "@@locale": "en",
 216 |         "greeting": "Hello, {name}!",
 217 |         "@greeting": {
 218 |           "description": "A greeting with a name placeholder",
 219 |           "placeholders": {
 220 |             "name": {
 221 |               "type": "String",
 222 |               "example": "John"
 223 |             }
 224 |           }
 225 |         }
 226 |       }`;
 227 |       const expectedOutput = { greeting: "Hello, {name}!" };
 228 | 
 229 |       mockFileOperations(input);
 230 | 
 231 |       const flutterLoader = createBucketLoader(
 232 |         "flutter",
 233 |         "lib/l10n/app_[locale].arb",
 234 |         {
 235 |           defaultLocale: "en",
 236 |         },
 237 |       );
 238 |       flutterLoader.setDefaultLocale("en");
 239 |       const data = await flutterLoader.pull("en");
 240 | 
 241 |       expect(data).toEqual(expectedOutput);
 242 |     });
 243 | 
 244 |     it("should save flutter data", async () => {
 245 |       setupFileMocks();
 246 | 
 247 |       const input = `{
 248 |         "@@locale": "en",
 249 |         "greeting": "Hello, {name}!",
 250 |         "@greeting": {
 251 |           "description": "A greeting with a name placeholder",
 252 |           "placeholders": {
 253 |             "name": {
 254 |               "type": "String",
 255 |               "example": "John"
 256 |             }
 257 |           }
 258 |         }
 259 |       }`;
 260 |       const payload = { greeting: "¡Hola, {name}!" };
 261 |       const expectedOutput = JSON.stringify(
 262 |         {
 263 |           "@@locale": "es",
 264 |           greeting: "¡Hola, {name}!",
 265 |           "@greeting": {
 266 |             description: "A greeting with a name placeholder",
 267 |             placeholders: {
 268 |               name: {
 269 |                 type: "String",
 270 |                 example: "John",
 271 |               },
 272 |             },
 273 |           },
 274 |         },
 275 |         null,
 276 |         2,
 277 |       );
 278 | 
 279 |       mockFileOperations(input);
 280 | 
 281 |       const flutterLoader = createBucketLoader(
 282 |         "flutter",
 283 |         "lib/l10n/app_[locale].arb",
 284 |         {
 285 |           defaultLocale: "en",
 286 |         },
 287 |       );
 288 |       flutterLoader.setDefaultLocale("en");
 289 |       await flutterLoader.pull("en");
 290 | 
 291 |       await flutterLoader.push("es", payload);
 292 | 
 293 |       expect(fs.writeFile).toHaveBeenCalledWith(
 294 |         "lib/l10n/app_es.arb",
 295 |         expectedOutput,
 296 |         {
 297 |           encoding: "utf-8",
 298 |           flag: "w",
 299 |         },
 300 |       );
 301 |     });
 302 | 
 303 |     it("should respect locked keys (pull)", async () => {
 304 |       setupFileMocks();
 305 | 
 306 |       const input = `{
 307 |         "@@locale": "en",
 308 |         "locked_key": "Original",
 309 |         "unlocked_key": "Hello"
 310 |       }`;
 311 | 
 312 |       mockFileOperations(input);
 313 | 
 314 |       const flutterLoader = createBucketLoader(
 315 |         "flutter",
 316 |         "lib/l10n/app_[locale].arb",
 317 |         { defaultLocale: "en" },
 318 |         ["locked_key"],
 319 |       );
 320 |       flutterLoader.setDefaultLocale("en");
 321 |       const data = await flutterLoader.pull("en");
 322 | 
 323 |       expect(data).toEqual({ unlocked_key: "Hello" });
 324 |     });
 325 |   });
 326 | 
 327 |   describe("html bucket loader", () => {
 328 |     it("should load html data", async () => {
 329 |       setupFileMocks();
 330 | 
 331 |       const input = `
 332 | <html>
 333 |   <head>
 334 |     <title>My Page</title>
 335 |     <meta name="description" content="Page description" />
 336 |   </head>
 337 |   <body>
 338 |     some simple text without an html tag
 339 |     <h1>Hello, world!</h1>
 340 |     <p>
 341 |       This is a paragraph with a 
 342 |       <a href="https://example.com">link</a>
 343 |       and 
 344 |       <b>
 345 |         bold and <i>italic text</i>
 346 |       </b>
 347 |       .
 348 |     </p>
 349 |   </body>
 350 | </html>
 351 |       `.trim();
 352 |       const expectedOutput = {
 353 |         "head/0/0": "My Page",
 354 |         "head/1#content": "Page description",
 355 |         "body/0": "some simple text without an html tag",
 356 |         "body/1/0": "Hello, world!",
 357 |         "body/2/0": "This is a paragraph with a",
 358 |         "body/2/1/0": "link",
 359 |         "body/2/2": "and",
 360 |         "body/2/3/0": "bold and",
 361 |         "body/2/3/1/0": "italic text",
 362 |         "body/2/4": ".",
 363 |       };
 364 | 
 365 |       mockFileOperations(input);
 366 | 
 367 |       const htmlLoader = createBucketLoader("html", "i18n/[locale].html", {
 368 |         defaultLocale: "en",
 369 |       });
 370 |       htmlLoader.setDefaultLocale("en");
 371 |       const data = await htmlLoader.pull("en");
 372 | 
 373 |       expect(data).toEqual(expectedOutput);
 374 |     });
 375 | 
 376 |     it("should save html data", async () => {
 377 |       const input = dedent`
 378 | <html>
 379 |   <head>
 380 |     <title>My Page</title>
 381 |     <meta name="description" content="Page description" />
 382 |   </head>
 383 |   <body>
 384 |     some simple text without an html tag
 385 |     <h1>Hello, world!</h1>
 386 |     <p>
 387 |       This is a paragraph with a <a href="https://example.com">link</a> and <b>bold and <i>italic text</i></b>
 388 |     </p>
 389 |   </body>
 390 | </html>
 391 |       `.trim();
 392 |       const payload = {
 393 |         "head/0/0": "Mi Página",
 394 |         "head/1#content": "Descripción de la página",
 395 |         "body/0": "texto simple sin etiqueta html",
 396 |         "body/1/0": "¡Hola, mundo!",
 397 |         "body/2/0": "Este es un párrafo con un ",
 398 |         "body/2/1/0": "enlace",
 399 |         "body/2/2": " y ",
 400 |         "body/2/3/0": "texto en negrita y ",
 401 |         "body/2/3/1/0": "texto en cursiva",
 402 |       };
 403 |       const expectedOutput = `<html lang="es">
 404 |   <head>
 405 |     <title>Mi Página</title>
 406 |     <meta name="description" content="Descripción de la página" />
 407 |   </head>
 408 |   <body>
 409 |     texto simple sin etiqueta html
 410 |     <h1>¡Hola, mundo!</h1>
 411 |     <p>
 412 |       Este es un párrafo con un
 413 |       <a href="https://example.com">enlace</a>
 414 |       y
 415 |       <b>
 416 |         texto en negrita y
 417 |         <i>texto en cursiva</i>
 418 |       </b>
 419 |     </p>
 420 |   </body>
 421 | </html>
 422 |       `.trim();
 423 | 
 424 |       mockFileOperations(input);
 425 | 
 426 |       const htmlLoader = createBucketLoader("html", "i18n/[locale].html", {
 427 |         defaultLocale: "en",
 428 |       });
 429 |       htmlLoader.setDefaultLocale("en");
 430 |       await htmlLoader.pull("en");
 431 | 
 432 |       await htmlLoader.push("es", payload);
 433 | 
 434 |       expect(fs.writeFile).toHaveBeenCalledWith(
 435 |         "i18n/es.html",
 436 |         expectedOutput,
 437 |         { encoding: "utf-8", flag: "w" },
 438 |       );
 439 |     });
 440 | 
 441 |     it("should respect locked keys (pull)", async () => {
 442 |       setupFileMocks();
 443 | 
 444 |       const input = `
 445 | <html>
 446 |   <head>
 447 |     <title>Locked Title</title>
 448 |   </head>
 449 |   <body>
 450 |     <h1>Hello</h1>
 451 |   </body>
 452 | </html>`;
 453 | 
 454 |       mockFileOperations(input);
 455 | 
 456 |       const htmlLoader = createBucketLoader(
 457 |         "html",
 458 |         "i18n/[locale].html",
 459 |         { defaultLocale: "en" },
 460 |         ["head/0/0"],
 461 |       );
 462 |       htmlLoader.setDefaultLocale("en");
 463 |       const data = await htmlLoader.pull("en");
 464 | 
 465 |       // Title is locked, only body text should remain
 466 |       expect(Object.values(data)).toContain("Hello");
 467 |       expect(Object.keys(data)).not.toContain("head/0/0");
 468 |     });
 469 |   });
 470 | 
 471 |   describe("jsonc bucket loader", () => {
 472 |     it("should load jsonc data with comments", async () => {
 473 |       setupFileMocks();
 474 | 
 475 |       const input = `{
 476 |         // This is a comment for title
 477 |         "title": "Submit",
 478 |         /* This is a block comment for description */
 479 |         "description": "Button description",
 480 |         "nested": {
 481 |           // Nested comment
 482 |           "key": "value"
 483 |         }
 484 |       }`;
 485 |       const expectedOutput = {
 486 |         title: "Submit",
 487 |         description: "Button description",
 488 |         "nested/key": "value",
 489 |       };
 490 | 
 491 |       mockFileOperations(input);
 492 | 
 493 |       const jsoncLoader = createBucketLoader("jsonc", "i18n/[locale].jsonc", {
 494 |         defaultLocale: "en",
 495 |       });
 496 |       jsoncLoader.setDefaultLocale("en");
 497 |       const data = await jsoncLoader.pull("en");
 498 | 
 499 |       expect(data).toEqual(expectedOutput);
 500 |     });
 501 | 
 502 |     it("should save jsonc data", async () => {
 503 |       setupFileMocks();
 504 | 
 505 |       const input = `{
 506 |         // This is a comment
 507 |         "title": "Submit"
 508 |       }`;
 509 |       const payload = { title: "Enviar" };
 510 |       const expectedOutput = JSON.stringify(payload, null, 2);
 511 | 
 512 |       mockFileOperations(input);
 513 | 
 514 |       const jsoncLoader = createBucketLoader("jsonc", "i18n/[locale].jsonc", {
 515 |         defaultLocale: "en",
 516 |       });
 517 |       jsoncLoader.setDefaultLocale("en");
 518 |       await jsoncLoader.pull("en");
 519 | 
 520 |       await jsoncLoader.push("es", payload);
 521 | 
 522 |       expect(fs.writeFile).toHaveBeenCalledWith(
 523 |         "i18n/es.jsonc",
 524 |         expectedOutput,
 525 |         { encoding: "utf-8", flag: "w" },
 526 |       );
 527 |     });
 528 | 
 529 |     it("should extract hints from jsonc comments", async () => {
 530 |       setupFileMocks();
 531 | 
 532 |       const input = `{
 533 |         "key1": "value1", // This is a comment for key1
 534 |         "key2": "value2" /* This is a comment for key2 */,
 535 |         // This is a comment for key3
 536 |         "key3": "value3",
 537 |         /* This is a block comment for key4 */
 538 |         "key4": "value4",
 539 |         /*
 540 |          This is a comment for key5
 541 |         */
 542 |         "key5": "value5",
 543 |         // This is a comment for key6
 544 |         "key6": {
 545 |           // This is a comment for key7
 546 |           "key7": "value7"
 547 |         }
 548 |       }`;
 549 | 
 550 |       mockFileOperations(input);
 551 | 
 552 |       const jsoncLoader = createBucketLoader("jsonc", "i18n/[locale].jsonc", {
 553 |         defaultLocale: "en",
 554 |       });
 555 |       jsoncLoader.setDefaultLocale("en");
 556 |       await jsoncLoader.pull("en");
 557 | 
 558 |       const hints = await jsoncLoader.pullHints();
 559 | 
 560 |       expect(hints).toEqual({
 561 |         key1: ["This is a comment for key1"],
 562 |         key2: ["This is a comment for key2"],
 563 |         key3: ["This is a comment for key3"],
 564 |         key4: ["This is a block comment for key4"],
 565 |         key5: ["This is a comment for key5"],
 566 |         "key6/key7": [
 567 |           "This is a comment for key6",
 568 |           "This is a comment for key7",
 569 |         ],
 570 |       });
 571 |     });
 572 | 
 573 |     it("should handle jsonc with trailing commas", async () => {
 574 |       setupFileMocks();
 575 | 
 576 |       const input = `{
 577 |         "hello": "Hello",
 578 |         "world": "World",
 579 |         "array": [
 580 |           "item1",
 581 |           "item2",
 582 |         ],
 583 |       }`;
 584 |       const expectedOutput = {
 585 |         hello: "Hello",
 586 |         world: "World",
 587 |         "array/0": "item1",
 588 |         "array/1": "item2",
 589 |       };
 590 | 
 591 |       mockFileOperations(input);
 592 | 
 593 |       const jsoncLoader = createBucketLoader("jsonc", "i18n/[locale].jsonc", {
 594 |         defaultLocale: "en",
 595 |       });
 596 |       jsoncLoader.setDefaultLocale("en");
 597 |       const data = await jsoncLoader.pull("en");
 598 | 
 599 |       expect(data).toEqual(expectedOutput);
 600 |     });
 601 | 
 602 |     it("should handle invalid jsonc gracefully", async () => {
 603 |       setupFileMocks();
 604 | 
 605 |       const input = `{
 606 |         "hello": "Hello"
 607 |         "world": "World" // missing comma
 608 |         invalid: syntax
 609 |       }`;
 610 | 
 611 |       mockFileOperations(input);
 612 | 
 613 |       const jsoncLoader = createBucketLoader("jsonc", "i18n/[locale].jsonc", {
 614 |         defaultLocale: "en",
 615 |       });
 616 |       jsoncLoader.setDefaultLocale("en");
 617 | 
 618 |       await expect(jsoncLoader.pull("en")).rejects.toThrow(
 619 |         "Failed to parse JSONC",
 620 |       );
 621 |     });
 622 | 
 623 |     it("should respect locked keys (pull)", async () => {
 624 |       setupFileMocks();
 625 | 
 626 |       const input = `{
 627 |         "locked_key": "Original",
 628 |         "unlocked_key": "Hello"
 629 |       }`;
 630 | 
 631 |       mockFileOperations(input);
 632 | 
 633 |       const jsoncLoader = createBucketLoader(
 634 |         "jsonc",
 635 |         "i18n/[locale].jsonc",
 636 |         {
 637 |           defaultLocale: "en",
 638 |         },
 639 |         ["locked_key"],
 640 |       );
 641 |       jsoncLoader.setDefaultLocale("en");
 642 |       const data = await jsoncLoader.pull("en");
 643 | 
 644 |       expect(data).toEqual({ unlocked_key: "Hello" });
 645 |     });
 646 |   });
 647 | 
 648 |   describe("json bucket loader", () => {
 649 |     it("should load json data", async () => {
 650 |       setupFileMocks();
 651 | 
 652 |       const input = { "button.title": "Submit" };
 653 |       mockFileOperations(JSON.stringify(input));
 654 | 
 655 |       const jsonLoader = createBucketLoader("json", "i18n/[locale].json", {
 656 |         defaultLocale: "en",
 657 |       });
 658 |       jsonLoader.setDefaultLocale("en");
 659 |       const data = await jsonLoader.pull("en");
 660 | 
 661 |       expect(data).toEqual(input);
 662 |     });
 663 | 
 664 |     it("should save json data", async () => {
 665 |       setupFileMocks();
 666 | 
 667 |       const input = { "button.title": "Submit" };
 668 |       const payload = { "button.title": "Enviar" };
 669 |       const expectedOutput = JSON.stringify(payload, null, 2);
 670 | 
 671 |       mockFileOperations(JSON.stringify(input));
 672 | 
 673 |       const jsonLoader = createBucketLoader("json", "i18n/[locale].json", {
 674 |         defaultLocale: "en",
 675 |       });
 676 |       jsonLoader.setDefaultLocale("en");
 677 |       await jsonLoader.pull("en");
 678 | 
 679 |       await jsonLoader.push("es", payload);
 680 | 
 681 |       expect(fs.writeFile).toHaveBeenCalledWith(
 682 |         "i18n/es.json",
 683 |         expectedOutput,
 684 |         { encoding: "utf-8", flag: "w" },
 685 |       );
 686 |     });
 687 | 
 688 |     it("should save json data with numeric keys", async () => {
 689 |       setupFileMocks();
 690 | 
 691 |       const input = { messages: { "1": "foo", "2": "bar", "3": "bar" } };
 692 |       const payload = {
 693 |         "messages/1": "foo",
 694 |         "messages/2": "bar",
 695 |         "messages/3": "bar",
 696 |       };
 697 |       const expectedOutput = JSON.stringify(input, null, 2);
 698 | 
 699 |       mockFileOperations(JSON.stringify(input));
 700 | 
 701 |       const jsonLoader = createBucketLoader("json", "i18n/[locale].json", {
 702 |         defaultLocale: "en",
 703 |       });
 704 |       jsonLoader.setDefaultLocale("en");
 705 |       await jsonLoader.pull("en");
 706 | 
 707 |       await jsonLoader.push("es", payload);
 708 | 
 709 |       expect(fs.writeFile).toHaveBeenCalledWith(
 710 |         "i18n/es.json",
 711 |         expectedOutput,
 712 |         { encoding: "utf-8", flag: "w" },
 713 |       );
 714 |     });
 715 | 
 716 |     it("should save json data with array", async () => {
 717 |       setupFileMocks();
 718 | 
 719 |       const input = { messages: ["foo", "bar"] };
 720 |       const payload = { "messages/0": "foo", "messages/1": "bar" };
 721 |       const expectedOutput = dedent`
 722 |         {
 723 |           "messages": ["foo", "bar"]
 724 |         }
 725 |       `.trim();
 726 | 
 727 |       mockFileOperations(JSON.stringify(input));
 728 | 
 729 |       const jsonLoader = createBucketLoader("json", "i18n/[locale].json", {
 730 |         defaultLocale: "en",
 731 |       });
 732 |       jsonLoader.setDefaultLocale("en");
 733 |       await jsonLoader.pull("en");
 734 | 
 735 |       await jsonLoader.push("es", payload);
 736 | 
 737 |       expect(fs.writeFile).toHaveBeenCalledWith(
 738 |         "i18n/es.json",
 739 |         expectedOutput,
 740 |         { encoding: "utf-8", flag: "w" },
 741 |       );
 742 |     });
 743 | 
 744 |     it("should return keys in correct order, should not use key values from original input for missing keys", async () => {
 745 |       setupFileMocks();
 746 | 
 747 |       const input = {
 748 |         "button.title": "Submit",
 749 |         "button.subtitle": "Submit subtitle",
 750 |         "button.description": "Submit description",
 751 |       };
 752 |       const payload = {
 753 |         "button.subtitle": "Subtítulo de envío",
 754 |         "button.title": "Enviar",
 755 |       };
 756 |       const expectedOutput = JSON.stringify(
 757 |         {
 758 |           "button.title": "Enviar",
 759 |           "button.subtitle": "Subtítulo de envío",
 760 |         },
 761 |         null,
 762 |         2,
 763 |       );
 764 | 
 765 |       mockFileOperations(JSON.stringify(input));
 766 | 
 767 |       const jsonLoader = createBucketLoader("json", "i18n/[locale].json", {
 768 |         defaultLocale: "en",
 769 |       });
 770 |       jsonLoader.setDefaultLocale("en");
 771 |       await jsonLoader.pull("en");
 772 | 
 773 |       await jsonLoader.push("es", payload);
 774 | 
 775 |       expect(fs.writeFile).toHaveBeenCalledWith(
 776 |         "i18n/es.json",
 777 |         expectedOutput,
 778 |         { encoding: "utf-8", flag: "w" },
 779 |       );
 780 |     });
 781 | 
 782 |     it("should load and save json data for paths with multiple locales", async () => {
 783 |       setupFileMocks();
 784 | 
 785 |       const input = { "button.title": "Submit" };
 786 |       const payload = { "button.title": "Enviar" };
 787 |       const expectedOutput = JSON.stringify(payload, null, 2);
 788 | 
 789 |       mockFileOperations(JSON.stringify(input));
 790 | 
 791 |       const jsonLoader = createBucketLoader(
 792 |         "json",
 793 |         "i18n/[locale]/[locale].json",
 794 |         {
 795 |           defaultLocale: "en",
 796 |         },
 797 |       );
 798 |       jsonLoader.setDefaultLocale("en");
 799 |       const data = await jsonLoader.pull("en");
 800 | 
 801 |       await jsonLoader.push("es", payload);
 802 | 
 803 |       expect(data).toEqual(input);
 804 |       expect(fs.access).toHaveBeenCalledWith("i18n/en/en.json");
 805 |       expect(fs.writeFile).toHaveBeenCalledWith(
 806 |         "i18n/es/es.json",
 807 |         expectedOutput,
 808 |         { encoding: "utf-8", flag: "w" },
 809 |       );
 810 |     });
 811 | 
 812 |     it("should remove injected locales from json data", async () => {
 813 |       setupFileMocks();
 814 | 
 815 |       const input = {
 816 |         "button.title": "Submit",
 817 |         settings: { locale: "en" },
 818 |         "not-a-locale": "bar",
 819 |       };
 820 |       mockFileOperations(JSON.stringify(input));
 821 | 
 822 |       const jsonLoader = createBucketLoader("json", "i18n/[locale].json", {
 823 |         defaultLocale: "en",
 824 |         injectLocale: ["settings/locale", "not-a-locale"],
 825 |       });
 826 |       jsonLoader.setDefaultLocale("en");
 827 |       const data = await jsonLoader.pull("en");
 828 | 
 829 |       expect(data).toEqual({ "button.title": "Submit", "not-a-locale": "bar" });
 830 |     });
 831 | 
 832 |     it("should inject locales into json data", async () => {
 833 |       setupFileMocks();
 834 | 
 835 |       const input = {
 836 |         "button.title": "Submit",
 837 |         "not-a-locale": "bar",
 838 |         settings: { locale: "en" },
 839 |       };
 840 |       const payload = { "button.title": "Enviar", "not-a-locale": "bar" };
 841 |       const expectedOutput = JSON.stringify(
 842 |         { ...payload, settings: { locale: "es" } },
 843 |         null,
 844 |         2,
 845 |       );
 846 | 
 847 |       mockFileOperations(JSON.stringify(input));
 848 | 
 849 |       const jsonLoader = createBucketLoader("json", "i18n/[locale].json", {
 850 |         defaultLocale: "en",
 851 |         injectLocale: ["settings/locale", "not-a-locale"],
 852 |       });
 853 |       jsonLoader.setDefaultLocale("en");
 854 |       await jsonLoader.pull("en");
 855 | 
 856 |       await jsonLoader.push("es", payload);
 857 | 
 858 |       expect(fs.writeFile).toHaveBeenCalledWith(
 859 |         "i18n/es.json",
 860 |         expectedOutput,
 861 |         { encoding: "utf-8", flag: "w" },
 862 |       );
 863 |     });
 864 |   });
 865 | 
 866 |   describe("locked keys functionality", () => {
 867 |     it("should respect locked keys for JSON format", async () => {
 868 |       setupFileMocks();
 869 | 
 870 |       const input = {
 871 |         "button.title": "Submit",
 872 |         "button.description": "Submit description",
 873 |         "locked.key": "Should not change",
 874 |         nested: {
 875 |           locked: "This is locked",
 876 |           unlocked: "This can change",
 877 |         },
 878 |       };
 879 |       const payload = {
 880 |         "button.title": "Enviar",
 881 |         "button.description": "Descripción de envío",
 882 |         "locked.key": "This should not be applied",
 883 |         "nested/locked": "This should not be applied either",
 884 |         "nested/unlocked": "Este puede cambiar",
 885 |       };
 886 | 
 887 |       mockFileOperations(JSON.stringify(input));
 888 | 
 889 |       const jsonLoader = createBucketLoader(
 890 |         "json",
 891 |         "i18n/[locale].json",
 892 |         { defaultLocale: "en" },
 893 |         ["locked.key", "nested/locked"],
 894 |       );
 895 | 
 896 |       jsonLoader.setDefaultLocale("en");
 897 |       await jsonLoader.pull("en");
 898 | 
 899 |       await jsonLoader.push("es", payload);
 900 | 
 901 |       expect(fs.writeFile).toHaveBeenCalled();
 902 |       const writeFileCall = (fs.writeFile as any).mock.calls[0];
 903 |       const writtenContent = JSON.parse(writeFileCall[1]);
 904 | 
 905 |       // Check that locked keys retain their original values
 906 |       expect(writtenContent["locked.key"]).toBe("Should not change");
 907 |       expect(writtenContent.nested.locked).toBe("This is locked");
 908 | 
 909 |       // Check that unlocked keys are updated
 910 |       expect(writtenContent["button.title"]).toBe("Enviar");
 911 |       expect(writtenContent["button.description"]).toBe("Descripción de envío");
 912 |       expect(writtenContent.nested.unlocked).toBe("Este puede cambiar");
 913 |     });
 914 | 
 915 |     it("should handle deeply nested locked keys", async () => {
 916 |       setupFileMocks();
 917 | 
 918 |       const input = {
 919 |         level1: {
 920 |           level2: {
 921 |             level3: {
 922 |               locked: "This is locked deep",
 923 |               unlocked: "This can change",
 924 |             },
 925 |           },
 926 |         },
 927 |       };
 928 |       const payload = {
 929 |         "level1/level2/level3/locked": "This should not be applied",
 930 |         "level1/level2/level3/unlocked": "This should change",
 931 |       };
 932 | 
 933 |       mockFileOperations(JSON.stringify(input));
 934 | 
 935 |       const jsonLoader = createBucketLoader(
 936 |         "json",
 937 |         "i18n/[locale].json",
 938 |         { defaultLocale: "en" },
 939 |         ["level1/level2/level3/locked"],
 940 |       );
 941 | 
 942 |       jsonLoader.setDefaultLocale("en");
 943 |       await jsonLoader.pull("en");
 944 | 
 945 |       await jsonLoader.push("es", payload);
 946 | 
 947 |       expect(fs.writeFile).toHaveBeenCalled();
 948 |       const writeFileCall = (fs.writeFile as any).mock.calls[0];
 949 |       const writtenContent = JSON.parse(writeFileCall[1]);
 950 | 
 951 |       // Check that deeply nested locked key retains its original value
 952 |       expect(writtenContent.level1.level2.level3.locked).toBe(
 953 |         "This is locked deep",
 954 |       );
 955 | 
 956 |       // Check that unlocked key is updated
 957 |       expect(writtenContent.level1.level2.level3.unlocked).toBe(
 958 |         "This should change",
 959 |       );
 960 |     });
 961 | 
 962 |     it("should lock keys that are arrays", async () => {
 963 |       setupFileMocks();
 964 | 
 965 |       const input = {
 966 |         messages: ["first", "second", "third"],
 967 |         unlocked: ["can", "be", "changed"],
 968 |       };
 969 |       const payload = {
 970 |         "messages/0": "should not change",
 971 |         "messages/1": "should not change either",
 972 |         "messages/2": "should definitely not change",
 973 |         "unlocked/0": "should",
 974 |         "unlocked/1": "definitely",
 975 |         "unlocked/2": "change",
 976 |       };
 977 | 
 978 |       mockFileOperations(JSON.stringify(input));
 979 | 
 980 |       const jsonLoader = createBucketLoader(
 981 |         "json",
 982 |         "i18n/[locale].json",
 983 |         { defaultLocale: "en" },
 984 |         ["messages/0", "messages/1", "messages/2"],
 985 |       );
 986 | 
 987 |       jsonLoader.setDefaultLocale("en");
 988 |       await jsonLoader.pull("en");
 989 | 
 990 |       await jsonLoader.push("es", payload);
 991 | 
 992 |       expect(fs.writeFile).toHaveBeenCalled();
 993 |       const writeFileCall = (fs.writeFile as any).mock.calls[0];
 994 |       const writtenContent = JSON.parse(writeFileCall[1]);
 995 | 
 996 |       // Check that locked array elements retain their original values
 997 |       expect(writtenContent.messages[0]).toBe("first");
 998 |       expect(writtenContent.messages[1]).toBe("second");
 999 |       expect(writtenContent.messages[2]).toBe("third");
1000 | 
1001 |       // Check that unlocked array elements are updated
1002 |       expect(writtenContent.unlocked[0]).toBe("should");
1003 |       expect(writtenContent.unlocked[1]).toBe("definitely");
1004 |       expect(writtenContent.unlocked[2]).toBe("change");
1005 |     });
1006 |   });
1007 | 
1008 |   describe("ignored keys functionality", () => {
1009 |     it("should omit ignored keys for JSON format", async () => {
1010 |       setupFileMocks();
1011 | 
1012 |       const input = {
1013 |         "button.title": "Submit",
1014 |         "button.description": "Submit description",
1015 |         "ignored.key": "Should be ignored",
1016 |         nested: {
1017 |           ignored: "This is ignored",
1018 |           kept: "This is kept",
1019 |         },
1020 |       };
1021 |       const payload = {
1022 |         "button.title": "Enviar",
1023 |         "button.description": "Descripción de envío",
1024 |         "nested/kept": "Esto se mantiene",
1025 |       };
1026 | 
1027 |       mockFileOperations(JSON.stringify(input));
1028 | 
1029 |       const jsonLoader = createBucketLoader(
1030 |         "json",
1031 |         "i18n/[locale].json",
1032 |         { defaultLocale: "en" },
1033 |         undefined, // lockedKeys
1034 |         undefined, // lockedPatterns
1035 |         ["ignored.key", "nested/ignored"], // ignoredKeys
1036 |       );
1037 | 
1038 |       jsonLoader.setDefaultLocale("en");
1039 |       const pulledData = await jsonLoader.pull("en");
1040 | 
1041 |       // Verify ignored keys are not in pulled data
1042 |       expect(pulledData).toEqual({
1043 |         "button.title": "Submit",
1044 |         "button.description": "Submit description",
1045 |         "nested/kept": "This is kept",
1046 |       });
1047 | 
1048 |       await jsonLoader.push("es", payload);
1049 | 
1050 |       expect(fs.writeFile).toHaveBeenCalled();
1051 |       const writeFileCall = (fs.writeFile as any).mock.calls[0];
1052 |       const writtenContent = JSON.parse(writeFileCall[1]);
1053 | 
1054 |       // Check that ignored keys are completely removed from output
1055 |       expect(writtenContent["ignored.key"]).toBeUndefined();
1056 |       expect(writtenContent.nested?.ignored).toBeUndefined();
1057 | 
1058 |       // Check that non-ignored keys are updated
1059 |       expect(writtenContent["button.title"]).toBe("Enviar");
1060 |       expect(writtenContent["button.description"]).toBe("Descripción de envío");
1061 |       expect(writtenContent.nested.kept).toBe("Esto se mantiene");
1062 |     });
1063 | 
1064 |     it("should handle wildcard patterns in ignored keys", async () => {
1065 |       setupFileMocks();
1066 | 
1067 |       const input = {
1068 |         "button.title": "Submit",
1069 |         wildcard_a: "Value A",
1070 |         wildcard_b: "Value B",
1071 |         other: "Other value",
1072 |       };
1073 |       const payload = {
1074 |         "button.title": "Enviar",
1075 |         other: "Otro valor",
1076 |       };
1077 | 
1078 |       mockFileOperations(JSON.stringify(input));
1079 | 
1080 |       const jsonLoader = createBucketLoader(
1081 |         "json",
1082 |         "i18n/[locale].json",
1083 |         { defaultLocale: "en" },
1084 |         undefined, // lockedKeys
1085 |         undefined, // lockedPatterns
1086 |         ["wildcard_*"], // ignoredKeys with wildcard
1087 |       );
1088 | 
1089 |       jsonLoader.setDefaultLocale("en");
1090 |       const pulledData = await jsonLoader.pull("en");
1091 | 
1092 |       // Verify wildcard ignored keys are not in pulled data
1093 |       expect(pulledData).toEqual({
1094 |         "button.title": "Submit",
1095 |         other: "Other value",
1096 |       });
1097 | 
1098 |       await jsonLoader.push("es", payload);
1099 | 
1100 |       expect(fs.writeFile).toHaveBeenCalled();
1101 |       const writeFileCall = (fs.writeFile as any).mock.calls[0];
1102 |       const writtenContent = JSON.parse(writeFileCall[1]);
1103 | 
1104 |       // Check that wildcard ignored keys are completely removed from output
1105 |       expect(writtenContent["wildcard_a"]).toBeUndefined();
1106 |       expect(writtenContent["wildcard_b"]).toBeUndefined();
1107 | 
1108 |       // Check that non-ignored keys are updated
1109 |       expect(writtenContent["button.title"]).toBe("Enviar");
1110 |       expect(writtenContent.other).toBe("Otro valor");
1111 |     });
1112 |   });
1113 | 
1114 |   describe("mdx bucket loader", () => {
1115 |     it("should skip locked keys", async () => {
1116 |       setupFileMocks();
1117 | 
1118 |       const input = dedent`
1119 | ---
1120 | title: Test Mdx
1121 | category: test
1122 | ---
1123 | 
1124 | # Heading 1
1125 | `;
1126 |       const expectedPayload = {
1127 |         "meta/title": "Test Mdx",
1128 |         "content/0": "\n# Heading 1",
1129 |       };
1130 | 
1131 |       mockFileOperations(input);
1132 | 
1133 |       const mdxLoader = createBucketLoader(
1134 |         "mdx",
1135 |         "i18n/[locale].mdx",
1136 |         { defaultLocale: "en" },
1137 |         ["meta/category"],
1138 |       );
1139 | 
1140 |       mdxLoader.setDefaultLocale("en");
1141 |       const data = await mdxLoader.pull("en");
1142 | 
1143 |       expect(data).toEqual(expectedPayload);
1144 |     });
1145 |   });
1146 | 
1147 |   describe("markdown bucket loader", () => {
1148 |     it("should load markdown data", async () => {
1149 |       setupFileMocks();
1150 | 
1151 |       const input = `---
1152 | title: Test Markdown
1153 | date: 2023-05-25
1154 | ---
1155 | 
1156 | # Heading 1
1157 | 
1158 | This is a paragraph.
1159 | 
1160 | ## Heading 2
1161 | 
1162 | Another paragraph with **bold** and *italic* text.`;
1163 |       const expectedOutput = {
1164 |         "fm-attr-title": "Test Markdown",
1165 |         "md-section-0": "# Heading 1",
1166 |         "md-section-1": "This is a paragraph.",
1167 |         "md-section-2": "## Heading 2",
1168 |         "md-section-3": "Another paragraph with **bold** and _italic_ text.",
1169 |       };
1170 | 
1171 |       mockFileOperations(input);
1172 | 
1173 |       const markdownLoader = createBucketLoader(
1174 |         "markdown",
1175 |         "i18n/[locale].md",
1176 |         {
1177 |           defaultLocale: "en",
1178 |         },
1179 |       );
1180 |       markdownLoader.setDefaultLocale("en");
1181 |       const data = await markdownLoader.pull("en");
1182 | 
1183 |       expect(data).toEqual(expectedOutput);
1184 |     });
1185 | 
1186 |     it("should save markdown data", async () => {
1187 |       setupFileMocks();
1188 | 
1189 |       const input = `---
1190 | title: Test Markdown
1191 | date: 2023-05-25
1192 | ---
1193 | 
1194 | # Heading 1
1195 | 
1196 | This is a paragraph.
1197 | 
1198 | ## Heading 2
1199 | 
1200 | Another paragraph with **bold** and *italic* text.`;
1201 |       const payload = {
1202 |         "fm-attr-title": "Prueba Markdown",
1203 |         "fm-attr-date": "2023-05-25",
1204 |         "md-section-0": "# Encabezado 1",
1205 |         "md-section-1": "Esto es un párrafo.",
1206 |         "md-section-2": "## Encabezado 2",
1207 |         "md-section-3": "Otro párrafo con texto en **negrita** y en _cursiva_.",
1208 |       };
1209 |       const expectedOutput = `---
1210 | title: Prueba Markdown
1211 | date: 2023-05-25
1212 | ---
1213 | 
1214 | # Encabezado 1
1215 | 
1216 | Esto es un párrafo.
1217 | 
1218 | ## Encabezado 2
1219 | 
1220 | Otro párrafo con texto en **negrita** y en _cursiva_.
1221 | `.trim();
1222 | 
1223 |       mockFileOperations(input);
1224 | 
1225 |       const markdownLoader = createBucketLoader(
1226 |         "markdown",
1227 |         "i18n/[locale].md",
1228 |         {
1229 |           defaultLocale: "en",
1230 |         },
1231 |       );
1232 |       markdownLoader.setDefaultLocale("en");
1233 |       await markdownLoader.pull("en");
1234 | 
1235 |       await markdownLoader.push("es", payload);
1236 | 
1237 |       expect(fs.writeFile).toHaveBeenCalledWith("i18n/es.md", expectedOutput, {
1238 |         encoding: "utf-8",
1239 |         flag: "w",
1240 |       });
1241 |     });
1242 | 
1243 |     it("should respect locked keys (pull)", async () => {
1244 |       setupFileMocks();
1245 | 
1246 |       const input = dedent`---
1247 | title: Locked Title
1248 | ---
1249 | 
1250 | Content here.`;
1251 | 
1252 |       mockFileOperations(input);
1253 | 
1254 |       const markdownLoader = createBucketLoader(
1255 |         "markdown",
1256 |         "i18n/[locale].md",
1257 |         { defaultLocale: "en" },
1258 |         ["fm-attr-title"],
1259 |       );
1260 |       markdownLoader.setDefaultLocale("en");
1261 |       const data = await markdownLoader.pull("en");
1262 | 
1263 |       // frontmatter title removed
1264 |       expect(Object.keys(data)).not.toContain("fm-attr-title");
1265 |     });
1266 |   });
1267 | 
1268 |   describe("properties bucket loader", () => {
1269 |     it("should load properties data", async () => {
1270 |       setupFileMocks();
1271 | 
1272 |       const input = `
1273 | # General messages
1274 | welcome.message=Welcome to our application!
1275 | error.message=An error has occurred. Please try again later.
1276 | 
1277 | # User-related messages
1278 | user.login=Please enter your username and password.
1279 | user.username=Username
1280 | user.password=Password
1281 |       `.trim();
1282 |       const expectedOutput = {
1283 |         "welcome.message": "Welcome to our application!",
1284 |         "error.message": "An error has occurred. Please try again later.",
1285 |         "user.login": "Please enter your username and password.",
1286 |         "user.username": "Username",
1287 |         "user.password": "Password",
1288 |       };
1289 | 
1290 |       mockFileOperations(input);
1291 | 
1292 |       const propertiesLoader = createBucketLoader(
1293 |         "properties",
1294 |         "i18n/[locale].properties",
1295 |         {
1296 |           defaultLocale: "en",
1297 |         },
1298 |       );
1299 |       propertiesLoader.setDefaultLocale("en");
1300 |       const data = await propertiesLoader.pull("en");
1301 | 
1302 |       expect(data).toEqual(expectedOutput);
1303 |     });
1304 | 
1305 |     it("should save properties data", async () => {
1306 |       setupFileMocks();
1307 | 
1308 |       const input = `
1309 | # General messages
1310 | welcome.message=Welcome to our application!
1311 | error.message=An error has occurred. Please try again later.
1312 | 
1313 | # User-related messages
1314 | user.login=Please enter your username and password.
1315 | user.username=Username
1316 | user.password=Password
1317 |       `.trim();
1318 |       const payload = {
1319 |         "welcome.message": "Bienvenido a nuestra aplicación!",
1320 |         "error.message":
1321 |           "Se ha producido un error. Por favor, inténtelo de nuevo más tarde.",
1322 |         "user.login":
1323 |           "Por favor, introduzca su nombre de usuario y contraseña.",
1324 |         "user.username": "Nombre de usuario",
1325 |         "user.password": "Contraseña",
1326 |       };
1327 |       const expectedOutput = `
1328 | welcome.message=Bienvenido a nuestra aplicación!
1329 | error.message=Se ha producido un error. Por favor, inténtelo de nuevo más tarde.
1330 | user.login=Por favor, introduzca su nombre de usuario y contraseña.
1331 | user.username=Nombre de usuario
1332 | user.password=Contraseña
1333 |       `.trim();
1334 | 
1335 |       mockFileOperations(input);
1336 | 
1337 |       const propertiesLoader = createBucketLoader(
1338 |         "properties",
1339 |         "i18n/[locale].properties",
1340 |         {
1341 |           defaultLocale: "en",
1342 |         },
1343 |       );
1344 |       propertiesLoader.setDefaultLocale("en");
1345 |       await propertiesLoader.pull("en");
1346 | 
1347 |       await propertiesLoader.push("es", payload);
1348 | 
1349 |       expect(fs.writeFile).toHaveBeenCalledWith(
1350 |         "i18n/es.properties",
1351 |         expectedOutput,
1352 |         { encoding: "utf-8", flag: "w" },
1353 |       );
1354 |     });
1355 | 
1356 |     it("should respect locked keys (pull)", async () => {
1357 |       setupFileMocks();
1358 | 
1359 |       const input = `locked=Original\nunlocked=Hello`;
1360 | 
1361 |       mockFileOperations(input);
1362 | 
1363 |       const propertiesLoader = createBucketLoader(
1364 |         "properties",
1365 |         "i18n/[locale].properties",
1366 |         { defaultLocale: "en" },
1367 |         ["locked"],
1368 |       );
1369 |       propertiesLoader.setDefaultLocale("en");
1370 |       const data = await propertiesLoader.pull("en");
1371 | 
1372 |       expect(data).toEqual({ unlocked: "Hello" });
1373 |     });
1374 |   });
1375 | 
1376 |   describe("xcode-strings bucket loader", () => {
1377 |     it("should load xcode-strings", async () => {
1378 |       setupFileMocks();
1379 | 
1380 |       const input = `
1381 | "key1" = "value1";
1382 | "key2" = "value2";
1383 | "key3" = "Line 1\\nLine 2\\"quoted\\"";
1384 |       `.trim();
1385 |       const expectedOutput = {
1386 |         key1: "value1",
1387 |         key2: "value2",
1388 |         key3: 'Line 1\nLine 2"quoted"',
1389 |       };
1390 | 
1391 |       mockFileOperations(input);
1392 | 
1393 |       const xcodeStringsLoader = createBucketLoader(
1394 |         "xcode-strings",
1395 |         "i18n/[locale].strings",
1396 |         {
1397 |           defaultLocale: "en",
1398 |         },
1399 |       );
1400 |       xcodeStringsLoader.setDefaultLocale("en");
1401 |       const data = await xcodeStringsLoader.pull("en");
1402 | 
1403 |       expect(data).toEqual(expectedOutput);
1404 |     });
1405 | 
1406 |     it("should save xcode-strings", async () => {
1407 |       setupFileMocks();
1408 | 
1409 |       const input = `
1410 | "hello" = "Hello!";
1411 |       `.trim();
1412 |       const payload = { hello: "¡Hola!" };
1413 |       const expectedOutput = `"hello" = "¡Hola!";`;
1414 | 
1415 |       mockFileOperations(input);
1416 | 
1417 |       const xcodeStringsLoader = createBucketLoader(
1418 |         "xcode-strings",
1419 |         "i18n/[locale].strings",
1420 |         {
1421 |           defaultLocale: "en",
1422 |         },
1423 |       );
1424 |       xcodeStringsLoader.setDefaultLocale("en");
1425 |       await xcodeStringsLoader.pull("en");
1426 | 
1427 |       await xcodeStringsLoader.push("es", payload);
1428 | 
1429 |       expect(fs.writeFile).toHaveBeenCalledWith(
1430 |         "i18n/es.strings",
1431 |         expectedOutput,
1432 |         { encoding: "utf-8", flag: "w" },
1433 |       );
1434 |     });
1435 | 
1436 |     it("should respect locked keys (pull)", async () => {
1437 |       setupFileMocks();
1438 | 
1439 |       const input = `
1440 | "locked" = "Original";
1441 | "hello" = "Hello!";
1442 |       `.trim();
1443 | 
1444 |       mockFileOperations(input);
1445 | 
1446 |       const xcodeStringsLoader = createBucketLoader(
1447 |         "xcode-strings",
1448 |         "i18n/[locale].strings",
1449 |         { defaultLocale: "en" },
1450 |         ["locked"],
1451 |       );
1452 |       xcodeStringsLoader.setDefaultLocale("en");
1453 |       const data = await xcodeStringsLoader.pull("en");
1454 | 
1455 |       expect(data).toEqual({ hello: "Hello!" });
1456 |     });
1457 |   });
1458 | 
1459 |   describe("xcode-stringsdict bucket loader", () => {
1460 |     it("should load xcode-stringsdict", async () => {
1461 |       setupFileMocks();
1462 | 
1463 |       const input = `
1464 | <?xml version="1.0" encoding="UTF-8"?>
1465 | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1466 | <plist version="1.0">
1467 | <dict>
1468 |   <key>greeting</key>
1469 |   <string>Hello!</string>
1470 |   <key>items_count</key>
1471 |   <dict>
1472 |     <key>NSStringLocalizedFormatKey</key>
1473 |     <string>%#@items@</string>
1474 |     <key>items</key>
1475 |     <dict>
1476 |       <key>NSStringFormatSpecTypeKey</key>
1477 |       <string>NSStringPluralRuleType</string>
1478 |       <key>NSStringFormatValueTypeKey</key>
1479 |       <string>d</string>
1480 |       <key>one</key>
1481 |       <string>%d item</string>
1482 |       <key>other</key>
1483 |       <string>%d items</string>
1484 |     </dict>
1485 |   </dict>
1486 | </dict>
1487 | </plist>
1488 |       `.trim();
1489 |       const expectedOutput = {
1490 |         greeting: "Hello!",
1491 |         "items_count/NSStringLocalizedFormatKey": "%#@items@",
1492 |         "items_count/items/NSStringFormatSpecTypeKey": "NSStringPluralRuleType",
1493 |         "items_count/items/NSStringFormatValueTypeKey": "d",
1494 |         "items_count/items/one": "%d item",
1495 |         "items_count/items/other": "%d items",
1496 |       };
1497 | 
1498 |       mockFileOperations(input);
1499 | 
1500 |       const xcodeStringsdictLoader = createBucketLoader(
1501 |         "xcode-stringsdict",
1502 |         "i18n/[locale].stringsdict",
1503 |         {
1504 |           defaultLocale: "en",
1505 |         },
1506 |       );
1507 |       xcodeStringsdictLoader.setDefaultLocale("en");
1508 |       const data = await xcodeStringsdictLoader.pull("en");
1509 | 
1510 |       expect(data).toEqual(expectedOutput);
1511 |     });
1512 | 
1513 |     it("should save xcode-stringsdict", async () => {
1514 |       setupFileMocks();
1515 | 
1516 |       const input = `
1517 | <?xml version="1.0" encoding="UTF-8"?>
1518 | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1519 | <plist version="1.0">
1520 |   <dict>
1521 |     <key>greeting</key>
1522 |     <string>Hello!</string>
1523 |   </dict>
1524 | </plist>
1525 |       `.trim();
1526 |       const payload = { greeting: "¡Hola!" };
1527 |       const expectedOutput = `<?xml version="1.0" encoding="UTF-8"?>
1528 | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1529 | <plist version="1.0">
1530 |   <dict>
1531 |     <key>greeting</key>
1532 |     <string>¡Hola!</string>
1533 |   </dict>
1534 | </plist>
1535 |       `.trim();
1536 | 
1537 |       mockFileOperations(input);
1538 | 
1539 |       const xcodeStringsdictLoader = createBucketLoader(
1540 |         "xcode-stringsdict",
1541 |         "[locale].lproj/Localizable.stringsdict",
1542 |         {
1543 |           defaultLocale: "en",
1544 |         },
1545 |       );
1546 |       xcodeStringsdictLoader.setDefaultLocale("en");
1547 |       await xcodeStringsdictLoader.pull("en");
1548 | 
1549 |       await xcodeStringsdictLoader.push("es", payload);
1550 | 
1551 |       expect(fs.writeFile).toHaveBeenCalledWith(
1552 |         "es.lproj/Localizable.stringsdict",
1553 |         expectedOutput,
1554 |         {
1555 |           encoding: "utf-8",
1556 |           flag: "w",
1557 |         },
1558 |       );
1559 |     });
1560 | 
1561 |     it("should respect locked keys (pull)", async () => {
1562 |       setupFileMocks();
1563 | 
1564 |       const input = `
1565 | <?xml version="1.0" encoding="UTF-8"?>
1566 | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1567 | <plist version="1.0">
1568 | <dict>
1569 |   <key>locked</key>
1570 |   <string>Original</string>
1571 |   <key>hello</key>
1572 |   <string>Hello!</string>
1573 | </dict>
1574 | </plist>
1575 |       `.trim();
1576 | 
1577 |       mockFileOperations(input);
1578 | 
1579 |       const xcodeStringsdictLoader = createBucketLoader(
1580 |         "xcode-stringsdict",
1581 |         "i18n/[locale].stringsdict",
1582 |         { defaultLocale: "en" },
1583 |         ["locked"],
1584 |       );
1585 |       xcodeStringsdictLoader.setDefaultLocale("en");
1586 |       const data = await xcodeStringsdictLoader.pull("en");
1587 | 
1588 |       expect(data).toEqual({ hello: "Hello!" });
1589 |     });
1590 |   });
1591 | 
1592 |   describe("xcode-xcstrings bucket loader", () => {
1593 |     it("should load xcode-xcstrings", async () => {
1594 |       setupFileMocks();
1595 | 
1596 |       const input = JSON.stringify({
1597 |         sourceLanguage: "en",
1598 |         strings: {
1599 |           greeting: {
1600 |             extractionState: "manual",
1601 |             localizations: {
1602 |               en: {
1603 |                 stringUnit: {
1604 |                   state: "translated",
1605 |                   value: "Hello!",
1606 |                 },
1607 |               },
1608 |             },
1609 |           },
1610 |           message: {
1611 |             extractionState: "manual",
1612 |             localizations: {
1613 |               en: {
1614 |                 stringUnit: {
1615 |                   state: "translated",
1616 |                   value: "Welcome to our app",
1617 |                 },
1618 |               },
1619 |             },
1620 |           },
1621 |           items_count: {
1622 |             extractionState: "manual",
1623 |             localizations: {
1624 |               en: {
1625 |                 variations: {
1626 |                   plural: {
1627 |                     zero: {
1628 |                       stringUnit: {
1629 |                         state: "translated",
1630 |                         value: "No items",
1631 |                       },
1632 |                     },
1633 |                     one: {
1634 |                       stringUnit: {
1635 |                         state: "translated",
1636 |                         value: "%d item",
1637 |                       },
1638 |                     },
1639 |                     other: {
1640 |                       stringUnit: {
1641 |                         state: "translated",
1642 |                         value: "%d items",
1643 |                       },
1644 |                     },
1645 |                   },
1646 |                 },
1647 |               },
1648 |             },
1649 |           },
1650 |         },
1651 |       });
1652 | 
1653 |       const expectedOutput = {
1654 |         greeting: "Hello!",
1655 |         message: "Welcome to our app",
1656 |         "items_count/zero": "No items",
1657 |         "items_count/one": "{variable:0} item",
1658 |         "items_count/other": "{variable:0} items",
1659 |       };
1660 | 
1661 |       mockFileOperations(input);
1662 | 
1663 |       const xcodeXcstringsLoader = createBucketLoader(
1664 |         "xcode-xcstrings",
1665 |         "i18n/[locale].xcstrings",
1666 |         {
1667 |           defaultLocale: "en",
1668 |         },
1669 |       );
1670 |       xcodeXcstringsLoader.setDefaultLocale("en");
1671 |       const data = await xcodeXcstringsLoader.pull("en");
1672 | 
1673 |       expect(data).toEqual(expectedOutput);
1674 |     });
1675 | 
1676 |     it("should load keys without default locale entries and use the key as value", async () => {
1677 |       setupFileMocks();
1678 | 
1679 |       const input = JSON.stringify({
1680 |         sourceLanguage: "en",
1681 |         strings: {
1682 |           greeting: {
1683 |             extractionState: "manual",
1684 |             localizations: {
1685 |               en: {
1686 |                 stringUnit: {
1687 |                   state: "translated",
1688 |                   value: "Hello!",
1689 |                 },
1690 |               },
1691 |             },
1692 |           },
1693 |           " and ": {
1694 |             extractionState: "manual",
1695 |             localizations: {
1696 |               en: {
1697 |                 stringUnit: {
1698 |                   state: "translated",
1699 |                   value: " and ",
1700 |                 },
1701 |               },
1702 |             },
1703 |           },
1704 |           key_with_no_default: {
1705 |             extractionState: "manual",
1706 |             localizations: {
1707 |               fr: {
1708 |                 stringUnit: {
1709 |                   state: "translated",
1710 |                   value: "Valeur traduite",
1711 |                 },
1712 |               },
1713 |             },
1714 |           },
1715 |         },
1716 |       });
1717 | 
1718 |       const expectedOutput = {
1719 |         greeting: "Hello!",
1720 |         "%20and%20": " and ",
1721 |         key_with_no_default: "key_with_no_default",
1722 |       };
1723 | 
1724 |       mockFileOperations(input);
1725 | 
1726 |       const xcodeXcstringsLoader = createBucketLoader(
1727 |         "xcode-xcstrings",
1728 |         "i18n/[locale].xcstrings",
1729 |         {
1730 |           defaultLocale: "en",
1731 |         },
1732 |       );
1733 |       xcodeXcstringsLoader.setDefaultLocale("en");
1734 |       const data = await xcodeXcstringsLoader.pull("en");
1735 | 
1736 |       expect(data).toEqual(expectedOutput);
1737 |     });
1738 | 
1739 |     it("should save xcode-xcstrings", async () => {
1740 |       setupFileMocks();
1741 | 
1742 |       const originalInput = {
1743 |         sourceLanguage: "en",
1744 |         strings: {
1745 |           greeting: {
1746 |             extractionState: "manual",
1747 |             localizations: {
1748 |               en: {
1749 |                 stringUnit: {
1750 |                   state: "translated",
1751 |                   value: "Hello!",
1752 |                 },
1753 |               },
1754 |             },
1755 |           },
1756 |         },
1757 |       };
1758 | 
1759 |       mockFileOperations(JSON.stringify(originalInput));
1760 | 
1761 |       const payload = {
1762 |         greeting: "Bonjour!",
1763 |         message: "Bienvenue dans notre application",
1764 |         "items_count/zero": "Aucun élément",
1765 |         "items_count/one": "%d élément",
1766 |         "items_count/other": "%d éléments",
1767 |       };
1768 | 
1769 |       const xcodeXcstringsLoader = createBucketLoader(
1770 |         "xcode-xcstrings",
1771 |         "i18n/[locale].xcstrings",
1772 |         {
1773 |           defaultLocale: "en",
1774 |         },
1775 |       );
1776 |       xcodeXcstringsLoader.setDefaultLocale("en");
1777 |       await xcodeXcstringsLoader.pull("en");
1778 |       await xcodeXcstringsLoader.push("fr", payload);
1779 | 
1780 |       expect(fs.writeFile).toHaveBeenCalled();
1781 |       const writeFileCall = (fs.writeFile as any).mock.calls[0];
1782 |       const writtenContent = JSON.parse(writeFileCall[1]);
1783 | 
1784 |       expect(writtenContent.strings.greeting.localizations.fr).toBeDefined();
1785 |       expect(
1786 |         writtenContent.strings.greeting.localizations.fr.stringUnit.value,
1787 |       ).toBe("Bonjour!");
1788 | 
1789 |       if (writtenContent.strings.message) {
1790 |         expect(
1791 |           writtenContent.strings.message.localizations.fr.stringUnit.value,
1792 |         ).toBe("Bienvenue dans notre application");
1793 |       }
1794 | 
1795 |       if (writtenContent.strings.items_count) {
1796 |         expect(
1797 |           writtenContent.strings.items_count.localizations.fr.variations.plural
1798 |             .zero.stringUnit.value,
1799 |         ).toBe("Aucun élément");
1800 |         expect(
1801 |           writtenContent.strings.items_count.localizations.fr.variations.plural
1802 |             .one.stringUnit.value,
1803 |         ).toBe("%d élément");
1804 |         expect(
1805 |           writtenContent.strings.items_count.localizations.fr.variations.plural
1806 |             .other.stringUnit.value,
1807 |         ).toBe("%d éléments");
1808 |       }
1809 |     });
1810 | 
1811 |     it("should maintain ASCII ordering with empty strings, whitespace, and numbers", async () => {
1812 |       setupFileMocks();
1813 | 
1814 |       const input = `{
1815 |   "sourceLanguage": "en",
1816 |   "strings": {
1817 |     "": {
1818 |       "extractionState": "manual",
1819 |       "localizations": {
1820 |         "en": {
1821 |           "stringUnit": {
1822 |             "state": "translated",
1823 |             "value": "Empty key"
1824 |           }
1825 |         }
1826 |       }
1827 |     },
1828 |     " ": {
1829 |       "extractionState": "manual",
1830 |       "localizations": {
1831 |         "en": {
1832 |           "stringUnit": {
1833 |             "state": "translated",
1834 |             "value": "Space key"
1835 |           }
1836 |         }
1837 |       }
1838 |     },
1839 |     "25": {
1840 |       "extractionState": "manual",
1841 |       "localizations": {
1842 |         "en": {
1843 |           "stringUnit": {
1844 |             "state": "translated",
1845 |             "value": "Numeric key"
1846 |           }
1847 |         }
1848 |       }
1849 |     },
1850 |     "apple": {
1851 |       "extractionState": "manual",
1852 |       "localizations": {
1853 |         "en": {
1854 |           "stringUnit": {
1855 |             "state": "translated",
1856 |             "value": "Apple"
1857 |           }
1858 |         }
1859 |       }
1860 |   }
1861 | }`;
1862 | 
1863 |       mockFileOperations(input);
1864 | 
1865 |       const xcodeXcstringsLoader = createBucketLoader(
1866 |         "xcode-xcstrings",
1867 |         "i18n/[locale].xcstrings",
1868 |         {
1869 |           defaultLocale: "en",
1870 |         },
1871 |       );
1872 |       xcodeXcstringsLoader.setDefaultLocale("en");
1873 |       const data = await xcodeXcstringsLoader.pull("en");
1874 | 
1875 |       Object.keys(data).forEach((key) => {
1876 |         if (key === "") {
1877 |           expect(data[key]).toBe("Empty key");
1878 |         } else if (key.includes("%20") || key === " ") {
1879 |           expect(data[key]).toBe("Space key");
1880 |         } else if (key === "25") {
1881 |           expect(data[key]).toBe("Numeric key");
1882 |         } else if (key === "apple") {
1883 |           expect(data[key]).toBe("Apple");
1884 |         }
1885 |       });
1886 | 
1887 |       const payload: Record<string, string> = {};
1888 | 
1889 |       Object.keys(data).forEach((key) => {
1890 |         if (key === "") {
1891 |           payload[key] = "Vide";
1892 |         } else if (key.includes("%20") || key === " ") {
1893 |           payload[key] = "Espace";
1894 |         } else if (key === "25") {
1895 |           payload[key] = "Numérique";
1896 |         } else if (key === "apple") {
1897 |           payload[key] = "Pomme";
1898 |         }
1899 |       });
1900 | 
1901 |       await xcodeXcstringsLoader.pull("en");
1902 |       await xcodeXcstringsLoader.push("fr", payload);
1903 | 
1904 |       expect(fs.writeFile).toHaveBeenCalled();
1905 |       const writeFileCall = (fs.writeFile as any).mock.calls[0];
1906 |       const writtenContent = JSON.parse(writeFileCall[1]);
1907 | 
1908 |       if (writtenContent.strings[""]) {
1909 |         expect(
1910 |           writtenContent.strings[""].localizations.fr.stringUnit.value,
1911 |         ).toBe("Vide");
1912 |       }
1913 | 
1914 |       const hasSpaceKey = Object.keys(writtenContent.strings).some(
1915 |         (key) => key === " " || key === "%20" || key.includes("%20"),
1916 |       );
1917 |       if (hasSpaceKey) {
1918 |         const spaceKey = Object.keys(writtenContent.strings).find(
1919 |           (key) => key === " " || key === "%20" || key.includes("%20"),
1920 |         );
1921 |         if (spaceKey) {
1922 |           expect(
1923 |             writtenContent.strings[spaceKey].localizations.fr.stringUnit.value,
1924 |           ).toBe("Espace");
1925 |         }
1926 |       }
1927 | 
1928 |       if (writtenContent.strings["25"]) {
1929 |         expect(
1930 |           writtenContent.strings["25"].localizations.fr.stringUnit.value,
1931 |         ).toBe("Numérique");
1932 |       }
1933 | 
1934 |       if (writtenContent.strings["apple"]) {
1935 |         expect(
1936 |           writtenContent.strings["apple"].localizations.fr.stringUnit.value,
1937 |         ).toBe("Pomme");
1938 |       }
1939 | 
1940 |       const stringKeys = Object.keys(writtenContent.strings);
1941 | 
1942 |       expect(stringKeys.includes("25")).toBe(true);
1943 |       expect(stringKeys.includes("")).toBe(true);
1944 |       expect(stringKeys.includes(" ") || stringKeys.includes("%20")).toBe(true);
1945 |       expect(stringKeys.includes("apple")).toBe(true);
1946 | 
1947 |       expect(stringKeys.indexOf("25")).toBeLessThan(stringKeys.indexOf(""));
1948 | 
1949 |       const spaceIdx =
1950 |         stringKeys.indexOf(" ") === -1
1951 |           ? stringKeys.indexOf("%20")
1952 |           : stringKeys.indexOf(" ");
1953 |       if (spaceIdx !== -1) {
1954 |         expect(stringKeys.indexOf("")).toBeLessThan(spaceIdx);
1955 |       }
1956 | 
1957 |       if (spaceIdx !== -1) {
1958 |         expect(spaceIdx).toBeLessThan(stringKeys.indexOf("apple"));
1959 |       }
1960 |     });
1961 | 
1962 |     it("should respect shouldTranslate: false flag", async () => {
1963 |       setupFileMocks();
1964 | 
1965 |       const input = `{
1966 |   "sourceLanguage": "en",
1967 |   "strings": {
1968 |     "do_not_translate": {
1969 |       "shouldTranslate": false,
1970 |       "localizations": {
1971 |         "en": {
1972 |           "stringUnit": {
1973 |             "state": "translated",
1974 |             "value": "This should not be translated"
1975 |           }
1976 |         }
1977 |       }
1978 |     },
1979 |     "normal_key": {
1980 |       "extractionState": "manual",
1981 |       "localizations": {
1982 |         "en": {
1983 |           "stringUnit": {
1984 |             "state": "translated",
1985 |             "value": "This should be translated"
1986 |           }
1987 |         }
1988 |       }
1989 |     }
1990 |   }
1991 | }`;
1992 | 
1993 |       mockFileOperations(input);
1994 | 
1995 |       const xcodeXcstringsLoader = createBucketLoader(
1996 |         "xcode-xcstrings",
1997 |         "i18n/[locale].xcstrings",
1998 |         {
1999 |           defaultLocale: "en",
2000 |         },
2001 |       );
2002 |       xcodeXcstringsLoader.setDefaultLocale("en");
2003 | 
2004 |       const data = await xcodeXcstringsLoader.pull("en");
2005 | 
2006 |       expect(data).toHaveProperty("normal_key", "This should be translated");
2007 |       expect(data).not.toHaveProperty("do_not_translate");
2008 | 
2009 |       const payload = {
2010 |         normal_key: "Ceci devrait être traduit",
2011 |       };
2012 | 
2013 |       await xcodeXcstringsLoader.push("fr", payload);
2014 | 
2015 |       expect(fs.writeFile).toHaveBeenCalled();
2016 |       const writeFileCall = (fs.writeFile as any).mock.calls[0];
2017 |       const writtenContent = JSON.parse(writeFileCall[1]);
2018 | 
2019 |       expect(
2020 |         writtenContent.strings.normal_key.localizations.fr.stringUnit.value,
2021 |       ).toBe("Ceci devrait être traduit");
2022 | 
2023 |       expect(writtenContent.strings.do_not_translate).toHaveProperty(
2024 |         "shouldTranslate",
2025 |         false,
2026 |       );
2027 | 
2028 |       expect(
2029 |         writtenContent.strings.do_not_translate.localizations,
2030 |       ).not.toHaveProperty("fr");
2031 | 
2032 |       await xcodeXcstringsLoader.push("fr", {});
2033 | 
2034 |       const secondWriteFileCall = (fs.writeFile as any).mock.calls[1];
2035 |       const secondWrittenContent = JSON.parse(secondWriteFileCall[1]);
2036 | 
2037 |       expect(secondWrittenContent.strings.do_not_translate).toHaveProperty(
2038 |         "shouldTranslate",
2039 |         false,
2040 |       );
2041 |     });
2042 | 
2043 |     it("should extract and restore variables during pull/push", async () => {
2044 |       setupFileMocks();
2045 | 
2046 |       const input = JSON.stringify({
2047 |         sourceLanguage: "en",
2048 |         strings: {
2049 |           message: {
2050 |             extractionState: "manual",
2051 |             localizations: {
2052 |               en: {
2053 |                 stringUnit: {
2054 |                   state: "translated",
2055 |                   value: "Value: %d items",
2056 |                 },
2057 |               },
2058 |             },
2059 |           },
2060 |         },
2061 |       });
2062 | 
2063 |       mockFileOperations(input);
2064 | 
2065 |       const xcLoader = createBucketLoader(
2066 |         "xcode-xcstrings",
2067 |         "i18n/[locale].xcstrings",
2068 |         {
2069 |           defaultLocale: "en",
2070 |         },
2071 |       );
2072 |       xcLoader.setDefaultLocale("en");
2073 | 
2074 |       const data = await xcLoader.pull("en");
2075 | 
2076 |       expect(data).toEqual({ message: "Value: {variable:0} items" });
2077 | 
2078 |       const payload = {
2079 |         message: "Valeur: {variable:0} éléments",
2080 |       };
2081 | 
2082 |       await xcLoader.push("fr", payload);
2083 | 
2084 |       expect(fs.writeFile).toHaveBeenCalled();
2085 |       const writeFileCall = (fs.writeFile as any).mock.calls[0];
2086 |       const writtenContent = JSON.parse(writeFileCall[1]);
2087 | 
2088 |       expect(
2089 |         writtenContent.strings.message.localizations.fr.stringUnit.value,
2090 |       ).toBe("Valeur: %d éléments");
2091 |     });
2092 | 
2093 |     it("should extract hints from xcstrings comments", async () => {
2094 |       setupFileMocks();
2095 | 
2096 |       const input = JSON.stringify({
2097 |         sourceLanguage: "en",
2098 |         strings: {
2099 |           welcome_message: {
2100 |             comment: "Greeting displayed on the main screen",
2101 |             extractionState: "manual",
2102 |             localizations: {
2103 |               en: {
2104 |                 stringUnit: {
2105 |                   state: "translated",
2106 |                   value: "Welcome!",
2107 |                 },
2108 |               },
2109 |             },
2110 |           },
2111 |           user_count: {
2112 |             comment: "Number of active users - supports pluralization",
2113 |             extractionState: "manual",
2114 |             localizations: {
2115 |               en: {
2116 |                 variations: {
2117 |                   plural: {
2118 |                     one: {
2119 |                       stringUnit: {
2120 |                         state: "translated",
2121 |                         value: "1 user",
2122 |                       },
2123 |                     },
2124 |                     other: {
2125 |                       stringUnit: {
2126 |                         state: "translated",
2127 |                         value: "%d users",
2128 |                       },
2129 |                     },
2130 |                   },
2131 |                 },
2132 |               },
2133 |             },
2134 |           },
2135 |           no_comment: {
2136 |             extractionState: "manual",
2137 |             localizations: {
2138 |               en: {
2139 |                 stringUnit: {
2140 |                   state: "translated",
2141 |                   value: "No comment here",
2142 |                 },
2143 |               },
2144 |             },
2145 |           },
2146 |         },
2147 |       });
2148 | 
2149 |       mockFileOperations(input);
2150 | 
2151 |       const xcodeXcstringsLoader = createBucketLoader(
2152 |         "xcode-xcstrings",
2153 |         "i18n/[locale].xcstrings",
2154 |         {
2155 |           defaultLocale: "en",
2156 |         },
2157 |       );
2158 |       xcodeXcstringsLoader.setDefaultLocale("en");
2159 |       await xcodeXcstringsLoader.pull("en");
2160 | 
2161 |       const hints = await xcodeXcstringsLoader.pullHints();
2162 | 
2163 |       // Note: The output is flattened because xcode-xcstrings bucket loader goes through the flat loader
2164 |       expect(hints).toEqual({
2165 |         welcome_message: ["Greeting displayed on the main screen"],
2166 |         user_count: ["Number of active users - supports pluralization"],
2167 |         "user_count/one": ["Number of active users - supports pluralization"],
2168 |         "user_count/other": ["Number of active users - supports pluralization"],
2169 |       });
2170 |     });
2171 | 
2172 |     it("should handle xcstrings without comments in full-stack loader", async () => {
2173 |       setupFileMocks();
2174 | 
2175 |       const input = JSON.stringify({
2176 |         sourceLanguage: "en",
2177 |         strings: {
2178 |           simple_key: {
2179 |             extractionState: "manual",
2180 |             localizations: {
2181 |               en: {
2182 |                 stringUnit: {
2183 |                   state: "translated",
2184 |                   value: "Simple value",
2185 |                 },
2186 |               },
2187 |             },
2188 |           },
2189 |         },
2190 |       });
2191 | 
2192 |       mockFileOperations(input);
2193 | 
2194 |       const xcodeXcstringsLoader = createBucketLoader(
2195 |         "xcode-xcstrings",
2196 |         "i18n/[locale].xcstrings",
2197 |         {
2198 |           defaultLocale: "en",
2199 |         },
2200 |       );
2201 |       xcodeXcstringsLoader.setDefaultLocale("en");
2202 |       await xcodeXcstringsLoader.pull("en");
2203 | 
2204 |       const hints = await xcodeXcstringsLoader.pullHints();
2205 | 
2206 |       expect(hints).toEqual({});
2207 |     });
2208 | 
2209 |     it("should properly filter lockedKeys from data during pull operations", async () => {
2210 |       setupFileMocks();
2211 | 
2212 |       const input = JSON.stringify({
2213 |         sourceLanguage: "en",
2214 |         strings: {
2215 |           welcome_message: {
2216 |             comment: "Welcome message - should be locked",
2217 |             extractionState: "manual",
2218 |             localizations: {
2219 |               en: {
2220 |                 stringUnit: {
2221 |                   state: "translated",
2222 |                   value: "Hello, world!",
2223 |                 },
2224 |               },
2225 |               es: {
2226 |                 stringUnit: {
2227 |                   state: "translated",
2228 |                   value: "¡Hola, mundo!",
2229 |                 },
2230 |               },
2231 |             },
2232 |           },
2233 |           user_count: {
2234 |             comment: "Number of users - should be translatable",
2235 |             extractionState: "manual",
2236 |             localizations: {
2237 |               en: {
2238 |                 variations: {
2239 |                   plural: {
2240 |                     one: {
2241 |                       stringUnit: {
2242 |                         state: "translated",
2243 |                         value: "1 user",
2244 |                       },
2245 |                     },
2246 |                     other: {
2247 |                       stringUnit: {
2248 |                         state: "translated",
2249 |                         value: "%d users",
2250 |                       },
2251 |                     },
2252 |                   },
2253 |                 },
2254 |               },
2255 |               es: {
2256 |                 variations: {
2257 |                   plural: {
2258 |                     one: {
2259 |                       stringUnit: {
2260 |                         state: "translated",
2261 |                         value: "1 usuario",
2262 |                       },
2263 |                     },
2264 |                     other: {
2265 |                       stringUnit: {
2266 |                         state: "translated",
2267 |                         value: "%d usuarios",
2268 |                       },
2269 |                     },
2270 |                   },
2271 |                 },
2272 |               },
2273 |             },
2274 |           },
2275 |           api_key: {
2276 |             comment: "API key - should be locked with wildcard pattern",
2277 |             extractionState: "manual",
2278 |             localizations: {
2279 |               en: {
2280 |                 stringUnit: {
2281 |                   state: "translated",
2282 |                   value: "sk-1234567890abcdef",
2283 |                 },
2284 |               },
2285 |             },
2286 |           },
2287 |         },
2288 |       });
2289 | 
2290 |       mockFileOperations(input);
2291 | 
2292 |       // Test with lockedKeys including both specific keys and wildcard patterns
2293 |       const xcodeXcstringsLoaderWithLockedKeys = createBucketLoader(
2294 |         "xcode-xcstrings",
2295 |         "i18n/[locale].xcstrings",
2296 |         {
2297 |           defaultLocale: "en",
2298 |         },
2299 |         ["welcome_message", "api*"], // lockedKeys parameter
2300 |       );
2301 |       xcodeXcstringsLoaderWithLockedKeys.setDefaultLocale("en");
2302 | 
2303 |       // First pull the default locale to initialize the loader
2304 |       await xcodeXcstringsLoaderWithLockedKeys.pull("en");
2305 | 
2306 |       // Pull data for translation - should filter out locked keys
2307 |       const dataForTranslation =
2308 |         await xcodeXcstringsLoaderWithLockedKeys.pull("es");
2309 | 
2310 |       // Locked keys should be filtered out
2311 |       expect(dataForTranslation).not.toHaveProperty("welcome_message");
2312 |       expect(dataForTranslation).not.toHaveProperty("api_key");
2313 | 
2314 |       // Non-locked keys should remain
2315 |       expect(dataForTranslation).toHaveProperty("user_count/one");
2316 |       expect(dataForTranslation).toHaveProperty("user_count/other");
2317 |       expect(dataForTranslation["user_count/one"]).toBe("1 usuario");
2318 |       expect(dataForTranslation["user_count/other"]).toBe(
2319 |         "{variable:0} usuarios",
2320 |       );
2321 | 
2322 |       // Test that push operations preserve locked keys from original
2323 | 
2324 |       const translationPayload = {
2325 |         "user_count/one": "1 usuario nuevo",
2326 |         "user_count/other": "{variable:0} usuarios nuevos",
2327 |         // Attempt to overwrite locked keys - should be ignored
2328 |         welcome_message: "This should be ignored",
2329 |         api_key: "This should also be ignored",
2330 |       };
2331 | 
2332 |       await xcodeXcstringsLoaderWithLockedKeys.push("es", translationPayload);
2333 | 
2334 |       expect(fs.writeFile).toHaveBeenCalled();
2335 |       const writeFileCall = (fs.writeFile as any).mock.calls[0];
2336 |       const writtenContent = JSON.parse(writeFileCall[1]);
2337 | 
2338 |       // Locked keys should preserve their original values from the input
2339 |       // Since welcome_message was locked, the Spanish translation should not be overwritten
2340 |       // But it might be replaced with the English value due to how the xcstrings loader works
2341 |       // The important thing is that it wasn't sent for translation
2342 |       expect(
2343 |         writtenContent.strings.welcome_message.localizations.es,
2344 |       ).toBeDefined();
2345 |       expect(
2346 |         writtenContent.strings.welcome_message.localizations.es.stringUnit
2347 |           .value,
2348 |       ).toMatch(/Hello, world!|¡Hola, mundo!/);
2349 |       expect(
2350 |         writtenContent.strings.api_key.localizations.en.stringUnit.value,
2351 |       ).toBe("sk-1234567890abcdef");
2352 |       // The api_key is locked, so it should preserve the original value even if we tried to overwrite it
2353 |       if (writtenContent.strings.api_key.localizations.es) {
2354 |         expect(
2355 |           writtenContent.strings.api_key.localizations.es.stringUnit.value,
2356 |         ).toBe("sk-1234567890abcdef");
2357 |       }
2358 | 
2359 |       // Non-locked keys should have new translations
2360 |       expect(
2361 |         writtenContent.strings.user_count.localizations.es.variations.plural.one
2362 |           .stringUnit.value,
2363 |       ).toBe("1 usuario nuevo");
2364 |       expect(
2365 |         writtenContent.strings.user_count.localizations.es.variations.plural
2366 |           .other.stringUnit.value,
2367 |       ).toBe("%d usuarios nuevos");
2368 |     });
2369 |   });
2370 | 
2371 |   describe("yaml bucket loader", () => {
2372 |     it("should load yaml", async () => {
2373 |       setupFileMocks();
2374 | 
2375 |       const input = `
2376 |         greeting: Hello!
2377 |       `.trim();
2378 |       const expectedOutput = { greeting: "Hello!" };
2379 | 
2380 |       mockFileOperations(input);
2381 | 
2382 |       const yamlLoader = createBucketLoader("yaml", "i18n/[locale].yaml", {
2383 |         defaultLocale: "en",
2384 |       });
2385 |       yamlLoader.setDefaultLocale("en");
2386 |       const data = await yamlLoader.pull("en");
2387 | 
2388 |       expect(data).toEqual(expectedOutput);
2389 |     });
2390 | 
2391 |     it("should respect locked keys (pull)", async () => {
2392 |       setupFileMocks();
2393 | 
2394 |       const input = `locked: Original\nhello: Hello!`;
2395 | 
2396 |       mockFileOperations(input);
2397 | 
2398 |       const yamlLoader = createBucketLoader(
2399 |         "yaml",
2400 |         "i18n/[locale].yaml",
2401 |         {
2402 |           defaultLocale: "en",
2403 |         },
2404 |         ["locked"],
2405 |       );
2406 |       yamlLoader.setDefaultLocale("en");
2407 |       const data = await yamlLoader.pull("en");
2408 | 
2409 |       expect(data).toEqual({ hello: "Hello!" });
2410 |     });
2411 | 
2412 |     it("should save yaml", async () => {
2413 |       setupFileMocks();
2414 | 
2415 |       const input = `
2416 |         greeting: Hello!
2417 |       `.trim();
2418 |       const payload = { greeting: "¡Hola!" };
2419 |       const expectedOutput = `greeting: ¡Hola!`;
2420 | 
2421 |       mockFileOperations(input);
2422 | 
2423 |       const yamlLoader = createBucketLoader("yaml", "i18n/[locale].yaml", {
2424 |         defaultLocale: "en",
2425 |       });
2426 |       yamlLoader.setDefaultLocale("en");
2427 |       await yamlLoader.pull("en");
2428 | 
2429 |       await yamlLoader.push("es", payload);
2430 | 
2431 |       expect(fs.writeFile).toHaveBeenCalledWith(
2432 |         "i18n/es.yaml",
2433 |         expectedOutput,
2434 |         { encoding: "utf-8", flag: "w" },
2435 |       );
2436 |     });
2437 | 
2438 |     describe("yaml with quoted keys and values", async () => {
2439 |       it.each([
2440 |         ["double quoted values", `greeting: "Hello!"`, `greeting: "¡Hola!"`],
2441 |         ["double quoted keys", `"greeting": Hello!`, `"greeting": ¡Hola!`],
2442 |         [
2443 |           "double quoted keys and values",
2444 |           `"greeting": "Hello!"`,
2445 |           `"greeting": "¡Hola!"`,
2446 |         ],
2447 |       ])(
2448 |         "should return correct value for %s",
2449 |         async (_, input, expectedOutput) => {
2450 |           const payload = { greeting: "¡Hola!" };
2451 | 
2452 |           mockFileOperations(input);
2453 | 
2454 |           const yamlLoader = createBucketLoader("yaml", "i18n/[locale].yaml", {
2455 |             defaultLocale: "en",
2456 |           });
2457 |           yamlLoader.setDefaultLocale("en");
2458 |           await yamlLoader.pull("en");
2459 | 
2460 |           await yamlLoader.push("es", payload);
2461 | 
2462 |           expect(fs.writeFile).toHaveBeenCalledWith(
2463 |             "i18n/es.yaml",
2464 |             expectedOutput,
2465 |             { encoding: "utf-8", flag: "w" },
2466 |           );
2467 |         },
2468 |       );
2469 |     });
2470 |   });
2471 | 
2472 |   describe("yaml-root-key bucket loader", () => {
2473 |     it("should load yaml-root-key", async () => {
2474 |       setupFileMocks();
2475 | 
2476 |       const input = `
2477 |       en:
2478 |         greeting: Hello!
2479 |     `.trim();
2480 |       const expectedOutput = { greeting: "Hello!" };
2481 | 
2482 |       mockFileOperations(input);
2483 | 
2484 |       const yamlRootKeyLoader = createBucketLoader(
2485 |         "yaml-root-key",
2486 |         "i18n/[locale].yaml",
2487 |         {
2488 |           defaultLocale: "en",
2489 |         },
2490 |       );
2491 |       yamlRootKeyLoader.setDefaultLocale("en");
2492 |       const data = await yamlRootKeyLoader.pull("en");
2493 | 
2494 |       expect(data).toEqual(expectedOutput);
2495 |     });
2496 | 
2497 |     it("should save yaml-root-key", async () => {
2498 |       setupFileMocks();
2499 | 
2500 |       const input = `
2501 |       en:
2502 |         greeting: Hello!
2503 |     `.trim();
2504 |       const payload = { greeting: "¡Hola!" };
2505 |       const expectedOutput = `es:\n  greeting: ¡Hola!`;
2506 | 
2507 |       mockFileOperations(input);
2508 | 
2509 |       const yamlRootKeyLoader = createBucketLoader(
2510 |         "yaml-root-key",
2511 |         "i18n/[locale].yaml",
2512 |         {
2513 |           defaultLocale: "en",
2514 |         },
2515 |       );
2516 |       yamlRootKeyLoader.setDefaultLocale("en");
2517 |       await yamlRootKeyLoader.pull("en");
2518 | 
2519 |       await yamlRootKeyLoader.push("es", payload);
2520 | 
2521 |       expect(fs.writeFile).toHaveBeenCalledWith(
2522 |         "i18n/es.yaml",
2523 |         expectedOutput,
2524 |         { encoding: "utf-8", flag: "w" },
2525 |       );
2526 |     });
2527 |   });
2528 | 
2529 |   describe("vtt bucket loader", () => {
2530 |     it("should load complex vtt data", async () => {
2531 |       setupFileMocks();
2532 | 
2533 |       const input = `
2534 |   WEBVTT
2535 | 
2536 | 00:00:00.000 --> 00:00:01.000
2537 | Hello world!
2538 | 
2539 | 00:00:30.000 --> 00:00:31.000 align:start line:0%
2540 | This is a subtitle
2541 | 
2542 | 00:01:00.000 --> 00:01:01.000
2543 | Foo
2544 | 
2545 | 00:01:50.000 --> 00:01:51.000
2546 | Bar
2547 |       `.trim();
2548 | 
2549 |       const expectedOutput = {
2550 |         "0#0-1#": "Hello world!",
2551 |         "1#30-31#": "This is a subtitle",
2552 |         "2#60-61#": "Foo",
2553 |         "3#110-111#": "Bar",
2554 |       };
2555 | 
2556 |       mockFileOperations(input);
2557 | 
2558 |       const vttLoader = createBucketLoader("vtt", "i18n/[locale].vtt", {
2559 |         defaultLocale: "en",
2560 |       });
2561 |       vttLoader.setDefaultLocale("en");
2562 |       const data = await vttLoader.pull("en");
2563 | 
2564 |       expect(data).toEqual(expectedOutput);
2565 |     });
2566 | 
2567 |     it("should save complex vtt data", async () => {
2568 |       setupFileMocks();
2569 |       const input = `
2570 |   WEBVTT
2571 | 
2572 | 00:00:00.000 --> 00:00:01.000
2573 | Hello world!
2574 | 
2575 | 00:00:30.000 --> 00:00:31.000 align:start line:0%
2576 | This is a subtitle
2577 | 
2578 | 00:01:00.000 --> 00:01:01.000
2579 | Foo
2580 | 
2581 | 00:01:50.000 --> 00:01:51.000
2582 | Bar
2583 |       `.trim();
2584 | 
2585 |       const payload = {
2586 |         "0#0-1#": "¡Hola mundo!",
2587 |         "1#30-31#": "Este es un subtítulo",
2588 |         "2#60-61#": "Foo",
2589 |         "3#110-111#": "Bar",
2590 |       };
2591 | 
2592 |       const expectedOutput = `
2593 |   WEBVTT
2594 | 
2595 | 00:00:00.000 --> 00:00:01.000
2596 | ¡Hola mundo!
2597 | 
2598 | 00:00:30.000 --> 00:00:31.000
2599 | Este es un subtítulo
2600 | 
2601 | 00:01:00.000 --> 00:01:01.000
2602 | Foo
2603 | 
2604 | 00:01:50.000 --> 00:01:51.000
2605 | Bar`.trim();
2606 | 
2607 |       mockFileOperations(input);
2608 | 
2609 |       const vttLoader = createBucketLoader("vtt", "i18n/[locale].vtt", {
2610 |         defaultLocale: "en",
2611 |       });
2612 |       vttLoader.setDefaultLocale("en");
2613 |       await vttLoader.pull("en");
2614 | 
2615 |       await vttLoader.push("es", payload);
2616 | 
2617 |       expect(fs.writeFile).toHaveBeenCalledWith("i18n/es.vtt", expectedOutput, {
2618 |         encoding: "utf-8",
2619 |         flag: "w",
2620 |       });
2621 |     });
2622 | 
2623 |     it("should respect locked keys (pull)", async () => {
2624 |       setupFileMocks();
2625 |       const input = `
2626 | WEBVTT
2627 | 
2628 | 00:00:00.000 --> 00:00:01.000
2629 | Hello world!
2630 | 
2631 | 00:00:30.000 --> 00:00:31.000
2632 | Another cue
2633 |       `.trim();
2634 | 
2635 |       mockFileOperations(input);
2636 | 
2637 |       const vttLoader = createBucketLoader(
2638 |         "vtt",
2639 |         "i18n/[locale].vtt",
2640 |         {
2641 |           defaultLocale: "en",
2642 |         },
2643 |         ["0#*"],
2644 |       );
2645 |       vttLoader.setDefaultLocale("en");
2646 |       const data = await vttLoader.pull("en");
2647 | 
2648 |       // First cue (index 0) locked, so only second remains
2649 |       expect(Object.values(data)).toContain("Another cue");
2650 |       expect(Object.values(data)).not.toContain("Hello world!");
2651 |     });
2652 |   });
2653 | 
2654 |   describe("XML bucket loader", () => {
2655 |     it("should load XML data", async () => {
2656 |       setupFileMocks();
2657 | 
2658 |       const input = `<root>
2659 |     <title>Test XML</title>
2660 |     <date>2023-05-25</date>
2661 |     <content>
2662 |       <section>Introduction</section>
2663 |       <section>
2664 |         <text>
2665 |           Detailed text. 
2666 |         </text>
2667 |       </section>
2668 |     </content>
2669 |   </root>`;
2670 | 
2671 |       const expectedOutput = {
2672 |         "root/title": "Test XML",
2673 |         "root/content/section/0": "Introduction",
2674 |         "root/content/section/1/text": "Detailed text.",
2675 |       };
2676 | 
2677 |       mockFileOperations(input);
2678 | 
2679 |       const xmlLoader = createBucketLoader("xml", "i18n/[locale].xml", {
2680 |         defaultLocale: "en",
2681 |       });
2682 |       xmlLoader.setDefaultLocale("en");
2683 |       const data = await xmlLoader.pull("en");
2684 | 
2685 |       expect(data).toEqual(expectedOutput);
2686 |     });
2687 | 
2688 |     it("should save XML data", async () => {
2689 |       setupFileMocks();
2690 | 
2691 |       const input = `<root>
2692 |     <title>Test XML</title>
2693 |     <date>2023-05-25</date>
2694 |     <content>
2695 |       <section>Introduction</section>
2696 |       <section>
2697 |         <text>
2698 |           Detailed text.
2699 |         </text>
2700 |       </section>
2701 |     </content>
2702 |   </root>`;
2703 | 
2704 |       const payload = {
2705 |         "root/title": "Prueba XML",
2706 |         "root/date": "2023-05-25",
2707 |         "root/content/section/0": "Introducción",
2708 |         "root/content/section/1/text": "Detalles texto.",
2709 |       };
2710 | 
2711 |       let expectedOutput = `
2712 |       <root>
2713 |         <title>Prueba XML</title>
2714 |         <date>2023-05-25</date>
2715 |         <content>
2716 |           <section>Introducción</section>
2717 |           <section>
2718 |             <text>Detalles texto.</text>
2719 |           </section>
2720 |         </content>
2721 |       </root>`
2722 |         .replace(/\s+/g, " ")
2723 |         .replace(/>\s+</g, "><")
2724 |         .trim();
2725 |       mockFileOperations(input);
2726 |       const xmlLoader = createBucketLoader("xml", "i18n/[locale].xml", {
2727 |         defaultLocale: "en",
2728 |       });
2729 |       xmlLoader.setDefaultLocale("en");
2730 |       await xmlLoader.pull("en");
2731 | 
2732 |       await xmlLoader.push("es", payload);
2733 | 
2734 |       expect(fs.writeFile).toHaveBeenCalledWith("i18n/es.xml", expectedOutput, {
2735 |         encoding: "utf-8",
2736 |         flag: "w",
2737 |       });
2738 |     });
2739 | 
2740 |     it("should respect locked keys (pull)", async () => {
2741 |       setupFileMocks();
2742 |       const input = `<root><locked>Original</locked><hello>Hello!</hello></root>`;
2743 |       mockFileOperations(input);
2744 | 
2745 |       const xmlLoader = createBucketLoader(
2746 |         "xml",
2747 |         "i18n/[locale].xml",
2748 |         {
2749 |           defaultLocale: "en",
2750 |         },
2751 |         ["root/locked"],
2752 |       );
2753 |       xmlLoader.setDefaultLocale("en");
2754 |       const data = await xmlLoader.pull("en");
2755 | 
2756 |       expect(data).toEqual({ "root/hello": "Hello!" });
2757 |     });
2758 |   });
2759 | 
2760 |   describe("srt bucket loader", () => {
2761 |     it("should load srt", async () => {
2762 |       setupFileMocks();
2763 | 
2764 |       const input = `
2765 | 1
2766 | 00:00:00,000 --> 00:00:01,000
2767 | Hello!
2768 | 
2769 | 2
2770 | 00:00:01,000 --> 00:00:02,000
2771 | World!
2772 |       `.trim();
2773 |       const expectedOutput = {
2774 |         "1#00:00:00,000-00:00:01,000": "Hello!",
2775 |         "2#00:00:01,000-00:00:02,000": "World!",
2776 |       };
2777 | 
2778 |       mockFileOperations(input);
2779 | 
2780 |       const srtLoader = createBucketLoader("srt", "i18n/[locale].srt", {
2781 |         defaultLocale: "en",
2782 |       });
2783 |       srtLoader.setDefaultLocale("en");
2784 |       const data = await srtLoader.pull("en");
2785 | 
2786 |       expect(data).toEqual(expectedOutput);
2787 |     });
2788 | 
2789 |     it("should save srt", async () => {
2790 |       setupFileMocks();
2791 | 
2792 |       const input = `
2793 | 1
2794 | 00:00:00,000 --> 00:00:01,000
2795 | Hello!
2796 | 
2797 | 2
2798 | 00:00:01,000 --> 00:00:02,000
2799 | World!
2800 |   `.trim();
2801 | 
2802 |       const payload = {
2803 |         "1#00:00:00,000-00:00:01,000": "¡Hola!",
2804 |         "2#00:00:01,000-00:00:02,000": "Mundo!",
2805 |       };
2806 | 
2807 |       const expectedOutput = `1
2808 | 00:00:00,000 --> 00:00:01,000
2809 | ¡Hola!
2810 | 
2811 | 2
2812 | 00:00:01,000 --> 00:00:02,000
2813 | Mundo!`;
2814 | 
2815 |       mockFileOperations(input);
2816 | 
2817 |       const srtLoader = createBucketLoader("srt", "i18n/[locale].srt", {
2818 |         defaultLocale: "en",
2819 |       });
2820 |       srtLoader.setDefaultLocale("en");
2821 |       await srtLoader.pull("en");
2822 | 
2823 |       await srtLoader.push("es", payload);
2824 | 
2825 |       expect(fs.writeFile).toHaveBeenCalledWith("i18n/es.srt", expectedOutput, {
2826 |         encoding: "utf-8",
2827 |         flag: "w",
2828 |       });
2829 |     });
2830 | 
2831 |     it("should respect locked keys (pull)", async () => {
2832 |       setupFileMocks();
2833 |       const input = `
2834 | 1
2835 | 00:00:00,000 --> 00:00:01,000
2836 | Hello!
2837 | 
2838 | 2
2839 | 00:00:01,000 --> 00:00:02,000
2840 | World!
2841 |       `.trim();
2842 | 
2843 |       mockFileOperations(input);
2844 | 
2845 |       const srtLoader = createBucketLoader(
2846 |         "srt",
2847 |         "i18n/[locale].srt",
2848 |         {
2849 |           defaultLocale: "en",
2850 |         },
2851 |         ["1#00:00:00,000-00:00:01,000"],
2852 |       );
2853 |       srtLoader.setDefaultLocale("en");
2854 |       const data = await srtLoader.pull("en");
2855 | 
2856 |       expect(data).toEqual({ "2#00:00:01,000-00:00:02,000": "World!" });
2857 |     });
2858 |   });
2859 | 
2860 |   describe("xliff bucket loader", () => {
2861 |     it("should load xliff data", async () => {
2862 |       setupFileMocks();
2863 | 
2864 |       const input = `
2865 |   <xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en-US">
2866 |     <file id="namespace1">
2867 |       <unit id="key1">
2868 |         <segment>
2869 |           <source>Hello</source>
2870 |         </segment>
2871 |       </unit>
2872 |       <unit id="key2">
2873 |         <segment>
2874 |           <source>An application to manipulate and process XLIFF documents</source>
2875 |         </segment>
2876 |       </unit>
2877 |       <unit id="key.nested">
2878 |         <segment>
2879 |           <source>XLIFF Data Manager</source>
2880 |         </segment>
2881 |       </unit>
2882 |       <group id="group">
2883 |         <unit id="groupUnit">
2884 |           <segment>
2885 |             <source>Group</source>
2886 |           </segment>
2887 |         </unit>
2888 |       </group>
2889 |     </file>
2890 |   </xliff>
2891 |       `.trim();
2892 | 
2893 |       // Keys must be encoded (e.g. / replaced with %2F)
2894 |       const expectedOutput = {
2895 |         "resources%2Fnamespace1%2Fgroup%2FgroupUnits%2FgroupUnit%2Fsource":
2896 |           "Group",
2897 |         "resources%2Fnamespace1%2Fkey.nested%2Fsource": "XLIFF Data Manager",
2898 |         "resources%2Fnamespace1%2Fkey1%2Fsource": "Hello",
2899 |         "resources%2Fnamespace1%2Fkey2%2Fsource":
2900 |           "An application to manipulate and process XLIFF documents",
2901 |         sourceLanguage: "en-US",
2902 |       };
2903 | 
2904 |       mockFileOperations(input);
2905 | 
2906 |       const xliffLoader = createBucketLoader("xliff", "i18n/[locale].xliff", {
2907 |         defaultLocale: "en",
2908 |       });
2909 |       xliffLoader.setDefaultLocale("en");
2910 |       const data = await xliffLoader.pull("en");
2911 | 
2912 |       expect(data).toEqual(expectedOutput);
2913 |     });
2914 | 
2915 |     it("should save xliff data", async () => {
2916 |       setupFileMocks();
2917 | 
2918 |       const input = `
2919 |     <xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en-US">
2920 |       <file id="namespace1">
2921 |         <unit id="key1">
2922 |           <segment>
2923 |             <source>Hello</source>
2924 |           </segment>
2925 |         </unit>
2926 |         <unit id="key2">
2927 |           <segment>
2928 |             <source>An application to manipulate and process XLIFF documents</source>
2929 |           </segment>
2930 |         </unit>
2931 |         <unit id="key.nested">
2932 |           <segment>
2933 |             <source>XLIFF Data Manager</source>
2934 |           </segment>
2935 |         </unit>
2936 |         <group id="group">
2937 |           <unit id="groupUnit">
2938 |             <segment>
2939 |               <source>Group</source>
2940 |             </segment>
2941 |           </unit>
2942 |         </group>
2943 |       </file>
2944 |     </xliff>
2945 |         `.trim();
2946 |       // Keys must be encoded (e.g. / replaced with %2F)
2947 |       const payload = {
2948 |         "resources%2Fnamespace1%2Fgroup%2FgroupUnits%2FgroupUnit%2Fsource":
2949 |           "Grupo",
2950 |         "resources%2Fnamespace1%2Fkey.nested%2Fsource":
2951 |           "Administrador de Datos XLIFF",
2952 |         "resources%2Fnamespace1%2Fkey1%2Fsource": "Hola",
2953 |         "resources%2Fnamespace1%2Fkey2%2Fsource":
2954 |           "Una aplicación para manipular y procesar documentos XLIFF",
2955 |         sourceLanguage: "es-ES",
2956 |       };
2957 | 
2958 |       const expectedOutput = `
2959 | <xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="es-ES">
2960 |   <file id="namespace1">
2961 |     <unit id="key1">
2962 |       <segment>
2963 |         <source>Hola</source>
2964 |       </segment>
2965 |     </unit>
2966 |     <unit id="key2">
2967 |       <segment>
2968 |         <source>Una aplicación para manipular y procesar documentos XLIFF</source>
2969 |       </segment>
2970 |     </unit>
2971 |     <unit id="key.nested">
2972 |       <segment>
2973 |         <source>Administrador de Datos XLIFF</source>
2974 |       </segment>
2975 |     </unit>
2976 |     <group id="group">
2977 |       <unit id="groupUnit">
2978 |         <segment>
2979 |           <source>Grupo</source>
2980 |         </segment>
2981 |       </unit>
2982 |     </group>
2983 |   </file>
2984 | </xliff>`.trim();
2985 | 
2986 |       mockFileOperations(input);
2987 | 
2988 |       const xliffLoader = createBucketLoader("xliff", "i18n/[locale].xlf", {
2989 |         defaultLocale: "en",
2990 |       });
2991 |       xliffLoader.setDefaultLocale("en");
2992 |       await xliffLoader.pull("en");
2993 | 
2994 |       await xliffLoader.push("es", payload);
2995 | 
2996 |       expect(fs.writeFile).toHaveBeenCalledWith("i18n/es.xlf", expectedOutput, {
2997 |         encoding: "utf-8",
2998 |         flag: "w",
2999 |       });
3000 |     });
3001 | 
3002 |     it("should respect locked keys (pull)", async () => {
3003 |       setupFileMocks();
3004 | 
3005 |       const input = `
3006 |   <xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en-US">
3007 |     <file id="namespace1">
3008 |       <unit id="locked">
3009 |         <segment>
3010 |           <source>Original</source>
3011 |         </segment>
3012 |       </unit>
3013 |       <unit id="key1">
3014 |         <segment>
3015 |           <source>Hello</source>
3016 |         </segment>
3017 |       </unit>
3018 |     </file>
3019 |   </xliff>
3020 |       `.trim();
3021 | 
3022 |       mockFileOperations(input);
3023 | 
3024 |       const xliffLoader = createBucketLoader(
3025 |         "xliff",
3026 |         "i18n/[locale].xliff",
3027 |         {
3028 |           defaultLocale: "en",
3029 |         },
3030 |         ["resources%2Fnamespace1%2Flocked%2Fsource"],
3031 |       );
3032 |       xliffLoader.setDefaultLocale("en");
3033 |       const data = await xliffLoader.pull("en");
3034 | 
3035 |       expect(data).toEqual({
3036 |         "resources%2Fnamespace1%2Fkey1%2Fsource": "Hello",
3037 |         sourceLanguage: "en-US",
3038 |       });
3039 |     });
3040 |   });
3041 | 
3042 |   describe("text-file", () => {
3043 |     describe("when there is no target locale file", () => {
3044 |       it("should preserve trailing new line based on the source locale", async () => {
3045 |         setupFileMocks();
3046 | 
3047 |         const input = "Hello\n";
3048 |         const expectedOutput = "Hola\n";
3049 | 
3050 |         mockFileOperationsForPaths({
3051 |           "i18n/en.txt": input,
3052 |           "i18n/es.txt": "",
3053 |         });
3054 | 
3055 |         const textFileLoader = createTextFileLoader("i18n/[locale].txt");
3056 |         textFileLoader.setDefaultLocale("en");
3057 |         await textFileLoader.pull("en");
3058 | 
3059 |         await textFileLoader.push("es", "Hola");
3060 | 
3061 |         expect(fs.writeFile).toHaveBeenCalledWith(
3062 |           "i18n/es.txt",
3063 |           expectedOutput,
3064 |           { encoding: "utf-8", flag: "w" },
3065 |         );
3066 |       });
3067 | 
3068 |       it("should not add trailing new line based on the source locale", async () => {
3069 |         setupFileMocks();
3070 | 
3071 |         const input = "Hello";
3072 |         const expectedOutput = "Hola";
3073 | 
3074 |         mockFileOperationsForPaths({
3075 |           "i18n/en.txt": input,
3076 |           "i18n/es.txt": "",
3077 |         });
3078 | 
3079 |         const textFileLoader = createTextFileLoader("i18n/[locale].txt");
3080 |         textFileLoader.setDefaultLocale("en");
3081 |         await textFileLoader.pull("en");
3082 | 
3083 |         await textFileLoader.push("es", "Hola");
3084 | 
3085 |         expect(fs.writeFile).toHaveBeenCalledWith(
3086 |           "i18n/es.txt",
3087 |           expectedOutput,
3088 |           { encoding: "utf-8", flag: "w" },
3089 |         );
3090 |       });
3091 |     });
3092 | 
3093 |     describe("when there is a target locale file", () => {
3094 |       it("should preserve trailing new lines based on the target locale", async () => {
3095 |         setupFileMocks();
3096 | 
3097 |         const input = "Hello";
3098 |         const targetInput = "Hola\n";
3099 |         const expectedOutput = "Hola (translated)\n";
3100 | 
3101 |         mockFileOperationsForPaths({
3102 |           "i18n/en.txt": input,
3103 |           "i18n/es.txt": targetInput,
3104 |         });
3105 | 
3106 |         const textFileLoader = createTextFileLoader("i18n/[locale].txt");
3107 |         textFileLoader.setDefaultLocale("en");
3108 |         await textFileLoader.pull("en");
3109 | 
3110 |         await textFileLoader.push("es", "Hola (translated)");
3111 | 
3112 |         expect(fs.writeFile).toHaveBeenCalledWith(
3113 |           "i18n/es.txt",
3114 |           expectedOutput,
3115 |           { encoding: "utf-8", flag: "w" },
3116 |         );
3117 |       });
3118 | 
3119 |       it("should not add trailing new line based on the target locale", async () => {
3120 |         setupFileMocks();
3121 | 
3122 |         const input = "Hello\n";
3123 |         const targetInput = "Hola";
3124 |         const expectedOutput = "Hola (translated)";
3125 | 
3126 |         mockFileOperationsForPaths({
3127 |           "i18n/en.txt": input,
3128 |           "i18n/es.txt": targetInput,
3129 |         });
3130 | 
3131 |         const textFileLoader = createTextFileLoader("i18n/[locale].txt");
3132 |         textFileLoader.setDefaultLocale("en");
3133 |         await textFileLoader.pull("en");
3134 | 
3135 |         await textFileLoader.push("es", "Hola (translated)");
3136 | 
3137 |         expect(fs.writeFile).toHaveBeenCalledWith(
3138 |           "i18n/es.txt",
3139 |           expectedOutput,
3140 |           { encoding: "utf-8", flag: "w" },
3141 |         );
3142 |       });
3143 |     });
3144 |   });
3145 | 
3146 |   describe("php bucket loader", () => {
3147 |     it("should load php array", async () => {
3148 |       setupFileMocks();
3149 | 
3150 |       const input = `<?php return ['button.title' => 'Submit'];`;
3151 |       const expectedOutput = { "button.title": "Submit" };
3152 | 
3153 |       mockFileOperations(input);
3154 | 
3155 |       const phpLoader = createBucketLoader("php", "i18n/[locale].php", {
3156 |         defaultLocale: "en",
3157 |       });
3158 |       phpLoader.setDefaultLocale("en");
3159 |       const data = await phpLoader.pull("en");
3160 | 
3161 |       expect(data).toEqual(expectedOutput);
3162 |     });
3163 | 
3164 |     it("should save php array", async () => {
3165 |       setupFileMocks();
3166 | 
3167 |       const input = `<?php 
3168 | // this is locale
3169 | 
3170 | return array(
3171 |   'button.title' => 'Submit',
3172 |   'button.description' => ['Hello', 'Goodbye'],
3173 |   'button.index' => 1,
3174 |   'button.class' => null,
3175 | );`;
3176 |       const expectedOutput = `<?php 
3177 | // this is locale
3178 | 
3179 | return array(
3180 |   'button.title' => 'Enviar',
3181 |   'button.description' => array(
3182 |     'Hola',
3183 |     'Adiós'
3184 |   ),
3185 |   'button.index' => 1,
3186 |   'button.class' => null
3187 | );`;
3188 | 
3189 |       mockFileOperations(input);
3190 | 
3191 |       const phpLoader = createBucketLoader("php", "i18n/[locale].php", {
3192 |         defaultLocale: "en",
3193 |       });
3194 |       phpLoader.setDefaultLocale("en");
3195 |       await phpLoader.pull("en");
3196 | 
3197 |       await phpLoader.push("es", {
3198 |         "button.title": "Enviar",
3199 |         "button.description/0": "Hola",
3200 |         "button.description/1": "Adiós",
3201 |       });
3202 | 
3203 |       expect(fs.writeFile).toHaveBeenCalledWith("i18n/es.php", expectedOutput, {
3204 |         encoding: "utf-8",
3205 |         flag: "w",
3206 |       });
3207 |     });
3208 | 
3209 |     it("should respect locked keys (pull)", async () => {
3210 |       setupFileMocks();
3211 | 
3212 |       const input = `<?php\n\nreturn [\n  'locked' => 'Original',\n  'hello' => 'Hello'\n];`;
3213 | 
3214 |       mockFileOperations(input);
3215 | 
3216 |       const phpLoader = createBucketLoader(
3217 |         "php",
3218 |         "i18n/[locale].php",
3219 |         {
3220 |           defaultLocale: "en",
3221 |         },
3222 |         ["locked"],
3223 |       );
3224 |       phpLoader.setDefaultLocale("en");
3225 |       const data = await phpLoader.pull("en");
3226 | 
3227 |       expect(data).toEqual({ hello: "Hello" });
3228 |     });
3229 |   });
3230 | 
3231 |   describe("po bucket loader", () => {
3232 |     it("should load po file", async () => {
3233 |       setupFileMocks();
3234 | 
3235 |       const input = `msgid "Hello"\nmsgstr "Hello"`;
3236 |       const expectedOutput = { "Hello/singular": "Hello" };
3237 | 
3238 |       mockFileOperations(input);
3239 | 
3240 |       const poLoader = createBucketLoader("po", "i18n/[locale].po", {
3241 |         defaultLocale: "en",
3242 |       });
3243 |       poLoader.setDefaultLocale("en");
3244 |       const data = await poLoader.pull("en");
3245 | 
3246 |       expect(data).toEqual(expectedOutput);
3247 |     });
3248 | 
3249 |     it("should save po file", async () => {
3250 |       setupFileMocks();
3251 | 
3252 |       const input = `msgid "Hello"\nmsgstr "Hello"`;
3253 |       const expectedOutput = `msgid "Hello"\nmsgstr "Hola"`;
3254 | 
3255 |       mockFileOperations(input);
3256 | 
3257 |       const poLoader = createBucketLoader("po", "i18n/[locale].po", {
3258 |         defaultLocale: "en",
3259 |       });
3260 |       poLoader.setDefaultLocale("en");
3261 |       await poLoader.pull("en");
3262 | 
3263 |       await poLoader.push("es", {
3264 |         "Hello/singular": "Hola",
3265 |       });
3266 | 
3267 |       expect(fs.writeFile).toHaveBeenCalledWith("i18n/es.po", expectedOutput, {
3268 |         encoding: "utf-8",
3269 |         flag: "w",
3270 |       });
3271 |     });
3272 | 
3273 |     it("should extract and restore variables", async () => {
3274 |       setupFileMocks();
3275 | 
3276 |       const input = `msgid "You have %(count)d items"\nmsgstr "You have %(count)d items"\n\n#~ msgid "I am obsolete"\n#~ msgstr "I am obsolete"`;
3277 | 
3278 |       const expectedPullOutput = {
3279 |         "You%20have%20%25(count)d%20items/singular":
3280 |           "You have {variable:0} items",
3281 |       };
3282 | 
3283 |       mockFileOperations(input);
3284 | 
3285 |       const poLoader = createBucketLoader("po", "i18n/[locale].po", {
3286 |         defaultLocale: "en",
3287 |       });
3288 |       poLoader.setDefaultLocale("en");
3289 | 
3290 |       const data = await poLoader.pull("en");
3291 | 
3292 |       expect(data).toEqual(expectedPullOutput);
3293 | 
3294 |       const payload = {
3295 |         "You%20have%20%25(count)d%20items/singular":
3296 |           "Sie haben {variable:0} Elemente",
3297 |       };
3298 | 
3299 |       await poLoader.push("de", payload);
3300 | 
3301 |       expect(fs.writeFile).toHaveBeenCalledWith(
3302 |         "i18n/de.po",
3303 |         `msgid "You have %(count)d items"\nmsgstr "Sie haben %(count)d Elemente"`,
3304 |         { encoding: "utf-8", flag: "w" },
3305 |       );
3306 |     });
3307 | 
3308 |     it("should respect locked keys (pull)", async () => {
3309 |       setupFileMocks();
3310 |       const input = `# Comment\n\nmsgid "greeting"\nmsgstr "Hello"\n\nmsgid "farewell"\nmsgstr "Bye"`;
3311 |       const payload = input; // same for mocking
3312 | 
3313 |       mockFileOperations(payload);
3314 | 
3315 |       const poLoader = createBucketLoader(
3316 |         "po",
3317 |         "i18n/[locale].po",
3318 |         {
3319 |           defaultLocale: "en",
3320 |         },
3321 |         ["greeting/singular"],
3322 |       );
3323 |       poLoader.setDefaultLocale("en");
3324 |       const data = await poLoader.pull("en");
3325 | 
3326 |       // Only farewell remains (po loader returns structured values, flattened to keys)
3327 |       expect(Object.keys(data)).toContain("farewell/singular");
3328 |       expect(Object.keys(data)).not.toContain("greeting/singular");
3329 |     });
3330 |   });
3331 | 
3332 |   describe("vue-json bucket loader", () => {
3333 |     const template = `<template>
3334 |   <div id="app">
3335 |     <label for="locale">locale</label>
3336 |     <select v-model="locale">
3337 |       <option>en</option>
3338 |       <option>ja</option>
3339 |     </select>
3340 |     <p>message: {{ $t('hello') }}</p>
3341 |   </div>
3342 | </template>`;
3343 |     const script = `<script>
3344 | export default {
3345 |   name: 'app',
3346 |   data () {
3347 |     this.$i18n.locale = 'en';
3348 |     return { locale: 'en' }
3349 |   },
3350 |   watch: {
3351 |     locale (val) {
3352 |       this.$i18n.locale = val
3353 |     }
3354 |   }
3355 | }
3356 | </script>`;
3357 | 
3358 |     it("should load vue-json file", async () => {
3359 |       setupFileMocks();
3360 | 
3361 |       const input = `${template}
3362 | 
3363 | <i18n>
3364 | {
3365 |   "en": {
3366 |     "hello": "hello world!"
3367 |   }
3368 | }
3369 | </i18n>
3370 | 
3371 | ${script}`;
3372 |       const expectedOutput = { hello: "hello world!" };
3373 | 
3374 |       mockFileOperations(input);
3375 | 
3376 |       const vueLoader = createBucketLoader("vue-json", "i18n/[locale].vue", {
3377 |         defaultLocale: "en",
3378 |       });
3379 |       vueLoader.setDefaultLocale("en");
3380 |       const data = await vueLoader.pull("en");
3381 | 
3382 |       expect(data).toEqual(expectedOutput);
3383 |     });
3384 | 
3385 |     it("should save vue-json file", async () => {
3386 |       setupFileMocks();
3387 | 
3388 |       const input = `${template}
3389 | 
3390 | <i18n>
3391 | {
3392 |   "en": {
3393 |     "hello": "hello world!"
3394 |   }
3395 | }
3396 | </i18n>
3397 | 
3398 | ${script}`;
3399 |       const expectedOutput = `${template}
3400 | 
3401 | <i18n>
3402 | {
3403 |   "en": {
3404 |     "hello": "hello world!"
3405 |   },
3406 |   "es": {
3407 |     "hello": "hola mundo!"
3408 |   }
3409 | }
3410 | </i18n>
3411 | 
3412 | ${script}`;
3413 | 
3414 |       mockFileOperations(input);
3415 | 
3416 |       const vueLoader = createBucketLoader("vue-json", "i18n/App.vue", {
3417 |         defaultLocale: "en",
3418 |       });
3419 |       vueLoader.setDefaultLocale("en");
3420 |       await vueLoader.pull("en");
3421 | 
3422 |       await vueLoader.push("es", {
3423 |         hello: "hola mundo!",
3424 |       });
3425 | 
3426 |       expect(fs.writeFile).toHaveBeenCalledWith(
3427 |         "i18n/App.vue",
3428 |         expectedOutput,
3429 |         { encoding: "utf-8", flag: "w" },
3430 |       );
3431 |     });
3432 | 
3433 |     it("should ignore vue file without i18n tag", async () => {
3434 |       setupFileMocks();
3435 | 
3436 |       const input = `${template}
3437 | 
3438 | ${script}`;
3439 |       const expectedOutput = `${template}
3440 | 
3441 | ${script}`;
3442 | 
3443 |       mockFileOperations(input);
3444 | 
3445 |       const vueLoader = createBucketLoader("vue-json", "i18n/App.vue", {
3446 |         defaultLocale: "en",
3447 |       });
3448 |       vueLoader.setDefaultLocale("en");
3449 |       await vueLoader.pull("en");
3450 | 
3451 |       await vueLoader.push("es", {
3452 |         hello: "hola mundo!",
3453 |       });
3454 | 
3455 |       expect(fs.writeFile).toHaveBeenCalledWith(
3456 |         "i18n/App.vue",
3457 |         expectedOutput,
3458 |         { encoding: "utf-8", flag: "w" },
3459 |       );
3460 |     });
3461 | 
3462 |     it("should respect locked keys (pull)", async () => {
3463 |       setupFileMocks();
3464 | 
3465 |       const input = `${template}
3466 | 
3467 | <i18n>
3468 | {
3469 |   "en": {
3470 |     "locked": "Original",
3471 |     "hello": "Hello!"
3472 |   }
3473 | }
3474 | </i18n>
3475 | 
3476 | ${script}`;
3477 | 
3478 |       mockFileOperations(input);
3479 | 
3480 |       const vueLoader = createBucketLoader(
3481 |         "vue-json",
3482 |         "i18n/[locale].vue",
3483 |         {
3484 |           defaultLocale: "en",
3485 |         },
3486 |         ["locked"],
3487 |       );
3488 |       vueLoader.setDefaultLocale("en");
3489 |       const data = await vueLoader.pull("en");
3490 | 
3491 |       expect(data).toEqual({ hello: "Hello!" });
3492 |     });
3493 |   });
3494 |   describe("ejs bucket loader", () => {
3495 |     it("should load ejs data", async () => {
3496 |       setupFileMocks();
3497 | 
3498 |       const input = `<!DOCTYPE html>
3499 | <html>
3500 | <head>
3501 |   <title>Welcome Page</title>
3502 | </head>
3503 | <body>
3504 |   <h1>Hello <%= user.name %>!</h1>
3505 |   <% if (user.isLoggedIn) { %>
3506 |     <p>Welcome back to our application.</p>
3507 |     <p>You have <%= notifications.length %> new notifications.</p>
3508 |   <% } else { %>
3509 |     <p>Please log in to continue.</p>
3510 |   <% } %>
3511 |   <ul>
3512 |     <% items.forEach(function(item, index) { %>
3513 |       <li>Item <%= index + 1 %>: <%= item.title %></li>
3514 |     <% }); %>
3515 |   </ul>
3516 |   <footer>© 2024 My Company. All rights reserved.</footer>
3517 | </body>
3518 | </html>`;
3519 | 
3520 |       const expectedOutput = {
3521 |         text_0: "Welcome Page",
3522 |         text_1: "Hello",
3523 |         text_2: "!",
3524 |         text_3: "Welcome back to our application.",
3525 |         text_4: "You have",
3526 |         text_5: "new notifications.",
3527 |         text_6: "Please log in to continue.",
3528 |         text_7: "Item",
3529 |         text_8: ":",
3530 |         text_9: "© 2024 My Company. All rights reserved.",
3531 |       };
3532 | 
3533 |       mockFileOperations(input);
3534 | 
3535 |       const ejsLoader = createBucketLoader("ejs", "templates/[locale].ejs", {
3536 |         defaultLocale: "en",
3537 |       });
3538 |       ejsLoader.setDefaultLocale("en");
3539 |       const data = await ejsLoader.pull("en");
3540 | 
3541 |       expect(data).toEqual(expectedOutput);
3542 |     });
3543 | 
3544 |     it("should save ejs data", async () => {
3545 |       setupFileMocks();
3546 | 
3547 |       const input = `<!DOCTYPE html>
3548 | <html>
3549 | <head>
3550 |   <title>Welcome Page</title>
3551 | </head>
3552 | <body>
3553 |   <h1>Hello <%= user.name %>!</h1>
3554 |   <p>Welcome to our application.</p>
3555 |   <footer>© 2024 My Company. All rights reserved.</footer>
3556 | </body>
3557 | </html>`;
3558 | 
3559 |       const payload = {
3560 |         text_0: "Página de Bienvenida",
3561 |         text_1: "Hola",
3562 |         text_2: "!",
3563 |         text_3: "Bienvenido a nuestra aplicación.",
3564 |         text_4: "© 2024 Mi Empresa. Todos los derechos reservados.",
3565 |       };
3566 | 
3567 |       const expectedOutput = `<!DOCTYPE html>
3568 | <html>
3569 | <head>
3570 |   <title>Página de Bienvenida</title>
3571 | </head>
3572 | <body>
3573 |   <h1>Hola <%= user.name %>!</h1>
3574 |   <p>Bienvenido a nuestra aplicación.</p>
3575 |   <footer>© 2024 Mi Empresa. Todos los derechos reservados.</footer>
3576 | </body>
3577 | </html>`;
3578 | 
3579 |       mockFileOperations(input);
3580 | 
3581 |       const ejsLoader = createBucketLoader("ejs", "templates/[locale].ejs", {
3582 |         defaultLocale: "en",
3583 |       });
3584 |       ejsLoader.setDefaultLocale("en");
3585 |       await ejsLoader.pull("en");
3586 | 
3587 |       await ejsLoader.push("es", payload);
3588 | 
3589 |       expect(fs.writeFile).toHaveBeenCalledWith(
3590 |         "templates/es.ejs",
3591 |         expectedOutput,
3592 |         { encoding: "utf-8", flag: "w" },
3593 |       );
3594 |     });
3595 | 
3596 |     it("should respect locked keys (pull)", async () => {
3597 |       setupFileMocks();
3598 | 
3599 |       const input = `<!DOCTYPE html>
3600 | <html>
3601 | <head>
3602 |   <title>Welcome Page</title>
3603 | </head>
3604 | <body>
3605 |   <h1>Hello <%= user.name %>!</h1>
3606 |   <p>Welcome to our application.</p>
3607 | </body>
3608 | </html>`;
3609 | 
3610 |       mockFileOperations(input);
3611 | 
3612 |       const ejsLoader = createBucketLoader(
3613 |         "ejs",
3614 |         "templates/[locale].ejs",
3615 |         {
3616 |           defaultLocale: "en",
3617 |         },
3618 |         ["text_0"],
3619 |       );
3620 |       ejsLoader.setDefaultLocale("en");
3621 |       const data = await ejsLoader.pull("en");
3622 | 
3623 |       // text_0 (title) is locked; remaining translatables present
3624 |       expect(Object.keys(data)).not.toContain("text_0");
3625 |       expect(Object.keys(data)).toContain("text_1");
3626 |     });
3627 |   });
3628 | 
3629 |   describe("txt bucket loader", () => {
3630 |     it("should load txt", async () => {
3631 |       setupFileMocks();
3632 | 
3633 |       const input = `Welcome to our application!
3634 | This is a sample text file for fastlane metadata.
3635 | It contains app description that needs to be translated.`;
3636 | 
3637 |       const expectedOutput = {
3638 |         "1": "Welcome to our application!",
3639 |         "2": "This is a sample text file for fastlane metadata.",
3640 |         "3": "It contains app description that needs to be translated.",
3641 |       };
3642 | 
3643 |       mockFileOperations(input);
3644 | 
3645 |       const txtLoader = createBucketLoader(
3646 |         "txt",
3647 |         "fastlane/metadata/[locale]/description.txt",
3648 |         {
3649 |           defaultLocale: "en",
3650 |         },
3651 |       );
3652 |       txtLoader.setDefaultLocale("en");
3653 |       const data = await txtLoader.pull("en");
3654 | 
3655 |       expect(data).toEqual(expectedOutput);
3656 |     });
3657 | 
3658 |     it("should save txt", async () => {
3659 |       setupFileMocks();
3660 | 
3661 |       const input = `Welcome to our application!
3662 | This is a sample text file for fastlane metadata.
3663 | It contains app description that needs to be translated.`;
3664 | 
3665 |       const payload = {
3666 |         "1": "¡Bienvenido a nuestra aplicación!",
3667 |         "2": "Este es un archivo de texto de muestra para metadatos de fastlane.",
3668 |         "3": "Contiene la descripción de la aplicación que necesita ser traducida.",
3669 |       };
3670 | 
3671 |       const expectedOutput = `¡Bienvenido a nuestra aplicación!
3672 | Este es un archivo de texto de muestra para metadatos de fastlane.
3673 | Contiene la descripción de la aplicación que necesita ser traducida.`;
3674 | 
3675 |       mockFileOperations(input);
3676 | 
3677 |       const txtLoader = createBucketLoader(
3678 |         "txt",
3679 |         "fastlane/metadata/[locale]/description.txt",
3680 |         {
3681 |           defaultLocale: "en",
3682 |         },
3683 |       );
3684 |       txtLoader.setDefaultLocale("en");
3685 |       await txtLoader.pull("en");
3686 | 
3687 |       await txtLoader.push("es", payload);
3688 | 
3689 |       expect(fs.writeFile).toHaveBeenCalledWith(
3690 |         "fastlane/metadata/es/description.txt",
3691 |         expectedOutput,
3692 |         { encoding: "utf-8", flag: "w" },
3693 |       );
3694 |     });
3695 | 
3696 |     it("should handle empty txt files", async () => {
3697 |       setupFileMocks();
3698 | 
3699 |       const input = "";
3700 |       const expectedOutput = {};
3701 | 
3702 |       mockFileOperations(input);
3703 | 
3704 |       const txtLoader = createBucketLoader(
3705 |         "txt",
3706 |         "fastlane/metadata/[locale]/description.txt",
3707 |         {
3708 |           defaultLocale: "en",
3709 |         },
3710 |       );
3711 |       txtLoader.setDefaultLocale("en");
3712 |       const data = await txtLoader.pull("en");
3713 | 
3714 |       expect(data).toEqual(expectedOutput);
3715 |     });
3716 | 
3717 |     it("should filter out empty lines during pull", async () => {
3718 |       setupFileMocks();
3719 | 
3720 |       const input = `Line 1
3721 | 
3722 | Line 3`;
3723 |       const expectedOutput = {
3724 |         "1": "Line 1",
3725 |         "3": "Line 3",
3726 |       };
3727 | 
3728 |       mockFileOperations(input);
3729 | 
3730 |       const txtLoader = createBucketLoader(
3731 |         "txt",
3732 |         "fastlane/metadata/[locale]/description.txt",
3733 |         {
3734 |           defaultLocale: "en",
3735 |         },
3736 |       );
3737 |       txtLoader.setDefaultLocale("en");
3738 |       const data = await txtLoader.pull("en");
3739 | 
3740 |       expect(data).toEqual(expectedOutput);
3741 |     });
3742 | 
3743 |     it("should reconstruct file with empty lines restored", async () => {
3744 |       setupFileMocks();
3745 | 
3746 |       const input = `Line 1
3747 | 
3748 | Line 3`;
3749 | 
3750 |       const payload = {
3751 |         "1": "Línea 1",
3752 |         "3": "Línea 3",
3753 |       };
3754 | 
3755 |       const expectedOutput = `Línea 1
3756 | 
3757 | Línea 3`;
3758 | 
3759 |       mockFileOperations(input);
3760 | 
3761 |       const txtLoader = createBucketLoader(
3762 |         "txt",
3763 |         "fastlane/metadata/[locale]/description.txt",
3764 |         {
3765 |           defaultLocale: "en",
3766 |         },
3767 |       );
3768 |       txtLoader.setDefaultLocale("en");
3769 |       await txtLoader.pull("en");
3770 | 
3771 |       await txtLoader.push("es", payload);
3772 | 
3773 |       expect(fs.writeFile).toHaveBeenCalledWith(
3774 |         "fastlane/metadata/es/description.txt",
3775 |         expectedOutput,
3776 |         { encoding: "utf-8", flag: "w" },
3777 |       );
3778 |     });
3779 | 
3780 |     it("should respect locked keys (pull)", async () => {
3781 |       setupFileMocks();
3782 | 
3783 |       const input = `Secret\nHello`;
3784 |       mockFileOperations(input);
3785 | 
3786 |       const txtLoader = createBucketLoader(
3787 |         "txt",
3788 |         "fastlane/metadata/[locale]/description.txt",
3789 |         { defaultLocale: "en" },
3790 |         ["1"],
3791 |       );
3792 |       txtLoader.setDefaultLocale("en");
3793 |       const data = await txtLoader.pull("en");
3794 | 
3795 |       expect(data).toEqual({ 2: "Hello" } as any);
3796 |     });
3797 |   });
3798 | 
3799 |   describe("json-dictionary bucket loader", () => {
3800 |     it("should add target locale keys only where source locale keys exist", async () => {
3801 |       setupFileMocks();
3802 |       const input = {
3803 |         title: { en: "I am a title" },
3804 |         logoPosition: "right",
3805 |         pages: [
3806 |           {
3807 |             name: "Welcome to my world",
3808 |             elements: [
3809 |               {
3810 |                 title: { en: "I am an element title" },
3811 |                 description: { en: "I am an element description" },
3812 |               },
3813 |             ],
3814 |           },
3815 |         ],
3816 |       };
3817 |       mockFileOperations(JSON.stringify(input));
3818 |       const loader = createBucketLoader(
3819 |         "json-dictionary",
3820 |         "i18n/[locale].json",
3821 |         {
3822 |           defaultLocale: "en",
3823 |         },
3824 |       );
3825 |       loader.setDefaultLocale("en");
3826 |       await loader.pull("en");
3827 |       await loader.push("es", {
3828 |         title: "Yo soy un titulo",
3829 |         "pages/0/elements/0/title": "Yo soy un elemento de titulo",
3830 |         "pages/0/elements/0/description": "Yo soy una descripcion de elemento",
3831 |       });
3832 |       const expectedOutput = `{
3833 |   "title": {
3834 |     "en": "I am a title",
3835 |     "es": "Yo soy un titulo"
3836 |   },
3837 |   "logoPosition": "right",
3838 |   "pages": [
3839 |     {
3840 |       "name": "Welcome to my world",
3841 |       "elements": [
3842 |         {
3843 |           "title": {
3844 |             "en": "I am an element title",
3845 |             "es": "Yo soy un elemento de titulo"
3846 |           },
3847 |           "description": {
3848 |             "en": "I am an element description",
3849 |             "es": "Yo soy una descripcion de elemento"
3850 |           }
3851 |         }
3852 |       ]
3853 |     }
3854 |   ]
3855 | }`;
3856 |       expect(fs.writeFile).toHaveBeenCalledWith(
3857 |         "i18n/es.json",
3858 |         expectedOutput,
3859 |         { encoding: "utf-8", flag: "w" },
3860 |       );
3861 |     });
3862 | 
3863 |     it("should respect locked keys (pull)", async () => {
3864 |       setupFileMocks();
3865 |       const input = {
3866 |         title: { en: "I am a title" },
3867 |         subtitle: { en: "Sub" },
3868 |       };
3869 |       mockFileOperations(JSON.stringify(input));
3870 |       const loader = createBucketLoader(
3871 |         "json-dictionary",
3872 |         "i18n/[locale].json",
3873 |         { defaultLocale: "en" },
3874 |         ["title"],
3875 |       );
3876 |       loader.setDefaultLocale("en");
3877 |       const data = await loader.pull("en");
3878 |       expect(data).toEqual({ subtitle: "Sub" });
3879 |     });
3880 |   });
3881 | 
3882 |   describe("yaml-root-key bucket loader", () => {
3883 |     it("should respect locked keys (pull)", async () => {
3884 |       setupFileMocks();
3885 |       const input = `en:\n  locked: Original\n  hello: Hello!`;
3886 |       mockFileOperations(input);
3887 |       const loader = createBucketLoader(
3888 |         "yaml-root-key",
3889 |         "i18n/[locale].yml",
3890 |         { defaultLocale: "en" },
3891 |         ["locked"],
3892 |       );
3893 |       loader.setDefaultLocale("en");
3894 |       const data = await loader.pull("en");
3895 |       expect(data).toEqual({ hello: "Hello!" });
3896 |     });
3897 |   });
3898 | 
3899 |   describe("xcode-xcstrings-v2 bucket loader", () => {
3900 |     it("should respect locked keys (pull)", async () => {
3901 |       setupFileMocks();
3902 |       const input = JSON.stringify({
3903 |         sourceLanguage: "en",
3904 |         strings: {
3905 |           locked: {
3906 |             extractionState: "manual",
3907 |             localizations: {
3908 |               en: { stringUnit: { state: "translated", value: "Original" } },
3909 |             },
3910 |           },
3911 |           hello: {
3912 |             extractionState: "manual",
3913 |             localizations: {
3914 |               en: { stringUnit: { state: "translated", value: "Hello" } },
3915 |             },
3916 |           },
3917 |         },
3918 |       });
3919 |       mockFileOperations(input);
3920 | 
3921 |       const loader = createBucketLoader(
3922 |         "xcode-xcstrings-v2",
3923 |         "i18n/[locale].xcstrings",
3924 |         { defaultLocale: "en" },
3925 |         ["locked"],
3926 |       );
3927 |       loader.setDefaultLocale("en");
3928 |       const data = await loader.pull("en");
3929 |       expect(data).toEqual({ hello: "Hello" });
3930 |     });
3931 |   });
3932 | 
3933 |   describe("typescript bucket loader", () => {
3934 |     it("should respect locked keys (pull)", async () => {
3935 |       setupFileMocks();
3936 |       const input = `export default { locked: "Original", hello: "Hello" };`;
3937 |       mockFileOperations(input);
3938 |       const loader = createBucketLoader(
3939 |         "typescript",
3940 |         "i18n/[locale].ts",
3941 |         { defaultLocale: "en" },
3942 |         ["locked"],
3943 |       );
3944 |       loader.setDefaultLocale("en");
3945 |       const data = await loader.pull("en");
3946 |       expect(data).toEqual({ hello: "Hello" });
3947 |     });
3948 |   });
3949 | });
3950 | 
3951 | function setupFileMocks() {
3952 |   vi.mock("fs/promises", () => ({
3953 |     default: {
3954 |       readFile: vi.fn(),
3955 |       writeFile: vi.fn(),
3956 |       mkdir: vi.fn(),
3957 |       access: vi.fn(),
3958 |     },
3959 |   }));
3960 | 
3961 |   vi.mock("path", () => ({
3962 |     default: {
3963 |       resolve: vi.fn((path) => path),
3964 |       dirname: vi.fn((path) => path.split("/").slice(0, -1).join("/")),
3965 |     },
3966 |   }));
3967 | }
3968 | 
3969 | function mockFileOperations(input: string) {
3970 |   (fs.access as any).mockImplementation(() => Promise.resolve());
3971 |   (fs.readFile as any).mockImplementation(() => Promise.resolve(input));
3972 |   (fs.writeFile as any).mockImplementation(() => Promise.resolve());
3973 | }
3974 | 
3975 | function mockFileOperationsForPaths(input: Record<string, string>) {
3976 |   (fs.access as any).mockImplementation((path) =>
3977 |     input.hasOwnProperty(path)
3978 |       ? Promise.resolve()
3979 |       : Promise.reject(`fs.access: ${path} not mocked`),
3980 |   );
3981 |   (fs.readFile as any).mockImplementation((path) =>
3982 |     input.hasOwnProperty(path)
3983 |       ? Promise.resolve(input[path])
3984 |       : Promise.reject(`fs.readFile: ${path} not mocked`),
3985 |   );
3986 |   (fs.writeFile as any).mockImplementation((path) =>
3987 |     input.hasOwnProperty(path)
3988 |       ? Promise.resolve()
3989 |       : Promise.reject(`fs:writeFile: ${path} not mocked`),
3990 |   );
3991 | }
3992 | 
```
Page 20/20FirstPrevNextLast