This is page 5 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/unlocalizable.spec.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, expect, it } from "vitest";
2 | import createUnlocalizableLoader from "./unlocalizable";
3 |
4 | describe("unlocalizable loader", () => {
5 | const data = {
6 | foo: "bar",
7 | num: 1,
8 | numStr: "1.0",
9 | empty: "",
10 | boolTrue: true,
11 | boolFalse: false,
12 | boolStr: "false",
13 | isoDate: "2025-02-21",
14 | isoDateTime: "2025-02-21T00:00:00.000Z",
15 | bar: "foo",
16 | url: "https://example.com",
17 | systemId: "Ab1cdefghijklmnopqrst2",
18 | };
19 |
20 | it("should remove unlocalizable keys on pull", async () => {
21 | const loader = createUnlocalizableLoader();
22 | loader.setDefaultLocale("en");
23 | const result = await loader.pull("en", data);
24 |
25 | expect(result).toEqual({
26 | foo: "bar",
27 | numStr: "1.0",
28 | boolStr: "false",
29 | bar: "foo",
30 | });
31 | });
32 |
33 | it("should handle unlocalizable keys on push", async () => {
34 | const pushData = {
35 | foo: "bar-es",
36 | bar: "foo-es",
37 | numStr: "2.0",
38 | boolStr: "true",
39 | };
40 |
41 | const loader = createUnlocalizableLoader();
42 | loader.setDefaultLocale("en");
43 | await loader.pull("en", data);
44 | const result = await loader.push("es", pushData);
45 |
46 | expect(result).toEqual({ ...data, ...pushData });
47 | });
48 |
49 | describe("return unlocalizable keys", () => {
50 | describe.each([true, false])("%s", (returnUnlocalizedKeys) => {
51 | it("should return unlocalizable keys on pull", async () => {
52 | const loader = createUnlocalizableLoader(returnUnlocalizedKeys);
53 | loader.setDefaultLocale("en");
54 | const result = await loader.pull("en", data);
55 |
56 | const extraUnlocalizableData = returnUnlocalizedKeys
57 | ? {
58 | unlocalizable: {
59 | num: 1,
60 | empty: "",
61 | boolTrue: true,
62 | boolFalse: false,
63 | isoDate: "2025-02-21",
64 | isoDateTime: "2025-02-21T00:00:00.000Z",
65 | url: "https://example.com",
66 | systemId: "Ab1cdefghijklmnopqrst2",
67 | },
68 | }
69 | : {};
70 |
71 | expect(result).toEqual({
72 | foo: "bar",
73 | numStr: "1.0",
74 | boolStr: "false",
75 | bar: "foo",
76 | ...extraUnlocalizableData,
77 | });
78 | });
79 |
80 | it("should not affect push", async () => {
81 | const pushData = {
82 | foo: "bar-es",
83 | bar: "foo-es",
84 | numStr: "2.0",
85 | boolStr: "true",
86 | };
87 |
88 | const loader = createUnlocalizableLoader(returnUnlocalizedKeys);
89 | loader.setDefaultLocale("en");
90 | await loader.pull("en", data);
91 | const result = await loader.push("es", pushData);
92 |
93 | const expectedData = { ...data, ...pushData };
94 | expect(result).toEqual(expectedData);
95 | });
96 | });
97 | });
98 | });
99 |
```
--------------------------------------------------------------------------------
/integrations/directus/CHANGELOG.md:
--------------------------------------------------------------------------------
```markdown
1 | # @replexica/integration-directus
2 |
3 | ## 0.1.10
4 |
5 | ### Patch Changes
6 |
7 | - [#937](https://github.com/lingodotdev/lingo.dev/pull/937) [`4e5983d`](https://github.com/lingodotdev/lingo.dev/commit/4e5983d7e59ebf9eb529c4b7c1c87689432ac873) Thanks [@devin-ai-integration](https://github.com/apps/devin-ai-integration)! - Update documentation URLs from docs.lingo.dev to lingo.dev/cli and lingo.dev/compiler
8 |
9 | ## 0.1.9
10 |
11 | ### Patch Changes
12 |
13 | - [`fc3cb88`](https://github.com/lingodotdev/lingo.dev/commit/fc3cb8839cbbb574b69087625dd5f97cf37d5d35) Thanks [@vrcprl](https://github.com/vrcprl)! - Updated README file with target languages changes
14 |
15 | ## 0.1.8
16 |
17 | ### Patch Changes
18 |
19 | - [`2571fcd`](https://github.com/lingodotdev/lingo.dev/commit/2571fcdce6e969d9df96526188c9f3f89dd80677) Thanks [@vrcprl](https://github.com/vrcprl)! - Added multimple target languages option
20 |
21 | ## 0.1.7
22 |
23 | ### Patch Changes
24 |
25 | - [`bd7c0a6`](https://github.com/lingodotdev/lingo.dev/commit/bd7c0a62ddfc5144690f6f572667a27d572e521a) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - update `@replexica/sdk` version
26 |
27 | ## 0.1.6
28 |
29 | ### Patch Changes
30 |
31 | - [`e808190`](https://github.com/lingodotdev/lingo.dev/commit/e80819059b89f4a3f69980bab0979432cb7823bf) Thanks [@vrcprl](https://github.com/vrcprl)! - Fixed screenshot
32 |
33 | - Updated dependencies []:
34 | - @replexica/[email protected]
35 |
36 | ## 0.1.5
37 |
38 | ### Patch Changes
39 |
40 | - [`ca7a085`](https://github.com/lingodotdev/lingo.dev/commit/ca7a085033ff31780001db1e6d58d818b60beded) Thanks [@vrcprl](https://github.com/vrcprl)! - Add README
41 |
42 | ## 0.1.4
43 |
44 | ### Patch Changes
45 |
46 | - [`998a4a6`](https://github.com/lingodotdev/lingo.dev/commit/998a4a6267ff8542279a8f6d812d5579e3a78a42) Thanks [@vrcprl](https://github.com/vrcprl)! - Update primary key selection
47 |
48 | ## 0.1.3
49 |
50 | ### Patch Changes
51 |
52 | - [`75253b6`](https://github.com/lingodotdev/lingo.dev/commit/75253b66833b000bf80d6880e92e3c60f5bcd068) Thanks [@vrcprl](https://github.com/vrcprl)! - Update replexica sdk version
53 |
54 | ## 0.1.2
55 |
56 | ### Patch Changes
57 |
58 | - Updated dependencies []:
59 | - @replexica/[email protected]
60 |
61 | ## 0.1.1
62 |
63 | ### Patch Changes
64 |
65 | - [`22490ab`](https://github.com/lingodotdev/lingo.dev/commit/22490ab94f22d8e5769b23dc58d811fc8483f714) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - add Directus integration
66 |
67 | ## 0.1.0
68 |
69 | ### Minor Changes
70 |
71 | - [`03b4506`](https://github.com/lingodotdev/lingo.dev/commit/03b45063f435715967825f70daf3f5bbdb05b9a0) Thanks [@vrcprl](https://github.com/vrcprl)! - Lingo.dev integration for Directus
72 |
73 | ## 0.0.1
74 |
75 | ### Patch Changes
76 |
77 | - [#341](https://github.com/lingodotdev/lingo.dev/pull/341) [`1df47d6`](https://github.com/lingodotdev/lingo.dev/commit/1df47d6095f907e1d756524f5e2cc2e043fdb093) Thanks [@maxprilutskiy](https://github.com/maxprilutskiy)! - empty directus integration package
78 |
```
--------------------------------------------------------------------------------
/scripts/packagist-publish.php:
--------------------------------------------------------------------------------
```php
1 | <?php
2 | /**
3 | * Packagist Publishing Script
4 | *
5 | * This script handles publishing a package to Packagist using the Packagist API.
6 | * It requires the following environment variables:
7 | * - PACKAGIST_USERNAME: The Packagist username
8 | * - PACKAGIST_API_TOKEN: The Packagist API token
9 | * - PACKAGE_NAME: The name of the package to publish (e.g., vendor/package)
10 | *
11 | * @php 7.4
12 | */
13 |
14 | $username = getenv('PACKAGIST_USERNAME');
15 | $apiToken = getenv('PACKAGIST_API_TOKEN');
16 | $packageName = getenv('PACKAGE_NAME');
17 |
18 | if (!$username || !$apiToken || !$packageName) {
19 | echo "Error: Missing required environment variables.\n";
20 | echo "Please ensure PACKAGIST_USERNAME, PACKAGIST_API_TOKEN, and PACKAGE_NAME are set.\n";
21 | exit(1);
22 | }
23 |
24 | echo "Starting Packagist publishing process for package: $packageName\n";
25 |
26 | $checkUrl = "https://packagist.org/packages/$packageName.json";
27 | $ch = curl_init($checkUrl);
28 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
29 | curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
30 | curl_setopt($ch, CURLOPT_HTTPHEADER, [
31 | 'Accept: application/json'
32 | ]);
33 |
34 | echo "Checking if package exists on Packagist...\n";
35 | $response = curl_exec($ch);
36 | $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
37 | curl_close($ch);
38 |
39 | $packageExists = ($httpCode === 200);
40 |
41 | if ($packageExists) {
42 | echo "Package $packageName already exists on Packagist. Updating...\n";
43 | $apiUrl = "https://packagist.org/api/update-package?username=$username&apiToken=$apiToken";
44 | } else {
45 | echo "Package $packageName does not exist on Packagist. Creating new package...\n";
46 | $apiUrl = "https://packagist.org/api/create-package?username=$username&apiToken=$apiToken";
47 | }
48 |
49 | $repoUrl = "https://github.com/lingodotdev/lingo.dev";
50 |
51 | $data = [
52 | 'repository' => [
53 | 'url' => $repoUrl
54 | ]
55 | ];
56 |
57 | $ch = curl_init($apiUrl);
58 |
59 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
60 | curl_setopt($ch, CURLOPT_POST, true);
61 | curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
62 | curl_setopt($ch, CURLOPT_HTTPHEADER, [
63 | 'Content-Type: application/json',
64 | 'Accept: application/json'
65 | ]);
66 |
67 | echo "Sending request to Packagist API ($apiUrl)...\n";
68 | $response = curl_exec($ch);
69 | $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
70 |
71 | if (curl_errno($ch)) {
72 | echo "Error: " . curl_error($ch) . "\n";
73 | curl_close($ch);
74 | exit(1);
75 | }
76 |
77 | curl_close($ch);
78 |
79 | $responseData = json_decode($response, true);
80 |
81 | echo "HTTP Response Code: $httpCode\n";
82 | echo "Response: " . print_r($responseData, true) . "\n";
83 |
84 | if ($httpCode >= 200 && $httpCode < 300) {
85 | echo "Package $packageName successfully " . ($packageExists ? "updated" : "submitted") . " to Packagist!\n";
86 | exit(0);
87 | } else {
88 | echo "Failed to " . ($packageExists ? "update" : "submit") . " package $packageName to Packagist.\n";
89 | exit(1);
90 | }
91 |
```
--------------------------------------------------------------------------------
/packages/cli/i18n.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "version": "1.10",
3 | "locale": {
4 | "source": "en",
5 | "targets": ["es"]
6 | },
7 | "buckets": {
8 | "xliff": {
9 | "include": ["demo/xliff/[locale]/*.xliff"]
10 | },
11 | "android": {
12 | "include": ["demo/android/[locale]/*.xml"]
13 | },
14 | "csv": {
15 | "include": ["demo/csv/example.csv"]
16 | },
17 | "ejs": {
18 | "include": ["demo/ejs/[locale]/*.ejs"]
19 | },
20 | "flutter": {
21 | "include": ["demo/flutter/[locale]/*.arb"]
22 | },
23 | "html": {
24 | "include": ["demo/html/[locale]/*.html"]
25 | },
26 | "json": {
27 | "include": ["demo/json/[locale]/example.json"],
28 | "lockedKeys": ["locked_key_1"],
29 | "ignoredKeys": ["ignored_key_1"]
30 | },
31 | "jsonc": {
32 | "include": ["demo/jsonc/[locale]/example.jsonc"],
33 | "lockedKeys": ["locked_key_1"],
34 | "ignoredKeys": ["ignored_key_1"]
35 | },
36 | "json5": {
37 | "include": ["demo/json5/[locale]/example.json5"]
38 | },
39 | "json-dictionary": {
40 | "include": ["demo/json-dictionary/example.json"]
41 | },
42 | "markdoc": {
43 | "include": ["demo/markdoc/[locale]/*.markdoc"]
44 | },
45 | "markdown": {
46 | "include": ["demo/markdown/[locale]/*.md"],
47 | "exclude": ["demo/markdown/[locale]/ignored.md"]
48 | },
49 | "mdx": {
50 | "include": ["demo/mdx/[locale]/*.mdx"],
51 | "lockedKeys": ["meta/locked_key_1"],
52 | "ignoredKeys": ["meta/ignored_key_1"]
53 | },
54 | "po": {
55 | "include": ["demo/po/[locale]/*.po"]
56 | },
57 | "properties": {
58 | "include": ["demo/properties/[locale]/*.properties"]
59 | },
60 | "srt": {
61 | "include": ["demo/srt/[locale]/*.srt"]
62 | },
63 | "txt": {
64 | "include": ["demo/txt/[locale]/*.txt"]
65 | },
66 | "typescript": {
67 | "include": ["demo/typescript/[locale]/*.ts"],
68 | "lockedKeys": ["forms/locked_key_1"],
69 | "ignoredKeys": ["forms/ignored_key_1"]
70 | },
71 | "vtt": {
72 | "include": ["demo/vtt/[locale]/*.vtt"]
73 | },
74 | "xcode-strings": {
75 | "include": ["demo/xcode-strings/[locale]/*.strings"]
76 | },
77 | "xcode-stringsdict": {
78 | "include": ["demo/xcode-stringsdict/[locale]/*.stringsdict"]
79 | },
80 | "xcode-xcstrings": {
81 | "include": ["demo/xcode-xcstrings/*.xcstrings"],
82 | "lockedKeys": ["api_key"],
83 | "ignoredKeys": ["item_count"]
84 | },
85 | "xcode-xcstrings-v2": {
86 | "include": ["demo/xcode-xcstrings-v2/*.xcstrings"]
87 | },
88 | "xml": {
89 | "include": ["demo/xml/[locale]/*.xml"]
90 | },
91 | "yaml": {
92 | "include": ["demo/yaml/[locale]/*.yml"],
93 | "lockedKeys": ["locked_key_1"],
94 | "ignoredKeys": ["ignored_key_1"]
95 | },
96 | "yaml-root-key": {
97 | "include": ["demo/yaml-root-key/[locale]/*.yml"]
98 | },
99 | "php": {
100 | "include": ["demo/php/[locale]/*.php"]
101 | },
102 | "vue-json": {
103 | "include": ["demo/vue-json/*.vue"]
104 | }
105 | },
106 | "$schema": "https://lingo.dev/schema/i18n.json"
107 | }
108 |
```
--------------------------------------------------------------------------------
/packages/cli/src/cli/utils/observability.ts:
--------------------------------------------------------------------------------
```typescript
1 | import pkg from "node-machine-id";
2 | const { machineIdSync } = pkg;
3 | import https from "https";
4 |
5 | const POSTHOG_API_KEY = "phc_eR0iSoQufBxNY36k0f0T15UvHJdTfHlh8rJcxsfhfXk";
6 | const POSTHOG_HOST = "eu.i.posthog.com";
7 | const POSTHOG_PATH = "/i/v0/e/"; // Correct PostHog capture endpoint
8 | const REQUEST_TIMEOUT_MS = 1000;
9 |
10 | /**
11 | * Sends an analytics event to PostHog using direct HTTPS API.
12 | * This is a fire-and-forget implementation that won't block the process.
13 | *
14 | * @param distinctId - Unique identifier for the user/device
15 | * @param event - Name of the event to track
16 | * @param properties - Additional properties to attach to the event
17 | */
18 | export default function trackEvent(
19 | distinctId: string | null | undefined,
20 | event: string,
21 | properties?: Record<string, any>,
22 | ): void {
23 | // Skip tracking if explicitly disabled or in CI environment
24 | if (process.env.DO_NOT_TRACK === "1") {
25 | return;
26 | }
27 |
28 | // Defer execution to next tick to avoid blocking
29 | setImmediate(() => {
30 | try {
31 | const actualId = distinctId || `device-${machineIdSync()}`;
32 |
33 | // PostHog expects distinct_id at the root level, not nested in properties
34 | const eventData = {
35 | api_key: POSTHOG_API_KEY,
36 | event,
37 | distinct_id: actualId,
38 | properties: {
39 | ...properties,
40 | $lib: "lingo.dev-cli",
41 | $lib_version: process.env.npm_package_version || "unknown",
42 | // Essential debugging context only
43 | node_version: process.version,
44 | is_ci: !!process.env.CI,
45 | debug_enabled: process.env.DEBUG === "true",
46 | },
47 | timestamp: new Date().toISOString(),
48 | };
49 |
50 | const payload = JSON.stringify(eventData);
51 |
52 | const options: https.RequestOptions = {
53 | hostname: POSTHOG_HOST,
54 | path: POSTHOG_PATH,
55 | method: "POST",
56 | headers: {
57 | "Content-Type": "application/json",
58 | "Content-Length": Buffer.byteLength(payload).toString(),
59 | },
60 | timeout: REQUEST_TIMEOUT_MS,
61 | };
62 |
63 | const req = https.request(options);
64 |
65 | // Handle timeout by destroying the request
66 | req.on("timeout", () => {
67 | req.destroy();
68 | });
69 |
70 | // Silently ignore errors to prevent crashes
71 | req.on("error", (error) => {
72 | if (process.env.DEBUG === "true") {
73 | console.error("[Tracking] Error ignored:", error.message);
74 | }
75 | });
76 |
77 | // Send payload and close the request
78 | req.write(payload);
79 | req.end();
80 |
81 | // Ensure cleanup after timeout
82 | setTimeout(() => {
83 | if (!req.destroyed) {
84 | req.destroy();
85 | }
86 | }, REQUEST_TIMEOUT_MS);
87 | } catch (error) {
88 | // Catch-all for any synchronous errors
89 | if (process.env.DEBUG === "true") {
90 | console.error("[Tracking] Failed to send event:", error);
91 | }
92 | }
93 | });
94 | }
95 |
```
--------------------------------------------------------------------------------
/packages/react/src/rsc/utils.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { cookies, headers } from "next/headers";
2 | import { LOCALE_HEADER_NAME, LOCALE_COOKIE_NAME } from "../core";
3 |
4 | /**
5 | * Gets the current locale code from the `"x-lingo-locale"` header.
6 | *
7 | * @returns Promise that resolves to the current locale code, or `"en"` if no header is found.
8 | *
9 | * @example Get locale from headers in a server component
10 | * ```typescript
11 | * import { loadLocaleFromHeaders } from "lingo.dev/react/rsc";
12 | *
13 | * export default async function ServerComponent() {
14 | * const locale = await loadLocaleFromHeaders();
15 | * return <div>Current locale: {locale}</div>;
16 | * }
17 | * ```
18 | */
19 | export async function loadLocaleFromHeaders() {
20 | const requestHeaders = await headers();
21 | const result = requestHeaders.get(LOCALE_HEADER_NAME);
22 | return result;
23 | }
24 |
25 | /**
26 | * Gets the current locale code from the `"lingo-locale"` cookie.
27 | *
28 | * @returns Promise that resolves to the current locale code, or `"en"` if no cookie is found.
29 | *
30 | * @example Get locale from cookies in a server component
31 | * ```typescript
32 | * import { loadLocaleFromCookies } from "lingo.dev/react/rsc";
33 | *
34 | * export default async function ServerComponent() {
35 | * const locale = await loadLocaleFromCookies();
36 | * return <div>User's saved locale: {locale}</div>;
37 | * }
38 | * ```
39 | */
40 | export async function loadLocaleFromCookies() {
41 | const requestCookies = await cookies();
42 | const result = requestCookies.get(LOCALE_COOKIE_NAME)?.value || "en";
43 | return result;
44 | }
45 |
46 | /**
47 | * Sets the current locale in the `"lingo-locale"` cookie.
48 | *
49 | * @param locale - The locale code to store in the cookie.
50 | *
51 | * @example Set locale in a server action
52 | * ```typescript
53 | * import { setLocaleInCookies } from "lingo.dev/react/rsc";
54 | *
55 | * export async function changeLocale(locale: string) {
56 | * "use server";
57 | * await setLocaleInCookies(locale);
58 | * redirect("/");
59 | * }
60 | * ```
61 | */
62 | export async function setLocaleInCookies(locale: string) {
63 | const requestCookies = await cookies();
64 | requestCookies.set(LOCALE_COOKIE_NAME, locale);
65 | }
66 |
67 | /**
68 | * Loads the dictionary for the current locale.
69 | *
70 | * The current locale is determined by the `"lingo-locale"` cookie.
71 | *
72 | * @param loader - A callback function that loads the dictionary for the current locale.
73 | *
74 | * @returns Promise that resolves to the dictionary object containing localized content.
75 | *
76 | * @example Load dictionary from request in a server component
77 | * ```typescript
78 | * import { loadDictionaryFromRequest, loadDictionary } from "lingo.dev/react/rsc";
79 | *
80 | * export default async function ServerComponent() {
81 | * const dictionary = await loadDictionaryFromRequest(loadDictionary);
82 | * return <div>{dictionary.welcome}</div>;
83 | * }
84 | * ```
85 | */
86 | export async function loadDictionaryFromRequest(
87 | loader: (locale: string) => Promise<any>,
88 | ) {
89 | const locale = await loadLocaleFromCookies();
90 | return loader(locale);
91 | }
92 |
```
--------------------------------------------------------------------------------
/packages/react/src/react-router/loader.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { LOCALE_COOKIE_NAME, getDictionary } from "../core";
2 |
3 | /**
4 | * A placeholder function for loading dictionaries that contain localized content.
5 | *
6 | * This function:
7 | *
8 | * - Should be used in React Router and Remix applications
9 | * - Should be passed into the `LingoProvider` component
10 | * - Is transformed into functional code by Lingo.dev Compiler
11 | *
12 | * @param requestOrExplicitLocale - Either a `Request` object (from loader functions) or an explicit locale string.
13 | *
14 | * @returns Promise that resolves to the dictionary object containing localized content.
15 | *
16 | * @example Use in a React Router application
17 | * ```tsx
18 | * import { LingoProvider } from "lingo.dev/react/client";
19 | * import { loadDictionary } from "lingo.dev/react/react-router";
20 | * import {
21 | * Links,
22 | * Meta,
23 | * Outlet,
24 | * Scripts,
25 | * ScrollRestoration,
26 | * useLoaderData,
27 | * type LoaderFunctionArgs,
28 | * } from "react-router";
29 | * import "./app.css";
30 | *
31 | * export const loader = async ({ request }: LoaderFunctionArgs) => {
32 | * return {
33 | * lingoDictionary: await loadDictionary(request),
34 | * };
35 | * };
36 | *
37 | * export function Layout({ children }: { children: React.ReactNode }) {
38 | * const { lingoDictionary } = useLoaderData<typeof loader>();
39 | *
40 | * return (
41 | * <LingoProvider dictionary={lingoDictionary}>
42 | * <html lang="en">
43 | * <head>
44 | * <meta charSet="utf-8" />
45 | * <meta name="viewport" content="width=device-width, initial-scale=1" />
46 | * <Meta />
47 | * <Links />
48 | * </head>
49 | * <body>
50 | * {children}
51 | * <ScrollRestoration />
52 | * <Scripts />
53 | * </body>
54 | * </html>
55 | * </LingoProvider>
56 | * );
57 | * }
58 | *
59 | * export default function App() {
60 | * return <Outlet />;
61 | * }
62 | * ```
63 | */
64 | export const loadDictionary = async (
65 | requestOrExplicitLocale: Request | string,
66 | ): Promise<any> => {
67 | return null;
68 | };
69 |
70 | function loadLocaleFromCookies(request: Request) {
71 | // it's a Request, so get the Cookie header
72 | const cookieHeaderValue = request.headers.get("Cookie");
73 |
74 | // there's no Cookie header, so return null
75 | if (!cookieHeaderValue) {
76 | return null;
77 | }
78 |
79 | // get the lingo-locale cookie
80 | const cookiePrefix = `${LOCALE_COOKIE_NAME}=`;
81 | const cookie = cookieHeaderValue
82 | .split(";")
83 | .find((cookie) => cookie.trim().startsWith(cookiePrefix));
84 |
85 | // there's no lingo-locale cookie, so return null
86 | if (!cookie) {
87 | return null;
88 | }
89 |
90 | // extract the locale value from the cookie
91 | return cookie.trim().substring(cookiePrefix.length);
92 | }
93 |
94 | export async function loadDictionary_internal(
95 | requestOrExplicitLocale: Request | string,
96 | dictionaryLoaders: Record<string, () => Promise<any>>,
97 | ) {
98 | // gets the locale (falls back to "en")
99 | const locale =
100 | typeof requestOrExplicitLocale === "string"
101 | ? requestOrExplicitLocale
102 | : loadLocaleFromCookies(requestOrExplicitLocale);
103 |
104 | return getDictionary(locale, dictionaryLoaders);
105 | }
106 |
```
--------------------------------------------------------------------------------
/packages/cli/src/cli/loaders/csv.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { parse } from "csv-parse/sync";
2 | import { stringify } from "csv-stringify/sync";
3 | import _ from "lodash";
4 | import { ILoader } from "./_types";
5 | import { composeLoaders, createLoader } from "./_utils";
6 |
7 | /**
8 | * Tries to detect the key column name from a csvString.
9 | *
10 | * Current logic: get first cell > 'KEY' fallback if empty
11 | */
12 | export function detectKeyColumnName(csvString: string) {
13 | const row: string[] | undefined = parse(csvString)[0];
14 | const firstColumn = row?.[0]?.trim();
15 | return firstColumn || "KEY";
16 | }
17 |
18 | export default function createCsvLoader() {
19 | return composeLoaders(_createCsvLoader(), createPullOutputCleaner());
20 | }
21 |
22 | type InternalTransferState = {
23 | keyColumnName: string;
24 | inputParsed: Record<string, any>[];
25 | items: Record<string, string>;
26 | };
27 |
28 | function _createCsvLoader(): ILoader<string, InternalTransferState> {
29 | return createLoader({
30 | async pull(locale, input) {
31 | const keyColumnName = detectKeyColumnName(
32 | input.split("\n").find((l) => l.length)!,
33 | );
34 | const inputParsed = parse(input, {
35 | columns: true,
36 | skip_empty_lines: true,
37 | relax_column_count_less: true,
38 | }) as Record<string, any>[];
39 |
40 | const items: Record<string, string> = {};
41 |
42 | // Assign keys that already have translation so AI doesn't re-generate it.
43 | _.forEach(inputParsed, (row) => {
44 | const key = row[keyColumnName];
45 | if (key && row[locale] && row[locale].trim() !== "") {
46 | items[key] = row[locale];
47 | }
48 | });
49 |
50 | return {
51 | inputParsed,
52 | keyColumnName,
53 | items,
54 | };
55 | },
56 | async push(locale, { inputParsed, keyColumnName, items }) {
57 | const columns =
58 | inputParsed.length > 0
59 | ? Object.keys(inputParsed[0])
60 | : [keyColumnName, locale];
61 | if (!columns.includes(locale)) {
62 | columns.push(locale);
63 | }
64 |
65 | const updatedRows = inputParsed.map((row) => ({
66 | ...row,
67 | [locale]: items[row[keyColumnName]] || row[locale] || "",
68 | }));
69 | const existingKeys = new Set(
70 | inputParsed.map((row) => row[keyColumnName]),
71 | );
72 |
73 | Object.entries(items).forEach(([key, value]) => {
74 | if (!existingKeys.has(key)) {
75 | const newRow: Record<string, string> = {
76 | [keyColumnName]: key,
77 | ...Object.fromEntries(columns.map((column) => [column, ""])),
78 | };
79 | newRow[locale] = value;
80 | updatedRows.push(newRow);
81 | }
82 | });
83 |
84 | return stringify(updatedRows, {
85 | header: true,
86 | columns,
87 | });
88 | },
89 | });
90 | }
91 |
92 | /**
93 | * This is a simple extra loader that is used to clean the data written to lockfile
94 | */
95 | function createPullOutputCleaner(): ILoader<
96 | InternalTransferState,
97 | Record<string, string>
98 | > {
99 | return createLoader({
100 | async pull(_locale, input) {
101 | return input.items;
102 | },
103 | async push(_locale, data, _oI, _oL, pullInput) {
104 | return { ...pullInput!, items: data };
105 | },
106 | });
107 | }
108 |
```
--------------------------------------------------------------------------------
/packages/cli/src/cli/loaders/locked-patterns.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { ILoader } from "./_types";
2 | import { createLoader } from "./_utils";
3 | import { md5 } from "../utils/md5";
4 |
5 | /**
6 | * Extracts content matching regex patterns and replaces it with placeholders.
7 | * Returns the transformed content and a mapping of placeholders to original content.
8 | */
9 | function extractLockedPatterns(
10 | content: string,
11 | patterns: string[] = [],
12 | ): {
13 | content: string;
14 | lockedPlaceholders: Record<string, string>;
15 | } {
16 | let finalContent = content;
17 | const lockedPlaceholders: Record<string, string> = {};
18 |
19 | if (!patterns || patterns.length === 0) {
20 | return { content: finalContent, lockedPlaceholders };
21 | }
22 |
23 | for (const patternStr of patterns) {
24 | try {
25 | const pattern = new RegExp(patternStr, "gm");
26 | const matches = Array.from(finalContent.matchAll(pattern));
27 |
28 | for (const match of matches) {
29 | const matchedText = match[0];
30 | const matchHash = md5(matchedText);
31 | const placeholder = `---LOCKED-PATTERN-${matchHash}---`;
32 |
33 | lockedPlaceholders[placeholder] = matchedText;
34 | finalContent = finalContent.replace(matchedText, placeholder);
35 | }
36 | } catch (error) {
37 | console.warn(`Invalid regex pattern: ${patternStr}`);
38 | }
39 | }
40 |
41 | return {
42 | content: finalContent,
43 | lockedPlaceholders,
44 | };
45 | }
46 |
47 | /**
48 | * Creates a loader that preserves content matching regex patterns during translation.
49 | *
50 | * This loader extracts content matching the provided regex patterns and replaces it
51 | * with placeholders before translation. After translation, the placeholders are
52 | * restored with the original content.
53 | *
54 | * This is useful for preserving technical terms, code snippets, URLs, template
55 | * variables, and other non-translatable content within translatable files.
56 | *
57 | * Works with any string-based format (JSON, YAML, XML, Markdown, HTML, etc.).
58 | * Note: For structured formats (JSON, XML, YAML), ensure patterns only match
59 | * content within values, not structural syntax, to avoid breaking parsing.
60 | *
61 | * @param defaultPatterns - Array of regex pattern strings to match and preserve
62 | * @returns A loader that handles pattern locking/unlocking
63 | */
64 | export default function createLockedPatternsLoader(
65 | defaultPatterns?: string[],
66 | ): ILoader<string, string> {
67 | return createLoader({
68 | async pull(locale, input, initCtx, originalLocale) {
69 | const patterns = defaultPatterns || [];
70 |
71 | const { content } = extractLockedPatterns(input || "", patterns);
72 |
73 | return content;
74 | },
75 |
76 | async push(
77 | locale,
78 | data,
79 | originalInput,
80 | originalLocale,
81 | pullInput,
82 | pullOutput,
83 | ) {
84 | const patterns = defaultPatterns || [];
85 |
86 | if (!pullInput) {
87 | return data;
88 | }
89 |
90 | const { lockedPlaceholders } = extractLockedPatterns(
91 | pullInput as string,
92 | patterns,
93 | );
94 |
95 | let result = data;
96 | for (const [placeholder, original] of Object.entries(
97 | lockedPlaceholders,
98 | )) {
99 | result = result.replaceAll(placeholder, original);
100 | }
101 |
102 | return result;
103 | },
104 | });
105 | }
106 |
```
--------------------------------------------------------------------------------
/packages/compiler/src/lib/lcp/api/xml2obj.spec.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect } from "vitest";
2 | import { xml2obj, obj2xml } from "./xml2obj";
3 |
4 | function normalize(xml: string) {
5 | return xml.replace(/\s+/g, " ").trim();
6 | }
7 |
8 | describe("xml2obj / obj2xml", () => {
9 | it("should convert simple XML to object with key attributes", () => {
10 | const xml = `
11 | <object>
12 | <object key="user">
13 | <value key="id">123</value>
14 | <value key="dataValue">abc</value>
15 | <value key="firstName">John</value>
16 | <value key="lastName">Doe</value>
17 | </object>
18 | </object>
19 | `;
20 | const obj = xml2obj(xml);
21 | expect(obj).toEqual({
22 | user: {
23 | id: 123,
24 | dataValue: "abc",
25 | firstName: "John",
26 | lastName: "Doe",
27 | },
28 | });
29 | });
30 |
31 | it("should preserve complex structures through round-trip conversion", () => {
32 | const original = {
33 | root: {
34 | id: 123,
35 | name: "John & Jane <> \" '",
36 | notes: "Line1\nLine2",
37 | isActive: true,
38 | tags: {
39 | tag: ["a & b", "c < d"],
40 | },
41 | nestedObj: {
42 | childId: 456,
43 | weirdSymbols: "@#$%^&*()_+",
44 | },
45 | items: {
46 | item: [
47 | { keyOne: "value1", keyTwo: "value2" },
48 | { keyOne: "value3", keyTwo: "value4" },
49 | ],
50 | },
51 | },
52 | } as const;
53 |
54 | const result = xml2obj(obj2xml(original));
55 | expect(result).toEqual(original);
56 | });
57 |
58 | it("should handle empty elements, arrays and self-closing tags", () => {
59 | const original = `
60 | <object>
61 | <value key="products" />
62 | <array key="prices">
63 | <value>1.99</value>
64 | <value>9.99</value>
65 | </array>
66 | </object>
67 | `;
68 | const expected = {
69 | products: "",
70 | prices: [1.99, 9.99],
71 | };
72 | expect(xml2obj(original)).toEqual(expected);
73 | });
74 |
75 | it("should correctly escape special characters when building XML", () => {
76 | const original = { message: "5 < 6 & 7 > 4" } as const;
77 | const result = xml2obj(obj2xml(original));
78 | expect(result).toEqual(original);
79 | });
80 |
81 | it("check 1", () => {
82 | const original = `<?xml version="1.0" encoding="UTF-8"?>
83 | <object>
84 | <value key="version">0.1.1</value>
85 | <value key="locale">ja</value>
86 | <object key="files">
87 | <object key="routes/($locale).z.tsx">
88 | <object key="entries">
89 | <value key="1/declaration/body/3/argument"><element:select><element:option>使用済み</element:option><element:option>合計</element:option></element:select> 🚀 あなたの使用状況: {wordType} {subscription.words[wordType]}</value>
90 | </object>
91 | </object>
92 | </object>
93 | </object>`;
94 |
95 | const result = xml2obj(original);
96 | expect(result).toEqual({
97 | version: "0.1.1",
98 | locale: "ja",
99 | files: {
100 | "routes/($locale).z.tsx": {
101 | entries: {
102 | "1/declaration/body/3/argument":
103 | "<element:select><element:option>使用済み</element:option><element:option>合計</element:option></element:select> 🚀 あなたの使用状況: {wordType} {subscription.words[wordType]}",
104 | },
105 | },
106 | },
107 | });
108 | });
109 | });
110 |
```
--------------------------------------------------------------------------------
/packages/cli/src/cli/cmd/show/_shared-key-command.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { resolveOverriddenLocale, I18nConfig } from "@lingo.dev/_spec";
2 | import createBucketLoader from "../../loaders";
3 | import {
4 | matchesKeyPattern,
5 | formatDisplayValue,
6 | } from "../../utils/key-matching";
7 |
8 | export type KeyFilterType = "lockedKeys" | "ignoredKeys";
9 |
10 | export interface KeyCommandOptions {
11 | bucket?: string;
12 | }
13 |
14 | export interface KeyCommandConfig {
15 | filterType: KeyFilterType;
16 | displayName: string; // e.g., "locked", "ignored"
17 | }
18 |
19 | export async function executeKeyCommand(
20 | i18nConfig: I18nConfig,
21 | buckets: any[],
22 | options: KeyCommandOptions,
23 | config: KeyCommandConfig,
24 | ): Promise<void> {
25 | let hasAnyKeys = false;
26 |
27 | for (const bucket of buckets) {
28 | // Filter by bucket name if specified
29 | if (options.bucket && bucket.type !== options.bucket) {
30 | continue;
31 | }
32 |
33 | // Skip buckets without the specified key patterns
34 | const keyPatterns = bucket[config.filterType];
35 | if (!keyPatterns || keyPatterns.length === 0) {
36 | continue;
37 | }
38 |
39 | hasAnyKeys = true;
40 |
41 | console.log(`\nBucket: ${bucket.type}`);
42 | console.log(
43 | `${capitalize(config.displayName)} key patterns: ${keyPatterns.join(", ")}`,
44 | );
45 |
46 | for (const bucketConfig of bucket.paths) {
47 | const sourceLocale = resolveOverriddenLocale(
48 | i18nConfig.locale.source,
49 | bucketConfig.delimiter,
50 | );
51 | const sourcePath = bucketConfig.pathPattern.replace(
52 | /\[locale\]/g,
53 | sourceLocale,
54 | );
55 |
56 | try {
57 | // Create a loader to read the source file
58 | const loader = createBucketLoader(
59 | bucket.type,
60 | bucketConfig.pathPattern,
61 | {
62 | defaultLocale: sourceLocale,
63 | injectLocale: bucket.injectLocale,
64 | },
65 | [], // Don't apply any filtering when reading
66 | [],
67 | [],
68 | );
69 | loader.setDefaultLocale(sourceLocale);
70 |
71 | // Read the source file content
72 | const data = await loader.pull(sourceLocale);
73 |
74 | if (!data || Object.keys(data).length === 0) {
75 | continue;
76 | }
77 |
78 | // Filter keys that match the patterns
79 | const matchedEntries = Object.entries(data).filter(([key]) =>
80 | matchesKeyPattern(key, keyPatterns),
81 | );
82 |
83 | if (matchedEntries.length > 0) {
84 | console.log(`\nMatches in ${sourcePath}:`);
85 | for (const [key, value] of matchedEntries) {
86 | const displayValue = formatDisplayValue(value);
87 | console.log(` - ${key}: ${displayValue}`);
88 | }
89 | console.log(
90 | `Total: ${matchedEntries.length} ${config.displayName} key(s)`,
91 | );
92 | }
93 | } catch (error: any) {
94 | console.error(` Error reading ${sourcePath}: ${error.message}`);
95 | }
96 | }
97 | }
98 |
99 | if (!hasAnyKeys) {
100 | if (options.bucket) {
101 | console.log(
102 | `No ${config.displayName} keys configured for bucket: ${options.bucket}`,
103 | );
104 | } else {
105 | console.log(`No ${config.displayName} keys configured in any bucket.`);
106 | }
107 | }
108 | }
109 |
110 | function capitalize(str: string): string {
111 | return str.charAt(0).toUpperCase() + str.slice(1);
112 | }
113 |
```
--------------------------------------------------------------------------------
/packages/cli/src/cli/loaders/_utils.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { ILoader, ILoaderDefinition } from "./_types";
2 |
3 | export function composeLoaders(
4 | ...loaders: ILoader<any, any, any>[]
5 | ): ILoader<any, any> {
6 | return {
7 | init: async () => {
8 | for (const loader of loaders) {
9 | await loader.init?.();
10 | }
11 | },
12 | setDefaultLocale(locale: string) {
13 | for (const loader of loaders) {
14 | loader.setDefaultLocale?.(locale);
15 | }
16 | return this;
17 | },
18 | pull: async (locale, input) => {
19 | let result: any = input;
20 | for (let i = 0; i < loaders.length; i++) {
21 | result = await loaders[i].pull(locale, result);
22 | }
23 | return result;
24 | },
25 | push: async (locale, data) => {
26 | let result: any = data;
27 | for (let i = loaders.length - 1; i >= 0; i--) {
28 | result = await loaders[i].push(locale, result);
29 | }
30 | return result;
31 | },
32 | pullHints: async (originalInput) => {
33 | let result: any = originalInput;
34 | for (let i = 0; i < loaders.length; i++) {
35 | const subResult = await loaders[i].pullHints?.(result);
36 | if (subResult) {
37 | result = subResult;
38 | }
39 | }
40 | return result;
41 | },
42 | };
43 | }
44 |
45 | export function createLoader<I, O, C>(
46 | lDefinition: ILoaderDefinition<I, O, C>,
47 | ): ILoader<I, O, C> {
48 | const state = {
49 | defaultLocale: undefined as string | undefined,
50 | originalInput: undefined as I | undefined | null,
51 | pullInput: undefined as I | undefined | null,
52 | pullOutput: undefined as O | undefined | null,
53 | initCtx: undefined as C | undefined,
54 | };
55 | return {
56 | async init() {
57 | if (state.initCtx) {
58 | return state.initCtx;
59 | }
60 | state.initCtx = await lDefinition.init?.();
61 | return state.initCtx as C;
62 | },
63 | setDefaultLocale(locale) {
64 | if (state.defaultLocale) {
65 | throw new Error("Default locale already set");
66 | }
67 | state.defaultLocale = locale;
68 | return this;
69 | },
70 | async pullHints() {
71 | return lDefinition.pullHints?.(state.originalInput!);
72 | },
73 | async pull(locale, input) {
74 | if (!state.defaultLocale) {
75 | throw new Error("Default locale not set");
76 | }
77 | if (state.originalInput === undefined && locale !== state.defaultLocale) {
78 | throw new Error("The first pull must be for the default locale");
79 | }
80 | if (locale === state.defaultLocale) {
81 | state.originalInput = input || null;
82 | }
83 |
84 | state.pullInput = input;
85 | const result = await lDefinition.pull(
86 | locale,
87 | input,
88 | state.initCtx!,
89 | state.defaultLocale,
90 | state.originalInput!,
91 | );
92 | state.pullOutput = result;
93 |
94 | return result;
95 | },
96 | async push(locale, data) {
97 | if (!state.defaultLocale) {
98 | throw new Error("Default locale not set");
99 | }
100 | if (state.originalInput === undefined) {
101 | throw new Error("Cannot push data without pulling first");
102 | }
103 |
104 | const pushResult = await lDefinition.push(
105 | locale,
106 | data,
107 | state.originalInput,
108 | state.defaultLocale,
109 | state.pullInput!,
110 | state.pullOutput!,
111 | );
112 | return pushResult;
113 | },
114 | };
115 | }
116 |
```
--------------------------------------------------------------------------------
/.claude/commands/analyze-bucket-type.md:
--------------------------------------------------------------------------------
```markdown
1 | ---
2 | argument-hint: <bucket-type>
3 | description: Analyze a bucket type implementation to identify all behaviors and configurations
4 | ---
5 |
6 | Given the bucket type ID "$ARGUMENTS" (e.g., "json", "mdx", "typescript"), analyze the implementation code to identify ALL bucket-specific behaviors, configurations, and characteristics.
7 |
8 | ## Instructions
9 |
10 | 1. **Locate where this bucket type is processed** in the codebase by searching for the bucket type string. Start with the main loader composition/pipeline code.
11 |
12 | 2. **Trace the complete execution pipeline** for this bucket:
13 |
14 | - List every function/loader in the processing chain, in order
15 | - For each function/loader, read its implementation to understand:
16 | - Input parameters it receives
17 | - Transformations it performs on the data
18 | - Output format it produces
19 | - Any side effects or file operations
20 |
21 | 3. **Identify configuration parameters** by:
22 |
23 | - Finding which variables are passed into the loaders (e.g., lockedKeys, ignoredKeys)
24 | - Tracing these variables back to their source (configuration parsing)
25 | - Determining if they're bucket-specific or universal
26 |
27 | 4. **Analyze file I/O behavior**:
28 |
29 | - How are file paths constructed?
30 | - Does the path pattern contain locale placeholders that would create separate files?
31 | - What file operations are performed (read, write, create, delete)?
32 | - Are files overwritten or are new files created?
33 | - **IMPORTANT**: Note that "overwrites existing files completely" and "[locale] placeholder support" are mutually exclusive in practice:
34 | - If a bucket type stores all locales in a single file (like CSV with columns per locale), it overwrites that single file and does NOT support `[locale]` placeholders
35 | - If a bucket type creates separate files per locale using `[locale]` placeholders, each locale file is overwritten individually
36 | - Clarify which pattern the bucket type follows
37 |
38 | 5. **Examine data transformation logic**:
39 |
40 | - How is the file content parsed?
41 | - What internal data structures are used?
42 | - How is the data serialized back to file format?
43 | - Are there any format-preserving mechanisms?
44 |
45 | 6. **Identify special behaviors** by examining:
46 |
47 | - Conditional logic specific to this bucket
48 | - Error handling unique to this format
49 | - Any validation or normalization steps
50 | - Interactions between multiple loaders in the pipeline
51 |
52 | 7. **Determine constraints and capabilities**:
53 |
54 | - What data types/structures are supported?
55 | - Are there any size or complexity limitations?
56 | - What happens with edge cases (empty files, malformed content)?
57 |
58 | ## Required Depth
59 |
60 | - Read the ACTUAL implementation of each loader/function
61 | - Follow all function calls to understand the complete flow
62 | - Don't make assumptions - verify behavior in the code
63 | - Consider the order of operations in the pipeline
64 |
65 | ## Output Format
66 |
67 | List all findings categorized as:
68 |
69 | - Configuration parameters (with their types and defaults)
70 | - Processing pipeline (ordered list of transformations)
71 | - File handling behavior
72 | - Data transformation characteristics
73 | - Special capabilities or limitations
74 | - Edge case handling
75 |
```
--------------------------------------------------------------------------------
/packages/spec/src/config.spec.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect } from "vitest";
2 | import {
3 | parseI18nConfig,
4 | defaultConfig,
5 | LATEST_CONFIG_DEFINITION,
6 | } from "./config";
7 |
8 | // Helper function to create a v0 config
9 | const createV0Config = () => ({
10 | version: 0,
11 | });
12 |
13 | // Helper function to create a v1 config
14 | const createV1Config = () => ({
15 | version: 1,
16 | locale: {
17 | source: "en",
18 | targets: ["es"],
19 | },
20 | buckets: {
21 | "src/ui/[locale]/.json": "json",
22 | "src/blog/[locale]/*.md": "markdown",
23 | },
24 | });
25 |
26 | // Helper function to create a v1.1 config
27 | const createV1_1Config = () => ({
28 | version: 1.1,
29 | locale: {
30 | source: "en",
31 | targets: ["es", "fr", "pt-PT", "pt_BR"],
32 | },
33 | buckets: {
34 | json: {
35 | include: ["src/ui/[locale]/.json"],
36 | },
37 | markdown: {
38 | include: ["src/blog/[locale]/*.md"],
39 | exclude: ["src/blog/[locale]/drafts.md"],
40 | },
41 | },
42 | });
43 |
44 | const createV1_2Config = () => ({
45 | ...createV1_1Config(),
46 | version: 1.2,
47 | });
48 |
49 | const createV1_3Config = () => ({
50 | ...createV1_2Config(),
51 | version: 1.3,
52 | });
53 |
54 | const createV1_4Config = () => ({
55 | ...createV1_3Config(),
56 | version: 1.4,
57 | $schema: "https://lingo.dev/schema/i18n.json",
58 | });
59 |
60 | const createInvalidLocaleConfig = () => ({
61 | version: 1,
62 | locale: {
63 | source: "bbbb",
64 | targets: ["es", "aaaa"],
65 | },
66 | buckets: {
67 | "src/ui/[locale]/.json": "json",
68 | "src/blog/[locale]/*.md": "markdown",
69 | },
70 | });
71 |
72 | describe("I18n Config Parser", () => {
73 | it("should upgrade v0 config to latest version", () => {
74 | const v0Config = createV0Config();
75 | const result = parseI18nConfig(v0Config);
76 |
77 | expect(result["$schema"]).toBeDefined();
78 | expect(result.version).toBe(LATEST_CONFIG_DEFINITION.defaultValue.version);
79 | expect(result.locale).toEqual(defaultConfig.locale);
80 | expect(result.buckets).toEqual({});
81 | });
82 |
83 | it("should upgrade v1 config to latest version", () => {
84 | const v1Config = createV1Config();
85 | const result = parseI18nConfig(v1Config);
86 |
87 | expect(result["$schema"]).toBeDefined();
88 | expect(result.version).toBe(LATEST_CONFIG_DEFINITION.defaultValue.version);
89 | expect(result.locale).toEqual(v1Config.locale);
90 | expect(result.buckets).toEqual({
91 | json: {
92 | include: ["src/ui/[locale]/.json"],
93 | },
94 | markdown: {
95 | include: ["src/blog/[locale]/*.md"],
96 | },
97 | });
98 | });
99 |
100 | it("should handle empty config and use defaults", () => {
101 | const emptyConfig = {};
102 | const result = parseI18nConfig(emptyConfig);
103 |
104 | expect(result).toEqual(defaultConfig);
105 | });
106 |
107 | it("should ignore extra fields in the config", () => {
108 | const configWithExtra = {
109 | ...createV1_4Config(),
110 | extraField: "should be ignored",
111 | };
112 | const result = parseI18nConfig(configWithExtra);
113 |
114 | expect(result).not.toHaveProperty("extraField");
115 | expect(result).toEqual(createV1_4Config());
116 | });
117 |
118 | it("should throw an error for unsupported locales", () => {
119 | const invalidLocaleConfig = createInvalidLocaleConfig();
120 | expect(() => parseI18nConfig(invalidLocaleConfig)).toThrow(
121 | `\nUnsupported locale: ${invalidLocaleConfig.locale.source}\nUnsupported locale: ${invalidLocaleConfig.locale.targets[1]}`,
122 | );
123 | });
124 | });
125 |
```
--------------------------------------------------------------------------------
/packages/compiler/src/utils/jsx-content-whitespace.spec.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect } from "vitest";
2 | import { extractJsxContent } from "./jsx-content";
3 | import * as t from "@babel/types";
4 | import traverse, { NodePath } from "@babel/traverse";
5 | import { parse } from "@babel/parser";
6 |
7 | describe("Whitespace Issue Test", () => {
8 | function parseJSX(code: string): t.File {
9 | return parse(code, {
10 | plugins: ["jsx"],
11 | sourceType: "module",
12 | });
13 | }
14 |
15 | function getJSXElementPath(code: string): NodePath<t.JSXElement> {
16 | const ast = parseJSX(code);
17 | let result: NodePath<t.JSXElement>;
18 |
19 | traverse(ast, {
20 | JSXElement(path) {
21 | result = path;
22 | path.stop();
23 | },
24 | });
25 |
26 | return result!;
27 | }
28 |
29 | it("should preserve leading space in nested elements", () => {
30 | const path = getJSXElementPath(`
31 | <h1 className="text-5xl md:text-7xl font-bold text-white mb-6 leading-tight">
32 | Hello World
33 | <span className="bg-gradient-to-r from-purple-400 via-pink-400 to-yellow-400 bg-clip-text text-transparent"> From Lingo.dev Compiler</span>
34 | </h1>
35 | `);
36 |
37 | const content = extractJsxContent(path);
38 | console.log("Extracted content:", JSON.stringify(content));
39 |
40 | // Let's also check the raw JSX structure to understand what's happening
41 | let jsxTexts: string[] = [];
42 | path.traverse({
43 | JSXText(textPath) {
44 | jsxTexts.push(JSON.stringify(textPath.node.value));
45 | },
46 | });
47 | console.log("JSXText nodes found:", jsxTexts);
48 |
49 | // The span should have " From Lingo.dev Compiler" with the leading space
50 | expect(content).toContain(
51 | "<element:span> From Lingo.dev Compiler</element:span>",
52 | );
53 | });
54 |
55 | it("should handle explicit whitespace correctly", () => {
56 | const path = getJSXElementPath(`
57 | <div>
58 | Hello{" "}
59 | <span> World</span>
60 | </div>
61 | `);
62 |
63 | const content = extractJsxContent(path);
64 | console.log("Explicit whitespace test:", JSON.stringify(content));
65 |
66 | // Should preserve both the explicit space and the leading space in span
67 | expect(content).toContain("Hello <element:span> World</element:span>");
68 | });
69 |
70 | it("should preserve space before nested bold element like in HeroSubtitle", () => {
71 | const path = getJSXElementPath(`
72 | <p className="text-lg sm:text-xl text-gray-600 mb-10 max-w-xl mx-auto leading-relaxed">
73 | Localize your React app in every language in minutes. Scale to millions
74 | <b> from day one</b>.
75 | </p>
76 | `);
77 |
78 | const content = extractJsxContent(path);
79 | console.log("HeroSubtitle test content:", JSON.stringify(content));
80 |
81 | // Let's also check the raw JSX structure
82 | let jsxTexts: string[] = [];
83 | path.traverse({
84 | JSXText(textPath) {
85 | jsxTexts.push(JSON.stringify(textPath.node.value));
86 | },
87 | });
88 | console.log("HeroSubtitle JSXText nodes found:", jsxTexts);
89 |
90 | // The bold element should have " from day one" with the leading space
91 | expect(content).toContain("<element:b> from day one</element:b>");
92 | // The full content should preserve the space between "millions" and the bold element
93 | expect(content).toContain("millions <element:b> from day one</element:b>");
94 | });
95 | });
96 |
```
--------------------------------------------------------------------------------
/packages/cli/src/cli/loaders/csv.spec.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, expect, it } from "vitest";
2 | import { parse } from "csv-parse/sync";
3 | import createCsvLoader from "./csv";
4 |
5 | // Helper to build CSV strings easily
6 | function buildCsv(rows: string[][]): string {
7 | return rows.map((r) => r.join(",")).join("\n");
8 | }
9 |
10 | describe("csv loader", () => {
11 | const sampleCsv = buildCsv([
12 | ["id", "en", "es"],
13 | ["hello", "Hello", "Hola"],
14 | ["bye", "Bye", "Adiós"],
15 | ["unused", "", "Sin uso"],
16 | ]);
17 |
18 | it("pull should extract translation map for the requested locale and skip empty values", async () => {
19 | const loader = createCsvLoader();
20 | loader.setDefaultLocale("en");
21 |
22 | const enResult = await loader.pull("en", sampleCsv);
23 | expect(enResult).toEqual({ hello: "Hello", bye: "Bye" });
24 |
25 | const esResult = await loader.pull("es", sampleCsv);
26 | expect(esResult).toEqual({
27 | hello: "Hola",
28 | bye: "Adiós",
29 | unused: "Sin uso",
30 | });
31 | });
32 |
33 | it("push should update existing rows and append new keys for the same locale", async () => {
34 | const loader = createCsvLoader();
35 | loader.setDefaultLocale("en");
36 | await loader.pull("en", sampleCsv);
37 |
38 | const updatedCsv = await loader.push("en", {
39 | hello: "Hello edited",
40 | newKey: "New Message",
41 | });
42 |
43 | const parsed = parse(updatedCsv, { columns: true, skip_empty_lines: true });
44 | expect(parsed).toEqual([
45 | { id: "hello", en: "Hello edited", es: "Hola" },
46 | { id: "bye", en: "Bye", es: "Adiós" },
47 | { id: "unused", en: "", es: "Sin uso" },
48 | { id: "", en: "New Message", es: "" },
49 | ]);
50 | });
51 |
52 | it("push should add a new locale column when pushing for a different locale", async () => {
53 | const loader = createCsvLoader();
54 | loader.setDefaultLocale("en");
55 | await loader.pull("en", sampleCsv);
56 |
57 | const esCsv = await loader.push("es", {
58 | hello: "Hola",
59 | bye: "Adiós",
60 | });
61 |
62 | const parsed = parse(esCsv, { columns: true, skip_empty_lines: true });
63 | expect(parsed).toEqual([
64 | { id: "hello", en: "Hello", es: "Hola" },
65 | { id: "bye", en: "Bye", es: "Adiós" },
66 | { id: "unused", en: "", es: "Sin uso" },
67 | ]);
68 | });
69 |
70 | it("push should add a completely new locale column when it previously didn't exist", async () => {
71 | const loader = createCsvLoader();
72 | loader.setDefaultLocale("en");
73 | await loader.pull("en", sampleCsv); // sampleCsv only has en & es columns
74 |
75 | const frCsv = await loader.push("fr", {
76 | hello: "Bonjour",
77 | bye: "Au revoir",
78 | });
79 |
80 | const parsed = parse(frCsv, { columns: true, skip_empty_lines: true });
81 | // Expect new column 'fr' to exist alongside existing ones, with empty strings when no translation provided
82 | expect(parsed).toEqual([
83 | { id: "hello", en: "Hello", es: "Hola", fr: "Bonjour" },
84 | { id: "bye", en: "Bye", es: "Adiós", fr: "Au revoir" },
85 | { id: "unused", en: "", es: "Sin uso", fr: "" },
86 | ]);
87 | });
88 |
89 | it("should throw an error if the first pull is not for the default locale", async () => {
90 | const loader = createCsvLoader();
91 | loader.setDefaultLocale("en");
92 |
93 | await expect(loader.pull("es", sampleCsv)).rejects.toThrow(
94 | "The first pull must be for the default locale",
95 | );
96 | });
97 | });
98 |
```
--------------------------------------------------------------------------------
/packages/compiler/src/utils/invokations.spec.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { it, describe, expect } from "vitest";
2 | import { parse } from "@babel/parser";
3 | import * as t from "@babel/types";
4 | import { findInvokations } from "./invokations";
5 |
6 | describe("findInvokations", () => {
7 | it("should find named import invocation", () => {
8 | const ast = parseCode(`
9 | import { targetFunc } from 'target-module';
10 |
11 | function test() {
12 | targetFunc(1, 2);
13 | otherFunc();
14 | }
15 | `);
16 |
17 | const result = findInvokations(ast, {
18 | moduleName: "target-module",
19 | functionName: "targetFunc",
20 | });
21 |
22 | expect(result.length).toBe(1);
23 | expect(result[0].type).toBe("CallExpression");
24 |
25 | const callee = result[0].callee as t.Identifier;
26 | expect(callee.name).toBe("targetFunc");
27 | });
28 |
29 | it("should find default import invocation", () => {
30 | const ast = parseCode(`
31 | import defaultFunc from 'target-module';
32 |
33 | function test() {
34 | defaultFunc('test');
35 | }
36 | `);
37 |
38 | const result = findInvokations(ast, {
39 | moduleName: "target-module",
40 | functionName: "default",
41 | });
42 |
43 | expect(result.length).toBe(1);
44 |
45 | const callee = result[0].callee as t.Identifier;
46 | expect(callee.name).toBe("defaultFunc");
47 | });
48 |
49 | it("should find namespace import invocation", () => {
50 | const ast = parseCode(`
51 | import * as targetModule from 'target-module';
52 |
53 | function test() {
54 | targetModule.targetFunc();
55 | targetModule.otherFunc();
56 | }
57 | `);
58 |
59 | const result = findInvokations(ast, {
60 | moduleName: "target-module",
61 | functionName: "targetFunc",
62 | });
63 |
64 | expect(result.length).toBe(1);
65 |
66 | const callee = result[0].callee as t.MemberExpression;
67 | expect((callee.object as t.Identifier).name).toBe("targetModule");
68 | expect((callee.property as t.Identifier).name).toBe("targetFunc");
69 | });
70 |
71 | it("should find renamed import invocation", () => {
72 | const ast = parseCode(`
73 | import { targetFunc as renamedFunc } from 'target-module';
74 |
75 | function test() {
76 | renamedFunc();
77 | }
78 | `);
79 |
80 | const result = findInvokations(ast, {
81 | moduleName: "target-module",
82 | functionName: "targetFunc",
83 | });
84 |
85 | expect(result.length).toBe(1);
86 |
87 | const callee = result[0].callee as t.Identifier;
88 | expect(callee.name).toBe("renamedFunc");
89 | });
90 |
91 | it("should return empty array when no matching imports exist", () => {
92 | const ast = parseCode(`
93 | import { otherFunc } from 'other-module';
94 |
95 | function test() {
96 | otherFunc();
97 | }
98 | `);
99 |
100 | const result = findInvokations(ast, {
101 | moduleName: "target-module",
102 | functionName: "targetFunc",
103 | });
104 |
105 | expect(result.length).toBe(0);
106 | });
107 |
108 | it("should return empty array when import exists but not invoked", () => {
109 | const ast = parseCode(`
110 | import { targetFunc } from 'target-module';
111 |
112 | function test() {
113 | // No invocation here
114 | }
115 | `);
116 |
117 | const result = findInvokations(ast, {
118 | moduleName: "target-module",
119 | functionName: "targetFunc",
120 | });
121 |
122 | expect(result.length).toBe(0);
123 | });
124 | });
125 |
126 | function parseCode(code: string): t.File {
127 | return parse(code, {
128 | sourceType: "module",
129 | plugins: ["typescript"],
130 | });
131 | }
132 |
```
--------------------------------------------------------------------------------
/packages/compiler/src/utils/jsx-functions.spec.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { parse } from "@babel/parser";
2 | import traverse from "@babel/traverse";
3 | import generate from "@babel/generator";
4 | import { getJsxFunctions } from "./jsx-functions";
5 | import { describe, expect, it } from "vitest";
6 |
7 | function parseJSX(code: string) {
8 | return parse(code, {
9 | plugins: ["jsx"],
10 | sourceType: "module",
11 | });
12 | }
13 |
14 | describe("getJsxFunctions", () => {
15 | it("extracts simple function calls", () => {
16 | const ast = parseJSX("<div>{getName()}</div>");
17 | let result;
18 |
19 | traverse(ast, {
20 | JSXElement(path) {
21 | result = getJsxFunctions(path);
22 | path.stop();
23 | },
24 | });
25 |
26 | expect(generate(result).code).toBe('{\n "getName": [getName()]\n}');
27 | });
28 |
29 | it("extracts function calls with arguments", () => {
30 | const ast = parseJSX("<div>{getName(user, 123)}</div>");
31 | let result;
32 |
33 | traverse(ast, {
34 | JSXElement(path) {
35 | result = getJsxFunctions(path);
36 | path.stop();
37 | },
38 | });
39 |
40 | expect(generate(result).code).toBe(
41 | '{\n "getName": [getName(user, 123)]\n}',
42 | );
43 | });
44 |
45 | it("extracts multiple function calls", () => {
46 | const ast = parseJSX("<div>{getName(user)} {getCount()}</div>");
47 | let result;
48 |
49 | traverse(ast, {
50 | JSXElement(path) {
51 | result = getJsxFunctions(path);
52 | path.stop();
53 | },
54 | });
55 |
56 | expect(generate(result).code).toBe(
57 | '{\n "getName": [getName(user)],\n "getCount": [getCount()]\n}',
58 | );
59 | });
60 |
61 | it("ignores non-function expressions", () => {
62 | const ast = parseJSX("<div>{user.name} {getCount()}</div>");
63 | let result;
64 |
65 | traverse(ast, {
66 | JSXElement(path) {
67 | result = getJsxFunctions(path);
68 | path.stop();
69 | },
70 | });
71 |
72 | expect(generate(result).code).toBe('{\n "getCount": [getCount()]\n}');
73 | });
74 |
75 | it("extracts function with chained names", () => {
76 | const ast = parseJSX(
77 | "<div>{getCount()} {user.details.products.items.map((item) => item.value).filter(value => value > 0)}</div>",
78 | );
79 | let result;
80 |
81 | traverse(ast, {
82 | JSXElement(path) {
83 | result = getJsxFunctions(path);
84 | path.stop();
85 | },
86 | });
87 |
88 | expect(generate(result!).code).toBe(
89 | '{\n "getCount": [getCount()],\n "user.details.products.items.map": [user.details.products.items.map(item => item.value).filter(value => value > 0)]\n}',
90 | );
91 | });
92 |
93 | it("extracts multiple usages of the same function", () => {
94 | const ast = parseJSX(
95 | "<div>{getCount(foo)} is more than {getCount(bar)} but less than {getCount(baz)}</div>",
96 | );
97 | let result;
98 |
99 | traverse(ast, {
100 | JSXElement(path) {
101 | result = getJsxFunctions(path);
102 | path.stop();
103 | },
104 | });
105 |
106 | expect(generate(result!).code).toBe(
107 | '{\n "getCount": [getCount(foo), getCount(bar), getCount(baz)]\n}',
108 | );
109 | });
110 |
111 | it("should extract function calls on classes with 'new' keyword", () => {
112 | const ast = parseJSX("<div>© {new Date().getFullYear()} vitest</div>");
113 | let result;
114 | traverse(ast, {
115 | JSXElement(path) {
116 | result = getJsxFunctions(path);
117 | path.stop();
118 | },
119 | });
120 |
121 | expect(generate(result!).code).toBe(
122 | '{\n "Date.getFullYear": [new Date().getFullYear()]\n}',
123 | );
124 | });
125 | });
126 |
```
--------------------------------------------------------------------------------
/scripts/docs/src/utils.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { existsSync } from "fs";
2 | import path from "path";
3 | import { fileURLToPath } from "url";
4 | import { readFileSync } from "fs";
5 | import { Octokit } from "@octokit/rest";
6 | import * as prettier from "prettier";
7 |
8 | export function getRepoRoot(): string {
9 | const __filename = fileURLToPath(import.meta.url);
10 | const __dirname = path.dirname(__filename);
11 | let currentDir = __dirname;
12 |
13 | while (currentDir !== path.parse(currentDir).root) {
14 | if (existsSync(path.join(currentDir, ".git"))) {
15 | return currentDir;
16 | }
17 | currentDir = path.dirname(currentDir);
18 | }
19 |
20 | throw new Error("Could not find project root");
21 | }
22 |
23 | export function getGitHubToken() {
24 | const token = process.env.GITHUB_TOKEN;
25 |
26 | if (!token) {
27 | throw new Error("GITHUB_TOKEN environment variable is required.");
28 | }
29 |
30 | return token;
31 | }
32 |
33 | export function getGitHubRepo() {
34 | const repository = process.env.GITHUB_REPOSITORY;
35 |
36 | if (!repository) {
37 | throw new Error("GITHUB_REPOSITORY environment variable is missing.");
38 | }
39 |
40 | const [_, repo] = repository.split("/");
41 |
42 | return repo;
43 | }
44 |
45 | export function getGitHubOwner() {
46 | const repository = process.env.GITHUB_REPOSITORY;
47 |
48 | if (!repository) {
49 | throw new Error("GITHUB_REPOSITORY environment variable is missing.");
50 | }
51 |
52 | const [owner] = repository.split("/");
53 |
54 | return owner;
55 | }
56 |
57 | export function getGitHubPRNumber() {
58 | const prNumber = process.env.PR_NUMBER;
59 |
60 | if (prNumber) {
61 | return Number(prNumber);
62 | }
63 |
64 | const eventPath = process.env.GITHUB_EVENT_PATH;
65 |
66 | if (eventPath && existsSync(eventPath)) {
67 | try {
68 | const eventData = JSON.parse(readFileSync(eventPath, "utf8"));
69 | return Number(eventData.pull_request?.number);
70 | } catch (err) {
71 | console.warn("Failed to parse GITHUB_EVENT_PATH JSON:", err);
72 | }
73 | }
74 |
75 | throw new Error("Could not determine pull request number.");
76 | }
77 |
78 | export type GitHubCommentOptions = {
79 | commentMarker: string;
80 | body: string;
81 | };
82 |
83 | export async function createOrUpdateGitHubComment(
84 | options: GitHubCommentOptions,
85 | ): Promise<void> {
86 | const token = getGitHubToken();
87 | const owner = getGitHubOwner();
88 | const repo = getGitHubRepo();
89 | const prNumber = getGitHubPRNumber();
90 |
91 | const octokit = new Octokit({ auth: token });
92 |
93 | const commentsResponse = await octokit.rest.issues.listComments({
94 | owner,
95 | repo,
96 | issue_number: prNumber,
97 | per_page: 100,
98 | });
99 |
100 | const comments = commentsResponse.data;
101 |
102 | const existing = comments.find((c) => {
103 | if (!c.body) {
104 | return false;
105 | }
106 | return c.body.startsWith(options.commentMarker);
107 | });
108 |
109 | if (existing) {
110 | console.log(`Updating existing comment (id: ${existing.id}).`);
111 | await octokit.rest.issues.updateComment({
112 | owner,
113 | repo,
114 | comment_id: existing.id,
115 | body: options.body,
116 | });
117 | return;
118 | }
119 |
120 | console.log("Creating new comment.");
121 | await octokit.rest.issues.createComment({
122 | owner,
123 | repo,
124 | issue_number: prNumber,
125 | body: options.body,
126 | });
127 | }
128 |
129 | export async function formatMarkdown(markdown: string): Promise<string> {
130 | const repoRoot = getRepoRoot();
131 | const prettierConfig = await prettier.resolveConfig(repoRoot);
132 | return await prettier.format(markdown, {
133 | ...prettierConfig,
134 | parser: "markdown",
135 | });
136 | }
137 |
```
--------------------------------------------------------------------------------
/packages/cli/src/cli/loaders/ensure-key-order.spec.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect } from "vitest";
2 | import createEnsureKeyOrderLoader from "./ensure-key-order";
3 |
4 | describe("ensure-key-order loader", () => {
5 | const loader = createEnsureKeyOrderLoader();
6 | loader.setDefaultLocale("en");
7 |
8 | it("should return input unchanged on pull", async () => {
9 | const input = { b: 1, a: 2 };
10 | const result = await loader.pull("en", input);
11 | expect(result).toEqual(input);
12 | });
13 |
14 | it("should reorder keys to match original input order on push", async () => {
15 | const originalInput = { a: 1, b: 2, c: 3 };
16 | await loader.pull("en", originalInput);
17 | const data = { b: 22, a: 11, c: 33 };
18 | const result = await loader.push("en", data);
19 | expect(result).toEqual({ a: 11, b: 22, c: 33 });
20 | });
21 |
22 | it("should reorder keys in objects of nested arrays to match original input order on push", async () => {
23 | const originalInput = [
24 | { a: 1, b: 2, c: 3 },
25 | { a: 4, b: 5, c: 6 },
26 | {
27 | values: [
28 | { a: 7, b: 8, c: 9 },
29 | { a: 10, b: 11, c: 12 },
30 | ],
31 | },
32 | ];
33 | await loader.pull("en", originalInput);
34 | const data = [
35 | { b: 22, a: 11, c: 33 },
36 | { b: 55, c: 66, a: 44 },
37 | {
38 | values: [
39 | { b: 88, c: 99, a: 77 },
40 | { c: 122, b: 111, a: 100 },
41 | ],
42 | },
43 | ];
44 | const result = await loader.push("en", data);
45 | expect(result).toEqual([
46 | { a: 11, b: 22, c: 33 },
47 | { a: 44, b: 55, c: 66 },
48 | {
49 | values: [
50 | { a: 77, b: 88, c: 99 },
51 | { a: 100, b: 111, c: 122 },
52 | ],
53 | },
54 | ]);
55 | });
56 |
57 | it("should reorder falsy keys to match original input order on push", async () => {
58 | const originalInput = {
59 | a: 1,
60 | b: 0,
61 | c: null,
62 | d: "a",
63 | e: false,
64 | g: "",
65 | h: undefined,
66 | };
67 | await loader.pull("en", originalInput);
68 | const data = {
69 | b: 0,
70 | a: 11,
71 | c: null,
72 | d: "b",
73 | e: false,
74 | g: "",
75 | h: undefined,
76 | };
77 | const result = await loader.push("en", data);
78 | expect(result).toEqual({
79 | a: 11,
80 | b: 0,
81 | c: null,
82 | d: "b",
83 | e: false,
84 | g: "",
85 | h: undefined,
86 | });
87 | });
88 |
89 | it("should handle nested objects and preserve key order", async () => {
90 | const originalInput = { x: { b: 2, a: 1 }, y: 3, z: { d: 9, f: 7, e: 8 } };
91 | await loader.pull("en", originalInput);
92 | const data = { x: { a: 11, b: 22 }, z: { d: 99, e: 88, f: 77 }, y: 33 };
93 | const result = await loader.push("en", data);
94 | expect(result).toEqual({
95 | x: { b: 22, a: 11 },
96 | y: 33,
97 | z: { d: 99, e: 88, f: 77 },
98 | });
99 | });
100 |
101 | it("should skip keys not in original input of source locale", async () => {
102 | const originalInput = { a: 1, b: 2 };
103 | await loader.pull("en", originalInput);
104 | const data = { a: 11, b: 22, c: 33 };
105 | const result = await loader.push("en", data);
106 | expect(result).toEqual({ a: 11, b: 22 });
107 | });
108 |
109 | it("should skip keys not in the target locale data", async () => {
110 | const originalInput = { a: 1, b: 2, c: 2 };
111 | await loader.pull("en", originalInput);
112 | const data = { a: 11, c: 33 };
113 | const result = await loader.push("en", data);
114 | expect(result).toEqual({ a: 11, c: 33 });
115 | });
116 | });
117 |
```
--------------------------------------------------------------------------------
/packages/cli/src/cli/utils/exec.spec.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect, vi } from "vitest";
2 | import { execAsync, ExecAsyncOptions } from "./exec";
3 |
4 | // describe('execAsync', () => {
5 | // // Helper function to create a delayed async function
6 | // const createDelayedFunction = (value: any, delay: number) => {
7 | // return async () => {
8 | // console.log(`[${Date.now()}] start`, value);
9 | // await new Promise(resolve => setTimeout(resolve, delay));
10 | // console.log(`[${Date.now()}] end`, value);
11 | // return value;
12 | // };
13 | // };
14 |
15 | // it('run', async () => {
16 | // await execAsync([
17 | // createDelayedFunction(1, 750),
18 | // createDelayedFunction(2, 750),
19 | // createDelayedFunction(3, 750),
20 | // createDelayedFunction(4, 750),
21 | // ], {
22 | // concurrency: 2,
23 | // delay: 250,
24 | // });
25 | // });
26 | // });
27 |
28 | describe("execAsync", () => {
29 | it("executes all functions and returns their results", async () => {
30 | const fns = [async () => 1, async () => 2, async () => 3];
31 | const options: ExecAsyncOptions = { concurrency: 1, delay: 0 };
32 | const results = await execAsync(fns, options);
33 | expect(results).toEqual([1, 2, 3]);
34 | });
35 |
36 | it("calls onProgress with correct values", async () => {
37 | const fns = [async () => 1, async () => 2, async () => 3];
38 | const onProgress = vi.fn();
39 | const options: ExecAsyncOptions = { concurrency: 1, delay: 0, onProgress };
40 | await execAsync(fns, options);
41 | expect(onProgress).toHaveBeenCalledTimes(4);
42 | expect(onProgress).toHaveBeenNthCalledWith(1, 0, 3);
43 | expect(onProgress).toHaveBeenNthCalledWith(2, 1, 3);
44 | expect(onProgress).toHaveBeenNthCalledWith(3, 2, 3);
45 | expect(onProgress).toHaveBeenNthCalledWith(4, 3, 3);
46 | });
47 |
48 | it("starts next function if previous finishes before delay", async () => {
49 | const delay = 100;
50 | const fns = [
51 | vi.fn().mockResolvedValue(1),
52 | vi
53 | .fn()
54 | .mockImplementation(
55 | () => new Promise((resolve) => setTimeout(() => resolve(2), 50)),
56 | ),
57 | vi.fn().mockResolvedValue(3),
58 | ];
59 | const options: ExecAsyncOptions = { concurrency: 1, delay };
60 | const start = Date.now();
61 | await execAsync(fns, options);
62 | const end = Date.now();
63 | expect(end - start).toBeLessThan(delay * 3);
64 | });
65 |
66 | it("respects concurrency limit", async () => {
67 | const concurrency = 2;
68 | const delay = 100;
69 | let maxConcurrent = 0;
70 | let currentConcurrent = 0;
71 |
72 | const fns = Array(5)
73 | .fill(null)
74 | .map(() => async () => {
75 | currentConcurrent++;
76 | maxConcurrent = Math.max(maxConcurrent, currentConcurrent);
77 | await new Promise((resolve) => setTimeout(resolve, delay));
78 | currentConcurrent--;
79 | });
80 |
81 | const options: ExecAsyncOptions = { concurrency, delay: 0 };
82 | await execAsync(fns, options);
83 | expect(maxConcurrent).toBe(concurrency);
84 | });
85 |
86 | it("handles empty array of functions", async () => {
87 | const options: ExecAsyncOptions = { concurrency: 1, delay: 0 };
88 | const results = await execAsync([], options);
89 | expect(results).toEqual([]);
90 | });
91 |
92 | it("handles single function", async () => {
93 | const fn = async () => 42;
94 | const options: ExecAsyncOptions = { concurrency: 1, delay: 0 };
95 | const results = await execAsync([fn], options);
96 | expect(results).toEqual([42]);
97 | });
98 | });
99 |
```
--------------------------------------------------------------------------------
/packages/locales/src/names/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | import {
2 | loadTerritoryNames,
3 | loadLanguageNames,
4 | loadScriptNames,
5 | } from "./loader";
6 |
7 | /**
8 | * Gets a country name in the specified display language
9 | *
10 | * @param countryCode - The ISO country code (e.g., "US", "CN", "DE")
11 | * @param displayLanguage - The language to display the name in (default: "en")
12 | * @returns Promise<string> - The localized country name
13 | *
14 | * @example
15 | * ```typescript
16 | * // Default English
17 | * await getCountryName("US"); // "United States"
18 | * await getCountryName("CN"); // "China"
19 | *
20 | * // Spanish
21 | * await getCountryName("US", "es"); // "Estados Unidos"
22 | * await getCountryName("CN", "es"); // "China"
23 | *
24 | * // French
25 | * await getCountryName("US", "fr"); // "États-Unis"
26 | * ```
27 | */
28 | export async function getCountryName(
29 | countryCode: string,
30 | displayLanguage: string = "en",
31 | ): Promise<string> {
32 | if (!countryCode) {
33 | throw new Error("Country code is required");
34 | }
35 |
36 | const territories = await loadTerritoryNames(displayLanguage);
37 | const name = territories[countryCode.toUpperCase()];
38 |
39 | if (!name) {
40 | throw new Error(`Country code "${countryCode}" not found`);
41 | }
42 |
43 | return name;
44 | }
45 |
46 | /**
47 | * Gets a language name in the specified display language
48 | *
49 | * @param languageCode - The ISO language code (e.g., "en", "zh", "es")
50 | * @param displayLanguage - The language to display the name in (default: "en")
51 | * @returns Promise<string> - The localized language name
52 | *
53 | * @example
54 | * ```typescript
55 | * // Default English
56 | * await getLanguageName("en"); // "English"
57 | * await getLanguageName("zh"); // "Chinese"
58 | *
59 | * // Spanish
60 | * await getLanguageName("en", "es"); // "inglés"
61 | * await getLanguageName("zh", "es"); // "chino"
62 | *
63 | * // Chinese
64 | * await getLanguageName("en", "zh"); // "英语"
65 | * ```
66 | */
67 | export async function getLanguageName(
68 | languageCode: string,
69 | displayLanguage: string = "en",
70 | ): Promise<string> {
71 | if (!languageCode) {
72 | throw new Error("Language code is required");
73 | }
74 |
75 | const languages = await loadLanguageNames(displayLanguage);
76 | const name = languages[languageCode.toLowerCase()];
77 |
78 | if (!name) {
79 | throw new Error(`Language code "${languageCode}" not found`);
80 | }
81 |
82 | return name;
83 | }
84 |
85 | /**
86 | * Gets a script name in the specified display language
87 | *
88 | * @param scriptCode - The ISO script code (e.g., "Hans", "Hant", "Latn")
89 | * @param displayLanguage - The language to display the name in (default: "en")
90 | * @returns Promise<string> - The localized script name
91 | *
92 | * @example
93 | * ```typescript
94 | * // Default English
95 | * await getScriptName("Hans"); // "Simplified"
96 | * await getScriptName("Hant"); // "Traditional"
97 | * await getScriptName("Latn"); // "Latin"
98 | *
99 | * // Spanish
100 | * await getScriptName("Hans", "es"); // "simplificado"
101 | * await getScriptName("Cyrl", "es"); // "cirílico"
102 | *
103 | * // Chinese
104 | * await getScriptName("Latn", "zh"); // "拉丁文"
105 | * ```
106 | */
107 | export async function getScriptName(
108 | scriptCode: string,
109 | displayLanguage: string = "en",
110 | ): Promise<string> {
111 | if (!scriptCode) {
112 | throw new Error("Script code is required");
113 | }
114 |
115 | const scripts = await loadScriptNames(displayLanguage);
116 | const name = scripts[scriptCode];
117 |
118 | if (!name) {
119 | throw new Error(`Script code "${scriptCode}" not found`);
120 | }
121 |
122 | return name;
123 | }
124 |
```
--------------------------------------------------------------------------------
/packages/compiler/src/utils/jsx-attribute-scope.ts:
--------------------------------------------------------------------------------
```typescript
1 | import * as t from "@babel/types";
2 | import traverse from "@babel/traverse";
3 | import { NodePath } from "@babel/traverse";
4 |
5 | export function collectJsxAttributeScopes(
6 | node: t.Node,
7 | ): Array<[NodePath<t.JSXElement>, string[]]> {
8 | const result: Array<[NodePath<t.JSXElement>, string[]]> = [];
9 |
10 | traverse(node, {
11 | JSXElement(path: NodePath<t.JSXElement>) {
12 | if (!hasJsxAttributeScopeAttribute(path)) return;
13 |
14 | const localizableAttributes = getJsxAttributeScopeAttribute(path);
15 | if (!localizableAttributes) return;
16 |
17 | result.push([path, localizableAttributes]);
18 | },
19 | });
20 |
21 | return result;
22 | }
23 |
24 | export function getJsxAttributeScopes(
25 | node: t.Node,
26 | ): Array<[NodePath<t.JSXElement>, string[]]> {
27 | const result: Array<[NodePath<t.JSXElement>, string[]]> = [];
28 |
29 | // List of attributes that should be considered localizable
30 | const LOCALIZABLE_ATTRIBUTES = [
31 | "title",
32 | "aria-label",
33 | "aria-description",
34 | "alt",
35 | "label",
36 | "description",
37 | "placeholder",
38 | "content",
39 | "subtitle",
40 | ];
41 |
42 | traverse(node, {
43 | JSXElement(path: NodePath<t.JSXElement>) {
44 | const openingElement = path.node.openingElement;
45 |
46 | // Only process lowercase HTML elements (not components)
47 | const elementName = openingElement.name;
48 | if (!t.isJSXIdentifier(elementName) || !elementName.name) {
49 | return;
50 | }
51 |
52 | const hasAttributeScope = openingElement.attributes.find(
53 | (attr) =>
54 | t.isJSXAttribute(attr) &&
55 | attr.name.name === "data-jsx-attribute-scope",
56 | );
57 | if (hasAttributeScope) {
58 | return;
59 | }
60 |
61 | // Find all localizable attributes
62 | const localizableAttrs = openingElement.attributes
63 | .filter(
64 | (
65 | attr: t.JSXAttribute | t.JSXSpreadAttribute,
66 | ): attr is t.JSXAttribute => {
67 | if (!t.isJSXAttribute(attr) || !t.isStringLiteral(attr.value)) {
68 | return false;
69 | }
70 |
71 | const name = attr.name.name;
72 | return (
73 | typeof name === "string" && LOCALIZABLE_ATTRIBUTES.includes(name)
74 | );
75 | },
76 | )
77 | .map((attr: t.JSXAttribute) => attr.name.name as string);
78 |
79 | // Only add the element if we found localizable attributes
80 | if (localizableAttrs.length > 0) {
81 | result.push([path, localizableAttrs]);
82 | }
83 | },
84 | });
85 |
86 | return result;
87 | }
88 |
89 | export function hasJsxAttributeScopeAttribute(path: NodePath<t.JSXElement>) {
90 | return !!getJsxAttributeScopeAttribute(path);
91 | }
92 |
93 | export function getJsxAttributeScopeAttribute(path: NodePath<t.JSXElement>) {
94 | const attribute = path.node.openingElement.attributes.find(
95 | (attr) =>
96 | attr.type === "JSXAttribute" &&
97 | attr.name.name === "data-jsx-attribute-scope",
98 | );
99 |
100 | if (!attribute || !t.isJSXAttribute(attribute)) {
101 | return undefined;
102 | }
103 |
104 | // Handle array of string literals
105 | if (
106 | t.isJSXExpressionContainer(attribute.value) &&
107 | t.isArrayExpression(attribute.value.expression)
108 | ) {
109 | const arrayExpr = attribute.value.expression;
110 | return arrayExpr.elements
111 | .filter((el): el is t.StringLiteral => t.isStringLiteral(el))
112 | .map((el) => el.value);
113 | }
114 |
115 | // Fallback for single string literal
116 | if (t.isStringLiteral(attribute.value)) {
117 | return [attribute.value.value];
118 | }
119 |
120 | return undefined;
121 | }
122 |
```
--------------------------------------------------------------------------------
/packages/compiler/src/jsx-provider.ts:
--------------------------------------------------------------------------------
```typescript
1 | import traverse, { NodePath } from "@babel/traverse";
2 | import * as t from "@babel/types";
3 | import { CompilerPayload, createCodeMutation } from "./_base";
4 | import { getJsxElementName } from "./utils/jsx-element";
5 | import { getModuleExecutionMode, getOrCreateImport } from "./utils";
6 | import { ModuleId } from "./_const";
7 |
8 | /**
9 | * This mutation is used to wrap the html component with the LingoProvider component.
10 | * It only works with server components.
11 | */
12 | const jsxProviderMutation = createCodeMutation((payload) => {
13 | traverse(payload.ast, {
14 | JSXElement: (path) => {
15 | if (getJsxElementName(path)?.toLowerCase() === "html") {
16 | const mode = getModuleExecutionMode(payload.ast, payload.params.rsc);
17 | if (mode === "client") {
18 | return;
19 | }
20 |
21 | // TODO: later
22 | // replaceHtmlComponent(payload, path);
23 |
24 | const lingoProviderImport = getOrCreateImport(payload.ast, {
25 | moduleName: ModuleId.ReactRSC,
26 | exportedName: "LingoProvider",
27 | });
28 | const loadDictionaryImport = getOrCreateImport(payload.ast, {
29 | moduleName: ModuleId.ReactRSC,
30 | exportedName: "loadDictionary",
31 | });
32 |
33 | const loadDictionaryArrow = t.arrowFunctionExpression(
34 | [t.identifier("locale")],
35 | t.callExpression(t.identifier(loadDictionaryImport.importedName), [
36 | t.identifier("locale"),
37 | ]),
38 | );
39 |
40 | const providerProps = [
41 | t.jsxAttribute(
42 | t.jsxIdentifier("loadDictionary"),
43 | t.jsxExpressionContainer(loadDictionaryArrow),
44 | ),
45 | ];
46 |
47 | const provider = t.jsxElement(
48 | t.jsxOpeningElement(
49 | t.jsxIdentifier(lingoProviderImport.importedName),
50 | providerProps,
51 | false,
52 | ),
53 | t.jsxClosingElement(
54 | t.jsxIdentifier(lingoProviderImport.importedName),
55 | ),
56 | [path.node],
57 | false,
58 | );
59 |
60 | path.replaceWith(provider);
61 | path.skip();
62 | }
63 | },
64 | });
65 |
66 | return payload;
67 | });
68 |
69 | export default jsxProviderMutation;
70 |
71 | function replaceHtmlComponent(
72 | payload: CompilerPayload,
73 | path: NodePath<t.JSXElement>,
74 | ) {
75 | // Find the parent function and make it async since locale is retrieved from cookies asynchronously
76 | const parentFunction = path.findParent(
77 | (p): p is NodePath<t.FunctionDeclaration | t.ArrowFunctionExpression> =>
78 | t.isFunctionDeclaration(p.node) || t.isArrowFunctionExpression(p.node),
79 | );
80 | if (
81 | parentFunction?.node.type === "FunctionDeclaration" ||
82 | parentFunction?.node.type === "ArrowFunctionExpression"
83 | ) {
84 | parentFunction.node.async = true;
85 | }
86 |
87 | // html lang attribute
88 | const loadLocaleFromCookiesImport = getOrCreateImport(payload.ast, {
89 | moduleName: ModuleId.ReactRSC,
90 | exportedName: "loadLocaleFromCookies",
91 | });
92 | let langAttribute = path.node.openingElement.attributes.find(
93 | (attr) => attr.type === "JSXAttribute" && attr.name.name === "lang",
94 | );
95 | if (!t.isJSXAttribute(langAttribute)) {
96 | (langAttribute = t.jsxAttribute(
97 | t.jsxIdentifier("lang"),
98 | t.stringLiteral(""),
99 | )),
100 | path.node.openingElement.attributes.push(langAttribute);
101 | }
102 | langAttribute.value = t.jsxExpressionContainer(
103 | t.awaitExpression(
104 | t.callExpression(
105 | t.identifier(loadLocaleFromCookiesImport.importedName),
106 | [],
107 | ),
108 | ),
109 | );
110 | }
111 |
```
--------------------------------------------------------------------------------
/demo/adonisjs/adonisrc.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { defineConfig } from '@adonisjs/core/app'
2 |
3 | export default defineConfig({
4 | /*
5 | |--------------------------------------------------------------------------
6 | | Experimental flags
7 | |--------------------------------------------------------------------------
8 | |
9 | | The following features will be enabled by default in the next major release
10 | | of AdonisJS. You can opt into them today to avoid any breaking changes
11 | | during upgrade.
12 | |
13 | */
14 | experimental: {
15 | mergeMultipartFieldsAndFiles: true,
16 | shutdownInReverseOrder: true,
17 | },
18 |
19 | /*
20 | |--------------------------------------------------------------------------
21 | | Commands
22 | |--------------------------------------------------------------------------
23 | |
24 | | List of ace commands to register from packages. The application commands
25 | | will be scanned automatically from the "./commands" directory.
26 | |
27 | */
28 | commands: [() => import('@adonisjs/core/commands')],
29 |
30 | /*
31 | |--------------------------------------------------------------------------
32 | | Service providers
33 | |--------------------------------------------------------------------------
34 | |
35 | | List of service providers to import and register when booting the
36 | | application
37 | |
38 | */
39 | providers: [
40 | () => import('@adonisjs/core/providers/app_provider'),
41 | () => import('@adonisjs/core/providers/hash_provider'),
42 | {
43 | file: () => import('@adonisjs/core/providers/repl_provider'),
44 | environment: ['repl', 'test'],
45 | },
46 | () => import('@adonisjs/core/providers/vinejs_provider'),
47 | () => import('@adonisjs/core/providers/edge_provider'),
48 | () => import('@adonisjs/session/session_provider'),
49 | () => import('@adonisjs/vite/vite_provider'),
50 | () => import('@adonisjs/shield/shield_provider'),
51 | () => import('@adonisjs/static/static_provider'),
52 | () => import('@adonisjs/cors/cors_provider'),
53 | () => import('@adonisjs/inertia/inertia_provider'),
54 | ],
55 |
56 | /*
57 | |--------------------------------------------------------------------------
58 | | Preloads
59 | |--------------------------------------------------------------------------
60 | |
61 | | List of modules to import before starting the application.
62 | |
63 | */
64 | preloads: [() => import('#start/routes'), () => import('#start/kernel')],
65 |
66 | /*
67 | |--------------------------------------------------------------------------
68 | | Tests
69 | |--------------------------------------------------------------------------
70 | |
71 | | List of test suites to organize tests by their type. Feel free to remove
72 | | and add additional suites.
73 | |
74 | */
75 | tests: {
76 | suites: [
77 | {
78 | files: ['tests/unit/**/*.spec(.ts|.js)'],
79 | name: 'unit',
80 | timeout: 2000,
81 | },
82 | {
83 | files: ['tests/functional/**/*.spec(.ts|.js)'],
84 | name: 'functional',
85 | timeout: 30000,
86 | },
87 | ],
88 | forceExit: false,
89 | },
90 |
91 | /*
92 | |--------------------------------------------------------------------------
93 | | Metafiles
94 | |--------------------------------------------------------------------------
95 | |
96 | | A collection of files you want to copy to the build folder when creating
97 | | the production build.
98 | |
99 | */
100 | metaFiles: [
101 | {
102 | pattern: 'resources/views/**/*.edge',
103 | reloadServer: false,
104 | },
105 | {
106 | pattern: 'public/**',
107 | reloadServer: false,
108 | },
109 | ],
110 |
111 | assetsBundler: false,
112 | hooks: {
113 | onBuildStarting: [() => import('@adonisjs/vite/build_hook')],
114 | },
115 | })
116 |
```
--------------------------------------------------------------------------------
/packages/cli/src/cli/cmd/ci/platforms/gitlab.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Gitlab } from "@gitbeaker/rest";
2 | import Z from "zod";
3 | import { PlatformKit } from "./_base";
4 |
5 | const gl = new Gitlab({ token: "" });
6 |
7 | export class GitlabPlatformKit extends PlatformKit {
8 | private _gitlab?: InstanceType<typeof Gitlab>;
9 |
10 | constructor() {
11 | super();
12 |
13 | // change directory to current repository before executing replexica
14 | process.chdir(this.platformConfig.projectDir);
15 | }
16 |
17 | private get gitlab(): InstanceType<typeof Gitlab> {
18 | if (!this._gitlab) {
19 | this._gitlab = new Gitlab({
20 | token: this.platformConfig.glToken || "",
21 | });
22 | }
23 | return this._gitlab;
24 | }
25 |
26 | get platformConfig() {
27 | const env = Z.object({
28 | GL_TOKEN: Z.string().optional(),
29 | CI_COMMIT_BRANCH: Z.string(),
30 | CI_MERGE_REQUEST_SOURCE_BRANCH_NAME: Z.string().optional(),
31 | CI_PROJECT_NAMESPACE: Z.string(),
32 | CI_PROJECT_NAME: Z.string(),
33 | CI_PROJECT_ID: Z.string(),
34 | CI_PROJECT_DIR: Z.string(),
35 | CI_REPOSITORY_URL: Z.string(),
36 | }).parse(process.env);
37 |
38 | const config = {
39 | glToken: env.GL_TOKEN,
40 | baseBranchName:
41 | env.CI_MERGE_REQUEST_SOURCE_BRANCH_NAME ?? env.CI_COMMIT_BRANCH,
42 | repositoryOwner: env.CI_PROJECT_NAMESPACE,
43 | repositoryName: env.CI_PROJECT_NAME,
44 | gitlabProjectId: env.CI_PROJECT_ID,
45 | projectDir: env.CI_PROJECT_DIR,
46 | reporitoryUrl: env.CI_REPOSITORY_URL,
47 | };
48 |
49 | return config;
50 | }
51 |
52 | async branchExists({ branch }: { branch: string }): Promise<boolean> {
53 | try {
54 | await this.gitlab.Branches.show(
55 | this.platformConfig.gitlabProjectId,
56 | branch,
57 | );
58 | return true;
59 | } catch {
60 | return false;
61 | }
62 | }
63 |
64 | async getOpenPullRequestNumber({
65 | branch,
66 | }: {
67 | branch: string;
68 | }): Promise<number | undefined> {
69 | const mergeRequests = await this.gitlab.MergeRequests.all({
70 | projectId: this.platformConfig.gitlabProjectId,
71 | sourceBranch: branch,
72 | state: "opened",
73 | });
74 | return mergeRequests[0]?.iid;
75 | }
76 |
77 | async closePullRequest({
78 | pullRequestNumber,
79 | }: {
80 | pullRequestNumber: number;
81 | }): Promise<void> {
82 | await this.gitlab.MergeRequests.edit(
83 | this.platformConfig.gitlabProjectId,
84 | pullRequestNumber,
85 | {
86 | stateEvent: "close",
87 | },
88 | );
89 | }
90 |
91 | async createPullRequest({
92 | head,
93 | title,
94 | body,
95 | }: {
96 | head: string;
97 | title: string;
98 | body?: string;
99 | }): Promise<number> {
100 | const mr = await this.gitlab.MergeRequests.create(
101 | this.platformConfig.gitlabProjectId,
102 | head,
103 | this.platformConfig.baseBranchName,
104 | title,
105 | {
106 | description: body,
107 | },
108 | );
109 | return mr.iid;
110 | }
111 |
112 | async commentOnPullRequest({
113 | pullRequestNumber,
114 | body,
115 | }: {
116 | pullRequestNumber: number;
117 | body: string;
118 | }): Promise<void> {
119 | await this.gitlab.MergeRequestNotes.create(
120 | this.platformConfig.gitlabProjectId,
121 | pullRequestNumber,
122 | body,
123 | );
124 | }
125 |
126 | gitConfig(): Promise<void> | void {
127 | const glToken = this.platformConfig.glToken;
128 | const url = `https://oauth2:${glToken}@gitlab.com/${this.platformConfig.repositoryOwner}/${this.platformConfig.repositoryName}.git`;
129 |
130 | super.gitConfig(glToken, url);
131 | }
132 |
133 | buildPullRequestUrl(pullRequestNumber: number): string {
134 | return `https://gitlab.com/${this.platformConfig.repositoryOwner}/${this.platformConfig.repositoryName}/-/merge_requests/${pullRequestNumber}`;
135 | }
136 | }
137 |
```
--------------------------------------------------------------------------------
/demo/adonisjs/inertia/lingo/meta.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "files": {
3 | "pages/errors/not_found.tsx": {
4 | "scopes": {
5 | "1/declaration/body/0/argument/1/1": {
6 | "type": "element",
7 | "hash": "97612e6230bc7a1ebd99380bf561b732",
8 | "context": "",
9 | "skip": false,
10 | "overrides": {},
11 | "content": "Page not found"
12 | },
13 | "1/declaration/body/0/argument/1/3": {
14 | "type": "element",
15 | "hash": "7b6bcd0a4f23e42eeb0c972c2004efad",
16 | "context": "",
17 | "skip": false,
18 | "overrides": {},
19 | "content": "This page does not exist."
20 | }
21 | }
22 | },
23 | "pages/errors/server_error.tsx": {
24 | "scopes": {
25 | "1/declaration/body/0/argument/1/1": {
26 | "type": "element",
27 | "hash": "d574aa7e2d84d112dc79ac0e59d794cf",
28 | "context": "",
29 | "skip": false,
30 | "overrides": {},
31 | "content": "Server Error"
32 | }
33 | }
34 | },
35 | "pages/home.tsx": {
36 | "scopes": {
37 | "2/declaration/body/0/argument/1-title": {
38 | "type": "attribute",
39 | "hash": "7c2d68be7446e6de191c11d53f1e07b4",
40 | "context": "",
41 | "skip": false,
42 | "overrides": {},
43 | "content": "Homepage"
44 | },
45 | "2/declaration/body/0/argument/3/1/1": {
46 | "type": "element",
47 | "hash": "0468579ef2fbc83c9d520c2f2f1c5059",
48 | "context": "",
49 | "skip": false,
50 | "overrides": {},
51 | "content": "Hello, world!"
52 | },
53 | "2/declaration/body/0/argument/3/1/3": {
54 | "type": "element",
55 | "hash": "82b29979a52b215b94b2e811e8c03005",
56 | "context": "",
57 | "skip": false,
58 | "overrides": {},
59 | "content": "This is an example app that demonstrates how <element:strong>Lingo.dev Compiler</element:strong> can be used to localize apps built with <element:a>AdonisJS</element:a> ."
60 | },
61 | "2/declaration/body/0/argument/3/1/5": {
62 | "type": "element",
63 | "hash": "9ffb5f98cf11c88f3903e060f4028b46",
64 | "context": "",
65 | "skip": false,
66 | "overrides": {},
67 | "content": "To switch between locales, use the following dropdown:"
68 | },
69 | "3/declaration/body/0/argument/1-title": {
70 | "type": "attribute",
71 | "hash": "7c2d68be7446e6de191c11d53f1e07b4",
72 | "context": "",
73 | "skip": false,
74 | "overrides": {},
75 | "content": "Homepage"
76 | },
77 | "3/declaration/body/0/argument/3/1/1": {
78 | "type": "element",
79 | "hash": "0468579ef2fbc83c9d520c2f2f1c5059",
80 | "context": "",
81 | "skip": false,
82 | "overrides": {},
83 | "content": "Hello, world!"
84 | },
85 | "3/declaration/body/0/argument/3/1/3": {
86 | "type": "element",
87 | "hash": "82b29979a52b215b94b2e811e8c03005",
88 | "context": "",
89 | "skip": false,
90 | "overrides": {},
91 | "content": "This is an example app that demonstrates how <element:strong>Lingo.dev Compiler</element:strong> can be used to localize apps built with <element:a>AdonisJS</element:a> ."
92 | },
93 | "3/declaration/body/0/argument/3/1/5": {
94 | "type": "element",
95 | "hash": "9ffb5f98cf11c88f3903e060f4028b46",
96 | "context": "",
97 | "skip": false,
98 | "overrides": {},
99 | "content": "To switch between locales, use the following dropdown:"
100 | }
101 | }
102 | }
103 | }
104 | }
105 |
```
--------------------------------------------------------------------------------
/packages/cli/src/cli/utils/key-matching.spec.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect } from "vitest";
2 | import {
3 | matchesKeyPattern,
4 | filterEntriesByPattern,
5 | formatDisplayValue,
6 | } from "./key-matching";
7 |
8 | describe("matchesKeyPattern", () => {
9 | it("should match keys with prefix matching", () => {
10 | const patterns = ["api", "settings"];
11 |
12 | expect(matchesKeyPattern("api/users", patterns)).toBe(true);
13 | expect(matchesKeyPattern("api/posts", patterns)).toBe(true);
14 | expect(matchesKeyPattern("settings/theme", patterns)).toBe(true);
15 | expect(matchesKeyPattern("other/key", patterns)).toBe(false);
16 | });
17 |
18 | it("should match keys with glob patterns", () => {
19 | const patterns = ["api/*/users", "settings/*"];
20 |
21 | expect(matchesKeyPattern("api/v1/users", patterns)).toBe(true);
22 | expect(matchesKeyPattern("api/v2/users", patterns)).toBe(true);
23 | expect(matchesKeyPattern("settings/theme", patterns)).toBe(true);
24 | expect(matchesKeyPattern("settings/notifications", patterns)).toBe(true);
25 | expect(matchesKeyPattern("api/users", patterns)).toBe(false);
26 | });
27 |
28 | it("should return false for empty patterns", () => {
29 | expect(matchesKeyPattern("any/key", [])).toBe(false);
30 | });
31 |
32 | it("should handle complex glob patterns", () => {
33 | const patterns = ["steps/*/type", "learningGoals/*/goal"];
34 |
35 | expect(matchesKeyPattern("steps/0/type", patterns)).toBe(true);
36 | expect(matchesKeyPattern("steps/1/type", patterns)).toBe(true);
37 | expect(matchesKeyPattern("learningGoals/0/goal", patterns)).toBe(true);
38 | expect(matchesKeyPattern("steps/0/name", patterns)).toBe(false);
39 | });
40 | });
41 |
42 | describe("filterEntriesByPattern", () => {
43 | it("should filter entries that match patterns", () => {
44 | const entries: [string, any][] = [
45 | ["api/users", "Users API"],
46 | ["api/posts", "Posts API"],
47 | ["settings/theme", "Dark"],
48 | ["other/key", "Value"],
49 | ];
50 | const patterns = ["api", "settings"];
51 |
52 | const result = filterEntriesByPattern(entries, patterns);
53 |
54 | expect(result).toHaveLength(3);
55 | expect(result).toEqual([
56 | ["api/users", "Users API"],
57 | ["api/posts", "Posts API"],
58 | ["settings/theme", "Dark"],
59 | ]);
60 | });
61 |
62 | it("should return empty array when no matches", () => {
63 | const entries: [string, any][] = [
64 | ["key1", "value1"],
65 | ["key2", "value2"],
66 | ];
67 | const patterns = ["nonexistent"];
68 |
69 | const result = filterEntriesByPattern(entries, patterns);
70 |
71 | expect(result).toHaveLength(0);
72 | });
73 | });
74 |
75 | describe("formatDisplayValue", () => {
76 | it("should return short strings as-is", () => {
77 | expect(formatDisplayValue("Hello")).toBe("Hello");
78 | expect(formatDisplayValue("Short text")).toBe("Short text");
79 | });
80 |
81 | it("should truncate long strings", () => {
82 | const longString = "a".repeat(100);
83 | const result = formatDisplayValue(longString);
84 |
85 | expect(result).toHaveLength(53); // 50 chars + "..."
86 | expect(result.endsWith("...")).toBe(true);
87 | });
88 |
89 | it("should use custom max length", () => {
90 | const text = "Hello, World!";
91 | const result = formatDisplayValue(text, 5);
92 |
93 | expect(result).toBe("Hello...");
94 | });
95 |
96 | it("should stringify non-string values", () => {
97 | expect(formatDisplayValue(42)).toBe("42");
98 | expect(formatDisplayValue(true)).toBe("true");
99 | expect(formatDisplayValue({ key: "value" })).toBe('{"key":"value"}');
100 | expect(formatDisplayValue(null)).toBe("null");
101 | });
102 |
103 | it("should handle arrays", () => {
104 | expect(formatDisplayValue([1, 2, 3])).toBe("[1,2,3]");
105 | });
106 | });
107 |
```
--------------------------------------------------------------------------------
/packages/compiler/src/jsx-attribute-scope-inject.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { createCodeMutation } from "./_base";
2 | import { getModuleExecutionMode, getOrCreateImport } from "./utils";
3 | import * as t from "@babel/types";
4 | import _ from "lodash";
5 | import { ModuleId } from "./_const";
6 | import { getJsxElementName, getNestedJsxElements } from "./utils/jsx-element";
7 | import { collectJsxAttributeScopes } from "./utils/jsx-attribute-scope";
8 | import { setJsxAttributeValue } from "./utils/jsx-attribute";
9 |
10 | export const lingoJsxAttributeScopeInjectMutation = createCodeMutation(
11 | (payload) => {
12 | const mode = getModuleExecutionMode(payload.ast, payload.params.rsc);
13 | const jsxAttributeScopes = collectJsxAttributeScopes(payload.ast);
14 |
15 | for (const [jsxScope, attributes] of jsxAttributeScopes) {
16 | // Import LingoComponent based on the module execution mode
17 | const packagePath =
18 | mode === "client" ? ModuleId.ReactClient : ModuleId.ReactRSC;
19 | const lingoComponentImport = getOrCreateImport(payload.ast, {
20 | moduleName: packagePath,
21 | exportedName: "LingoAttributeComponent",
22 | });
23 |
24 | // Get the original JSX element name
25 | const originalJsxElementName = getJsxElementName(jsxScope);
26 | if (!originalJsxElementName) {
27 | continue;
28 | }
29 |
30 | // Replace the name with the lingo component
31 | jsxScope.node.openingElement.name = t.jsxIdentifier(
32 | lingoComponentImport.importedName,
33 | );
34 | if (jsxScope.node.closingElement) {
35 | jsxScope.node.closingElement.name = t.jsxIdentifier(
36 | lingoComponentImport.importedName,
37 | );
38 | }
39 |
40 | // Add $attrAs ($as) prop
41 | const as = /^[A-Z]/.test(originalJsxElementName)
42 | ? t.jsxExpressionContainer(t.identifier(originalJsxElementName))
43 | : t.stringLiteral(originalJsxElementName);
44 |
45 | jsxScope.node.openingElement.attributes.push(
46 | t.jsxAttribute(t.jsxIdentifier("$attrAs"), as),
47 | );
48 |
49 | // Add $fileKey prop
50 | setJsxAttributeValue(jsxScope, "$fileKey", payload.relativeFilePath);
51 |
52 | // Add $attributes prop
53 | setJsxAttributeValue(
54 | jsxScope,
55 | "$attributes",
56 | t.objectExpression(
57 | attributes.map((attributeDefinition) => {
58 | const [attribute, key = ""] = attributeDefinition.split(":");
59 | return t.objectProperty(
60 | t.stringLiteral(attribute),
61 | t.stringLiteral(key),
62 | );
63 | }),
64 | ),
65 | );
66 |
67 | // // Extract $variables from original JSX scope
68 | // const $variables = getJsxVariables(originalJsxScope);
69 | // if ($variables.properties.length > 0) {
70 | // setJsxAttributeValue(jsxScope, "$variables", $variables);
71 | // }
72 |
73 | // // Extract nested JSX elements
74 | // const $elements = getNestedJsxElements(originalJsxScope);
75 | // if ($elements.elements.length > 0) {
76 | // setJsxAttributeValue(jsxScope, "$elements", $elements);
77 | // }
78 |
79 | if (mode === "server") {
80 | // Add $loadDictionary prop
81 | const loadDictionaryImport = getOrCreateImport(payload.ast, {
82 | exportedName: "loadDictionary",
83 | moduleName: ModuleId.ReactRSC,
84 | });
85 | setJsxAttributeValue(
86 | jsxScope,
87 | "$loadDictionary",
88 | t.arrowFunctionExpression(
89 | [t.identifier("locale")],
90 | t.callExpression(t.identifier(loadDictionaryImport.importedName), [
91 | t.identifier("locale"),
92 | ]),
93 | ),
94 | );
95 | }
96 | }
97 |
98 | return payload;
99 | },
100 | );
101 |
```
--------------------------------------------------------------------------------
/packages/cli/src/cli/loaders/formatters/biome.ts:
--------------------------------------------------------------------------------
```typescript
1 | import path from "path";
2 | import fs from "fs/promises";
3 | import { Biome, Distribution } from "@biomejs/js-api";
4 | import { parse as parseJsonc } from "jsonc-parser";
5 | import { ILoader } from "../_types";
6 | import { createBaseFormatterLoader } from "./_base";
7 |
8 | export type BiomeLoaderOptions = {
9 | bucketPathPattern: string;
10 | stage?: "pull" | "push" | "both";
11 | alwaysFormat?: boolean;
12 | };
13 |
14 | export default function createBiomeLoader(
15 | options: BiomeLoaderOptions,
16 | ): ILoader<string, string> {
17 | return createBaseFormatterLoader(options, async (data, filePath) => {
18 | return await formatDataWithBiome(data, filePath, options);
19 | });
20 | }
21 |
22 | async function findBiomeConfig(startPath: string): Promise<string | null> {
23 | let currentDir = path.dirname(startPath);
24 | const root = path.parse(currentDir).root;
25 |
26 | while (currentDir !== root) {
27 | for (const configName of ["biome.json", "biome.jsonc"]) {
28 | const configPath = path.join(currentDir, configName);
29 | try {
30 | await fs.access(configPath);
31 | return configPath;
32 | } catch {
33 | // Config file doesn't exist, continue searching
34 | }
35 | }
36 |
37 | const parentDir = path.dirname(currentDir);
38 | if (parentDir === currentDir) break;
39 | currentDir = parentDir;
40 | }
41 |
42 | return null;
43 | }
44 |
45 | async function formatDataWithBiome(
46 | data: string,
47 | filePath: string,
48 | options: BiomeLoaderOptions,
49 | ): Promise<string> {
50 | let configPath: string | null = null;
51 |
52 | try {
53 | const biome = await Biome.create({
54 | distribution: Distribution.NODE,
55 | });
56 |
57 | // Open a project (required in v3.0.0+)
58 | const openResult = biome.openProject(".");
59 | const projectKey = openResult.projectKey;
60 |
61 | // Load config from biome.json/biome.jsonc if exists
62 | configPath = await findBiomeConfig(filePath);
63 | if (!configPath && !options.alwaysFormat) {
64 | console.log();
65 | console.log(
66 | `⚠️ Biome config not found for ${path.basename(filePath)} - skipping formatting`,
67 | );
68 | return data;
69 | }
70 |
71 | if (configPath) {
72 | const configContent = await fs.readFile(configPath, "utf-8");
73 | try {
74 | // Parse JSONC (JSON with comments) properly using jsonc-parser
75 | const config = parseJsonc(configContent);
76 |
77 | // WORKAROUND: Biome JS API v3 has a bug where applying the full config
78 | // causes formatter settings to be ignored. Apply only relevant sections.
79 | // Specifically, exclude $schema, vcs, and files from the config.
80 | const { $schema, vcs, files, ...relevantConfig } = config;
81 |
82 | biome.applyConfiguration(projectKey, relevantConfig);
83 | } catch (parseError) {
84 | throw new Error(
85 | `Invalid Biome configuration in ${configPath}: ${parseError instanceof Error ? parseError.message : "JSON parse error"}`,
86 | );
87 | }
88 | }
89 |
90 | const formatted = biome.formatContent(projectKey, data, {
91 | filePath,
92 | });
93 |
94 | return formatted.content;
95 | } catch (error) {
96 | // Extract error message from Biome
97 | const errorMessage =
98 | error instanceof Error
99 | ? error.message || (error as any).stackTrace?.toString().split("\n")[0]
100 | : "";
101 |
102 | if (errorMessage?.includes("does not exist in the workspace")) {
103 | // Biome says "file does not exist in workspace" for unsupported formats - skip
104 | } else {
105 | console.log(`⚠️ Biome skipped ${path.basename(filePath)}`);
106 | if (errorMessage) {
107 | console.log(` ${errorMessage}`);
108 | }
109 | }
110 |
111 | return data; // Fallback to unformatted
112 | }
113 | }
114 |
```
--------------------------------------------------------------------------------
/packages/spec/src/locales.spec.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect } from "vitest";
2 | import {
3 | getLocaleCodeDelimiter,
4 | normalizeLocale,
5 | resolveLocaleCode,
6 | resolveOverriddenLocale,
7 | } from "./locales";
8 |
9 | describe("normalizeLocale", () => {
10 | it("should return normalized locale for short locale codes", () => {
11 | expect(normalizeLocale("en")).toEqual("en");
12 | expect(normalizeLocale("fr")).toEqual("fr");
13 | });
14 |
15 | it("should return normalized locale for full locale codes", () => {
16 | expect(normalizeLocale("en-US")).toEqual("en-US");
17 | expect(normalizeLocale("fr-FR")).toEqual("fr-FR");
18 | });
19 |
20 | it("should return normalized locale for full underscore locale codes", () => {
21 | expect(normalizeLocale("en_US")).toEqual("en-US");
22 | expect(normalizeLocale("fr_FR")).toEqual("fr-FR");
23 | expect(normalizeLocale("zh_Hans_CN")).toEqual("zh-Hans-CN");
24 | });
25 |
26 | it("should return normalized locale for full explicit region locale codes", () => {
27 | expect(normalizeLocale("en-rUS")).toEqual("en-US");
28 | expect(normalizeLocale("fr-rFR")).toEqual("fr-FR");
29 | expect(normalizeLocale("zh-rCN")).toEqual("zh-CN");
30 | });
31 | });
32 |
33 | describe("resolveLocaleCode", () => {
34 | it("should resolve a short locale code to the first full locale code in the map", () => {
35 | expect(resolveLocaleCode("en")).toEqual("en-US");
36 | expect(resolveLocaleCode("fr")).toEqual("fr-FR");
37 | expect(resolveLocaleCode("az")).toEqual("az-AZ");
38 | });
39 |
40 | it("should return the full locale code if it is already provided", () => {
41 | expect(resolveLocaleCode("en-US")).toEqual("en-US");
42 | expect(resolveLocaleCode("fr-CA")).toEqual("fr-CA");
43 | expect(resolveLocaleCode("es-MX")).toEqual("es-MX");
44 | });
45 |
46 | it("should throw an error for an invalid or unsupported locale code", () => {
47 | expect(() => resolveLocaleCode("az-US")).toThrow("Invalid locale code");
48 | expect(() => resolveLocaleCode("au")).toThrow("Invalid locale code");
49 | });
50 |
51 | it("should return first code for locales with multiple variants", () => {
52 | expect(resolveLocaleCode("sr")).toEqual("sr-RS");
53 | expect(resolveLocaleCode("zh")).toEqual("zh-CN");
54 | });
55 | });
56 |
57 | describe("getLocaleCodeDelimiter", () => {
58 | it("should return '-' for locale codes with hyphen delimiter", () => {
59 | expect(getLocaleCodeDelimiter("en-US")).toEqual("-");
60 | expect(getLocaleCodeDelimiter("fr-FR")).toEqual("-");
61 | });
62 |
63 | it("should return '_' for locale codes with underscore delimiter", () => {
64 | expect(getLocaleCodeDelimiter("en_US")).toEqual("_");
65 | expect(getLocaleCodeDelimiter("fr_FR")).toEqual("_");
66 | });
67 |
68 | it("should return undefined for locale codes without a recognized delimiter", () => {
69 | expect(getLocaleCodeDelimiter("enUS")).toBeNull();
70 | expect(getLocaleCodeDelimiter("frFR")).toBeNull();
71 | expect(getLocaleCodeDelimiter("kaGE")).toBeNull();
72 | });
73 | });
74 |
75 | describe("resolveOverridenLocale", () => {
76 | it("should return the same locale if no delimiter is provided", () => {
77 | expect(resolveOverriddenLocale("en-US")).toEqual("en-US");
78 | expect(resolveOverriddenLocale("fr_FR")).toEqual("fr_FR");
79 | });
80 |
81 | it("should replace the delimiter with the specified one", () => {
82 | expect(resolveOverriddenLocale("en-US", "_")).toEqual("en_US");
83 | expect(resolveOverriddenLocale("fr_FR", "-")).toEqual("fr-FR");
84 | });
85 |
86 | it("should return the same locale if no recognized delimiter is found", () => {
87 | expect(resolveOverriddenLocale("enUS", "_")).toEqual("enUS");
88 | expect(resolveOverriddenLocale("frFR", "-")).toEqual("frFR");
89 | });
90 | });
91 |
```
--------------------------------------------------------------------------------
/packages/cli/src/cli/loaders/flutter.spec.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect } from "vitest";
2 | import createFlutterLoader from "./flutter";
3 |
4 | const locale = "en";
5 | const originalLocale = "en";
6 |
7 | describe("createFlutterLoader", () => {
8 | describe("pull", () => {
9 | it("should remove metadata keys starting with @", async () => {
10 | const loader = createFlutterLoader();
11 | loader.setDefaultLocale(locale);
12 | const input = {
13 | "@metadata": "some-data",
14 | hello: "world",
15 | another_key: "another_value",
16 | "@@locale": "en",
17 | };
18 | const expected = {
19 | hello: "world",
20 | another_key: "another_value",
21 | };
22 | const result = await loader.pull("en", input);
23 | expect(result).toEqual(expected);
24 | });
25 |
26 | it("should return an empty object if all keys are metadata", async () => {
27 | const loader = createFlutterLoader();
28 | loader.setDefaultLocale(locale);
29 | const input = {
30 | "@metadata": "some-data",
31 | "@@locale": "en",
32 | };
33 | const expected = {};
34 | const result = await loader.pull("en", input);
35 | expect(result).toEqual(expected);
36 | });
37 |
38 | it("should return the same object if no keys are metadata", async () => {
39 | const loader = createFlutterLoader();
40 | loader.setDefaultLocale(locale);
41 | const input = {
42 | hello: "world",
43 | another_key: "another_value",
44 | };
45 | const expected = {
46 | hello: "world",
47 | another_key: "another_value",
48 | };
49 | const result = await loader.pull("en", input);
50 | expect(result).toEqual(expected);
51 | });
52 |
53 | it("should handle empty input", async () => {
54 | const loader = createFlutterLoader();
55 | loader.setDefaultLocale(locale);
56 | const input = {};
57 | const expected = {};
58 | const result = await loader.pull("en", input);
59 | expect(result).toEqual(expected);
60 | });
61 | });
62 |
63 | describe("push", () => {
64 | it("should merge data and add locale", async () => {
65 | const loader = createFlutterLoader();
66 | loader.setDefaultLocale(locale);
67 | const originalInput = {
68 | hello: "world",
69 | "@metadata": "some-data",
70 | };
71 | await loader.pull(originalLocale, originalInput);
72 | const data = {
73 | foo: "bar",
74 | hello: "monde",
75 | };
76 | const expected = {
77 | hello: "monde",
78 | foo: "bar",
79 | "@metadata": "some-data",
80 | "@@locale": "fr",
81 | };
82 | const result = await loader.push("fr", data);
83 | expect(result).toEqual(expected);
84 | });
85 |
86 | it("should handle empty original input", async () => {
87 | const loader = createFlutterLoader();
88 | loader.setDefaultLocale(locale);
89 | const originalInput = {};
90 | await loader.pull(originalLocale, originalInput);
91 | const data = {
92 | foo: "bar",
93 | };
94 | const expected = {
95 | foo: "bar",
96 | "@@locale": "en",
97 | };
98 | const result = await loader.push("en", data);
99 | expect(result).toEqual(expected);
100 | });
101 |
102 | it("should handle empty data, not add extra keys from originalInput", async () => {
103 | const loader = createFlutterLoader();
104 | loader.setDefaultLocale(locale);
105 | const originalInput = {
106 | hello: "world",
107 | };
108 | await loader.pull(originalLocale, originalInput);
109 | const data = {
110 | goodbye: "moon",
111 | };
112 | const expected = {
113 | goodbye: "moon",
114 | "@@locale": "en",
115 | };
116 | const result = await loader.push("en", data);
117 | expect(result).toEqual(expected);
118 | });
119 | });
120 | });
121 |
```
--------------------------------------------------------------------------------
/packages/cli/src/cli/cmd/ci/platforms/github.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Octokit } from "octokit";
2 | import { PlatformKit } from "./_base";
3 | import Z from "zod";
4 |
5 | export class GitHubPlatformKit extends PlatformKit {
6 | private _octokit?: Octokit;
7 |
8 | private get octokit(): Octokit {
9 | if (!this._octokit) {
10 | this._octokit = new Octokit({ auth: this.platformConfig.ghToken });
11 | }
12 | return this._octokit;
13 | }
14 |
15 | async branchExists({ branch }: { branch: string }) {
16 | return await this.octokit.rest.repos
17 | .getBranch({
18 | branch,
19 | owner: this.platformConfig.repositoryOwner,
20 | repo: this.platformConfig.repositoryName,
21 | })
22 | .then((r) => r.data)
23 | .then((v) => !!v)
24 | .catch((r) => (r.status === 404 ? false : Promise.reject(r)));
25 | }
26 |
27 | async getOpenPullRequestNumber({ branch }: { branch: string }) {
28 | return await this.octokit.rest.pulls
29 | .list({
30 | head: `${this.platformConfig.repositoryOwner}:${branch}`,
31 | owner: this.platformConfig.repositoryOwner,
32 | repo: this.platformConfig.repositoryName,
33 | base: this.platformConfig.baseBranchName,
34 | state: "open",
35 | })
36 | .then(({ data }) => data[0])
37 | .then((pr) => pr?.number);
38 | }
39 |
40 | async closePullRequest({ pullRequestNumber }: { pullRequestNumber: number }) {
41 | await this.octokit.rest.pulls.update({
42 | pull_number: pullRequestNumber,
43 | owner: this.platformConfig.repositoryOwner,
44 | repo: this.platformConfig.repositoryName,
45 | state: "closed",
46 | });
47 | }
48 |
49 | async createPullRequest({
50 | head,
51 | title,
52 | body,
53 | }: {
54 | head: string;
55 | title: string;
56 | body?: string;
57 | }) {
58 | return await this.octokit.rest.pulls
59 | .create({
60 | head,
61 | title,
62 | body,
63 | owner: this.platformConfig.repositoryOwner,
64 | repo: this.platformConfig.repositoryName,
65 | base: this.platformConfig.baseBranchName,
66 | })
67 | .then(({ data }) => data.number);
68 | }
69 |
70 | async commentOnPullRequest({
71 | pullRequestNumber,
72 | body,
73 | }: {
74 | pullRequestNumber: number;
75 | body: string;
76 | }) {
77 | await this.octokit.rest.issues.createComment({
78 | issue_number: pullRequestNumber,
79 | body,
80 | owner: this.platformConfig.repositoryOwner,
81 | repo: this.platformConfig.repositoryName,
82 | });
83 | }
84 |
85 | async gitConfig() {
86 | const { ghToken, repositoryOwner, repositoryName } = this.platformConfig;
87 | const { processOwnCommits } = this.config;
88 |
89 | if (ghToken && processOwnCommits) {
90 | console.log(
91 | "Using provided GH_TOKEN. This will trigger your CI/CD pipeline to run again.",
92 | );
93 |
94 | const url = `https://${ghToken}@github.com/${repositoryOwner}/${repositoryName}.git`;
95 |
96 | super.gitConfig(ghToken, url);
97 | }
98 | }
99 |
100 | get platformConfig() {
101 | const env = Z.object({
102 | GITHUB_REPOSITORY: Z.string(),
103 | GITHUB_REPOSITORY_OWNER: Z.string(),
104 | GITHUB_REF_NAME: Z.string(),
105 | GITHUB_HEAD_REF: Z.string(),
106 | GH_TOKEN: Z.string().optional(),
107 | }).parse(process.env);
108 |
109 | const baseBranchName = !env.GITHUB_REF_NAME.endsWith("/merge")
110 | ? env.GITHUB_REF_NAME
111 | : env.GITHUB_HEAD_REF;
112 |
113 | return {
114 | ghToken: env.GH_TOKEN,
115 | baseBranchName,
116 | repositoryOwner: env.GITHUB_REPOSITORY_OWNER,
117 | repositoryName: env.GITHUB_REPOSITORY.split("/")[1],
118 | };
119 | }
120 |
121 | buildPullRequestUrl(pullRequestNumber: number) {
122 | const { repositoryOwner, repositoryName } = this.platformConfig;
123 | return `https://github.com/${repositoryOwner}/${repositoryName}/pull/${pullRequestNumber}`;
124 | }
125 | }
126 |
```
--------------------------------------------------------------------------------
/packages/locales/src/parser.spec.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect } from "vitest";
2 | import {
3 | parseLocale,
4 | getLanguageCode,
5 | getScriptCode,
6 | getRegionCode,
7 | } from "./parser";
8 |
9 | describe("parseLocale", () => {
10 | it("should parse basic language-region locales with hyphen", () => {
11 | expect(parseLocale("en-US")).toEqual({
12 | language: "en",
13 | region: "US",
14 | });
15 | });
16 |
17 | it("should parse basic language-region locales with underscore", () => {
18 | expect(parseLocale("en_US")).toEqual({
19 | language: "en",
20 | region: "US",
21 | });
22 | });
23 |
24 | it("should parse language-script-region locales with hyphen", () => {
25 | expect(parseLocale("zh-Hans-CN")).toEqual({
26 | language: "zh",
27 | script: "Hans",
28 | region: "CN",
29 | });
30 | });
31 |
32 | it("should parse language-script-region locales with underscore", () => {
33 | expect(parseLocale("zh_Hans_CN")).toEqual({
34 | language: "zh",
35 | script: "Hans",
36 | region: "CN",
37 | });
38 | });
39 |
40 | it("should parse language-only locales", () => {
41 | expect(parseLocale("es")).toEqual({
42 | language: "es",
43 | });
44 | });
45 |
46 | it("should parse complex script locales", () => {
47 | expect(parseLocale("sr-Cyrl-RS")).toEqual({
48 | language: "sr",
49 | script: "Cyrl",
50 | region: "RS",
51 | });
52 | });
53 |
54 | it("should handle numeric region codes", () => {
55 | expect(parseLocale("es-419")).toEqual({
56 | language: "es",
57 | region: "419",
58 | });
59 | });
60 |
61 | it("should normalize language to lowercase", () => {
62 | expect(parseLocale("EN-US")).toEqual({
63 | language: "en",
64 | region: "US",
65 | });
66 | });
67 |
68 | it("should normalize region to uppercase", () => {
69 | expect(parseLocale("en-us")).toEqual({
70 | language: "en",
71 | region: "US",
72 | });
73 | });
74 |
75 | it("should preserve script case", () => {
76 | expect(parseLocale("zh-hans-cn")).toEqual({
77 | language: "zh",
78 | script: "hans",
79 | region: "CN",
80 | });
81 | });
82 |
83 | it("should throw error for invalid locale format", () => {
84 | expect(() => parseLocale("invalid")).toThrow(
85 | "Invalid locale format: invalid",
86 | );
87 | });
88 |
89 | it("should throw error for empty string", () => {
90 | expect(() => parseLocale("")).toThrow("Locale cannot be empty");
91 | });
92 |
93 | it("should throw error for non-string input", () => {
94 | expect(() => parseLocale(null as any)).toThrow("Locale must be a string");
95 | });
96 | });
97 |
98 | describe("getLanguageCode", () => {
99 | it("should extract language code from various formats", () => {
100 | expect(getLanguageCode("en-US")).toBe("en");
101 | expect(getLanguageCode("zh-Hans-CN")).toBe("zh");
102 | expect(getLanguageCode("es-MX")).toBe("es");
103 | expect(getLanguageCode("fr_CA")).toBe("fr");
104 | expect(getLanguageCode("es")).toBe("es");
105 | });
106 | });
107 |
108 | describe("getScriptCode", () => {
109 | it("should extract script code when present", () => {
110 | expect(getScriptCode("zh-Hans-CN")).toBe("Hans");
111 | expect(getScriptCode("zh-Hant-TW")).toBe("Hant");
112 | expect(getScriptCode("sr-Cyrl-RS")).toBe("Cyrl");
113 | });
114 |
115 | it("should return null when script is not present", () => {
116 | expect(getScriptCode("en-US")).toBeNull();
117 | expect(getScriptCode("es")).toBeNull();
118 | });
119 | });
120 |
121 | describe("getRegionCode", () => {
122 | it("should extract region code when present", () => {
123 | expect(getRegionCode("en-US")).toBe("US");
124 | expect(getRegionCode("zh-Hans-CN")).toBe("CN");
125 | expect(getRegionCode("fr_CA")).toBe("CA");
126 | });
127 |
128 | it("should return null when region is not present", () => {
129 | expect(getRegionCode("es")).toBeNull();
130 | expect(getRegionCode("zh-Hans")).toBeNull();
131 | });
132 | });
133 |
```