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

```typescript
   1 | import { createRequire } from "node:module";
   2 | import { parseStringPromise, type XmlDeclarationAttributes } from "xml2js";
   3 | import { ILoader } from "./_types";
   4 | import { CLIError } from "../utils/errors";
   5 | import { createLoader } from "./_utils";
   6 | 
   7 | interface SaxParser {
   8 |   onopentag: (node: {
   9 |     name: string;
  10 |     attributes: Record<string, string>;
  11 |   }) => void;
  12 |   onclosetag: (name: string) => void;
  13 |   ontext: (text: string) => void;
  14 |   oncdata: (cdata: string) => void;
  15 |   write(data: string): SaxParser;
  16 |   close(): SaxParser;
  17 | }
  18 | 
  19 | interface SaxModule {
  20 |   parser(
  21 |     strict: boolean,
  22 |     options?: { trim?: boolean; normalize?: boolean; lowercase?: boolean },
  23 |   ): SaxParser;
  24 | }
  25 | 
  26 | const require = createRequire(import.meta.url);
  27 | const sax: SaxModule = require("sax") as SaxModule;
  28 | 
  29 | const defaultAndroidResourcesXml = `<?xml version="1.0" encoding="utf-8"?>
  30 | <resources>
  31 | </resources>`;
  32 | 
  33 | type AndroidResourceType =
  34 |   | "string"
  35 |   | "string-array"
  36 |   | "plurals"
  37 |   | "bool"
  38 |   | "integer";
  39 | 
  40 | type PrimitiveValue = boolean | number | string;
  41 | 
  42 | type ContentSegment =
  43 |   | { kind: "text"; value: string }
  44 |   | { kind: "cdata"; value: string };
  45 | 
  46 | interface TextualMeta {
  47 |   segments: ContentSegment[];
  48 |   hasCdata: boolean;
  49 | }
  50 | 
  51 | interface ArrayItemMeta extends TextualMeta {
  52 |   quantity?: string;
  53 | }
  54 | 
  55 | interface StringResourceNode {
  56 |   type: "string";
  57 |   name: string;
  58 |   translatable: boolean;
  59 |   node: any;
  60 |   meta: TextualMeta;
  61 | }
  62 | 
  63 | interface StringArrayItemNode {
  64 |   node: any;
  65 |   meta: TextualMeta;
  66 | }
  67 | 
  68 | interface StringArrayResourceNode {
  69 |   type: "string-array";
  70 |   name: string;
  71 |   translatable: boolean;
  72 |   node: any;
  73 |   items: StringArrayItemNode[];
  74 | }
  75 | 
  76 | interface PluralsItemNode {
  77 |   node: any;
  78 |   quantity: string;
  79 |   meta: TextualMeta;
  80 | }
  81 | 
  82 | interface PluralsResourceNode {
  83 |   type: "plurals";
  84 |   name: string;
  85 |   translatable: boolean;
  86 |   node: any;
  87 |   items: PluralsItemNode[];
  88 | }
  89 | 
  90 | interface BoolResourceNode {
  91 |   type: "bool";
  92 |   name: string;
  93 |   translatable: boolean;
  94 |   node: any;
  95 |   meta: TextualMeta;
  96 | }
  97 | 
  98 | interface IntegerResourceNode {
  99 |   type: "integer";
 100 |   name: string;
 101 |   translatable: boolean;
 102 |   node: any;
 103 |   meta: TextualMeta;
 104 | }
 105 | 
 106 | type AndroidResourceNode =
 107 |   | StringResourceNode
 108 |   | StringArrayResourceNode
 109 |   | PluralsResourceNode
 110 |   | BoolResourceNode
 111 |   | IntegerResourceNode;
 112 | 
 113 | interface AndroidDocument {
 114 |   resources: any;
 115 |   resourceNodes: AndroidResourceNode[];
 116 | }
 117 | 
 118 | interface XmlDeclarationOptions {
 119 |   xmldec?: XmlDeclarationAttributes;
 120 |   headless: boolean;
 121 | }
 122 | 
 123 | export default function createAndroidLoader(): ILoader<
 124 |   string,
 125 |   Record<string, any>
 126 | > {
 127 |   return createLoader({
 128 |     async pull(locale, input) {
 129 |       try {
 130 |         if (!input) {
 131 |           return {};
 132 |         }
 133 | 
 134 |         const document = await parseAndroidDocument(input);
 135 |         return buildPullResult(document);
 136 |       } catch (error) {
 137 |         console.error("Error parsing Android resource file:", error);
 138 |         throw new CLIError({
 139 |           message: "Failed to parse Android resource file",
 140 |           docUrl: "androidResouceError",
 141 |         });
 142 |       }
 143 |     },
 144 |     async push(
 145 |       locale,
 146 |       payload,
 147 |       originalInput,
 148 |       originalLocale,
 149 |       pullInput,
 150 |       pullOutput,
 151 |     ) {
 152 |       try {
 153 |         const selectedBase = selectBaseXml(
 154 |           locale,
 155 |           originalLocale,
 156 |           pullInput,
 157 |           originalInput,
 158 |         );
 159 | 
 160 |         const existingDocument = await parseAndroidDocument(selectedBase);
 161 |         const sourceDocument = await parseAndroidDocument(originalInput);
 162 |         const translatedDocument = buildTranslatedDocument(
 163 |           payload,
 164 |           existingDocument,
 165 |           sourceDocument,
 166 |         );
 167 | 
 168 |         const referenceXml =
 169 |           selectedBase || originalInput || defaultAndroidResourcesXml;
 170 |         const declaration = resolveXmlDeclaration(referenceXml);
 171 | 
 172 |         return buildAndroidXml(translatedDocument, declaration);
 173 |       } catch (error) {
 174 |         console.error("Error generating Android resource file:", error);
 175 |         throw new CLIError({
 176 |           message: "Failed to generate Android resource file",
 177 |           docUrl: "androidResouceError",
 178 |         });
 179 |       }
 180 |     },
 181 |   });
 182 | }
 183 | 
 184 | function resolveXmlDeclaration(xml: string | null): XmlDeclarationOptions {
 185 |   if (!xml) {
 186 |     const xmldec: XmlDeclarationAttributes = {
 187 |       version: "1.0",
 188 |       encoding: "utf-8",
 189 |     };
 190 |     return {
 191 |       xmldec,
 192 |       headless: false,
 193 |     };
 194 |   }
 195 | 
 196 |   const match = xml.match(
 197 |     /<\?xml\s+version="([^"]+)"(?:\s+encoding="([^"]+)")?\s*\?>/,
 198 |   );
 199 |   if (match) {
 200 |     const version = match[1] && match[1].trim().length > 0 ? match[1] : "1.0";
 201 |     const encoding =
 202 |       match[2] && match[2].trim().length > 0 ? match[2] : undefined;
 203 |     const xmldec: XmlDeclarationAttributes = encoding
 204 |       ? { version, encoding }
 205 |       : { version };
 206 |     return {
 207 |       xmldec,
 208 |       headless: false,
 209 |     };
 210 |   }
 211 | 
 212 |   return { headless: true };
 213 | }
 214 | 
 215 | async function parseAndroidDocument(
 216 |   input?: string | null,
 217 | ): Promise<AndroidDocument> {
 218 |   const xmlToParse =
 219 |     input && input.trim().length > 0 ? input : defaultAndroidResourcesXml;
 220 | 
 221 |   const parsed = await parseStringPromise(xmlToParse, {
 222 |     explicitArray: true,
 223 |     explicitChildren: true,
 224 |     preserveChildrenOrder: true,
 225 |     charsAsChildren: true,
 226 |     includeWhiteChars: true,
 227 |     mergeAttrs: false,
 228 |     normalize: false,
 229 |     normalizeTags: false,
 230 |     trim: false,
 231 |     attrkey: "$",
 232 |     charkey: "_",
 233 |     childkey: "$$",
 234 |   });
 235 | 
 236 |   if (!parsed || !parsed.resources) {
 237 |     return {
 238 |       resources: { $$: [] },
 239 |       resourceNodes: [],
 240 |     };
 241 |   }
 242 | 
 243 |   const resourcesNode = parsed.resources;
 244 |   resourcesNode["#name"] = resourcesNode["#name"] ?? "resources";
 245 |   resourcesNode.$$ = resourcesNode.$$ ?? [];
 246 | 
 247 |   const metadata = extractResourceMetadata(xmlToParse);
 248 | 
 249 |   const resourceNodes: AndroidResourceNode[] = [];
 250 |   let metaIndex = 0;
 251 | 
 252 |   for (const child of resourcesNode.$$ as any[]) {
 253 |     const elementName = child?.["#name"];
 254 |     if (!isResourceElementName(elementName)) {
 255 |       continue;
 256 |     }
 257 | 
 258 |     const meta = metadata[metaIndex++];
 259 |     if (!meta || meta.type !== elementName) {
 260 |       continue;
 261 |     }
 262 | 
 263 |     const name = child?.$?.name ?? meta.name;
 264 |     if (!name) {
 265 |       continue;
 266 |     }
 267 | 
 268 |     const translatable =
 269 |       (child?.$?.translatable ?? "").toLowerCase() !== "false";
 270 | 
 271 |     switch (meta.type) {
 272 |       case "string": {
 273 |         resourceNodes.push({
 274 |           type: "string",
 275 |           name,
 276 |           translatable,
 277 |           node: child,
 278 |           meta: cloneTextMeta(meta.meta),
 279 |         });
 280 |         break;
 281 |       }
 282 |       case "string-array": {
 283 |         const itemNodes = (child?.item ?? []) as any[];
 284 |         const items: StringArrayItemNode[] = [];
 285 |         const templateItems = meta.items;
 286 | 
 287 |         for (
 288 |           let i = 0;
 289 |           i < Math.max(itemNodes.length, templateItems.length);
 290 |           i++
 291 |         ) {
 292 |           const nodeItem = itemNodes[i];
 293 |           const templateItem =
 294 |             templateItems[i] ?? templateItems[templateItems.length - 1];
 295 |           if (!nodeItem) {
 296 |             continue;
 297 |           }
 298 |           items.push({
 299 |             node: nodeItem,
 300 |             meta: cloneTextMeta(templateItem.meta),
 301 |           });
 302 |         }
 303 | 
 304 |         resourceNodes.push({
 305 |           type: "string-array",
 306 |           name,
 307 |           translatable,
 308 |           node: child,
 309 |           items,
 310 |         });
 311 |         break;
 312 |       }
 313 |       case "plurals": {
 314 |         const itemNodes = (child?.item ?? []) as any[];
 315 |         const templateItems = meta.items;
 316 |         const items: PluralsItemNode[] = [];
 317 | 
 318 |         for (const templateItem of templateItems) {
 319 |           const quantity = templateItem.quantity;
 320 |           if (!quantity) {
 321 |             continue;
 322 |           }
 323 |           const nodeItem = itemNodes.find(
 324 |             (item: any) => item?.$?.quantity === quantity,
 325 |           );
 326 |           if (!nodeItem) {
 327 |             continue;
 328 |           }
 329 |           items.push({
 330 |             node: nodeItem,
 331 |             quantity,
 332 |             meta: cloneTextMeta(templateItem.meta),
 333 |           });
 334 |         }
 335 | 
 336 |         resourceNodes.push({
 337 |           type: "plurals",
 338 |           name,
 339 |           translatable,
 340 |           node: child,
 341 |           items,
 342 |         });
 343 |         break;
 344 |       }
 345 |       case "bool": {
 346 |         resourceNodes.push({
 347 |           type: "bool",
 348 |           name,
 349 |           translatable,
 350 |           node: child,
 351 |           meta: cloneTextMeta(meta.meta),
 352 |         });
 353 |         break;
 354 |       }
 355 |       case "integer": {
 356 |         resourceNodes.push({
 357 |           type: "integer",
 358 |           name,
 359 |           translatable,
 360 |           node: child,
 361 |           meta: cloneTextMeta(meta.meta),
 362 |         });
 363 |         break;
 364 |       }
 365 |     }
 366 |   }
 367 | 
 368 |   return { resources: resourcesNode, resourceNodes };
 369 | }
 370 | 
 371 | function buildPullResult(document: AndroidDocument): Record<string, any> {
 372 |   const result: Record<string, any> = {};
 373 | 
 374 |   for (const resource of document.resourceNodes) {
 375 |     if (!isTranslatable(resource)) {
 376 |       continue;
 377 |     }
 378 | 
 379 |     switch (resource.type) {
 380 |       case "string": {
 381 |         result[resource.name] = decodeAndroidText(
 382 |           segmentsToString(resource.meta.segments),
 383 |         );
 384 |         break;
 385 |       }
 386 |       case "string-array": {
 387 |         result[resource.name] = resource.items.map((item) =>
 388 |           decodeAndroidText(segmentsToString(item.meta.segments)),
 389 |         );
 390 |         break;
 391 |       }
 392 |       case "plurals": {
 393 |         const pluralMap: Record<string, string> = {};
 394 |         for (const item of resource.items) {
 395 |           pluralMap[item.quantity] = decodeAndroidText(
 396 |             segmentsToString(item.meta.segments),
 397 |           );
 398 |         }
 399 |         result[resource.name] = pluralMap;
 400 |         break;
 401 |       }
 402 |       case "bool": {
 403 |         const value = segmentsToString(resource.meta.segments).trim();
 404 |         result[resource.name] = value === "true";
 405 |         break;
 406 |       }
 407 |       case "integer": {
 408 |         const value = parseInt(
 409 |           segmentsToString(resource.meta.segments).trim(),
 410 |           10,
 411 |         );
 412 |         result[resource.name] = Number.isNaN(value) ? 0 : value;
 413 |         break;
 414 |       }
 415 |     }
 416 |   }
 417 | 
 418 |   return result;
 419 | }
 420 | 
 421 | function isTranslatable(resource: AndroidResourceNode): boolean {
 422 |   return resource.translatable;
 423 | }
 424 | 
 425 | function buildTranslatedDocument(
 426 |   payload: Record<string, any>,
 427 |   existingDocument: AndroidDocument,
 428 |   sourceDocument: AndroidDocument,
 429 | ): AndroidDocument {
 430 |   const templateDocument = sourceDocument;
 431 |   const finalDocument = cloneDocumentStructure(templateDocument);
 432 | 
 433 |   const templateMap = createResourceMap(templateDocument);
 434 |   const existingMap = createResourceMap(existingDocument);
 435 |   const payloadEntries = payload ?? {};
 436 |   const finalMap = createResourceMap(finalDocument);
 437 | 
 438 |   for (const resource of finalDocument.resourceNodes) {
 439 |     if (!resource.translatable) {
 440 |       continue;
 441 |     }
 442 | 
 443 |     const templateResource = templateMap.get(resource.name);
 444 |     let translationValue: any;
 445 | 
 446 |     if (
 447 |       Object.prototype.hasOwnProperty.call(payloadEntries, resource.name) &&
 448 |       payloadEntries[resource.name] !== undefined &&
 449 |       payloadEntries[resource.name] !== null
 450 |     ) {
 451 |       translationValue = payloadEntries[resource.name];
 452 |     } else if (existingMap.has(resource.name)) {
 453 |       translationValue = extractValueFromResource(
 454 |         existingMap.get(resource.name)!,
 455 |       );
 456 |     } else {
 457 |       translationValue = extractValueFromResource(templateResource ?? resource);
 458 |     }
 459 | 
 460 |     updateResourceNode(resource, translationValue, templateResource);
 461 |   }
 462 | 
 463 |   for (const resource of existingDocument.resourceNodes) {
 464 |     if (finalMap.has(resource.name)) {
 465 |       continue;
 466 |     }
 467 |     if (!isTranslatable(resource)) {
 468 |       continue;
 469 |     }
 470 |     const cloned = cloneResourceNode(resource);
 471 |     appendResourceNode(finalDocument, cloned);
 472 |     finalMap.set(cloned.name, cloned);
 473 |   }
 474 | 
 475 |   for (const [name, value] of Object.entries(payloadEntries)) {
 476 |     if (finalMap.has(name)) {
 477 |       continue;
 478 |     }
 479 |     try {
 480 |       const inferred = createResourceNodeFromValue(name, value);
 481 |       appendResourceNode(finalDocument, inferred);
 482 |       finalMap.set(name, inferred);
 483 |     } catch (error) {
 484 |       if (error instanceof CLIError) {
 485 |         throw error;
 486 |       }
 487 |     }
 488 |   }
 489 | 
 490 |   return finalDocument;
 491 | }
 492 | 
 493 | function buildAndroidXml(
 494 |   document: AndroidDocument,
 495 |   declaration: XmlDeclarationOptions,
 496 | ): string {
 497 |   const xmlBody = serializeElement(document.resources);
 498 | 
 499 |   if (declaration.headless) {
 500 |     return xmlBody;
 501 |   }
 502 | 
 503 |   if (declaration.xmldec) {
 504 |     const { version, encoding } = declaration.xmldec;
 505 |     const encodingPart = encoding ? ` encoding="${encoding}"` : "";
 506 |     return `<?xml version="${version}"${encodingPart}?>\n${xmlBody}`;
 507 |   }
 508 | 
 509 |   return `<?xml version="1.0" encoding="utf-8"?>\n${xmlBody}`;
 510 | }
 511 | 
 512 | function selectBaseXml(
 513 |   locale: string,
 514 |   originalLocale: string,
 515 |   pullInput: string | null,
 516 |   originalInput: string | null,
 517 | ): string | null {
 518 |   if (locale === originalLocale) {
 519 |     return pullInput ?? originalInput;
 520 |   }
 521 |   return pullInput ?? originalInput;
 522 | }
 523 | 
 524 | function updateResourceNode(
 525 |   target: AndroidResourceNode,
 526 |   rawValue: any,
 527 |   template: AndroidResourceNode | undefined,
 528 | ): void {
 529 |   switch (target.type) {
 530 |     case "string": {
 531 |       const value = asString(rawValue, target.name);
 532 |       const templateMeta =
 533 |         template && template.type === "string" ? template.meta : target.meta;
 534 |       const useCdata = templateMeta.hasCdata;
 535 |       setTextualNodeContent(target.node, value, useCdata);
 536 |       target.meta = makeTextMeta([
 537 |         { kind: useCdata ? "cdata" : "text", value },
 538 |       ]);
 539 |       break;
 540 |     }
 541 |     case "string-array": {
 542 |       const values = asStringArray(rawValue, target.name);
 543 |       const templateItems =
 544 |         template && template.type === "string-array"
 545 |           ? template.items
 546 |           : target.items;
 547 |       const maxLength = Math.max(target.items.length, templateItems.length);
 548 |       for (let index = 0; index < maxLength; index++) {
 549 |         const targetItem = target.items[index];
 550 |         const templateItem =
 551 |           templateItems[index] ??
 552 |           templateItems[templateItems.length - 1] ??
 553 |           target.items[index];
 554 |         if (!targetItem || !templateItem) {
 555 |           continue;
 556 |         }
 557 |         const translation =
 558 |           index < values.length
 559 |             ? values[index]
 560 |             : segmentsToString(templateItem.meta.segments);
 561 |         const useCdata = templateItem.meta.hasCdata;
 562 |         setTextualNodeContent(targetItem.node, translation, useCdata);
 563 |         targetItem.meta = makeTextMeta([
 564 |           { kind: useCdata ? "cdata" : "text", value: translation },
 565 |         ]);
 566 |       }
 567 |       break;
 568 |     }
 569 |     case "plurals": {
 570 |       const pluralValues = asPluralMap(rawValue, target.name);
 571 |       const templateItems =
 572 |         template && template.type === "plurals" ? template.items : target.items;
 573 |       const templateMap = new Map(
 574 |         templateItems.map((item) => [item.quantity, item]),
 575 |       );
 576 |       for (const item of target.items) {
 577 |         const templateItem =
 578 |           templateMap.get(item.quantity) ?? templateMap.values().next().value;
 579 |         const fallback = templateItem
 580 |           ? segmentsToString(templateItem.meta.segments)
 581 |           : segmentsToString(item.meta.segments);
 582 |         const translation =
 583 |           typeof pluralValues[item.quantity] === "string"
 584 |             ? pluralValues[item.quantity]
 585 |             : fallback;
 586 |         const useCdata = templateItem
 587 |           ? templateItem.meta.hasCdata
 588 |           : item.meta.hasCdata;
 589 |         setTextualNodeContent(item.node, translation, useCdata);
 590 |         item.meta = makeTextMeta([
 591 |           { kind: useCdata ? "cdata" : "text", value: translation },
 592 |         ]);
 593 |       }
 594 |       break;
 595 |     }
 596 |     case "bool": {
 597 |       const boolValue = asBoolean(rawValue, target.name);
 598 |       const strValue = boolValue ? "true" : "false";
 599 |       setTextualNodeContent(target.node, strValue, false);
 600 |       target.meta = makeTextMeta([{ kind: "text", value: strValue }]);
 601 |       break;
 602 |     }
 603 |     case "integer": {
 604 |       const intValue = asInteger(rawValue, target.name);
 605 |       const strValue = intValue.toString();
 606 |       setTextualNodeContent(target.node, strValue, false);
 607 |       target.meta = makeTextMeta([{ kind: "text", value: strValue }]);
 608 |       break;
 609 |     }
 610 |   }
 611 | }
 612 | 
 613 | function appendResourceNode(
 614 |   document: AndroidDocument,
 615 |   resourceNode: AndroidResourceNode,
 616 | ): void {
 617 |   document.resources.$$ = document.resources.$$ ?? [];
 618 |   const children = document.resources.$$ as any[];
 619 | 
 620 |   if (
 621 |     children.length === 0 ||
 622 |     (children[children.length - 1]["#name"] !== "__text__" &&
 623 |       children[children.length - 1]["#name"] !== "__comment__")
 624 |   ) {
 625 |     children.push({ "#name": "__text__", _: "\n    " });
 626 |   }
 627 | 
 628 |   children.push(resourceNode.node);
 629 |   children.push({ "#name": "__text__", _: "\n" });
 630 |   document.resourceNodes.push(resourceNode);
 631 | }
 632 | 
 633 | function setTextualNodeContent(
 634 |   node: any,
 635 |   value: string,
 636 |   useCdata: boolean,
 637 | ): void {
 638 |   // CDATA needs apostrophe escaping but not XML entity escaping
 639 |   const escapedValue = useCdata
 640 |     ? escapeApostrophesOnly(value)
 641 |     : escapeAndroidString(value);
 642 |   node._ = escapedValue;
 643 | 
 644 |   node.$$ = node.$$ ?? [];
 645 |   let textNode = node.$$.find(
 646 |     (child: any) =>
 647 |       child["#name"] === "__text__" || child["#name"] === "__cdata",
 648 |   );
 649 | 
 650 |   if (!textNode) {
 651 |     textNode = {};
 652 |     node.$$.push(textNode);
 653 |   }
 654 | 
 655 |   textNode["#name"] = useCdata ? "__cdata" : "__text__";
 656 |   textNode._ = escapedValue;
 657 | }
 658 | 
 659 | function buildResourceNameMap(
 660 |   document: AndroidDocument,
 661 | ): Map<string, AndroidResourceNode> {
 662 |   const map = new Map<string, AndroidResourceNode>();
 663 |   for (const node of document.resourceNodes) {
 664 |     if (!map.has(node.name)) {
 665 |       map.set(node.name, node);
 666 |     }
 667 |   }
 668 |   return map;
 669 | }
 670 | 
 671 | function createResourceMap(
 672 |   document: AndroidDocument,
 673 | ): Map<string, AndroidResourceNode> {
 674 |   return buildResourceNameMap(document);
 675 | }
 676 | 
 677 | function cloneResourceNode(resource: AndroidResourceNode): AndroidResourceNode {
 678 |   switch (resource.type) {
 679 |     case "string": {
 680 |       const nodeClone = deepClone(resource.node);
 681 |       return {
 682 |         type: "string",
 683 |         name: resource.name,
 684 |         translatable: resource.translatable,
 685 |         node: nodeClone,
 686 |         meta: cloneTextMeta(resource.meta),
 687 |       };
 688 |     }
 689 |     case "string-array": {
 690 |       const nodeClone = deepClone(resource.node);
 691 |       const itemNodes = (nodeClone.item ?? []) as any[];
 692 |       const items: StringArrayItemNode[] = itemNodes.map((itemNode, index) => {
 693 |         const templateMeta =
 694 |           resource.items[index]?.meta ??
 695 |           resource.items[resource.items.length - 1]?.meta ??
 696 |           makeTextMeta([]);
 697 |         return {
 698 |           node: itemNode,
 699 |           meta: cloneTextMeta(templateMeta),
 700 |         };
 701 |       });
 702 |       return {
 703 |         type: "string-array",
 704 |         name: resource.name,
 705 |         translatable: resource.translatable,
 706 |         node: nodeClone,
 707 |         items,
 708 |       };
 709 |     }
 710 |     case "plurals": {
 711 |       const nodeClone = deepClone(resource.node);
 712 |       const itemNodes = (nodeClone.item ?? []) as any[];
 713 |       const items: PluralsItemNode[] = [];
 714 |       for (const templateItem of resource.items) {
 715 |         const cloneNode = itemNodes.find(
 716 |           (item: any) => item?.$?.quantity === templateItem.quantity,
 717 |         );
 718 |         if (!cloneNode) {
 719 |           continue;
 720 |         }
 721 |         items.push({
 722 |           node: cloneNode,
 723 |           quantity: templateItem.quantity,
 724 |           meta: cloneTextMeta(templateItem.meta),
 725 |         });
 726 |       }
 727 |       return {
 728 |         type: "plurals",
 729 |         name: resource.name,
 730 |         translatable: resource.translatable,
 731 |         node: nodeClone,
 732 |         items,
 733 |       };
 734 |     }
 735 |     case "bool": {
 736 |       const nodeClone = deepClone(resource.node);
 737 |       return {
 738 |         type: "bool",
 739 |         name: resource.name,
 740 |         translatable: resource.translatable,
 741 |         node: nodeClone,
 742 |         meta: cloneTextMeta(resource.meta),
 743 |       };
 744 |     }
 745 |     case "integer": {
 746 |       const nodeClone = deepClone(resource.node);
 747 |       return {
 748 |         type: "integer",
 749 |         name: resource.name,
 750 |         translatable: resource.translatable,
 751 |         node: nodeClone,
 752 |         meta: cloneTextMeta(resource.meta),
 753 |       };
 754 |     }
 755 |   }
 756 | }
 757 | 
 758 | function cloneTextMeta(meta: TextualMeta): TextualMeta {
 759 |   return {
 760 |     hasCdata: meta.hasCdata,
 761 |     segments: meta.segments.map((segment) => ({ ...segment })),
 762 |   };
 763 | }
 764 | 
 765 | function asString(value: any, name: string): string {
 766 |   if (typeof value === "string") {
 767 |     return value;
 768 |   }
 769 |   throw new CLIError({
 770 |     message: `Expected string value for resource "${name}"`,
 771 |     docUrl: "androidResouceError",
 772 |   });
 773 | }
 774 | 
 775 | function asStringArray(value: any, name: string): string[] {
 776 |   if (Array.isArray(value) && value.every((item) => typeof item === "string")) {
 777 |     return value;
 778 |   }
 779 |   throw new CLIError({
 780 |     message: `Expected array of strings for resource "${name}"`,
 781 |     docUrl: "androidResouceError",
 782 |   });
 783 | }
 784 | 
 785 | function asPluralMap(value: any, name: string): Record<string, string> {
 786 |   if (value && typeof value === "object" && !Array.isArray(value)) {
 787 |     const result: Record<string, string> = {};
 788 |     for (const [quantity, pluralValue] of Object.entries(value)) {
 789 |       if (typeof pluralValue !== "string") {
 790 |         throw new CLIError({
 791 |           message: `Expected plural item "${quantity}" of "${name}" to be a string`,
 792 |           docUrl: "androidResouceError",
 793 |         });
 794 |       }
 795 |       result[quantity] = pluralValue;
 796 |     }
 797 |     return result;
 798 |   }
 799 |   throw new CLIError({
 800 |     message: `Expected object value for plurals resource "${name}"`,
 801 |     docUrl: "androidResouceError",
 802 |   });
 803 | }
 804 | 
 805 | function asBoolean(value: any, name: string): boolean {
 806 |   if (typeof value === "boolean") {
 807 |     return value;
 808 |   }
 809 |   if (typeof value === "string") {
 810 |     if (value === "true" || value === "false") {
 811 |       return value === "true";
 812 |     }
 813 |   }
 814 |   throw new CLIError({
 815 |     message: `Expected boolean value for resource "${name}"`,
 816 |     docUrl: "androidResouceError",
 817 |   });
 818 | }
 819 | 
 820 | function asInteger(value: any, name: string): number {
 821 |   if (typeof value === "number" && Number.isInteger(value)) {
 822 |     return value;
 823 |   }
 824 |   throw new CLIError({
 825 |     message: `Expected number value for resource "${name}"`,
 826 |     docUrl: "androidResouceError",
 827 |   });
 828 | }
 829 | 
 830 | function escapeAndroidString(value: string): string {
 831 |   return value
 832 |     .replace(/&/g, "&amp;")
 833 |     .replace(/</g, "&lt;")
 834 |     .replace(/>/g, "&gt;")
 835 |     .replace(/(?<!\\)'/g, "\\'");
 836 | }
 837 | 
 838 | function escapeApostrophesOnly(value: string): string {
 839 |   // Even inside CDATA, apostrophes must be escaped for Android AAPT
 840 |   return value.replace(/(?<!\\)'/g, "\\'");
 841 | }
 842 | 
 843 | function segmentsToString(segments: ContentSegment[]): string {
 844 |   return segments.map((segment) => segment.value).join("");
 845 | }
 846 | 
 847 | function makeTextMeta(segments: ContentSegment[]): TextualMeta {
 848 |   return {
 849 |     segments,
 850 |     hasCdata: segments.some((segment) => segment.kind === "cdata"),
 851 |   };
 852 | }
 853 | 
 854 | function createResourceNodeFromValue(
 855 |   name: string,
 856 |   value: any,
 857 | ): AndroidResourceNode {
 858 |   const inferredType = inferTypeFromValue(value);
 859 | 
 860 |   switch (inferredType) {
 861 |     case "string": {
 862 |       const stringValue = asString(value, name);
 863 |       const escaped = escapeAndroidString(stringValue);
 864 |       const node = {
 865 |         "#name": "string",
 866 |         $: { name },
 867 |         _: escaped,
 868 |         $$: [{ "#name": "__text__", _: escaped }],
 869 |       };
 870 |       return {
 871 |         type: "string",
 872 |         name,
 873 |         translatable: true,
 874 |         node,
 875 |         meta: makeTextMeta([{ kind: "text", value: stringValue }]),
 876 |       };
 877 |     }
 878 |     case "string-array": {
 879 |       const items = asStringArray(value, name);
 880 |       const node = {
 881 |         "#name": "string-array",
 882 |         $: { name },
 883 |         $$: [] as any[],
 884 |         item: [] as any[],
 885 |       };
 886 |       const itemNodes: StringArrayItemNode[] = [];
 887 |       for (const itemValue of items) {
 888 |         const escaped = escapeAndroidString(itemValue);
 889 |         const itemNode = {
 890 |           "#name": "item",
 891 |           _: escaped,
 892 |           $$: [{ "#name": "__text__", _: escaped }],
 893 |         };
 894 |         node.$$!.push(itemNode);
 895 |         node.item!.push(itemNode);
 896 |         itemNodes.push({
 897 |           node: itemNode,
 898 |           meta: makeTextMeta([{ kind: "text", value: itemValue }]),
 899 |         });
 900 |       }
 901 |       return {
 902 |         type: "string-array",
 903 |         name,
 904 |         translatable: true,
 905 |         node,
 906 |         items: itemNodes,
 907 |       };
 908 |     }
 909 |     case "plurals": {
 910 |       const pluralMap = asPluralMap(value, name);
 911 |       const node = {
 912 |         "#name": "plurals",
 913 |         $: { name },
 914 |         $$: [] as any[],
 915 |         item: [] as any[],
 916 |       };
 917 |       const items: PluralsItemNode[] = [];
 918 |       for (const [quantity, pluralValue] of Object.entries(pluralMap)) {
 919 |         const escaped = escapeAndroidString(pluralValue);
 920 |         const itemNode = {
 921 |           "#name": "item",
 922 |           $: { quantity },
 923 |           _: escaped,
 924 |           $$: [{ "#name": "__text__", _: escaped }],
 925 |         };
 926 |         node.$$!.push(itemNode);
 927 |         node.item!.push(itemNode);
 928 |         items.push({
 929 |           node: itemNode,
 930 |           quantity,
 931 |           meta: makeTextMeta([{ kind: "text", value: pluralValue }]),
 932 |         });
 933 |       }
 934 |       return {
 935 |         type: "plurals",
 936 |         name,
 937 |         translatable: true,
 938 |         node,
 939 |         items,
 940 |       };
 941 |     }
 942 |     case "bool": {
 943 |       const boolValue = asBoolean(value, name);
 944 |       const textValue = boolValue ? "true" : "false";
 945 |       const node = {
 946 |         "#name": "bool",
 947 |         $: { name },
 948 |         _: textValue,
 949 |         $$: [{ "#name": "__text__", _: textValue }],
 950 |       };
 951 |       return {
 952 |         type: "bool",
 953 |         name,
 954 |         translatable: true,
 955 |         node,
 956 |         meta: makeTextMeta([{ kind: "text", value: textValue }]),
 957 |       };
 958 |     }
 959 |     case "integer": {
 960 |       const intValue = asInteger(value, name);
 961 |       const textValue = intValue.toString();
 962 |       const node = {
 963 |         "#name": "integer",
 964 |         $: { name },
 965 |         _: textValue,
 966 |         $$: [{ "#name": "__text__", _: textValue }],
 967 |       };
 968 |       return {
 969 |         type: "integer",
 970 |         name,
 971 |         translatable: true,
 972 |         node,
 973 |         meta: makeTextMeta([{ kind: "text", value: textValue }]),
 974 |       };
 975 |     }
 976 |   }
 977 | }
 978 | 
 979 | function cloneDocumentStructure(document: AndroidDocument): AndroidDocument {
 980 |   // Filter first - only keep translatable resources
 981 |   const translatableResources = document.resourceNodes.filter(isTranslatable);
 982 | 
 983 |   const resourcesClone = deepClone(document.resources);
 984 |   const lookup = buildResourceLookup(resourcesClone);
 985 |   const resourceNodes: AndroidResourceNode[] = [];
 986 | 
 987 |   for (const resource of translatableResources) {
 988 |     const cloned = cloneResourceNodeFromLookup(resource, lookup);
 989 |     resourceNodes.push(cloned);
 990 |   }
 991 | 
 992 |   // Clean up XML structure - only keep translatable resource nodes
 993 |   if (resourcesClone.$$ && Array.isArray(resourcesClone.$$)) {
 994 |     const includedKeys = new Set(
 995 |       resourceNodes.map((r) => resourceLookupKey(r.type, r.name)),
 996 |     );
 997 | 
 998 |     // Filter out non-translatable resources
 999 |     let filtered = resourcesClone.$$.filter((child: any) => {
1000 |       const elementName = child?.["#name"];
1001 |       const name = child?.$?.name;
1002 |       if (!isResourceElementName(elementName) || !name) {
1003 |         return true; // Keep whitespace, comments, etc.
1004 |       }
1005 |       return includedKeys.has(resourceLookupKey(elementName, name));
1006 |     });
1007 | 
1008 |     // Remove consecutive whitespace nodes (fixes extra blank lines)
1009 |     const cleaned: any[] = [];
1010 |     let lastWasWhitespace = false;
1011 | 
1012 |     for (const child of filtered) {
1013 |       const isWhitespace =
1014 |         child?.["#name"] === "__text__" && (!child._ || child._.trim() === "");
1015 | 
1016 |       if (isWhitespace) {
1017 |         if (!lastWasWhitespace) {
1018 |           cleaned.push(child);
1019 |           lastWasWhitespace = true;
1020 |         }
1021 |         // Skip consecutive whitespace
1022 |       } else {
1023 |         cleaned.push(child);
1024 |         lastWasWhitespace = false;
1025 |       }
1026 |     }
1027 | 
1028 |     resourcesClone.$$ = cleaned;
1029 |   }
1030 | 
1031 |   return {
1032 |     resources: resourcesClone,
1033 |     resourceNodes,
1034 |   };
1035 | }
1036 | 
1037 | function buildResourceLookup(resources: any): Map<string, any[]> {
1038 |   const lookup = new Map<string, any[]>();
1039 |   const children = Array.isArray(resources.$$) ? resources.$$ : [];
1040 |   for (const child of children) {
1041 |     const type = child?.["#name"];
1042 |     const name = child?.$?.name;
1043 |     if (!type || !name || !isResourceElementName(type)) {
1044 |       continue;
1045 |     }
1046 |     const key = resourceLookupKey(type, name);
1047 |     if (!lookup.has(key)) {
1048 |       lookup.set(key, []);
1049 |     }
1050 |     lookup.get(key)!.push(child);
1051 |   }
1052 |   return lookup;
1053 | }
1054 | 
1055 | function cloneResourceNodeFromLookup(
1056 |   resource: AndroidResourceNode,
1057 |   lookup: Map<string, any[]>,
1058 | ): AndroidResourceNode {
1059 |   const node = takeResourceNode(lookup, resource.type, resource.name);
1060 |   if (!node) {
1061 |     return cloneResourceNode(resource);
1062 |   }
1063 | 
1064 |   switch (resource.type) {
1065 |     case "string": {
1066 |       return {
1067 |         type: "string",
1068 |         name: resource.name,
1069 |         translatable: resource.translatable,
1070 |         node,
1071 |         meta: cloneTextMeta(resource.meta),
1072 |       };
1073 |     }
1074 |     case "string-array": {
1075 |       const childItems = (Array.isArray(node.$$) ? node.$$ : []).filter(
1076 |         (child: any) => child?.["#name"] === "item",
1077 |       );
1078 |       node.item = childItems;
1079 |       if (childItems.length < resource.items.length) {
1080 |         return cloneResourceNode(resource);
1081 |       }
1082 |       const items: StringArrayItemNode[] = resource.items.map((item, index) => {
1083 |         const nodeItem = childItems[index];
1084 |         if (!nodeItem) {
1085 |           return {
1086 |             node: deepClone(item.node),
1087 |             meta: cloneTextMeta(item.meta),
1088 |           };
1089 |         }
1090 |         return {
1091 |           node: nodeItem,
1092 |           meta: cloneTextMeta(item.meta),
1093 |         };
1094 |       });
1095 |       return {
1096 |         type: "string-array",
1097 |         name: resource.name,
1098 |         translatable: resource.translatable,
1099 |         node,
1100 |         items,
1101 |       };
1102 |     }
1103 |     case "plurals": {
1104 |       const childItems = (Array.isArray(node.$$) ? node.$$ : []).filter(
1105 |         (child: any) => child?.["#name"] === "item",
1106 |       );
1107 |       node.item = childItems;
1108 |       const itemMap = new Map<string, any>();
1109 |       for (const item of childItems) {
1110 |         if (item?.$?.quantity) {
1111 |           itemMap.set(item.$.quantity, item);
1112 |         }
1113 |       }
1114 |       const items: PluralsItemNode[] = [];
1115 |       for (const templateItem of resource.items) {
1116 |         const nodeItem = itemMap.get(templateItem.quantity);
1117 |         if (!nodeItem) {
1118 |           return cloneResourceNode(resource);
1119 |         }
1120 |         items.push({
1121 |           node: nodeItem,
1122 |           quantity: templateItem.quantity,
1123 |           meta: cloneTextMeta(templateItem.meta),
1124 |         });
1125 |       }
1126 |       return {
1127 |         type: "plurals",
1128 |         name: resource.name,
1129 |         translatable: resource.translatable,
1130 |         node,
1131 |         items,
1132 |       };
1133 |     }
1134 |     case "bool": {
1135 |       return {
1136 |         type: "bool",
1137 |         name: resource.name,
1138 |         translatable: resource.translatable,
1139 |         node,
1140 |         meta: cloneTextMeta(resource.meta),
1141 |       };
1142 |     }
1143 |     case "integer": {
1144 |       return {
1145 |         type: "integer",
1146 |         name: resource.name,
1147 |         translatable: resource.translatable,
1148 |         node,
1149 |         meta: cloneTextMeta(resource.meta),
1150 |       };
1151 |     }
1152 |   }
1153 | }
1154 | 
1155 | function takeResourceNode(
1156 |   lookup: Map<string, any[]>,
1157 |   type: AndroidResourceType,
1158 |   name: string,
1159 | ): any | undefined {
1160 |   const key = resourceLookupKey(type, name);
1161 |   const list = lookup.get(key);
1162 |   if (!list || list.length === 0) {
1163 |     return undefined;
1164 |   }
1165 |   return list.shift();
1166 | }
1167 | 
1168 | function resourceLookupKey(type: string, name: string): string {
1169 |   return `${type}:${name}`;
1170 | }
1171 | 
1172 | function extractValueFromResource(resource: AndroidResourceNode): any {
1173 |   switch (resource.type) {
1174 |     case "string":
1175 |       return decodeAndroidText(segmentsToString(resource.meta.segments));
1176 |     case "string-array":
1177 |       return resource.items.map((item) =>
1178 |         decodeAndroidText(segmentsToString(item.meta.segments)),
1179 |       );
1180 |     case "plurals": {
1181 |       const result: Record<string, string> = {};
1182 |       for (const item of resource.items) {
1183 |         result[item.quantity] = decodeAndroidText(
1184 |           segmentsToString(item.meta.segments),
1185 |         );
1186 |       }
1187 |       return result;
1188 |     }
1189 |     case "bool": {
1190 |       const value = segmentsToString(resource.meta.segments).trim();
1191 |       return value === "true";
1192 |     }
1193 |     case "integer": {
1194 |       const value = parseInt(
1195 |         segmentsToString(resource.meta.segments).trim(),
1196 |         10,
1197 |       );
1198 |       return Number.isNaN(value) ? 0 : value;
1199 |     }
1200 |   }
1201 | }
1202 | 
1203 | function inferTypeFromValue(value: any): AndroidResourceType {
1204 |   if (typeof value === "string") {
1205 |     return "string";
1206 |   }
1207 |   if (Array.isArray(value)) {
1208 |     return "string-array";
1209 |   }
1210 |   if (value && typeof value === "object") {
1211 |     return "plurals";
1212 |   }
1213 |   if (typeof value === "boolean") {
1214 |     return "bool";
1215 |   }
1216 |   if (typeof value === "number" && Number.isInteger(value)) {
1217 |     return "integer";
1218 |   }
1219 |   throw new CLIError({
1220 |     message: "Unable to infer Android resource type from payload",
1221 |     docUrl: "androidResouceError",
1222 |   });
1223 | }
1224 | 
1225 | function extractResourceMetadata(xml: string) {
1226 |   interface StackEntry {
1227 |     name: string;
1228 |     rawName: string;
1229 |     attributes: Record<string, string>;
1230 |     segments: ContentSegment[];
1231 |     items: Array<{ quantity?: string; meta: TextualMeta }>;
1232 |   }
1233 | 
1234 |   interface StringMeta {
1235 |     type: "string";
1236 |     name: string;
1237 |     translatable: boolean;
1238 |     meta: TextualMeta;
1239 |   }
1240 | 
1241 |   interface StringArrayMeta {
1242 |     type: "string-array";
1243 |     name: string;
1244 |     translatable: boolean;
1245 |     items: Array<{ meta: TextualMeta }>;
1246 |   }
1247 | 
1248 |   interface PluralsMeta {
1249 |     type: "plurals";
1250 |     name: string;
1251 |     translatable: boolean;
1252 |     items: Array<{ quantity: string; meta: TextualMeta }>;
1253 |   }
1254 | 
1255 |   interface BoolMeta {
1256 |     type: "bool";
1257 |     name: string;
1258 |     translatable: boolean;
1259 |     meta: TextualMeta;
1260 |   }
1261 | 
1262 |   interface IntegerMeta {
1263 |     type: "integer";
1264 |     name: string;
1265 |     translatable: boolean;
1266 |     meta: TextualMeta;
1267 |   }
1268 | 
1269 |   type ResourceMeta =
1270 |     | StringMeta
1271 |     | StringArrayMeta
1272 |     | PluralsMeta
1273 |     | BoolMeta
1274 |     | IntegerMeta;
1275 | 
1276 |   const parser = sax.parser(true, {
1277 |     trim: false,
1278 |     normalize: false,
1279 |     lowercase: false,
1280 |   });
1281 | 
1282 |   const stack: StackEntry[] = [];
1283 |   const result: ResourceMeta[] = [];
1284 | 
1285 |   parser.onopentag = (node) => {
1286 |     const lowerName = node.name.toLowerCase();
1287 |     const attributes: Record<string, string> = {};
1288 |     for (const [key, value] of Object.entries(node.attributes ?? {})) {
1289 |       attributes[key.toLowerCase()] = String(value);
1290 |     }
1291 |     stack.push({
1292 |       name: lowerName,
1293 |       rawName: node.name,
1294 |       attributes,
1295 |       segments: [],
1296 |       items: [],
1297 |     });
1298 | 
1299 |     if (
1300 |       lowerName !== "resources" &&
1301 |       lowerName !== "item" &&
1302 |       !isResourceElementName(lowerName)
1303 |     ) {
1304 |       const attrString = Object.entries(node.attributes ?? {})
1305 |         .map(
1306 |           ([key, value]) => ` ${key}="${escapeAttributeValue(String(value))}"`,
1307 |         )
1308 |         .join("");
1309 |       appendSegmentToNearestResource(stack, {
1310 |         kind: "text",
1311 |         value: `<${node.name}${attrString}>`,
1312 |       });
1313 |     }
1314 |   };
1315 | 
1316 |   parser.ontext = (text) => {
1317 |     if (!text) {
1318 |       return;
1319 |     }
1320 |     appendSegmentToNearestResource(stack, { kind: "text", value: text });
1321 |   };
1322 | 
1323 |   parser.oncdata = (cdata) => {
1324 |     appendSegmentToNearestResource(stack, { kind: "cdata", value: cdata });
1325 |   };
1326 | 
1327 |   parser.onclosetag = () => {
1328 |     const entry = stack.pop();
1329 |     if (!entry) {
1330 |       return;
1331 |     }
1332 | 
1333 |     const parent = stack[stack.length - 1];
1334 | 
1335 |     if (entry.name === "item" && parent) {
1336 |       const meta = makeTextMeta(entry.segments);
1337 |       parent.items.push({
1338 |         quantity: entry.attributes.quantity,
1339 |         meta,
1340 |       });
1341 |       return;
1342 |     }
1343 | 
1344 |     if (
1345 |       entry.name !== "resources" &&
1346 |       entry.name !== "item" &&
1347 |       !isResourceElementName(entry.name)
1348 |     ) {
1349 |       appendSegmentToNearestResource(stack, {
1350 |         kind: "text",
1351 |         value: `</${entry.rawName}>`,
1352 |       });
1353 |       return;
1354 |     }
1355 | 
1356 |     if (!isResourceElementName(entry.name)) {
1357 |       return;
1358 |     }
1359 | 
1360 |     const name = entry.attributes.name;
1361 |     if (!name) {
1362 |       return;
1363 |     }
1364 | 
1365 |     const translatable =
1366 |       (entry.attributes.translatable ?? "").toLowerCase() !== "false";
1367 | 
1368 |     switch (entry.name) {
1369 |       case "string": {
1370 |         result.push({
1371 |           type: "string",
1372 |           name,
1373 |           translatable,
1374 |           meta: makeTextMeta(entry.segments),
1375 |         });
1376 |         break;
1377 |       }
1378 |       case "string-array": {
1379 |         result.push({
1380 |           type: "string-array",
1381 |           name,
1382 |           translatable,
1383 |           items: entry.items.map((item) => ({
1384 |             meta: cloneTextMeta(item.meta),
1385 |           })),
1386 |         });
1387 |         break;
1388 |       }
1389 |       case "plurals": {
1390 |         const items: Array<{ quantity: string; meta: TextualMeta }> = [];
1391 |         for (const item of entry.items) {
1392 |           if (!item.quantity) {
1393 |             continue;
1394 |           }
1395 |           items.push({
1396 |             quantity: item.quantity,
1397 |             meta: cloneTextMeta(item.meta),
1398 |           });
1399 |         }
1400 |         result.push({
1401 |           type: "plurals",
1402 |           name,
1403 |           translatable,
1404 |           items,
1405 |         });
1406 |         break;
1407 |       }
1408 |       case "bool": {
1409 |         result.push({
1410 |           type: "bool",
1411 |           name,
1412 |           translatable,
1413 |           meta: makeTextMeta(entry.segments),
1414 |         });
1415 |         break;
1416 |       }
1417 |       case "integer": {
1418 |         result.push({
1419 |           type: "integer",
1420 |           name,
1421 |           translatable,
1422 |           meta: makeTextMeta(entry.segments),
1423 |         });
1424 |         break;
1425 |       }
1426 |     }
1427 |   };
1428 | 
1429 |   parser.write(xml).close();
1430 | 
1431 |   return result;
1432 | }
1433 | 
1434 | function appendSegmentToNearestResource(
1435 |   stack: Array<{
1436 |     name: string;
1437 |     segments: ContentSegment[];
1438 |     attributes: Record<string, string>;
1439 |   }>,
1440 |   segment: ContentSegment,
1441 | ) {
1442 |   for (let index = stack.length - 1; index >= 0; index--) {
1443 |     const entry = stack[index];
1444 |     if (
1445 |       entry.name === "string" ||
1446 |       entry.name === "item" ||
1447 |       entry.name === "bool" ||
1448 |       entry.name === "integer"
1449 |     ) {
1450 |       entry.segments.push(segment);
1451 |       return;
1452 |     }
1453 |   }
1454 | }
1455 | 
1456 | function isResourceElementName(
1457 |   value: string | undefined,
1458 | ): value is AndroidResourceType {
1459 |   return (
1460 |     value === "string" ||
1461 |     value === "string-array" ||
1462 |     value === "plurals" ||
1463 |     value === "bool" ||
1464 |     value === "integer"
1465 |   );
1466 | }
1467 | 
1468 | function deepClone<T>(value: T): T {
1469 |   return value === undefined ? value : JSON.parse(JSON.stringify(value));
1470 | }
1471 | 
1472 | function serializeElement(node: any): string {
1473 |   if (!node) {
1474 |     return "";
1475 |   }
1476 | 
1477 |   const name = node["#name"] ?? "resources";
1478 | 
1479 |   if (name === "__text__") {
1480 |     return node._ ?? "";
1481 |   }
1482 | 
1483 |   if (name === "__cdata") {
1484 |     return `<![CDATA[${node._ ?? ""}]]>`;
1485 |   }
1486 | 
1487 |   if (name === "__comment__") {
1488 |     return `<!--${node._ ?? ""}-->`;
1489 |   }
1490 | 
1491 |   const attributes = node.$ ?? {};
1492 |   const attrString = Object.entries(attributes)
1493 |     .map(([key, value]) => ` ${key}="${escapeAttributeValue(String(value))}"`)
1494 |     .join("");
1495 | 
1496 |   const children = Array.isArray(node.$$) ? node.$$ : [];
1497 | 
1498 |   if (children.length === 0) {
1499 |     const textContent = node._ ?? "";
1500 |     return `<${name}${attrString}>${textContent}</${name}>`;
1501 |   }
1502 | 
1503 |   const childContent = children.map(serializeElement).join("");
1504 |   return `<${name}${attrString}>${childContent}</${name}>`;
1505 | }
1506 | 
1507 | function escapeAttributeValue(value: string): string {
1508 |   return value
1509 |     .replace(/&/g, "&amp;")
1510 |     .replace(/"/g, "&quot;")
1511 |     .replace(/</g, "&lt;")
1512 |     .replace(/>/g, "&gt;")
1513 |     .replace(/'/g, "&apos;");
1514 | }
1515 | 
1516 | function decodeAndroidText(value: string): string {
1517 |   return value.replace(/\\'/g, "'");
1518 | }
1519 | 
```
Page 18/20FirstPrevNextLast