This is page 3 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/react/build.config.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { defineBuildConfig } from "unbuild";
2 |
3 | export default defineBuildConfig({
4 | /* Clean the output directory before each build */
5 | clean: true,
6 |
7 | /* Where generated files are written */
8 | outDir: "build",
9 |
10 | /* Generate type declarations */
11 | declaration: true,
12 |
13 | /* Generate source-maps */
14 | sourcemap: true,
15 |
16 | /* Treat these as external – they must be provided by the host app */
17 | externals: ["react", "next"],
18 |
19 | /* Transpile every file in src/ one-to-one into build/ keeping the folder structure */
20 | entries: [
21 | {
22 | builder: "mkdist",
23 | /* All TS/TSX/JS/JSX files under src become part of the build */
24 | input: "./src",
25 | /* Mirror the structure in the build directory */
26 | outDir: "./build",
27 | /* Emit ESM with the standard .js extension */
28 | format: "esm",
29 | ext: "js",
30 | /* Produce matching .d.ts files next to their JS counterparts */
31 | declaration: true,
32 | /* Ensure relative imports inside declaration files include the .js extension */
33 | addRelativeDeclarationExtensions: true,
34 | /* Use React 17+ automatic JSX runtime so output imports jsx from react/jsx-runtime */
35 | esbuild: {
36 | jsx: "automatic",
37 | jsxImportSource: "react",
38 | },
39 | },
40 | ],
41 | });
42 |
```
--------------------------------------------------------------------------------
/packages/compiler/src/lingo-turbopack-loader.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { loadDictionary, transformComponent } from "./_loader-utils";
2 |
3 | // This loader handles component transformations and dictionary generation
4 | export default async function (this: any, source: string) {
5 | const callback = this.async();
6 | const params = this.getOptions();
7 | const isDev = process.env.NODE_ENV !== "production";
8 |
9 | try {
10 | // Dictionary loading
11 | const dictionary = await loadDictionary({
12 | resourcePath: this.resourcePath,
13 | resourceQuery: this.resourceQuery,
14 | params,
15 | sourceRoot: params.sourceRoot,
16 | lingoDir: params.lingoDir,
17 | isDev,
18 | });
19 |
20 | if (dictionary) {
21 | const code = `export default ${JSON.stringify(dictionary, null, 2)};`;
22 | return callback(null, code);
23 | }
24 |
25 | // Component transformation
26 | const result = transformComponent({
27 | code: source,
28 | params,
29 | resourcePath: this.resourcePath,
30 | sourceRoot: params.sourceRoot,
31 | });
32 |
33 | return callback(
34 | null,
35 | result.code,
36 | result.map ? JSON.stringify(result.map) : undefined,
37 | );
38 | } catch (error) {
39 | console.error(
40 | `⚠️ Lingo.dev compiler (Turbopack) failed for ${this.resourcePath}:`,
41 | );
42 | console.error("⚠️ Details:", error);
43 | callback(error as Error);
44 | }
45 | }
46 |
```
--------------------------------------------------------------------------------
/packages/compiler/src/index.spec.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect, vi, beforeEach } from "vitest";
2 | import compiler from "./index";
3 |
4 | // Silence logs in tests
5 | vi.spyOn(console, "log").mockImplementation(() => undefined as any);
6 | vi.spyOn(console, "warn").mockImplementation(() => undefined as any);
7 |
8 | vi.mock("./utils/env", () => ({ isRunningInCIOrDocker: () => true }));
9 | vi.mock("./lib/lcp/cache", () => ({
10 | LCPCache: { ensureDictionaryFile: vi.fn() },
11 | }));
12 | vi.mock("unplugin", () => ({
13 | createUnplugin: () => ({
14 | vite: vi.fn(() => ({ name: "test-plugin" })),
15 | webpack: vi.fn(() => ({ name: "test-plugin" })),
16 | }),
17 | }));
18 |
19 | describe("compiler integration", () => {
20 | beforeEach(() => {
21 | (process as any).env = { ...process.env };
22 | });
23 |
24 | it("next() returns a function and sets webpack wrapper when turbopack disabled", () => {
25 | const cfg: any = { webpack: (c: any) => c };
26 | const out = compiler.next({
27 | sourceRoot: "src",
28 | models: "lingo.dev",
29 | turbopack: { enabled: false },
30 | })(cfg);
31 | expect(typeof out.webpack).toBe("function");
32 | });
33 |
34 | it("vite() pushes plugin to front and detects framework label", () => {
35 | const cfg: any = { plugins: [{ name: "react-router" }] };
36 | const out = compiler.vite({})(cfg);
37 | expect(out.plugins[0]).toBeDefined();
38 | });
39 | });
40 |
```
--------------------------------------------------------------------------------
/packages/react/src/client/attribute-component.spec.tsx:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect, vi } from "vitest";
2 | import React from "react";
3 | import { LingoContext } from "./context";
4 |
5 | vi.mock("../core", () => {
6 | return {
7 | LingoAttributeComponent: (props: any) => {
8 | return React.createElement("div", {
9 | "data-testid": "core-attr",
10 | "data-has-dictionary": props.$dictionary ? "yes" : "no",
11 | "data-file": props.$fileKey,
12 | });
13 | },
14 | };
15 | });
16 |
17 | describe("client/attribute-component", () => {
18 | describe("LingoAttributeComponent wrapper", () => {
19 | it("injects dictionary from context into core attribute component", async () => {
20 | const dictionary = { locale: "en" } as any;
21 | const { LingoAttributeComponent } = await import("./attribute-component");
22 | const { render, screen } = await import("@testing-library/react");
23 |
24 | render(
25 | <LingoContext.Provider value={{ dictionary }}>
26 | <LingoAttributeComponent
27 | $attrAs="a"
28 | $fileKey="messages"
29 | $attributes={{ title: "title" }}
30 | />
31 | </LingoContext.Provider>,
32 | );
33 |
34 | const el = await screen.findByTestId("core-attr");
35 | expect(el.getAttribute("data-has-dictionary")).toBe("yes");
36 | expect(el.getAttribute("data-file")).toBe("messages");
37 | });
38 | });
39 | });
40 |
```
--------------------------------------------------------------------------------
/demo/adonisjs/bin/server.ts:
--------------------------------------------------------------------------------
```typescript
1 | /*
2 | |--------------------------------------------------------------------------
3 | | HTTP server entrypoint
4 | |--------------------------------------------------------------------------
5 | |
6 | | The "server.ts" file is the entrypoint for starting the AdonisJS HTTP
7 | | server. Either you can run this file directly or use the "serve"
8 | | command to run this file and monitor file changes
9 | |
10 | */
11 |
12 | import 'reflect-metadata'
13 | import { Ignitor, prettyPrintError } from '@adonisjs/core'
14 |
15 | /**
16 | * URL to the application root. AdonisJS need it to resolve
17 | * paths to file and directories for scaffolding commands
18 | */
19 | const APP_ROOT = new URL('../', import.meta.url)
20 |
21 | /**
22 | * The importer is used to import files in context of the
23 | * application.
24 | */
25 | const IMPORTER = (filePath: string) => {
26 | if (filePath.startsWith('./') || filePath.startsWith('../')) {
27 | return import(new URL(filePath, APP_ROOT).href)
28 | }
29 | return import(filePath)
30 | }
31 |
32 | new Ignitor(APP_ROOT, { importer: IMPORTER })
33 | .tap((app) => {
34 | app.booting(async () => {
35 | await import('#start/env')
36 | })
37 | app.listen('SIGTERM', () => app.terminate())
38 | app.listenIf(app.managedByPm2, 'SIGINT', () => app.terminate())
39 | })
40 | .httpServer()
41 | .start()
42 | .catch((error) => {
43 | process.exitCode = 1
44 | prettyPrintError(error)
45 | })
46 |
```
--------------------------------------------------------------------------------
/demo/adonisjs/config/bodyparser.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { defineConfig } from '@adonisjs/core/bodyparser'
2 |
3 | const bodyParserConfig = defineConfig({
4 | /**
5 | * The bodyparser middleware will parse the request body
6 | * for the following HTTP methods.
7 | */
8 | allowedMethods: ['POST', 'PUT', 'PATCH', 'DELETE'],
9 |
10 | /**
11 | * Config for the "application/x-www-form-urlencoded"
12 | * content-type parser
13 | */
14 | form: {
15 | convertEmptyStringsToNull: true,
16 | types: ['application/x-www-form-urlencoded'],
17 | },
18 |
19 | /**
20 | * Config for the JSON parser
21 | */
22 | json: {
23 | convertEmptyStringsToNull: true,
24 | types: [
25 | 'application/json',
26 | 'application/json-patch+json',
27 | 'application/vnd.api+json',
28 | 'application/csp-report',
29 | ],
30 | },
31 |
32 | /**
33 | * Config for the "multipart/form-data" content-type parser.
34 | * File uploads are handled by the multipart parser.
35 | */
36 | multipart: {
37 | /**
38 | * Enabling auto process allows bodyparser middleware to
39 | * move all uploaded files inside the tmp folder of your
40 | * operating system
41 | */
42 | autoProcess: true,
43 | convertEmptyStringsToNull: true,
44 | processManually: [],
45 |
46 | /**
47 | * Maximum limit of data to parse including all files
48 | * and fields
49 | */
50 | limit: '20mb',
51 | types: ['multipart/form-data'],
52 | },
53 | })
54 |
55 | export default bodyParserConfig
56 |
```
--------------------------------------------------------------------------------
/packages/compiler/src/utils/ast-key.spec.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { it, describe, expect } from "vitest";
2 | import { parse } from "@babel/parser";
3 | import traverse, { NodePath } from "@babel/traverse";
4 | import { getAstKey, getAstByKey } from "./ast-key";
5 |
6 | describe("ast key", () => {
7 | it("getAstKey should calc nodePath key", () => {
8 | const mockData = createMockData();
9 |
10 | const key = getAstKey(mockData.testElementPath);
11 |
12 | expect(key).toBe(mockData.testElementKey);
13 | });
14 | });
15 |
16 | describe("getAstByKey", () => {
17 | it("should retrieve correct node by key", () => {
18 | const mockData = createMockData();
19 |
20 | const elementPath = getAstByKey(mockData.ast, mockData.testElementKey);
21 |
22 | expect(elementPath).toBe(mockData.testElementPath);
23 | });
24 | });
25 |
26 | // helpers
27 |
28 | function createMockData() {
29 | const ast = parse(
30 | `
31 | export function MyComponent() {
32 | return <div>Hello world!</div>;
33 | }
34 | `,
35 | { sourceType: "module", plugins: ["jsx"] },
36 | );
37 |
38 | let testElementPath: NodePath | null = null;
39 | traverse(ast, {
40 | JSXElement(nodePath) {
41 | testElementPath = nodePath;
42 | },
43 | });
44 | if (!testElementPath) {
45 | throw new Error(
46 | "testElementPath cannot be null - check test case definition",
47 | );
48 | }
49 |
50 | const testElementKey = `0/declaration/body/0/argument`;
51 |
52 | return {
53 | ast,
54 | testElementPath,
55 | testElementKey,
56 | };
57 | }
58 |
```
--------------------------------------------------------------------------------
/packages/cli/src/cli/loaders/vtt.ts:
--------------------------------------------------------------------------------
```typescript
1 | import webvtt from "node-webvtt";
2 | import { ILoader } from "./_types";
3 | import { createLoader } from "./_utils";
4 |
5 | export default function createVttLoader(): ILoader<
6 | string,
7 | Record<string, any>
8 | > {
9 | return createLoader({
10 | async pull(locale, input) {
11 | if (!input) {
12 | return ""; // if VTT file does not exist yet we can not parse it - return empty string
13 | }
14 | const vtt = webvtt.parse(input)?.cues;
15 | if (Object.keys(vtt).length === 0) {
16 | return {};
17 | } else {
18 | return vtt.reduce((result: any, cue: any, index: number) => {
19 | const key = `${index}#${cue.start}-${cue.end}#${cue.identifier}`;
20 | result[key] = cue.text;
21 | return result;
22 | }, {});
23 | }
24 | },
25 | async push(locale, payload) {
26 | const output = Object.entries(payload).map(([key, text]) => {
27 | const [id, timeRange, identifier] = key.split("#");
28 | const [startTime, endTime] = timeRange.split("-");
29 |
30 | return {
31 | end: Number(endTime),
32 | identifier: identifier,
33 | start: Number(startTime),
34 | styles: "",
35 | text: text,
36 | };
37 | });
38 |
39 | const input = {
40 | valid: true,
41 | strict: true,
42 | cues: output,
43 | };
44 |
45 | return webvtt.compile(input);
46 | },
47 | });
48 | }
49 |
```
--------------------------------------------------------------------------------
/packages/cli/src/cli/loaders/mdx2/frontmatter-split.ts:
--------------------------------------------------------------------------------
```typescript
1 | import matter from "gray-matter";
2 | import YAML from "yaml";
3 | import { ILoader } from "../_types";
4 | import { createLoader } from "../_utils";
5 | import { RawMdx } from "./_types";
6 |
7 | export default function createMdxFrontmatterSplitLoader(): ILoader<
8 | string,
9 | RawMdx
10 | > {
11 | const fmEngine = createFmEngine();
12 |
13 | return createLoader({
14 | async pull(locale, input) {
15 | const source = input || "";
16 | const { data: frontmatter, content } = fmEngine.parse(source);
17 |
18 | return {
19 | frontmatter: frontmatter as Record<string, any>,
20 | content,
21 | };
22 | },
23 |
24 | async push(locale, data) {
25 | const { frontmatter = {}, content = "" } = data || ({} as RawMdx);
26 |
27 | const result = fmEngine.stringify(content, frontmatter).trim();
28 |
29 | return result;
30 | },
31 | });
32 | }
33 |
34 | function createFmEngine() {
35 | const yamlEngine = {
36 | parse: (str: string) => YAML.parse(str),
37 | stringify: (obj: any) =>
38 | YAML.stringify(obj, { defaultStringType: "PLAIN" }),
39 | };
40 |
41 | return {
42 | parse: (input: string) =>
43 | matter(input, {
44 | engines: {
45 | yaml: yamlEngine,
46 | },
47 | }),
48 | stringify: (content: string, frontmatter: Record<string, any>) =>
49 | matter.stringify(content, frontmatter, {
50 | engines: {
51 | yaml: yamlEngine,
52 | },
53 | }),
54 | };
55 | }
56 |
```
--------------------------------------------------------------------------------
/packages/react/src/rsc/loader.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { 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 Server Components
9 | * - Should be passed into the `LingoProvider` component
10 | * - Is transformed into functional code by Lingo.dev Compiler
11 | *
12 | * @param locale - The locale code for which to load the dictionary.
13 | *
14 | * @returns Promise that resolves to the dictionary object containing localized content.
15 | *
16 | * @example Use in a Next.js (App Router) application
17 | * ```tsx file="app/layout.tsx"
18 | * import { LingoProvider, loadDictionary } from "lingo.dev/react/rsc";
19 | *
20 | * export default function RootLayout({
21 | * children,
22 | * }: Readonly<{
23 | * children: React.ReactNode;
24 | * }>) {
25 | * return (
26 | * <LingoProvider loadDictionary={(locale) => loadDictionary(locale)}>
27 | * <html lang="en">
28 | * <body>
29 | * {children}
30 | * </body>
31 | * </html>
32 | * </LingoProvider>
33 | * );
34 | * }
35 | * ```
36 | */
37 | export const loadDictionary = async (locale: string | null): Promise<any> => {
38 | return {};
39 | };
40 |
41 | export const loadDictionary_internal = async (
42 | locale: string | null,
43 | dictionaryLoaders: Record<string, () => Promise<any>> = {},
44 | ): Promise<any> => {
45 | return getDictionary(locale, dictionaryLoaders);
46 | };
47 |
```
--------------------------------------------------------------------------------
/packages/cli/src/cli/loaders/xml.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { parseStringPromise, Builder } from "xml2js";
2 | import { ILoader } from "./_types";
3 | import { createLoader } from "./_utils";
4 |
5 | function normalizeXMLString(xmlString: string): string {
6 | return xmlString
7 | .replace(/\s+/g, " ")
8 | .replace(/>\s+</g, "><")
9 | .replace("\n", "")
10 | .trim();
11 | }
12 |
13 | export default function createXmlLoader(): ILoader<
14 | string,
15 | Record<string, any>
16 | > {
17 | return createLoader({
18 | async pull(locale, input) {
19 | let result: Record<string, any> = {};
20 |
21 | try {
22 | const parsed = await parseStringPromise(input, {
23 | explicitArray: false,
24 | mergeAttrs: false,
25 | normalize: true,
26 | preserveChildrenOrder: true,
27 | normalizeTags: true,
28 | includeWhiteChars: true,
29 | trim: true,
30 | });
31 | result = parsed;
32 | } catch (error) {
33 | console.error("Failed to parse XML:", error);
34 | result = {};
35 | }
36 |
37 | return result;
38 | },
39 |
40 | async push(locale, data) {
41 | try {
42 | const builder = new Builder({ headless: true });
43 | const xmlOutput = builder.buildObject(data);
44 | const expectedOutput = normalizeXMLString(xmlOutput);
45 | return expectedOutput;
46 | } catch (error) {
47 | console.error("Failed to build XML:", error);
48 | return "";
49 | }
50 | },
51 | });
52 | }
53 |
```
--------------------------------------------------------------------------------
/packages/compiler/src/utils/observability.spec.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2 | import trackEvent from "./observability";
3 |
4 | vi.mock("./rc", () => ({ getRc: () => ({ auth: {} }) }));
5 | vi.mock("node-machine-id", () => ({ machineId: async () => "device-123" }));
6 |
7 | // Mock PostHog client used by dynamic import inside trackEvent
8 | const capture = vi.fn(async () => undefined);
9 | const shutdown = vi.fn(async () => undefined);
10 | const PostHogMock = vi.fn((_key: string, _cfg: any) => ({ capture, shutdown }));
11 | vi.mock("posthog-node", () => ({ PostHog: PostHogMock }));
12 |
13 | describe("trackEvent", () => {
14 | const originalEnv = { ...process.env };
15 | afterEach(() => {
16 | process.env = originalEnv;
17 | });
18 |
19 | it("captures the event with properties", async () => {
20 | await trackEvent("test.event", { foo: "bar" });
21 | expect(PostHogMock).toHaveBeenCalledTimes(1);
22 | expect(capture).toHaveBeenCalledWith(
23 | expect.objectContaining({
24 | event: "test.event",
25 | properties: expect.objectContaining({ foo: "bar" }),
26 | }),
27 | );
28 | expect(shutdown).toHaveBeenCalledTimes(1);
29 | });
30 |
31 | it("skips when DO_NOT_TRACK is set", async () => {
32 | process.env = { ...originalEnv, DO_NOT_TRACK: "1" };
33 | // Should not throw nor attempt network
34 | await expect(trackEvent("test.event", { a: 1 })).resolves.toBeUndefined();
35 | });
36 | });
37 |
```
--------------------------------------------------------------------------------
/packages/compiler/src/jsx-remove-attributes.spec.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect } from "vitest";
2 | import { jsxRemoveAttributesMutation } from "./jsx-remove-attributes";
3 | import { createPayload, createOutput, defaultParams } from "./_base";
4 |
5 | // Helper function to run mutation and get result
6 | function runMutation(code: string) {
7 | const input = createPayload({ code, params: defaultParams, fileKey: "test" });
8 | const mutated = jsxRemoveAttributesMutation(input);
9 | if (!mutated) return code; // Return original code if no changes made
10 | return createOutput(mutated).code;
11 | }
12 |
13 | describe("jsxRemoveAttributesMutation", () => {
14 | it("should remove only attributes added by compiler", () => {
15 | const input = `
16 | function Component() {
17 | return <div data-jsx-root>
18 | <p data-jsx-scope="foo" data-other="1">Hello world</p>
19 | <p data-jsx-attribute-scope="bar" className="text-success">Good night moon</p>
20 | <p data-jsx-scope="foobar" data-jsx-attribute-scope="barfoo" className="text-danger" data-other="2">Good morning sun</p>
21 | </div>;
22 | }
23 | `.trim();
24 |
25 | const expected = `
26 | function Component() {
27 | return <div>
28 | <p data-other="1">Hello world</p>
29 | <p className="text-success">Good night moon</p>
30 | <p className="text-danger" data-other="2">Good morning sun</p>
31 | </div>;
32 | }
33 | `.trim();
34 | const result = runMutation(input);
35 | expect(result).toBe(expected);
36 | });
37 | });
38 |
```
--------------------------------------------------------------------------------
/packages/react/src/client/loader.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { 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 client-side rendered applications (e.g., Vite-based apps)
9 | * - Should be passed into the `LingoProviderWrapper` component
10 | * - Is transformed into functional code by Lingo.dev Compiler
11 | *
12 | * @param locale - The locale to load the dictionary for.
13 | *
14 | * @returns Promise that resolves to the dictionary object containing localized content.
15 | *
16 | * @example Use in a Vite application
17 | * ```tsx
18 | * import React from "react";
19 | * import ReactDOM from "react-dom/client";
20 | * import { LingoProviderWrapper, loadDictionary } from "lingo.dev/react/client";
21 | * import { App } from "./App.tsx";
22 | *
23 | * ReactDOM.createRoot(document.getElementById("root")!).render(
24 | * <React.StrictMode>
25 | * <LingoProviderWrapper loadDictionary={(locale) => loadDictionary(locale)}>
26 | * <App />
27 | * </LingoProviderWrapper>
28 | * </React.StrictMode>,
29 | * );
30 | * ```
31 | */
32 | export const loadDictionary = async (locale: string | null): Promise<any> => {
33 | return {};
34 | };
35 |
36 | export const loadDictionary_internal = async (
37 | locale: string | null,
38 | dictionaryLoaders: Record<string, () => Promise<any>> = {},
39 | ): Promise<any> => {
40 | return getDictionary(locale, dictionaryLoaders);
41 | };
42 |
```
--------------------------------------------------------------------------------
/demo/adonisjs/bin/console.ts:
--------------------------------------------------------------------------------
```typescript
1 | /*
2 | |--------------------------------------------------------------------------
3 | | Ace entry point
4 | |--------------------------------------------------------------------------
5 | |
6 | | The "console.ts" file is the entrypoint for booting the AdonisJS
7 | | command-line framework and executing commands.
8 | |
9 | | Commands do not boot the application, unless the currently running command
10 | | has "options.startApp" flag set to true.
11 | |
12 | */
13 |
14 | import 'reflect-metadata'
15 | import { Ignitor, prettyPrintError } from '@adonisjs/core'
16 |
17 | /**
18 | * URL to the application root. AdonisJS need it to resolve
19 | * paths to file and directories for scaffolding commands
20 | */
21 | const APP_ROOT = new URL('../', import.meta.url)
22 |
23 | /**
24 | * The importer is used to import files in context of the
25 | * application.
26 | */
27 | const IMPORTER = (filePath: string) => {
28 | if (filePath.startsWith('./') || filePath.startsWith('../')) {
29 | return import(new URL(filePath, APP_ROOT).href)
30 | }
31 | return import(filePath)
32 | }
33 |
34 | new Ignitor(APP_ROOT, { importer: IMPORTER })
35 | .tap((app) => {
36 | app.booting(async () => {
37 | await import('#start/env')
38 | })
39 | app.listen('SIGTERM', () => app.terminate())
40 | app.listenIf(app.managedByPm2, 'SIGINT', () => app.terminate())
41 | })
42 | .ace()
43 | .handle(process.argv.splice(2))
44 | .catch((error) => {
45 | process.exitCode = 1
46 | prettyPrintError(error)
47 | })
48 |
```
--------------------------------------------------------------------------------
/packages/react/src/client/component.lingo-component.spec.tsx:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect, vi } from "vitest";
2 | import React from "react";
3 | import { LingoContext } from "./context";
4 |
5 | // Mock core LingoComponent to capture received props
6 | vi.mock("../core", () => {
7 | return {
8 | LingoComponent: (props: any) => {
9 | return React.createElement("div", {
10 | "data-testid": "core-lingo-component",
11 | "data-has-dictionary": props.$dictionary ? "yes" : "no",
12 | "data-entry": props.$entryKey,
13 | "data-file": props.$fileKey,
14 | });
15 | },
16 | };
17 | });
18 |
19 | describe("client/component", () => {
20 | describe("LingoComponent wrapper", () => {
21 | it("renders core component with dictionary from context and forwards keys", async () => {
22 | const dictionary = { locale: "en" } as any;
23 | const { LingoComponent } = await import("./component");
24 | const { render, screen } = await import("@testing-library/react");
25 |
26 | render(
27 | <LingoContext.Provider value={{ dictionary }}>
28 | <LingoComponent $as="span" $fileKey="messages" $entryKey="hello" />
29 | </LingoContext.Provider>,
30 | );
31 |
32 | const el = await screen.findByTestId("core-lingo-component");
33 | expect(el.getAttribute("data-has-dictionary")).toBe("yes");
34 | expect(el.getAttribute("data-file")).toBe("messages");
35 | expect(el.getAttribute("data-entry")).toBe("hello");
36 | });
37 | });
38 | });
39 |
```
--------------------------------------------------------------------------------
/packages/cli/src/cli/cmd/config/get.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Command } from "interactive-commander";
2 | import chalk from "chalk";
3 | import _ from "lodash";
4 | import { SETTINGS_KEYS, loadSystemSettings } from "../../utils/settings";
5 | import dedent from "dedent";
6 |
7 | export default new Command()
8 | .name("get")
9 | .description("Display the value of a CLI setting from ~/.lingodotdevrc")
10 | .addHelpText("afterAll", `\nAvailable keys:\n ${SETTINGS_KEYS.join("\n ")}`)
11 | .argument(
12 | "<key>",
13 | "Configuration key to read (choose from the available keys listed below)",
14 | )
15 | .helpOption("-h, --help", "Show help")
16 | .action(async (key: string) => {
17 | // Validate that the provided key is one of the recognised configuration keys.
18 | if (!SETTINGS_KEYS.includes(key)) {
19 | console.error(
20 | dedent`
21 | ${chalk.red("✖")} Unknown configuration key: ${chalk.bold(key)}
22 | Run ${chalk.dim("lingo.dev config get --help")} to see available keys.
23 | `,
24 | );
25 | process.exitCode = 1;
26 | return;
27 | }
28 |
29 | const settings = loadSystemSettings();
30 | const value = _.get(settings, key);
31 |
32 | if (!value) {
33 | // Key is valid but not set in the configuration file.
34 | console.log(`${chalk.cyan("ℹ")} ${chalk.bold(key)} is not set.`);
35 | return;
36 | }
37 |
38 | if (typeof value === "object") {
39 | console.log(JSON.stringify(value, null, 2));
40 | } else {
41 | console.log(value);
42 | }
43 | });
44 |
```
--------------------------------------------------------------------------------
/demo/next-app/public/next.svg:
--------------------------------------------------------------------------------
```
1 | <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
```
--------------------------------------------------------------------------------
/packages/react/src/rsc/component.lingo-component.spec.tsx:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect, vi } from "vitest";
2 | import React from "react";
3 |
4 | vi.mock("./utils", () => {
5 | return {
6 | loadDictionaryFromRequest: vi.fn(async (loader: any) => loader("es")),
7 | };
8 | });
9 |
10 | // Mock core LingoComponent to capture props
11 | vi.mock("../core", () => {
12 | return {
13 | LingoComponent: (props: any) => {
14 | return React.createElement("div", {
15 | "data-testid": "core-lingo-component",
16 | "data-dictionary-locale": props.$dictionary?.locale ?? "none",
17 | "data-entry": props.$entryKey,
18 | "data-file": props.$fileKey,
19 | });
20 | },
21 | };
22 | });
23 |
24 | describe("rsc/component", () => {
25 | describe("LingoComponent wrapper", () => {
26 | it("awaits dictionary and forwards props to core component", async () => {
27 | const { LingoComponent } = await import("./component");
28 | const { render, screen } = await import("@testing-library/react");
29 |
30 | render(
31 | await LingoComponent({
32 | $as: "span",
33 | $fileKey: "messages",
34 | $entryKey: "hello",
35 | $loadDictionary: async (locale: string | null) => ({ locale }),
36 | }),
37 | );
38 |
39 | const el = await screen.findByTestId("core-lingo-component");
40 | expect(el.getAttribute("data-dictionary-locale")).toBe("es");
41 | expect(el.getAttribute("data-file")).toBe("messages");
42 | expect(el.getAttribute("data-entry")).toBe("hello");
43 | });
44 | });
45 | });
46 |
```
--------------------------------------------------------------------------------
/demo/adonisjs/start/kernel.ts:
--------------------------------------------------------------------------------
```typescript
1 | /*
2 | |--------------------------------------------------------------------------
3 | | HTTP kernel file
4 | |--------------------------------------------------------------------------
5 | |
6 | | The HTTP kernel file is used to register the middleware with the server
7 | | or the router.
8 | |
9 | */
10 |
11 | import router from '@adonisjs/core/services/router'
12 | import server from '@adonisjs/core/services/server'
13 |
14 | /**
15 | * The error handler is used to convert an exception
16 | * to an HTTP response.
17 | */
18 | server.errorHandler(() => import('#exceptions/handler'))
19 |
20 | /**
21 | * The server middleware stack runs middleware on all the HTTP
22 | * requests, even if there is no route registered for
23 | * the request URL.
24 | */
25 | server.use([
26 | () => import('#middleware/container_bindings_middleware'),
27 | () => import('@adonisjs/static/static_middleware'),
28 | () => import('@adonisjs/cors/cors_middleware'),
29 | () => import('@adonisjs/vite/vite_middleware'),
30 | () => import('@adonisjs/inertia/inertia_middleware'),
31 | ])
32 |
33 | /**
34 | * The router middleware stack runs middleware on all the HTTP
35 | * requests with a registered route.
36 | */
37 | router.use([
38 | () => import('@adonisjs/core/bodyparser_middleware'),
39 | () => import('@adonisjs/session/session_middleware'),
40 | () => import('@adonisjs/shield/shield_middleware'),
41 | ])
42 |
43 | /**
44 | * Named middleware collection must be explicitly assigned to
45 | * the routes or the routes group.
46 | */
47 | export const middleware = router.named({})
48 |
```
--------------------------------------------------------------------------------
/packages/compiler/src/utils/locales.ts:
--------------------------------------------------------------------------------
```typescript
1 | export function getInvalidLocales(
2 | localeModels: Record<string, string>,
3 | sourceLocale: string,
4 | targetLocales: string[],
5 | ) {
6 | return targetLocales.filter((targetLocale) => {
7 | const { provider, model } = getLocaleModel(
8 | localeModels,
9 | sourceLocale,
10 | targetLocale,
11 | );
12 |
13 | return provider === undefined || model === undefined;
14 | });
15 | }
16 |
17 | export function getLocaleModel(
18 | localeModels: Record<string, string>,
19 | sourceLocale: string,
20 | targetLocale: string,
21 | ): { provider?: string; model?: string } {
22 | const localeKeys = [
23 | `${sourceLocale}:${targetLocale}`,
24 | `*:${targetLocale}`,
25 | `${sourceLocale}:*`,
26 | "*:*",
27 | ];
28 | const modelKey = localeKeys.find((key) => localeModels.hasOwnProperty(key));
29 | if (modelKey) {
30 | const value = localeModels[modelKey];
31 | // Split only on the first colon
32 | const firstColonIndex = value?.indexOf(":");
33 |
34 | if (value && firstColonIndex !== -1 && firstColonIndex !== undefined) {
35 | const provider = value.substring(0, firstColonIndex);
36 | const model = value.substring(firstColonIndex + 1);
37 |
38 | if (provider && model) {
39 | return { provider, model };
40 | }
41 | }
42 |
43 | // Fallback for strings without a colon or other issues
44 | const [provider, model] = value?.split(":") || [];
45 | if (provider && model) {
46 | return { provider, model };
47 | }
48 | }
49 | return { provider: undefined, model: undefined };
50 | }
51 |
```
--------------------------------------------------------------------------------
/packages/react/src/client/locale.ts:
--------------------------------------------------------------------------------
```typescript
1 | "use client";
2 |
3 | import { useEffect, useState } from "react";
4 | import { getLocaleFromCookies, setLocaleInCookies } from "./utils";
5 |
6 | /**
7 | * Gets the current locale used by the Lingo compiler.
8 | *
9 | * @returns The current locale code, or `null` if no locale is set.
10 | */
11 | export function useLingoLocale(): string | null {
12 | const [locale, setLocale] = useState<string | null>(null);
13 | useEffect(() => {
14 | setLocale(getLocaleFromCookies());
15 | }, []);
16 | return locale;
17 | }
18 |
19 | /**
20 | * Sets the current locale used by the Lingo compiler.
21 | *
22 | * **Note:** This function triggers a full page reload to ensure all components
23 | * are re-rendered with the new locale. This is necessary because locale changes
24 | * affect the entire application state.
25 | *
26 | * @param locale - The locale code to set. Must be a valid locale code (e.g., "en", "es", "fr-CA").
27 |
28 | *
29 | * @example Set the current locale
30 | * ```tsx
31 | * import { setLingoLocale } from "lingo.dev/react/client";
32 | *
33 | * export function LanguageSwitcher() {
34 | * const handleChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
35 | * setLingoLocale(event.target.value);
36 | * };
37 | *
38 | * return (
39 | * <select onChange={handleChange}>
40 | * <option value="en">English</option>
41 | * <option value="es">Spanish</option>
42 | * </select>
43 | * );
44 | * }
45 | * ```
46 | */
47 | export function setLingoLocale(locale: string) {
48 | setLocaleInCookies(locale);
49 | window.location.reload();
50 | }
51 |
```
--------------------------------------------------------------------------------
/packages/compiler/src/i18n-directive.spec.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect } from "vitest";
2 | import i18nDirectiveMutation from "./i18n-directive";
3 | import { createPayload, CompilerParams, defaultParams } from "./_base";
4 |
5 | describe("i18nDirectiveMutation", () => {
6 | it("should return payload when 'use i18n' directive is present", () => {
7 | const input = `
8 | "use i18n";
9 | function Component() {
10 | return <div>Hello</div>;
11 | }
12 | `.trim();
13 |
14 | const result = createPayload({
15 | code: input,
16 | params: defaultParams,
17 | fileKey: "test",
18 | });
19 | const mutated = i18nDirectiveMutation(result);
20 |
21 | expect(mutated).not.toBeNull();
22 | expect(mutated).toEqual(result);
23 | });
24 |
25 | it("should return null when 'use i18n' directive is not present", () => {
26 | const input = `
27 | function Component() {
28 | return <div>Hello</div>;
29 | }
30 | `.trim();
31 |
32 | const result = createPayload({
33 | code: input,
34 | params: { ...defaultParams, useDirective: true },
35 | fileKey: "test",
36 | });
37 | const mutated = i18nDirectiveMutation(result);
38 |
39 | expect(mutated).toBeNull();
40 | });
41 |
42 | it("should handle multiple directives correctly", () => {
43 | const input = `
44 | "use strict";
45 | "use i18n";
46 | function Component() {
47 | return <div>Hello</div>;
48 | }
49 | `.trim();
50 |
51 | const result = createPayload({
52 | code: input,
53 | params: defaultParams,
54 | fileKey: "test",
55 | });
56 | const mutated = i18nDirectiveMutation(result);
57 |
58 | expect(mutated).not.toBeNull();
59 | expect(mutated).toEqual(result);
60 | });
61 | });
62 |
```
--------------------------------------------------------------------------------
/packages/cli/src/cli/cmd/config/set.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Command } from "interactive-commander";
2 | import chalk from "chalk";
3 | import dedent from "dedent";
4 | import _ from "lodash";
5 | import {
6 | SETTINGS_KEYS,
7 | loadSystemSettings,
8 | saveSettings,
9 | } from "../../utils/settings";
10 |
11 | export default new Command()
12 | .name("set")
13 | .description("Set or update a CLI setting in ~/.lingodotdevrc")
14 | .addHelpText("afterAll", `\nAvailable keys:\n ${SETTINGS_KEYS.join("\n ")}`)
15 | .argument(
16 | "<key>",
17 | "Configuration key to set (dot notation, e.g., auth.apiKey)",
18 | )
19 | .argument("<value>", "The configuration value to set")
20 | .helpOption("-h, --help", "Show help")
21 | .action(async (key: string, value: string) => {
22 | if (!SETTINGS_KEYS.includes(key)) {
23 | console.error(
24 | dedent`
25 | ${chalk.red("✖")} Unknown configuration key: ${chalk.bold(key)}
26 | Run ${chalk.dim("lingo.dev config set --help")} to see available keys.
27 | `,
28 | );
29 | process.exitCode = 1;
30 | return;
31 | }
32 |
33 | const current = loadSystemSettings();
34 | const updated: any = _.cloneDeep(current);
35 | _.set(updated, key, value);
36 |
37 | try {
38 | saveSettings(updated as any);
39 | console.log(`${chalk.green("✔")} Set ${chalk.bold(key)}`);
40 | } catch (err) {
41 | console.error(
42 | chalk.red(
43 | `✖ Failed to save configuration: ${chalk.dim(
44 | err instanceof Error ? err.message : String(err),
45 | )}`,
46 | ),
47 | );
48 | process.exitCode = 1;
49 | }
50 | });
51 |
```
--------------------------------------------------------------------------------
/packages/cli/src/cli/loaders/mdx2/frontmatter-split.spec.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect } from "vitest";
2 | import createMdxFrontmatterSplitLoader from "./frontmatter-split";
3 | import matter from "gray-matter";
4 |
5 | const sampleMdx = `---
6 | title: Hello
7 | published: true
8 | tags:
9 | - foo
10 | - bar
11 | ---
12 |
13 | # Heading
14 |
15 | This is some text.`;
16 |
17 | // Helper to derive expected content string from the original sample – this mirrors what gray-matter returns
18 | const { content: originalContent } = matter(sampleMdx);
19 |
20 | describe("mdx frontmatter split loader", () => {
21 | it("should split frontmatter and content on pull", async () => {
22 | const loader = createMdxFrontmatterSplitLoader();
23 | loader.setDefaultLocale("en");
24 |
25 | const result = await loader.pull("en", sampleMdx);
26 |
27 | expect(result).toEqual({
28 | frontmatter: {
29 | title: "Hello",
30 | published: true,
31 | tags: ["foo", "bar"],
32 | },
33 | content: originalContent,
34 | });
35 | });
36 |
37 | it("should merge frontmatter and content on push", async () => {
38 | const loader = createMdxFrontmatterSplitLoader();
39 | loader.setDefaultLocale("en");
40 |
41 | const pulled = await loader.pull("en", sampleMdx);
42 | // modify the data
43 | pulled.frontmatter.title = "Hola";
44 | pulled.content = pulled.content.replace("# Heading", "# Título");
45 |
46 | const output = await loader.push("es", pulled);
47 |
48 | const expectedOutput = `
49 | ---
50 | title: Hola
51 | published: true
52 | tags:
53 | - foo
54 | - bar
55 | ---
56 |
57 | # Título
58 |
59 | This is some text.
60 | `.trim();
61 |
62 | expect(output).toBe(expectedOutput);
63 | });
64 | });
65 |
```
--------------------------------------------------------------------------------
/packages/compiler/src/lib/lcp/schema.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { z } from "zod";
2 |
3 | // LCP
4 |
5 | export const lcpScope = z.object({
6 | type: z.enum(["element", "attribute"]),
7 | content: z.string(),
8 | hash: z.string(),
9 | context: z.string().optional(),
10 | skip: z.boolean().optional(),
11 | overrides: z.record(z.string(), z.string()).optional(),
12 | });
13 |
14 | export type LCPScope = z.infer<typeof lcpScope>;
15 |
16 | export const lcpFile = z.object({
17 | scopes: z.record(z.string(), lcpScope).optional(),
18 | });
19 |
20 | export type LCPFile = z.infer<typeof lcpFile>;
21 |
22 | export const lcpSchema = z.object({
23 | version: z.number().default(0.1),
24 | files: z.record(z.string(), lcpFile).optional(),
25 | });
26 |
27 | export type LCPSchema = z.infer<typeof lcpSchema>;
28 |
29 | // Dictionary
30 |
31 | export const dictionaryFile = z.object({
32 | entries: z.record(z.string(), z.string()),
33 | });
34 |
35 | export type DictionaryFile = z.infer<typeof dictionaryFile>;
36 |
37 | export const dictionarySchema = z.object({
38 | version: z.number().default(0.1),
39 | locale: z.string(),
40 | files: z.record(z.string(), dictionaryFile),
41 | });
42 |
43 | export type DictionarySchema = z.infer<typeof dictionarySchema>;
44 |
45 | // Dictionary Cache
46 |
47 | export const dictionaryCacheFile = z.object({
48 | entries: z.record(
49 | z.string(),
50 | z.object({
51 | content: z.record(z.string(), z.string()),
52 | hash: z.string(),
53 | }),
54 | ),
55 | });
56 |
57 | export const dictionaryCacheSchema = z.object({
58 | version: z.number().default(0.1),
59 | files: z.record(z.string(), dictionaryCacheFile),
60 | });
61 |
62 | export type DictionaryCacheSchema = z.infer<typeof dictionaryCacheSchema>;
63 |
```
--------------------------------------------------------------------------------
/packages/react/src/core/get-dictionary.spec.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect, vi } from "vitest";
2 | import { getDictionary } from "./get-dictionary";
3 |
4 | describe("get-dictionary", () => {
5 | const mockLoaderEn = vi.fn().mockResolvedValue(
6 | Promise.resolve({
7 | default: { hello: "Hello", goodbye: "Goodbye" },
8 | otherExport: "ignored",
9 | }),
10 | );
11 | const mockLoaderEs = vi.fn().mockResolvedValue(
12 | Promise.resolve({
13 | default: { hello: "Hola", goodbye: "Adiós" },
14 | }),
15 | );
16 | const loaders = {
17 | en: mockLoaderEn,
18 | es: mockLoaderEs,
19 | };
20 |
21 | beforeEach(() => {
22 | vi.clearAllMocks();
23 | });
24 |
25 | describe("getDictionary", () => {
26 | it("should load dictionary for specific locale using correct async loader", async () => {
27 | const result = await getDictionary("es", loaders);
28 | expect(mockLoaderEs).toHaveBeenCalledTimes(1);
29 | expect(result).toEqual({ hello: "Hola", goodbye: "Adiós" });
30 | });
31 |
32 | it("should fallback to first available loader when specific locale not found", async () => {
33 | const result = await getDictionary("fr", loaders);
34 |
35 | expect(mockLoaderEn).toHaveBeenCalledTimes(1);
36 | expect(result).toEqual({ hello: "Hello", goodbye: "Goodbye" });
37 | });
38 |
39 | it("should throw error when no loaders are provided", async () => {
40 | expect(() => getDictionary("en", {})).toThrow(
41 | "No available dictionary loaders found",
42 | );
43 | expect(() => getDictionary("en")).toThrow(
44 | "No available dictionary loaders found",
45 | );
46 | });
47 | });
48 | });
49 |
```
--------------------------------------------------------------------------------
/packages/compiler/src/utils/locales.spec.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect } from "vitest";
2 | import { getInvalidLocales, getLocaleModel } from "./locales";
3 |
4 | describe("utils/locales", () => {
5 | describe("getLocaleModel", () => {
6 | const models = {
7 | "en:es": "groq:llama3",
8 | "en:*": "google:g2",
9 | "*:es": "mistral:m-small",
10 | "*:*": "openrouter:gpt",
11 | };
12 |
13 | it.each([
14 | ["en", "es", { provider: "groq", model: "llama3" }],
15 | ["en", "fr", { provider: "google", model: "g2" }],
16 | ["de", "es", { provider: "mistral", model: "m-small" }],
17 | ["de", "fr", { provider: "openrouter", model: "gpt" }],
18 | ])("resolves locales", (sourceLocale, targetLocale, expected) => {
19 | expect(getLocaleModel(models, sourceLocale, targetLocale)).toEqual(
20 | expected,
21 | );
22 | });
23 |
24 | it("returns undefined for missing mapping", () => {
25 | expect(getLocaleModel({ "en:es": "groq:llama3" }, "en", "fr")).toEqual({
26 | provider: undefined,
27 | model: undefined,
28 | });
29 | });
30 |
31 | it("returns undefined for invalid value", () => {
32 | expect(getLocaleModel({ "en:fr": "invalidFormat" }, "en", "fr")).toEqual({
33 | provider: undefined,
34 | model: undefined,
35 | });
36 | });
37 | });
38 |
39 | describe("getInvalidLocales", () => {
40 | it("returns targets with unresolved models", () => {
41 | const models = { "en:es": "groq:llama3", "*:fr": "google:g2" };
42 | const invalid = getInvalidLocales(models, "en", ["es", "fr", "de"]);
43 | expect(invalid).toEqual(["de"]);
44 | });
45 | });
46 | });
47 |
```
--------------------------------------------------------------------------------
/demo/vite-project/public/vite.svg:
--------------------------------------------------------------------------------
```
1 | <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
```
--------------------------------------------------------------------------------
/packages/compiler/src/client-dictionary-loader.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { createCodeMutation } from "./_base";
2 | import { ModuleId } from "./_const";
3 | import { getOrCreateImport } from "./utils";
4 | import { findInvokations } from "./utils/invokations";
5 | import * as t from "@babel/types";
6 | import { getDictionaryPath } from "./_utils";
7 | import { createLocaleImportMap } from "./utils/create-locale-import-map";
8 |
9 | export const clientDictionaryLoaderMutation = createCodeMutation((payload) => {
10 | const invokations = findInvokations(payload.ast, {
11 | moduleName: ModuleId.ReactClient,
12 | functionName: "loadDictionary",
13 | });
14 |
15 | const allLocales = Array.from(
16 | new Set([payload.params.sourceLocale, ...payload.params.targetLocales]),
17 | );
18 |
19 | for (const invokation of invokations) {
20 | const internalDictionaryLoader = getOrCreateImport(payload.ast, {
21 | moduleName: ModuleId.ReactClient,
22 | exportedName: "loadDictionary_internal",
23 | });
24 |
25 | // Replace the function identifier with internal version
26 | if (t.isIdentifier(invokation.callee)) {
27 | invokation.callee.name = internalDictionaryLoader.importedName;
28 | }
29 |
30 | const dictionaryPath = getDictionaryPath({
31 | sourceRoot: payload.params.sourceRoot,
32 | lingoDir: payload.params.lingoDir,
33 | relativeFilePath: payload.relativeFilePath,
34 | });
35 |
36 | // Create locale import map object
37 | const localeImportMap = createLocaleImportMap(allLocales, dictionaryPath);
38 |
39 | // Add the locale import map as the second argument
40 | invokation.arguments.push(localeImportMap);
41 | }
42 |
43 | return payload;
44 | });
45 |
```
--------------------------------------------------------------------------------
/packages/react/src/client/utils.ts:
--------------------------------------------------------------------------------
```typescript
1 | "use client";
2 |
3 | import { LOCALE_COOKIE_NAME } from "../core";
4 | import Cookies from "js-cookie";
5 |
6 | /**
7 | * Gets the current locale from the `"lingo-locale"` cookie.
8 | *
9 | * Defaults to `"en"` if:
10 | *
11 | * - Running in an environment that doesn't support cookies
12 | * - No `"lingo-locale"` cookie is found
13 | *
14 | * @returns The current locale code, or `"en"` as a fallback.
15 | *
16 | * @example Get the current locale
17 | * ```tsx
18 | * import { getLocaleFromCookies } from "lingo.dev/react/client";
19 | *
20 | * export function App() {
21 | * const currentLocale = getLocaleFromCookies();
22 | * return <div>Current locale: {currentLocale}</div>;
23 | * }
24 | * ```
25 | */
26 | export function getLocaleFromCookies(): string | null {
27 | if (typeof document === "undefined") return null;
28 |
29 | return Cookies.get(LOCALE_COOKIE_NAME) ?? null;
30 | }
31 |
32 | /**
33 | * Sets the current locale in the `"lingo-locale"` cookie.
34 | *
35 | * Does nothing in environments that don't support cookies.
36 | *
37 | * @param locale - The locale code to store in the `"lingo-locale"` cookie.
38 | *
39 | * @example Set the current locale
40 | * ```tsx
41 | * import { setLocaleInCookies } from "lingo.dev/react/client";
42 | *
43 | * export function LanguageButton() {
44 | * const handleClick = () => {
45 | * setLocaleInCookies("es");
46 | * window.location.reload();
47 | * };
48 | *
49 | * return <button onClick={handleClick}>Switch to Spanish</button>;
50 | * }
51 | * ```
52 | */
53 | export function setLocaleInCookies(locale: string): void {
54 | if (typeof document === "undefined") return;
55 |
56 | Cookies.set(LOCALE_COOKIE_NAME, locale, {
57 | path: "/",
58 | expires: 365,
59 | sameSite: "lax",
60 | });
61 | }
62 |
```
--------------------------------------------------------------------------------
/action.yml:
--------------------------------------------------------------------------------
```yaml
1 | name: "Lingo.Dev AI Localization"
2 | description: Automated AI localization for dev teams.
3 | author: Lingo.dev
4 |
5 | branding:
6 | icon: "aperture"
7 | color: "black"
8 |
9 | runs:
10 | using: "composite"
11 | steps:
12 | - name: Run
13 | run: |
14 | npx lingo.dev@${{ inputs.version }} ci \
15 | --api-key "${{ inputs.api-key }}" \
16 | --pull-request "${{ inputs.pull-request }}" \
17 | --commit-message "${{ inputs.commit-message }}" \
18 | --pull-request-title "${{ inputs.pull-request-title }}" \
19 | --working-directory "${{ inputs.working-directory }}" \
20 | --process-own-commits "${{ inputs.process-own-commits }}" \
21 | --parallel ${{ inputs.parallel }}
22 | shell: bash
23 | inputs:
24 | version:
25 | description: "Lingo.dev CLI version"
26 | default: "latest"
27 | required: false
28 | api-key:
29 | description: "Lingo.dev Platform API Key"
30 | required: true
31 | pull-request:
32 | description: "Create a pull request with the changes"
33 | default: false
34 | required: false
35 | commit-message:
36 | description: "Commit message"
37 | default: "feat: update translations via @LingoDotDev"
38 | required: false
39 | pull-request-title:
40 | description: "Pull request title"
41 | default: "feat: update translations via @LingoDotDev"
42 | required: false
43 | working-directory:
44 | description: "Working directory"
45 | default: "."
46 | required: false
47 | process-own-commits:
48 | description: "Process commits made by this action"
49 | default: false
50 | required: false
51 | parallel:
52 | description: "Run in parallel mode"
53 | default: false
54 | required: false
55 |
```
--------------------------------------------------------------------------------
/packages/react/src/client/utils.spec.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect, vi, beforeEach } from "vitest";
2 | import { getLocaleFromCookies, setLocaleInCookies } from "./utils";
3 |
4 | vi.mock("js-cookie", () => {
5 | return {
6 | default: {
7 | get: vi.fn(),
8 | set: vi.fn(),
9 | },
10 | };
11 | });
12 |
13 | // access mocked module
14 | import Cookies from "js-cookie";
15 |
16 | describe("client/utils", () => {
17 | beforeEach(() => {
18 | vi.clearAllMocks();
19 | });
20 |
21 | describe("getLocaleFromCookies", () => {
22 | it("returns null when document is undefined (SSR)", () => {
23 | const original = globalThis.document;
24 | // @ts-ignore
25 | delete (globalThis as any).document;
26 | expect(getLocaleFromCookies()).toBeNull();
27 | (globalThis as any).document = original;
28 | });
29 |
30 | it("returns cookie value when present", () => {
31 | (Cookies.get as any).mockReturnValue("es");
32 | (globalThis as any).document = {} as any;
33 | expect(getLocaleFromCookies()).toBe("es");
34 | });
35 | });
36 |
37 | describe("setLocaleInCookies", () => {
38 | it("is no-op when document is undefined", () => {
39 | const original = globalThis.document;
40 | // @ts-ignore
41 | delete (globalThis as any).document;
42 | setLocaleInCookies("fr");
43 | expect(Cookies.set).not.toHaveBeenCalled();
44 | (globalThis as any).document = original;
45 | });
46 |
47 | it("writes cookie with expected options", () => {
48 | (globalThis as any).document = {} as any;
49 | setLocaleInCookies("en");
50 | expect(Cookies.set).toHaveBeenCalledWith("lingo-locale", "en", {
51 | path: "/",
52 | expires: 365,
53 | sameSite: "lax",
54 | });
55 | });
56 | });
57 | });
58 |
```
--------------------------------------------------------------------------------
/demo/adonisjs/app/exceptions/handler.ts:
--------------------------------------------------------------------------------
```typescript
1 | import app from '@adonisjs/core/services/app'
2 | import { HttpContext, ExceptionHandler } from '@adonisjs/core/http'
3 | import type { StatusPageRange, StatusPageRenderer } from '@adonisjs/core/types/http'
4 |
5 | export default class HttpExceptionHandler extends ExceptionHandler {
6 | /**
7 | * In debug mode, the exception handler will display verbose errors
8 | * with pretty printed stack traces.
9 | */
10 | protected debug = !app.inProduction
11 |
12 | /**
13 | * Status pages are used to display a custom HTML pages for certain error
14 | * codes. You might want to enable them in production only, but feel
15 | * free to enable them in development as well.
16 | */
17 | protected renderStatusPages = app.inProduction
18 |
19 | /**
20 | * Status pages is a collection of error code range and a callback
21 | * to return the HTML contents to send as a response.
22 | */
23 | protected statusPages: Record<StatusPageRange, StatusPageRenderer> = {
24 | '404': (error, { inertia }) => inertia.render('errors/not_found', { error }),
25 | '500..599': (error, { inertia }) => inertia.render('errors/server_error', { error }),
26 | }
27 |
28 | /**
29 | * The method is used for handling errors and returning
30 | * response to the client
31 | */
32 | async handle(error: unknown, ctx: HttpContext) {
33 | return super.handle(error, ctx)
34 | }
35 |
36 | /**
37 | * The method is used to report error to the logging service or
38 | * the a third party error monitoring service.
39 | *
40 | * @note You should not attempt to send a response from this method.
41 | */
42 | async report(error: unknown, ctx: HttpContext) {
43 | return super.report(error, ctx)
44 | }
45 | }
46 |
```
--------------------------------------------------------------------------------
/packages/react/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "@lingo.dev/_react",
3 | "version": "0.5.0",
4 | "description": "Lingo.dev React Kit",
5 | "private": false,
6 | "publishConfig": {
7 | "access": "public"
8 | },
9 | "sideEffects": false,
10 | "type": "module",
11 | "exports": {
12 | ".": {
13 | "types": "./build/core/index.d.ts",
14 | "import": "./build/core/index.js"
15 | },
16 | "./client": {
17 | "types": "./build/client/index.d.ts",
18 | "import": "./build/client/index.js"
19 | },
20 | "./rsc": {
21 | "types": "./build/rsc/index.d.ts",
22 | "import": "./build/rsc/index.js"
23 | },
24 | "./react-router": {
25 | "types": "./build/react-router/index.d.ts",
26 | "import": "./build/react-router/index.js"
27 | }
28 | },
29 | "files": [
30 | "build"
31 | ],
32 | "scripts": {
33 | "dev": "unbuild && chokidar 'src/**/*' -c 'unbuild'",
34 | "build": "pnpm typecheck && unbuild",
35 | "typecheck": "tsc --noEmit",
36 | "clean": "rm -rf build",
37 | "test": "vitest --run",
38 | "test:watch": "vitest -w"
39 | },
40 | "keywords": [],
41 | "author": "",
42 | "license": "ISC",
43 | "devDependencies": {
44 | "@testing-library/react": "^16.3.0",
45 | "@types/js-cookie": "^3.0.6",
46 | "@types/lodash": "^4.17.4",
47 | "@types/react": "^18.3.18",
48 | "@types/react-dom": "^19.1.7",
49 | "@vitejs/plugin-react": "^4.4.1",
50 | "chokidar-cli": "^3.0.0",
51 | "next": "15.2.4",
52 | "react": "^19.0.0",
53 | "react-dom": "^19.0.0",
54 | "tsup": "^8.3.5",
55 | "typescript": "^5.4.5",
56 | "unbuild": "^3.5.0",
57 | "vitest": "^3.1.1"
58 | },
59 | "peerDependencies": {
60 | "next": "15.2.4"
61 | },
62 | "dependencies": {
63 | "js-cookie": "^3.0.5",
64 | "lodash": "^4.17.21"
65 | }
66 | }
67 |
```
--------------------------------------------------------------------------------
/packages/cli/src/cli/loaders/yaml.ts:
--------------------------------------------------------------------------------
```typescript
1 | import YAML, { ToStringOptions } from "yaml";
2 | import { ILoader } from "./_types";
3 | import { createLoader } from "./_utils";
4 |
5 | export default function createYamlLoader(): ILoader<
6 | string,
7 | Record<string, any>
8 | > {
9 | return createLoader({
10 | async pull(locale, input) {
11 | return YAML.parse(input) || {};
12 | },
13 | async push(locale, payload, originalInput) {
14 | return YAML.stringify(payload, {
15 | lineWidth: -1,
16 | defaultKeyType: getKeyType(originalInput),
17 | defaultStringType: getStringType(originalInput),
18 | });
19 | },
20 | });
21 | }
22 |
23 | // check if the yaml keys are using double quotes or single quotes
24 | function getKeyType(
25 | yamlString: string | null,
26 | ): ToStringOptions["defaultKeyType"] {
27 | if (yamlString) {
28 | const lines = yamlString.split("\n");
29 | const hasDoubleQuotes = lines.find((line) => {
30 | return line.trim().startsWith('"') && line.trim().match('":');
31 | });
32 | if (hasDoubleQuotes) {
33 | return "QUOTE_DOUBLE";
34 | }
35 | }
36 | return "PLAIN";
37 | }
38 |
39 | // check if the yaml string values are using double quotes or single quotes
40 | function getStringType(
41 | yamlString: string | null,
42 | ): ToStringOptions["defaultStringType"] {
43 | if (yamlString) {
44 | const lines = yamlString.split("\n");
45 | const hasDoubleQuotes = lines.find((line) => {
46 | const trimmedLine = line.trim();
47 | return (
48 | (trimmedLine.startsWith('"') || trimmedLine.match(/:\s*"/)) &&
49 | (trimmedLine.endsWith('"') || trimmedLine.endsWith('",'))
50 | );
51 | });
52 | if (hasDoubleQuotes) {
53 | return "QUOTE_DOUBLE";
54 | }
55 | }
56 | return "PLAIN";
57 | }
58 |
```
--------------------------------------------------------------------------------
/packages/cli/demo/mdx/en/example.mdx:
--------------------------------------------------------------------------------
```markdown
1 | ---
2 | title: "Restaurant Review: Bella Vista"
3 | description: "Our dining experience at the new Italian restaurant downtown"
4 | author: "not-localized-author"
5 | published: "2024-03-15"
6 | rating: 4.5
7 | locked_key_1: "This value should remain unchanged in all locales"
8 | ignored_key_1: "This field should not appear in target locales"
9 | ---
10 |
11 | # Dinner at Bella Vista
12 |
13 | We finally tried the new Italian restaurant that opened last month on Main Street. Here's our honest review.
14 |
15 | ## The Atmosphere
16 |
17 | The restaurant has a warm, inviting atmosphere with:
18 |
19 | - **Dim lighting** that creates an intimate setting
20 | - *Soft jazz music* playing in the background
21 | - Fresh flowers on every table
22 |
23 | ### Making Reservations
24 |
25 | [Book your table online](https://example.com) or call during business hours.
26 |
27 | > Tip: Weekend reservations fill up quickly, so book ahead!
28 |
29 | ## Menu Highlights
30 |
31 | ```javascript
32 | // Restaurant website code - not localized
33 | function displayMenu(category) {
34 | const items = "This code stays in original language";
35 | return renderMenuItems(items);
36 | }
37 | ```
38 |
39 | ```css
40 | /* Styling for menu display - not localized */
41 | .menu-item {
42 | color: "This CSS remains unchanged";
43 | }
44 | ```
45 |
46 | ## Our Order
47 |
48 | We started with the antipasto platter and house salad.
49 |
50 | The pasta was cooked perfectly - exactly `al_dente` as it should be.
51 |
52 | ## Service Quality
53 |
54 | The waitstaff was attentive but not overwhelming.
55 |
56 | Our server checked on us regularly, maintaining `service.quality = "excellent"` throughout the evening.
57 |
58 | ## Final Verdict
59 |
60 | | Course | Rating |
61 | |--------|--------|
62 | | Appetizers | `stars(4)` |
63 | | Main dishes | `stars(5)` |
64 |
```
--------------------------------------------------------------------------------
/packages/cli/src/cli/loaders/xcode-strings/escape.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Unescape a string value from .strings file format
3 | * Handles: \", \\, \n, \t, etc.
4 | */
5 | export function unescapeString(raw: string): string {
6 | let result = "";
7 | let i = 0;
8 |
9 | while (i < raw.length) {
10 | if (raw[i] === "\\" && i + 1 < raw.length) {
11 | const nextChar = raw[i + 1];
12 | switch (nextChar) {
13 | case '"':
14 | result += '"';
15 | i += 2;
16 | break;
17 | case "\\":
18 | result += "\\";
19 | i += 2;
20 | break;
21 | case "n":
22 | result += "\n";
23 | i += 2;
24 | break;
25 | case "t":
26 | result += "\t";
27 | i += 2;
28 | break;
29 | case "r":
30 | result += "\r";
31 | i += 2;
32 | break;
33 | default:
34 | // Unknown escape - keep as-is
35 | result += raw[i];
36 | i++;
37 | break;
38 | }
39 | } else {
40 | result += raw[i];
41 | i++;
42 | }
43 | }
44 |
45 | return result;
46 | }
47 |
48 | /**
49 | * Escape a string value for .strings file format
50 | * Escapes: \, ", newlines to \n
51 | */
52 | export function escapeString(str: string): string {
53 | if (str == null) {
54 | return "";
55 | }
56 |
57 | let result = "";
58 |
59 | for (let i = 0; i < str.length; i++) {
60 | const char = str[i];
61 | switch (char) {
62 | case "\\":
63 | result += "\\\\";
64 | break;
65 | case '"':
66 | result += '\\"';
67 | break;
68 | case "\n":
69 | result += "\\n";
70 | break;
71 | case "\r":
72 | result += "\\r";
73 | break;
74 | case "\t":
75 | result += "\\t";
76 | break;
77 | default:
78 | result += char;
79 | break;
80 | }
81 | }
82 |
83 | return result;
84 | }
85 |
```
--------------------------------------------------------------------------------
/packages/cli/demo/android/es/example.xml:
--------------------------------------------------------------------------------
```
1 | <?xml version="1.0" encoding="utf-8"?>
2 | <resources>
3 | <string name="app_name">MyApp</string>
4 | <string name="welcome_message">¡Hola, mundo!</string>
5 | <string name="button_text">Comenzar</string>
6 |
7 | <string-array name="color_names">
8 | <item>Rojo</item>
9 | <item>Verde</item>
10 | <item>¡Azul!</item>
11 | </string-array>
12 |
13 | <plurals name="notification_count">
14 | <item quantity="one">%d mensaje nuevo</item>
15 | <item quantity="other">%d mensajes nuevos</item>
16 | </plurals>
17 |
18 | <bool name="show_tutorial">true</bool>
19 | <bool name="enable_animations">false</bool>
20 |
21 | <integer name="max_retry_attempts">3</integer>
22 | <integer name="default_timeout">30</integer>
23 |
24 | <string name="html_snippet"><b>Negrita</b></string>
25 |
26 | <string name="apostrophe_example">¡No olvides!</string>
27 |
28 | <string name="cdata_example"><![CDATA[Los usuarios solo pueden ver tu comentario después de registrarse. <u>Más información.</u>]]></string>
29 |
30 | <string-array name="mixed_items">
31 | <item> Elemento con espacios </item>
32 | <item> </item>
33 | </string-array>
34 |
35 | <color name="primary_color">#FF6200EE</color>
36 | <color name="secondary_color">#FF03DAC5</color>
37 |
38 | <dimen name="text_size">16sp</dimen>
39 | <dimen name="margin">8dp</dimen>
40 |
41 | <integer-array name="numbers">
42 | <item>1</item>
43 | <item>2</item>
44 | <item>3</item>
45 | </integer-array>
46 |
47 | <array name="icons">
48 | <item>@drawable/icon1</item>
49 | <item>@drawable/icon2</item>
50 | </array>
51 |
52 | <item type="id" name="button_ok"></item>
53 |
54 | </resources>
```
--------------------------------------------------------------------------------
/packages/compiler/src/utils/module-params.spec.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect } from "vitest";
2 | import { parseParametrizedModuleId } from "./module-params";
3 | import _ from "lodash";
4 |
5 | describe("parseParametrizedModuleId", () => {
6 | it("should extract the module id without parameters", () => {
7 | const result = parseParametrizedModuleId("test-module");
8 |
9 | expect(result).toEqual({
10 | id: "test-module",
11 | params: {},
12 | });
13 | });
14 |
15 | it("should extract the module id with a single parameter", () => {
16 | const result = parseParametrizedModuleId("test-module?key=value");
17 |
18 | expect(result).toEqual({
19 | id: "test-module",
20 | params: { key: "value" },
21 | });
22 | });
23 |
24 | it("should extract the module id with multiple parameters", () => {
25 | const result = parseParametrizedModuleId(
26 | "test-module?key1=value1&key2=value2&key3=value3",
27 | );
28 |
29 | expect(result).toEqual({
30 | id: "test-module",
31 | params: {
32 | key1: "value1",
33 | key2: "value2",
34 | key3: "value3",
35 | },
36 | });
37 | });
38 |
39 | it("should handle parameters with special characters", () => {
40 | const result = parseParametrizedModuleId(
41 | "test-module?key=value%20with%20spaces&special=%21%40%23",
42 | );
43 |
44 | expect(result).toEqual({
45 | id: "test-module",
46 | params: {
47 | key: "value with spaces",
48 | special: "!@#",
49 | },
50 | });
51 | });
52 |
53 | it("should handle module ids with path-like structure", () => {
54 | const result = parseParametrizedModuleId(
55 | "parent/child/module?version=1.0.0",
56 | );
57 |
58 | expect(result).toEqual({
59 | id: "parent/child/module",
60 | params: { version: "1.0.0" },
61 | });
62 | });
63 | });
64 |
```
--------------------------------------------------------------------------------
/packages/cli/src/cli/cmd/run/_types.ts:
--------------------------------------------------------------------------------
```typescript
1 | import {
2 | bucketTypeSchema,
3 | I18nConfig,
4 | localeCodeSchema,
5 | bucketTypes,
6 | } from "@lingo.dev/_spec";
7 | import { z } from "zod";
8 | import { ILocalizer } from "../../localizer/_types";
9 |
10 | export type CmdRunContext = {
11 | flags: CmdRunFlags;
12 | config: I18nConfig | null;
13 | localizer: ILocalizer | null;
14 | tasks: CmdRunTask[];
15 | results: Map<CmdRunTask, CmdRunTaskResult>;
16 | };
17 |
18 | export type CmdRunTaskResult = {
19 | status: "success" | "error" | "skipped";
20 | error?: Error;
21 | pathPattern?: string;
22 | sourceLocale?: string;
23 | targetLocale?: string;
24 | };
25 |
26 | export type CmdRunTask = {
27 | sourceLocale: string;
28 | targetLocale: string;
29 | bucketType: (typeof bucketTypes)[number];
30 | bucketPathPattern: string;
31 | injectLocale: string[];
32 | lockedKeys: string[];
33 | lockedPatterns: string[];
34 | ignoredKeys: string[];
35 | onlyKeys: string[];
36 | formatter?: "prettier" | "biome";
37 | };
38 |
39 | export const flagsSchema = z.object({
40 | bucket: z.array(bucketTypeSchema).optional(),
41 | key: z.array(z.string()).optional(),
42 | file: z.array(z.string()).optional(),
43 | apiKey: z.string().optional(),
44 | force: z.boolean().optional(),
45 | frozen: z.boolean().optional(),
46 | verbose: z.boolean().optional(),
47 | strict: z.boolean().optional(),
48 | interactive: z.boolean().default(false),
49 | concurrency: z.number().positive().default(10),
50 | debug: z.boolean().default(false),
51 | sourceLocale: z.string().optional(),
52 | targetLocale: z.array(z.string()).optional(),
53 | watch: z.boolean().default(false),
54 | debounce: z.number().positive().default(5000), // 5 seconds default
55 | sound: z.boolean().optional(),
56 | });
57 | export type CmdRunFlags = z.infer<typeof flagsSchema>;
58 |
```
--------------------------------------------------------------------------------
/packages/compiler/src/jsx-attribute-scopes-export.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { getJsxAttributeValue } from "./utils";
2 | import { LCP } from "./lib/lcp";
3 | import { getJsxAttributeValueHash } from "./utils/hash";
4 | import { collectJsxAttributeScopes } from "./utils/jsx-attribute-scope";
5 | import { CompilerPayload } from "./_base";
6 | import _ from "lodash";
7 |
8 | // Processes only JSX attribute scopes
9 | export function jsxAttributeScopesExportMutation(
10 | payload: CompilerPayload,
11 | ): CompilerPayload {
12 | const attributeScopes = collectJsxAttributeScopes(payload.ast);
13 | if (_.isEmpty(attributeScopes)) {
14 | return payload;
15 | }
16 |
17 | const lcp = LCP.getInstance({
18 | sourceRoot: payload.params.sourceRoot,
19 | lingoDir: payload.params.lingoDir,
20 | });
21 |
22 | for (const [scope, attributes] of attributeScopes) {
23 | for (const attributeDefinition of attributes) {
24 | const [attribute, scopeKey] = attributeDefinition.split(":");
25 |
26 | lcp.resetScope(payload.relativeFilePath, scopeKey);
27 |
28 | const attributeValue = getJsxAttributeValue(scope, attribute);
29 | if (!attributeValue) {
30 | continue;
31 | }
32 |
33 | lcp.setScopeType(payload.relativeFilePath, scopeKey, "attribute");
34 |
35 | const hash = getJsxAttributeValueHash(String(attributeValue));
36 | lcp.setScopeHash(payload.relativeFilePath, scopeKey, hash);
37 |
38 | lcp.setScopeContext(payload.relativeFilePath, scopeKey, "");
39 | lcp.setScopeSkip(payload.relativeFilePath, scopeKey, false);
40 | lcp.setScopeOverrides(payload.relativeFilePath, scopeKey, {});
41 |
42 | lcp.setScopeContent(
43 | payload.relativeFilePath,
44 | scopeKey,
45 | String(attributeValue),
46 | );
47 | }
48 | }
49 |
50 | lcp.save();
51 |
52 | return payload;
53 | }
54 |
```
--------------------------------------------------------------------------------
/demo/vite-project/src/App.tsx:
--------------------------------------------------------------------------------
```typescript
1 | import { useState } from "react";
2 | import reactLogo from "./assets/react.svg";
3 | import viteLogo from "/vite.svg";
4 | import { LingoDotDev } from "./lingo-dot-dev";
5 | import "./App.css";
6 | import { TestComponent } from "./components/test";
7 |
8 | import { LocaleSwitcher } from "lingo.dev/react/client";
9 |
10 | function App() {
11 | const [count, setCount] = useState(0);
12 |
13 | return (
14 | <>
15 | <div className="logo-container">
16 | <a href="https://vite.dev" target="_blank">
17 | <img src={viteLogo} className="logo" alt="Vite logo" />
18 | </a>
19 | <a href="https://react.dev" target="_blank">
20 | <img src={reactLogo} className="logo react" alt="React logo" />
21 | </a>
22 | <a href="https://lingo.dev" target="_blank">
23 | <LingoDotDev />
24 | </a>
25 | </div>
26 | <h1>Lingo.dev loves Vite and React</h1>
27 | <p className="welcome-text">
28 | Welcome to your new Vite & React application! This starter template
29 | includes everything you need to get started with Vite & React and
30 | Lingo.dev for internationalization.
31 | </p>
32 | <div className="card">
33 | <button onClick={() => setCount((count) => count + 1)}>
34 | count is {count}
35 | </button>
36 | <p>
37 | Edit <code>src/App.tsx</code> and save to test HMR
38 | </p>
39 | </div>
40 | <p className="read-the-docs">Click on the logos above to learn more</p>
41 | <TestComponent />
42 | <div className="locale-switcher">
43 | <LocaleSwitcher
44 | locales={["en", "es", "fr", "ru", "de", "ja", "zh", "ar", "ko"]}
45 | />
46 | </div>
47 | </>
48 | );
49 | }
50 |
51 | export default App;
52 |
```
--------------------------------------------------------------------------------
/packages/cli/demo/html/en/example.html:
--------------------------------------------------------------------------------
```html
1 | <!DOCTYPE html>
2 | <html lang="en">
3 | <head>
4 | <title>MyApp - Hello World</title>
5 |
6 | <meta name="description" content="A simple demo app" />
7 | <meta property="og:title" content="MyApp Demo" />
8 |
9 | <meta name="viewport" content="width=device-width, initial-scale=1" />
10 | <meta charset="UTF-8" />
11 |
12 | <style>
13 | body {
14 | font-family: Arial, sans-serif;
15 | color: #333;
16 | }
17 | .highlight::before {
18 | content: "★ Featured";
19 | }
20 | </style>
21 |
22 | <script>
23 | const message = "User session initialized";
24 | console.log(message);
25 | </script>
26 | </head>
27 | <body>
28 | <h1>Welcome to MyApp</h1>
29 |
30 | <p>
31 | Hello, world! This is a simple demo with
32 | <strong>bold text</strong> and
33 | <em>italic text</em>.
34 | </p>
35 |
36 | <div>
37 | <div>
38 | <span>Nested content here</span>
39 | </div>
40 | </div>
41 |
42 | <img src="example.jpg" alt="Example image" />
43 |
44 | <img
45 | src="example.jpg"
46 | alt="Demo image"
47 | title="View product details"
48 | />
49 |
50 | <input type="text" placeholder="Enter text here" />
51 |
52 | <input
53 | type="text"
54 | placeholder="Enter value"
55 | value="Default search query"
56 | />
57 |
58 | <a href="#" title="Click for more">Learn more</a>
59 |
60 | <a
61 | href="not-localized.html"
62 | data-info="Navigation link"
63 | title="Navigate"
64 | >
65 | Go to page
66 | </a>
67 |
68 | <div class="content-area" data-value="Main content">
69 | Content area
70 | </div>
71 |
72 | <script>
73 | const inlineScript = "Page analytics loaded";
74 | </script>
75 |
76 | <style>
77 | .inline-style {
78 | content: "Notice: ";
79 | }
80 | </style>
81 | </body>
82 | </html>
```
--------------------------------------------------------------------------------
/packages/compiler/src/lib/lcp/api/prompt.spec.ts:
--------------------------------------------------------------------------------
```typescript
1 | import prompt from "./prompt";
2 | import { describe, it, expect, vi } from "vitest";
3 |
4 | const baseArgs = {
5 | sourceLocale: "en",
6 | targetLocale: "es",
7 | };
8 |
9 | describe("prompt", () => {
10 | it("returns user-defined prompt with replacements", () => {
11 | const args = {
12 | ...baseArgs,
13 | prompt: "Translate from {SOURCE_LOCALE} to {TARGET_LOCALE}.",
14 | };
15 | const result = prompt(args);
16 | expect(result).toBe("Translate from en to es.");
17 | });
18 |
19 | it("trims and replaces variables in user prompt", () => {
20 | const args = {
21 | ...baseArgs,
22 | prompt: " {SOURCE_LOCALE} => {TARGET_LOCALE} ",
23 | };
24 | const result = prompt(args);
25 | expect(result).toBe("en => es");
26 | });
27 |
28 | it("falls back to built-in prompt if no user prompt", () => {
29 | const args = { ...baseArgs };
30 | const result = prompt(args);
31 | expect(result).toContain("You are an advanced AI localization engine");
32 | expect(result).toContain("Source language (locale code): en");
33 | expect(result).toContain("Target language (locale code): es");
34 | });
35 |
36 | it("logs when using user-defined prompt", () => {
37 | const spy = vi.spyOn(console, "log");
38 | const args = {
39 | ...baseArgs,
40 | prompt: "Prompt {SOURCE_LOCALE} {TARGET_LOCALE}",
41 | };
42 | prompt(args);
43 | expect(spy).toHaveBeenCalledWith(
44 | "✨ Compiler is using user-defined prompt.",
45 | );
46 | spy.mockRestore();
47 | });
48 |
49 | it("returns built-in prompt if user prompt is empty or whitespace", () => {
50 | const args = {
51 | ...baseArgs,
52 | prompt: " ",
53 | };
54 | const result = prompt(args);
55 | expect(result).toContain("You are an advanced AI localization engine");
56 | });
57 | });
58 |
```
--------------------------------------------------------------------------------
/packages/compiler/src/jsx-scopes-export.spec.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect, vi, beforeEach } from "vitest";
2 | import { createPayload, defaultParams } from "./_base";
3 | import { jsxScopesExportMutation } from "./jsx-scopes-export";
4 |
5 | vi.mock("./lib/lcp", () => {
6 | const instance = {
7 | resetScope: vi.fn().mockReturnThis(),
8 | setScopeType: vi.fn().mockReturnThis(),
9 | setScopeHash: vi.fn().mockReturnThis(),
10 | setScopeContext: vi.fn().mockReturnThis(),
11 | setScopeSkip: vi.fn().mockReturnThis(),
12 | setScopeOverrides: vi.fn().mockReturnThis(),
13 | setScopeContent: vi.fn().mockReturnThis(),
14 | save: vi.fn(),
15 | };
16 | const getInstance = vi.fn(() => instance);
17 | return {
18 | LCP: {
19 | getInstance,
20 | },
21 | __test__: { instance, getInstance },
22 | };
23 | });
24 |
25 | describe("jsxScopesExportMutation", () => {
26 | beforeEach(() => {
27 | vi.clearAllMocks();
28 | });
29 |
30 | it("exports element scope with hash/content/flags", async () => {
31 | const code = `
32 | export default function X(){
33 | return <div data-jsx-scope="scope-1">Foobar</div>
34 | }`.trim();
35 | const input = createPayload({
36 | code,
37 | params: defaultParams,
38 | relativeFilePath: "src/App.tsx",
39 | } as any);
40 | jsxScopesExportMutation(input);
41 | const lcpMod: any = await import("./lib/lcp");
42 | const inst = lcpMod.__test__.instance;
43 | expect(lcpMod.LCP.getInstance).toHaveBeenCalled();
44 | expect(inst.setScopeType).toHaveBeenCalledWith(
45 | "src/App.tsx",
46 | "0/declaration/body/0/argument",
47 | "element",
48 | );
49 | expect(inst.setScopeContent).toHaveBeenCalledWith(
50 | "src/App.tsx",
51 | "0/declaration/body/0/argument",
52 | "Foobar",
53 | );
54 | expect(inst.save).toHaveBeenCalled();
55 | });
56 | });
57 |
```
--------------------------------------------------------------------------------
/packages/compiler/src/utils/jsx-variables.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { NodePath } from "@babel/traverse";
2 | import * as t from "@babel/types";
3 | import { Expression } from "@babel/types";
4 |
5 | export const getJsxVariables = (nodePath: NodePath<t.JSXElement>) => {
6 | /*
7 | example input:
8 |
9 | <div>You have {count} new messages.</div>
10 |
11 | example output:
12 |
13 | t.objectExpression([
14 | t.objectProperty(t.identifier("count"), t.identifier("count")),
15 | ])
16 | */
17 |
18 | const variables = new Set<string>();
19 |
20 | nodePath.traverse({
21 | JSXOpeningElement(path) {
22 | path.skip();
23 | },
24 | JSXExpressionContainer(path) {
25 | if (t.isIdentifier(path.node.expression)) {
26 | variables.add(path.node.expression.name);
27 | } else if (t.isMemberExpression(path.node.expression)) {
28 | // Handle nested expressions like object.property.value
29 | let current: Expression = path.node.expression;
30 | const parts: string[] = [];
31 |
32 | while (t.isMemberExpression(current)) {
33 | if (t.isIdentifier(current.property)) {
34 | if (current.computed) {
35 | parts.unshift(`[${current.property.name}]`);
36 | } else {
37 | parts.unshift(current.property.name);
38 | }
39 | }
40 | current = current.object;
41 | }
42 |
43 | if (t.isIdentifier(current)) {
44 | parts.unshift(current.name);
45 | variables.add(parts.join(".").replaceAll(".[", "["));
46 | }
47 | }
48 | // Skip traversing inside the expression
49 | path.skip();
50 | },
51 | });
52 |
53 | const properties = Array.from(variables).map((name) =>
54 | t.objectProperty(t.stringLiteral(name), t.identifier(name)),
55 | );
56 |
57 | const result = t.objectExpression(properties);
58 | return result;
59 | };
60 |
```
--------------------------------------------------------------------------------
/packages/cli/src/cli/cmd/auth.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Command } from "interactive-commander";
2 | import Ora from "ora";
3 | import { getSettings, saveSettings } from "../utils/settings";
4 | import { createAuthenticator } from "../utils/auth";
5 |
6 | export default new Command()
7 | .command("auth")
8 | .description("Show current authentication status and user email")
9 | .helpOption("-h, --help", "Show help")
10 | // Deprecated options, safe to remove after September 2025
11 | .option(
12 | "--login",
13 | "DEPRECATED: Shows deprecation warning and exits. Use `lingo.dev login` instead",
14 | )
15 | .option(
16 | "--logout",
17 | "DEPRECATED: Shows deprecation warning and exits. Use `lingo.dev logout` instead",
18 | )
19 | .action(async (options) => {
20 | try {
21 | // Handle deprecated login option
22 | if (options.login) {
23 | Ora().warn(
24 | "⚠️ DEPRECATED: '--login' is deprecated. Please use 'lingo.dev login' instead.",
25 | );
26 | process.exit(1);
27 | }
28 |
29 | // Handle deprecated logout option
30 | if (options.logout) {
31 | Ora().warn(
32 | "⚠️ DEPRECATED: '--logout' is deprecated. Please use 'lingo.dev logout' instead.",
33 | );
34 | process.exit(1);
35 | }
36 |
37 | // Default behavior: show authentication status
38 | const settings = await getSettings(undefined);
39 | const authenticator = createAuthenticator({
40 | apiUrl: settings.auth.apiUrl,
41 | apiKey: settings.auth.apiKey!,
42 | });
43 | const auth = await authenticator.whoami();
44 | if (!auth) {
45 | Ora().warn("Not authenticated");
46 | } else {
47 | Ora().succeed(`Authenticated as ${auth.email}`);
48 | }
49 | } catch (error: any) {
50 | Ora().fail(error.message);
51 | process.exit(1);
52 | }
53 | });
54 |
```
--------------------------------------------------------------------------------
/packages/react/src/core/get-dictionary.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Loads a dictionary for the specified locale.
3 | *
4 | * This function attempts to load a dictionary using the provided loaders. If the specified
5 | * locale is not available, it falls back to the first available loader. The function
6 | * expects the loader to return a promise that resolves to an object with a `default` property
7 | * containing the dictionary data (the default export from dictionary file).
8 | *
9 | * @param locale - The locale to load the dictionary for. Can be null to use the first available loader.
10 | * @param loaders - A record of locale keys to loader functions. Each loader should return a Promise
11 | * that resolves to an object with a `default` property containing the dictionary.
12 | * @returns A Promise that resolves to the dictionary data (the `default` export from the loader).
13 | * @throws {Error} When no loaders are provided or available.
14 | *
15 | * @example
16 | * ```typescript
17 | * const loaders = {
18 | * 'en': () => import('./en.json'),
19 | * 'es': () => import('./es.json')
20 | * };
21 | *
22 | * const dictionary = await loadDictionary('en', loaders);
23 | * // Returns the default export from the English dictionary
24 | * ```
25 | */
26 | export function getDictionary(
27 | locale: string | null,
28 | loaders: Record<string, () => Promise<any>> = {},
29 | ) {
30 | const loader = getDictionaryLoader(locale, loaders);
31 | if (!loader) {
32 | throw new Error("No available dictionary loaders found");
33 | }
34 | return loader().then((value) => value.default);
35 | }
36 |
37 | function getDictionaryLoader(
38 | locale: string | null,
39 | loaders: Record<string, () => Promise<any>> = {},
40 | ) {
41 | if (locale && loaders[locale]) {
42 | return loaders[locale];
43 | }
44 | return Object.values(loaders)[0];
45 | }
46 |
```
--------------------------------------------------------------------------------
/packages/cli/demo/html/es/example.html:
--------------------------------------------------------------------------------
```html
1 | <!DOCTYPE html>
2 | <html lang="es">
3 | <head>
4 | <title>MyApp - Hola Mundo</title>
5 |
6 | <meta name="description" content="Una aplicación de demostración simple" />
7 | <meta property="og:title" content="Demostración de MyApp" />
8 |
9 | <meta name="viewport" content="width=device-width, initial-scale=1" />
10 | <meta charset="UTF-8" />
11 |
12 | <style>
13 | body {
14 | font-family: Arial, sans-serif;
15 | color: #333;
16 | }
17 | .highlight::before {
18 | content: "★ Featured";
19 | }
20 | </style>
21 |
22 | <script>
23 | const message = "User session initialized";
24 | console.log(message);
25 | </script>
26 | </head>
27 | <body>
28 | <h1>Bienvenido a MyApp</h1>
29 |
30 | <p>
31 | ¡Hola, mundo! Esta es una demostración simple con
32 | <strong>texto en negrita</strong>
33 | y
34 | <em>texto en cursiva</em>
35 | .
36 | </p>
37 |
38 | <div>
39 | <div>
40 | <span>Contenido anidado aquí</span>
41 | </div>
42 | </div>
43 |
44 | <img src="example.jpg" alt="Imagen de ejemplo" />
45 |
46 | <img
47 | src="example.jpg"
48 | alt="Imagen de demostración"
49 | title="View product details"
50 | />
51 |
52 | <input type="text" placeholder="Ingresa texto aquí" />
53 |
54 | <input
55 | type="text"
56 | placeholder="Ingresa valor"
57 | value="Default search query"
58 | />
59 |
60 | <a href="#" title="Haz clic para más">Saber más</a>
61 |
62 | <a href="not-localized.html" data-info="Navigation link" title="Navegar">
63 | Ir a la página
64 | </a>
65 |
66 | <div class="content-area" data-value="Main content">Área de contenido</div>
67 |
68 | <script>
69 | const inlineScript = "Page analytics loaded";
70 | </script>
71 |
72 | <style>
73 | .inline-style {
74 | content: "Notice: ";
75 | }
76 | </style>
77 | </body>
78 | </html>
```
--------------------------------------------------------------------------------
/packages/cli/demo/mdx/es/example.mdx:
--------------------------------------------------------------------------------
```markdown
1 | ---
2 | title: "Reseña de restaurante: Bella Vista"
3 | description: Nuestra experiencia gastronómica en el nuevo restaurante italiano del centro
4 | author: not-localized-author
5 | published: 2024-03-15
6 | rating: 4.5
7 | locked_key_1: This value should remain unchanged in all locales
8 | ---
9 |
10 | # Cena en Bella Vista
11 |
12 | Finalmente probamos el nuevo restaurante italiano que abrió el mes pasado en la calle Principal. Aquí está nuestra reseña honesta.
13 |
14 | ## El ambiente
15 |
16 | El restaurante tiene un ambiente cálido y acogedor con:
17 |
18 | - **Iluminación tenue** que crea un entorno íntimo
19 | - _Música suave de jazz_ sonando de fondo
20 | - Flores frescas en cada mesa
21 |
22 | ### Cómo hacer reservas
23 |
24 | [Reserva tu mesa en línea](https://example.com) o llama durante el horario comercial.
25 |
26 | > Consejo: Las reservas de fin de semana se agotan rápidamente, ¡así que reserva con anticipación!
27 |
28 | ## Destacados del menú
29 |
30 | ```javascript
31 | // Restaurant website code - not localized
32 | function displayMenu(category) {
33 | const items = "This code stays in original language";
34 | return renderMenuItems(items);
35 | }
36 | ```
37 |
38 | ```css
39 | /* Styling for menu display - not localized */
40 | .menu-item {
41 | color: "This CSS remains unchanged";
42 | }
43 | ```
44 |
45 | ## Nuestro pedido
46 |
47 | Comenzamos con la tabla de antipasto y la ensalada de la casa.
48 |
49 | La pasta estaba cocinada perfectamente - exactamente `al_dente` como debe ser.
50 |
51 | ## Calidad del servicio
52 |
53 | El personal de servicio estuvo atento pero no abrumador.
54 |
55 | Nuestro camarero nos atendió regularmente, manteniendo `service.quality = "excellent"` durante toda la noche.
56 |
57 | ## Veredicto final
58 |
59 | | Plato | Calificación |
60 | | ------------------ | ------------ |
61 | | Entrantes | `stars(4)` |
62 | | Platos principales | `stars(5)` |
63 |
```
--------------------------------------------------------------------------------
/packages/compiler/src/rsc-dictionary-loader.ts:
--------------------------------------------------------------------------------
```typescript
1 | import path from "path";
2 | import { createCodeMutation } from "./_base";
3 | import { LCP_DICTIONARY_FILE_NAME, ModuleId } from "./_const";
4 | import { getModuleExecutionMode, getOrCreateImport } from "./utils";
5 | import { findInvokations } from "./utils/invokations";
6 | import * as t from "@babel/types";
7 | import { getDictionaryPath } from "./_utils";
8 | import { createLocaleImportMap } from "./utils/create-locale-import-map";
9 |
10 | export const rscDictionaryLoaderMutation = createCodeMutation((payload) => {
11 | const mode = getModuleExecutionMode(payload.ast, payload.params.rsc);
12 | if (mode === "client") {
13 | return payload;
14 | }
15 |
16 | const invokations = findInvokations(payload.ast, {
17 | moduleName: ModuleId.ReactRSC,
18 | functionName: "loadDictionary",
19 | });
20 |
21 | const allLocales = Array.from(
22 | new Set([payload.params.sourceLocale, ...payload.params.targetLocales]),
23 | );
24 |
25 | for (const invokation of invokations) {
26 | const internalDictionaryLoader = getOrCreateImport(payload.ast, {
27 | moduleName: ModuleId.ReactRSC,
28 | exportedName: "loadDictionary_internal",
29 | });
30 |
31 | // Replace the function identifier with internal version
32 | if (t.isIdentifier(invokation.callee)) {
33 | invokation.callee.name = internalDictionaryLoader.importedName;
34 | }
35 |
36 | const dictionaryPath = getDictionaryPath({
37 | sourceRoot: payload.params.sourceRoot,
38 | lingoDir: payload.params.lingoDir,
39 | relativeFilePath: payload.relativeFilePath,
40 | });
41 |
42 | // Create locale import map object
43 | const localeImportMap = createLocaleImportMap(allLocales, dictionaryPath);
44 |
45 | // Add the locale import map as the second argument
46 | invokation.arguments.push(localeImportMap);
47 | }
48 |
49 | return payload;
50 | });
51 |
```
--------------------------------------------------------------------------------
/packages/compiler/src/utils/llm-api-keys.spec.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2 | import * as dotenv from "dotenv";
3 | import * as path from "path";
4 | import { getKeyFromEnv } from "./llm-api-key";
5 |
6 | const ORIGINAL_ENV = { ...process.env };
7 |
8 | vi.mock("dotenv");
9 |
10 | describe("LLM API keys", () => {
11 | describe("getKeyFromEnv", () => {
12 | beforeEach(() => {
13 | vi.resetModules();
14 | process.env = { ...ORIGINAL_ENV };
15 | });
16 |
17 | afterEach(() => {
18 | process.env = { ...ORIGINAL_ENV };
19 | vi.restoreAllMocks();
20 | });
21 |
22 | it("returns API key from process.env if set", () => {
23 | process.env.FOOBAR_API_KEY = "env-key";
24 | expect(getKeyFromEnv("FOOBAR_API_KEY")).toBe("env-key");
25 | });
26 |
27 | it("returns API key from .env file if not in process.env", () => {
28 | delete process.env.FOOBAR_API_KEY;
29 | const fakeEnv = { FOOBAR_API_KEY: "file-key" };
30 | const configMock = vi
31 | .mocked(dotenv.config)
32 | .mockImplementation((opts: any) => {
33 | if (opts && opts.processEnv) {
34 | Object.assign(opts.processEnv, fakeEnv);
35 | }
36 | return { parsed: fakeEnv };
37 | });
38 | expect(getKeyFromEnv("FOOBAR_API_KEY")).toBe("file-key");
39 | expect(configMock).toHaveBeenCalledWith({
40 | path: [
41 | path.resolve(process.cwd(), ".env"),
42 | path.resolve(process.cwd(), ".env.local"),
43 | path.resolve(process.cwd(), ".env.development"),
44 | ],
45 | });
46 | });
47 |
48 | it("returns undefined if no GROQ_API_KEY in env or .env file", () => {
49 | delete process.env.GROQ_API_KEY;
50 | vi.mocked(dotenv.config).mockResolvedValue({ parsed: {} });
51 | expect(getKeyFromEnv("FOOBAR_API_KEY")).toBeUndefined();
52 | });
53 | });
54 | });
55 |
```
--------------------------------------------------------------------------------
/packages/compiler/src/utils/observability.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { machineId } from "node-machine-id";
2 | import { getRc } from "./rc";
3 |
4 | export default async function trackEvent(
5 | event: string,
6 | properties?: Record<string, any>,
7 | ) {
8 | if (process.env.DO_NOT_TRACK) {
9 | return;
10 | }
11 |
12 | try {
13 | const actualId = await getActualId();
14 |
15 | const { PostHog } = await import("posthog-node");
16 | const posthog = new PostHog(
17 | "phc_eR0iSoQufBxNY36k0f0T15UvHJdTfHlh8rJcxsfhfXk",
18 | {
19 | host: "https://eu.i.posthog.com",
20 | flushAt: 1,
21 | flushInterval: 0,
22 | },
23 | );
24 |
25 | await posthog.capture({
26 | distinctId: actualId,
27 | event,
28 | properties: {
29 | ...properties,
30 | isByokMode: properties?.models !== "lingo.dev",
31 | meta: {
32 | version: process.env.npm_package_version,
33 | isCi: process.env.CI === "true",
34 | },
35 | },
36 | });
37 |
38 | await posthog.shutdown();
39 | } catch (error) {
40 | if (process.env.DEBUG) {
41 | console.error(error);
42 | }
43 | }
44 | }
45 |
46 | async function getActualId() {
47 | const rc = getRc();
48 | const apiKey = process.env.LINGODOTDEV_API_KEY || rc?.auth?.apiKey;
49 | const apiUrl =
50 | process.env.LINGODOTDEV_API_URL ||
51 | rc?.auth?.apiUrl ||
52 | "https://engine.lingo.dev";
53 |
54 | if (apiKey) {
55 | try {
56 | const res = await fetch(`${apiUrl}/whoami`, {
57 | method: "POST",
58 | headers: {
59 | Authorization: `Bearer ${apiKey}`,
60 | ContentType: "application/json",
61 | },
62 | });
63 | if (res.ok) {
64 | const payload = await res.json();
65 | if (payload?.email) {
66 | return payload.email;
67 | }
68 | }
69 | } catch (err) {
70 | // ignore, fallback to device id
71 | }
72 | }
73 | const id = await machineId();
74 | return `device-${id}`;
75 | }
76 |
```
--------------------------------------------------------------------------------
/demo/next-app/src/app/client-component.tsx:
--------------------------------------------------------------------------------
```typescript
1 | "use client";
2 | import { useState } from "react";
3 |
4 | export function ClientComponent() {
5 | const [counter, setCounter] = useState(0);
6 |
7 | return (
8 | <div className="flex flex-col gap-4 bg-gray-300 p-4 rounded-lg">
9 | <span className="color-white">Interactive component</span>
10 | <div className="flex items-center gap-4 justify-center w-100 border-2 border-gray-300 rounded-lg p-6 bg-white">
11 | <button
12 | onClick={() => setCounter(counter - 1)}
13 | className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-auto whitespace-nowrap cursor-pointer"
14 | >
15 | - Decrement
16 | </button>
17 | <span className="text-4xl" title="This is current counter value">
18 | {counter}
19 | </span>
20 | <button
21 | onClick={() => setCounter(counter + 1)}
22 | className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-auto whitespace-nowrap cursor-pointer"
23 | >
24 | + Increment
25 | </button>
26 | </div>
27 | <div className="h-8">
28 | {counter < -3 && (
29 | <span className="text-red-500">
30 | <strong>Error:</strong> Counter too low!
31 | </span>
32 | )}
33 | {counter > 5 && (
34 | <span className="text-red-500">
35 | <strong>Error:</strong> Counter too high!
36 | </span>
37 | )}
38 | </div>
39 | </div>
40 | );
41 | }
42 |
```
--------------------------------------------------------------------------------
/packages/compiler/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "@lingo.dev/_compiler",
3 | "version": "0.7.15",
4 | "description": "Lingo.dev Compiler",
5 | "private": false,
6 | "publishConfig": {
7 | "access": "public"
8 | },
9 | "sideEffects": false,
10 | "type": "module",
11 | "main": "build/index.cjs",
12 | "types": "build/index.d.ts",
13 | "module": "build/index.mjs",
14 | "files": [
15 | "build"
16 | ],
17 | "scripts": {
18 | "dev": "tsup --watch",
19 | "build": "pnpm typecheck && tsup",
20 | "typecheck": "tsc --noEmit",
21 | "clean": "rm -rf build",
22 | "test": "vitest --run",
23 | "test:watch": "vitest -w"
24 | },
25 | "keywords": [],
26 | "author": "",
27 | "license": "ISC",
28 | "devDependencies": {
29 | "@types/babel__generator": "^7.6.8",
30 | "@types/babel__traverse": "^7.20.6",
31 | "@types/ini": "^4.1.1",
32 | "@types/lodash": "^4.17.4",
33 | "@types/object-hash": "^3.0.6",
34 | "@types/react": "^18.3.18",
35 | "next": "15.2.4",
36 | "tsup": "^8.3.5",
37 | "typescript": "^5.4.5",
38 | "vitest": "^2.1.4"
39 | },
40 | "dependencies": {
41 | "@ai-sdk/google": "^1.2.19",
42 | "@ai-sdk/groq": "^1.2.3",
43 | "@ai-sdk/mistral": "^1.2.8",
44 | "@babel/generator": "^7.26.5",
45 | "@babel/parser": "^7.26.7",
46 | "@babel/traverse": "^7.27.4",
47 | "@babel/types": "^7.26.7",
48 | "@lingo.dev/_sdk": "workspace:*",
49 | "@lingo.dev/_spec": "workspace:*",
50 | "@openrouter/ai-sdk-provider": "^0.7.1",
51 | "@prettier/sync": "^0.6.1",
52 | "ai": "^4.2.10",
53 | "dedent": "^1.6.0",
54 | "dotenv": "^16.4.5",
55 | "fast-xml-parser": "^5.0.8",
56 | "ini": "^5.0.0",
57 | "lodash": "^4.17.21",
58 | "object-hash": "^3.0.0",
59 | "ollama-ai-provider": "^1.2.0",
60 | "prettier": "^3.4.2",
61 | "unplugin": "^2.1.2",
62 | "zod": "^3.25.76",
63 | "posthog-node": "^5.5.1",
64 | "node-machine-id": "^1.1.12"
65 | },
66 | "packageManager": "[email protected]"
67 | }
68 |
```
--------------------------------------------------------------------------------
/packages/compiler/src/jsx-root-flag.spec.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect } from "vitest";
2 | import jsxRootFlagMutation from "./jsx-root-flag";
3 | import { createPayload, createOutput, defaultParams } from "./_base";
4 |
5 | // Helper function to run mutation and get result
6 | function runMutation(code: string) {
7 | const input = createPayload({ code, params: defaultParams, fileKey: "test" });
8 | const mutated = jsxRootFlagMutation(input);
9 | if (!mutated) throw new Error("Mutation returned null");
10 | return createOutput(mutated).code;
11 | }
12 |
13 | describe("jsxRootFlagMutation", () => {
14 | it("should add data-jsx-root flag to a single root JSX element", () => {
15 | const input = `
16 | function Component() {
17 | return <div>Hello</div>;
18 | }
19 | `.trim();
20 |
21 | const expected = `
22 | function Component() {
23 | return <div data-jsx-root>Hello</div>;
24 | }
25 | `.trim();
26 | const result = runMutation(input);
27 |
28 | expect(result).toBe(expected.trim());
29 | });
30 |
31 | it("should add data-jsx-root flag to multiple root JSX elements", () => {
32 | const input = `
33 | function Component() {
34 | if (condition) {
35 | return <div>True</div>;
36 | }
37 | return <span>False</span>;
38 | }
39 | `.trim();
40 |
41 | const expected = `
42 | function Component() {
43 | if (condition) {
44 | return <div data-jsx-root>True</div>;
45 | }
46 | return <span data-jsx-root>False</span>;
47 | }
48 | `.trim();
49 | const result = runMutation(input);
50 |
51 | expect(result).toBe(expected.trim());
52 | });
53 |
54 | it("should not add data-jsx-root flag to nested JSX elements", () => {
55 | const input = `
56 | function Component() {
57 | return <div>
58 | <span>Nested</span>
59 | </div>;
60 | }
61 | `.trim();
62 |
63 | const expected = `
64 | function Component() {
65 | return <div data-jsx-root>
66 | <span>Nested</span>
67 | </div>;
68 | }
69 | `.trim();
70 |
71 | const result = runMutation(input);
72 |
73 | expect(result).toBe(expected.trim());
74 | });
75 | });
76 |
```
--------------------------------------------------------------------------------
/demo/adonisjs/bin/test.ts:
--------------------------------------------------------------------------------
```typescript
1 | /*
2 | |--------------------------------------------------------------------------
3 | | Test runner entrypoint
4 | |--------------------------------------------------------------------------
5 | |
6 | | The "test.ts" file is the entrypoint for running tests using Japa.
7 | |
8 | | Either you can run this file directly or use the "test"
9 | | command to run this file and monitor file changes.
10 | |
11 | */
12 |
13 | // @ts-expect-error 2540
14 | process.env.NODE_ENV = 'test'
15 |
16 | import 'reflect-metadata'
17 | import { Ignitor, prettyPrintError } from '@adonisjs/core'
18 | import { configure, processCLIArgs, run } from '@japa/runner'
19 |
20 | /**
21 | * URL to the application root. AdonisJS need it to resolve
22 | * paths to file and directories for scaffolding commands
23 | */
24 | const APP_ROOT = new URL('../', import.meta.url)
25 |
26 | /**
27 | * The importer is used to import files in context of the
28 | * application.
29 | */
30 | const IMPORTER = (filePath: string) => {
31 | if (filePath.startsWith('./') || filePath.startsWith('../')) {
32 | return import(new URL(filePath, APP_ROOT).href)
33 | }
34 | return import(filePath)
35 | }
36 |
37 | new Ignitor(APP_ROOT, { importer: IMPORTER })
38 | .tap((app) => {
39 | app.booting(async () => {
40 | await import('#start/env')
41 | })
42 | app.listen('SIGTERM', () => app.terminate())
43 | app.listenIf(app.managedByPm2, 'SIGINT', () => app.terminate())
44 | })
45 | .testRunner()
46 | .configure(async (app) => {
47 | const { runnerHooks, ...config } = await import('../tests/bootstrap.js')
48 |
49 | processCLIArgs(process.argv.splice(2))
50 | configure({
51 | ...app.rcFile.tests,
52 | ...config,
53 | ...{
54 | setup: runnerHooks.setup,
55 | teardown: runnerHooks.teardown.concat([() => app.terminate()]),
56 | },
57 | })
58 | })
59 | .run(() => run())
60 | .catch((error) => {
61 | process.exitCode = 1
62 | prettyPrintError(error)
63 | })
64 |
```
--------------------------------------------------------------------------------
/packages/compiler/src/utils/ast-key.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { NodePath } from "@babel/traverse";
2 | import * as t from "@babel/types";
3 | import traverse from "@babel/traverse";
4 |
5 | export function getAstKey(nodePath: NodePath) {
6 | const keyChunks: any[] = [];
7 |
8 | let current: NodePath | null = nodePath;
9 | while (current) {
10 | keyChunks.push(current.key);
11 | current = current.parentPath;
12 |
13 | if (t.isProgram(current?.node)) {
14 | break;
15 | }
16 | }
17 |
18 | const result = keyChunks.reverse().join("/");
19 | return result;
20 | }
21 |
22 | export function getAstByKey(ast: t.File, key: string) {
23 | const programPath = _getProgramNodePath(ast);
24 | if (!programPath) {
25 | return null;
26 | }
27 |
28 | const keyParts = key.split("/").reverse();
29 |
30 | let result: NodePath = programPath;
31 |
32 | while (true) {
33 | let currentKeyPart = keyParts.pop();
34 | if (!currentKeyPart) {
35 | break;
36 | }
37 | const isIntegerPart = Number.isInteger(Number(currentKeyPart));
38 | if (isIntegerPart) {
39 | const maybeBodyItemsArray = result.get("body");
40 | const bodyItemsArray = Array.isArray(maybeBodyItemsArray)
41 | ? maybeBodyItemsArray
42 | : [maybeBodyItemsArray];
43 | const index = Number(currentKeyPart);
44 | const subResult = bodyItemsArray[index];
45 | result = subResult as NodePath;
46 | } else {
47 | const maybeSubResultArray = result.get(currentKeyPart);
48 | const subResultArray = Array.isArray(maybeSubResultArray)
49 | ? maybeSubResultArray
50 | : [maybeSubResultArray];
51 | const subResult = subResultArray[0];
52 | result = subResult;
53 | }
54 | }
55 |
56 | return result;
57 | }
58 |
59 | function _getProgramNodePath(ast: t.File): NodePath<t.Program> | null {
60 | let result: NodePath<t.Program> | null = null;
61 |
62 | traverse(ast, {
63 | Program(nodePath) {
64 | result = nodePath;
65 | nodePath.stop();
66 | },
67 | });
68 |
69 | return result;
70 | }
71 |
```
--------------------------------------------------------------------------------
/packages/compiler/src/utils/jsx-element.ts:
--------------------------------------------------------------------------------
```typescript
1 | import * as t from "@babel/types";
2 | import { NodePath } from "@babel/traverse";
3 |
4 | export function getJsxElementName(nodePath: NodePath<t.JSXElement>) {
5 | const openingElement = nodePath.node.openingElement;
6 |
7 | // elements with simple (string) name
8 | if (t.isJSXIdentifier(openingElement.name)) {
9 | return openingElement.name.name;
10 | }
11 |
12 | // elements with dots in name
13 | if (t.isJSXMemberExpression(openingElement.name)) {
14 | const memberExpr = openingElement.name;
15 | const parts: string[] = [];
16 |
17 | // Traverse the member expression to collect all parts
18 | let current: t.JSXMemberExpression | t.JSXIdentifier = memberExpr;
19 | while (t.isJSXMemberExpression(current)) {
20 | parts.unshift(current.property.name);
21 | current = current.object;
22 | }
23 |
24 | // Add the base identifier
25 | if (t.isJSXIdentifier(current)) {
26 | parts.unshift(current.name);
27 | }
28 |
29 | return parts.join(".");
30 | }
31 | return null;
32 | }
33 |
34 | export function getNestedJsxElements(nodePath: NodePath<t.JSXElement>) {
35 | const nestedElements: t.JSXElement[] = [];
36 |
37 | nodePath.traverse({
38 | JSXElement(path) {
39 | if (path.node !== nodePath.node) {
40 | nestedElements.push(path.node);
41 | }
42 | },
43 | });
44 |
45 | const arrayOfElements = nestedElements.map((element, index) => {
46 | // Create a function that takes children as param and returns the JSX element
47 | const param = t.identifier("children");
48 |
49 | // Replace the original children with the param
50 | const clonedElement = t.cloneNode(element);
51 | clonedElement.children = [t.jsxExpressionContainer(param)];
52 |
53 | return t.arrowFunctionExpression(
54 | [t.objectPattern([t.objectProperty(param, param, false, true)])],
55 | clonedElement,
56 | );
57 | });
58 | const result = t.arrayExpression(arrayOfElements);
59 | return result;
60 | }
61 |
```
--------------------------------------------------------------------------------
/packages/cli/src/cli/utils/exit-gracefully.ts:
--------------------------------------------------------------------------------
```typescript
1 | const STEP_WAIT_INTERVAL = 250;
2 | const MAX_WAIT_INTERVAL = 2000;
3 |
4 | export function exitGracefully(elapsedMs = 0) {
5 | // Check if there are any pending operations
6 | const hasPendingOperations = checkForPendingOperations();
7 |
8 | if (hasPendingOperations && elapsedMs < MAX_WAIT_INTERVAL) {
9 | // Wait a bit longer if there are pending operations
10 | setTimeout(
11 | () => exitGracefully(elapsedMs + STEP_WAIT_INTERVAL),
12 | STEP_WAIT_INTERVAL,
13 | );
14 | } else {
15 | // Exit immediately if no pending operations
16 | process.exit(0);
17 | }
18 | }
19 |
20 | function checkForPendingOperations(): boolean {
21 | // Check for active handles and requests using internal Node.js methods
22 | const activeHandles = (process as any)._getActiveHandles?.() || [];
23 | const activeRequests = (process as any)._getActiveRequests?.() || [];
24 |
25 | // Filter out standard handles that are always present
26 | const nonStandardHandles = activeHandles.filter((handle: any) => {
27 | // Skip standard handles like process.stdin, process.stdout, etc.
28 | if (
29 | handle === process.stdin ||
30 | handle === process.stdout ||
31 | handle === process.stderr
32 | ) {
33 | return false;
34 | }
35 | // Skip timers that are part of the normal process
36 | if (
37 | handle &&
38 | typeof handle === "object" &&
39 | "hasRef" in handle &&
40 | !handle.hasRef()
41 | ) {
42 | return false;
43 | }
44 | return true;
45 | });
46 |
47 | // Check if there are any file watchers or other async operations
48 | const hasFileWatchers = nonStandardHandles.some(
49 | (handle: any) => handle && typeof handle === "object" && "close" in handle,
50 | );
51 |
52 | // Check for pending promises or async operations
53 | const hasPendingPromises = activeRequests.length > 0;
54 |
55 | return nonStandardHandles.length > 0 || hasFileWatchers || hasPendingPromises;
56 | }
57 |
```
--------------------------------------------------------------------------------
/packages/compiler/src/lib/lcp/api/provider-details.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { openrouter } from "@openrouter/ai-sdk-provider";
2 |
3 | export const providerDetails: Record<
4 | string,
5 | {
6 | name: string; // Display name (e.g., "Groq", "Google")
7 | apiKeyEnvVar?: string; // Environment variable name (e.g., "GROQ_API_KEY")
8 | apiKeyConfigKey?: string; // Config key if applicable (e.g., "llm.groqApiKey")
9 | getKeyLink: string; // Link to get API key
10 | docsLink: string; // Link to API docs for troubleshooting
11 | }
12 | > = {
13 | groq: {
14 | name: "Groq",
15 | apiKeyEnvVar: "GROQ_API_KEY",
16 | apiKeyConfigKey: "llm.groqApiKey",
17 | getKeyLink: "https://groq.com",
18 | docsLink: "https://console.groq.com/docs/errors",
19 | },
20 | google: {
21 | name: "Google",
22 | apiKeyEnvVar: "GOOGLE_API_KEY",
23 | apiKeyConfigKey: "llm.googleApiKey",
24 | getKeyLink: "https://ai.google.dev/",
25 | docsLink: "https://ai.google.dev/gemini-api/docs/troubleshooting",
26 | },
27 | openrouter: {
28 | name: "OpenRouter",
29 | apiKeyEnvVar: "OPENROUTER_API_KEY",
30 | apiKeyConfigKey: "llm.openrouterApiKey",
31 | getKeyLink: "https://openrouter.ai",
32 | docsLink: "https://openrouter.ai/docs",
33 | },
34 | ollama: {
35 | name: "Ollama",
36 | apiKeyEnvVar: undefined, // Ollama doesn't require an API key
37 | apiKeyConfigKey: undefined, // Ollama doesn't require an API key
38 | getKeyLink: "https://ollama.com/download",
39 | docsLink: "https://github.com/ollama/ollama/tree/main/docs",
40 | },
41 | mistral: {
42 | name: "Mistral",
43 | apiKeyEnvVar: "MISTRAL_API_KEY",
44 | apiKeyConfigKey: "llm.mistralApiKey",
45 | getKeyLink: "https://console.mistral.ai",
46 | docsLink: "https://docs.mistral.ai",
47 | },
48 | "lingo.dev": {
49 | name: "Lingo.dev",
50 | apiKeyEnvVar: "LINGODOTDEV_API_KEY",
51 | apiKeyConfigKey: "auth.apiKey",
52 | getKeyLink: "https://lingo.dev",
53 | docsLink: "https://lingo.dev/docs",
54 | },
55 | };
56 |
```
--------------------------------------------------------------------------------
/packages/cli/src/cli/loaders/inject-locale.ts:
--------------------------------------------------------------------------------
```typescript
1 | import _ from "lodash";
2 | import { ILoader } from "./_types";
3 | import { createLoader } from "./_utils";
4 | import { minimatch } from "minimatch";
5 |
6 | export default function createInjectLocaleLoader(
7 | injectLocaleKeys?: string[],
8 | ): ILoader<Record<string, any>, Record<string, any>> {
9 | return createLoader({
10 | async pull(locale, data) {
11 | if (!injectLocaleKeys) {
12 | return data;
13 | }
14 | const omitKeys = _getKeysWithLocales(data, injectLocaleKeys, locale);
15 | const result = _.omit(data, omitKeys);
16 | return result;
17 | },
18 | async push(locale, data, originalInput, originalLocale) {
19 | if (!injectLocaleKeys || !originalInput) {
20 | return data;
21 | }
22 |
23 | const localeKeys = _getKeysWithLocales(
24 | originalInput,
25 | injectLocaleKeys,
26 | originalLocale,
27 | );
28 |
29 | localeKeys.forEach((key) => {
30 | _.set(data, key, locale);
31 | });
32 |
33 | return data;
34 | },
35 | });
36 | }
37 |
38 | function _getKeysWithLocales(
39 | data: Record<string, any>,
40 | injectLocaleKeys: string[],
41 | locale: string,
42 | ) {
43 | const allKeys = _getAllKeys(data);
44 | return allKeys.filter((key) => {
45 | return (
46 | injectLocaleKeys.some((pattern) => minimatch(key, pattern)) &&
47 | _.get(data, key) === locale
48 | );
49 | });
50 | }
51 |
52 | // Helper to get all deep keys in lodash path style (e.g., 'a.b.c')
53 | function _getAllKeys(obj: Record<string, any>, prefix = ""): string[] {
54 | let keys: string[] = [];
55 | for (const key in obj) {
56 | if (!Object.prototype.hasOwnProperty.call(obj, key)) continue;
57 | const path = prefix ? `${prefix}.${key}` : key;
58 | if (
59 | typeof obj[key] === "object" &&
60 | obj[key] !== null &&
61 | !Array.isArray(obj[key])
62 | ) {
63 | keys = keys.concat(_getAllKeys(obj[key], path));
64 | } else {
65 | keys.push(path);
66 | }
67 | }
68 | return keys;
69 | }
70 |
```
--------------------------------------------------------------------------------
/packages/react/src/rsc/provider.tsx:
--------------------------------------------------------------------------------
```typescript
1 | import { LingoProvider as LingoClientProvider } from "../client";
2 | import { loadDictionaryFromRequest, loadLocaleFromCookies } from "./utils";
3 |
4 | /**
5 | * The props for the `LingoProvider` component.
6 | */
7 | export type LingoProviderProps = {
8 | /**
9 | * A callback function that loads the dictionary for the current locale.
10 | *
11 | * @param locale - The locale code to load the dictionary for.
12 | *
13 | * @returns The dictionary object containing localized content.
14 | */
15 | loadDictionary: (locale: string | null) => Promise<any>;
16 | /**
17 | * The child components containing localizable content.
18 | */
19 | children: React.ReactNode;
20 | };
21 |
22 | /**
23 | * A context provider that loads the dictionary for the current locale and makes localized content available to its descendants.
24 | *
25 | * This component:
26 | *
27 | * - Should be placed at the top of the component tree
28 | * - Should be used in server-side rendering scenarios with React Server Components (RSC)
29 | *
30 | * @template D - The type of the dictionary object containing localized content.
31 | *
32 | * @example Use in a Next.js (App Router) application
33 | * ```tsx file="app/layout.tsx"
34 | * import { LingoProvider, loadDictionary } from "lingo.dev/react/rsc";
35 | *
36 | * export default function RootLayout({
37 | * children,
38 | * }: Readonly<{
39 | * children: React.ReactNode;
40 | * }>) {
41 | * return (
42 | * <LingoProvider loadDictionary={(locale) => loadDictionary(locale)}>
43 | * <html lang="en">
44 | * <body>
45 | * {children}
46 | * </body>
47 | * </html>
48 | * </LingoProvider>
49 | * );
50 | * }
51 | * ```
52 | */
53 | export async function LingoProvider(props: LingoProviderProps) {
54 | const dictionary = await loadDictionaryFromRequest(props.loadDictionary);
55 |
56 | return (
57 | <LingoClientProvider dictionary={dictionary}>
58 | {props.children}
59 | </LingoClientProvider>
60 | );
61 | }
62 |
```
--------------------------------------------------------------------------------
/packages/cli/src/cli/cmd/config/unset.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Command } from "interactive-commander";
2 | import chalk from "chalk";
3 | import dedent from "dedent";
4 | import _ from "lodash";
5 | import {
6 | SETTINGS_KEYS,
7 | loadSystemSettings,
8 | saveSettings,
9 | } from "../../utils/settings";
10 |
11 | export default new Command()
12 | .name("unset")
13 | .description("Remove a CLI setting from ~/.lingodotdevrc")
14 | .addHelpText("afterAll", `\nAvailable keys:\n ${SETTINGS_KEYS.join("\n ")}`)
15 | .argument(
16 | "<key>",
17 | "Configuration key to remove (must match one of the available keys listed below)",
18 | )
19 | .helpOption("-h, --help", "Show help")
20 | .action(async (key: string) => {
21 | // Validate key first (defensive; choices() should already restrict but keep for safety).
22 | if (!SETTINGS_KEYS.includes(key)) {
23 | console.error(
24 | dedent`
25 | ${chalk.red("✖")} Unknown configuration key: ${chalk.bold(key)}
26 | Run ${chalk.dim(
27 | "lingo.dev config unset --help",
28 | )} to see available keys.
29 | `,
30 | );
31 | process.exitCode = 1;
32 | return;
33 | }
34 |
35 | // Load existing settings.
36 | const settings = loadSystemSettings();
37 | const currentValue = _.get(settings, key);
38 |
39 | if (!_.trim(String(currentValue || ""))) {
40 | console.log(`${chalk.cyan("ℹ")} ${chalk.bold(key)} is not set.`);
41 | return;
42 | } else {
43 | const updated: any = _.cloneDeep(settings);
44 | _.unset(updated, key);
45 | try {
46 | saveSettings(updated as any);
47 | console.log(
48 | `${chalk.green("✔")} Removed configuration key ${chalk.bold(key)}`,
49 | );
50 | } catch (err) {
51 | console.error(
52 | chalk.red(
53 | `✖ Failed to save configuration: ${chalk.dim(
54 | err instanceof Error ? err.message : String(err),
55 | )}`,
56 | ),
57 | );
58 | process.exitCode = 1;
59 | }
60 | }
61 | });
62 |
```
--------------------------------------------------------------------------------
/packages/cli/src/cli/utils/plutil-formatter.ts:
--------------------------------------------------------------------------------
```typescript
1 | export function formatPlutilStyle(
2 | jsonData: any,
3 | existingJson?: string,
4 | ): string {
5 | // Detect indentation from existing JSON if provided
6 | const indent = existingJson ? detectIndentation(existingJson) : " ";
7 |
8 | function format(data: any, level = 0): string {
9 | const currentIndent = indent.repeat(level);
10 | const nextIndent = indent.repeat(level + 1);
11 |
12 | if (typeof data !== "object" || data === null) {
13 | return JSON.stringify(data);
14 | }
15 |
16 | if (Array.isArray(data)) {
17 | if (data.length === 0) return "[]";
18 | const items = data.map(
19 | (item) => `${nextIndent}${format(item, level + 1)}`,
20 | );
21 | return `[\n${items.join(",\n")}\n${currentIndent}]`;
22 | }
23 |
24 | const keys = Object.keys(data);
25 | if (keys.length === 0) {
26 | return `{\n\n${currentIndent}}`; // Empty object with proper indentation
27 | }
28 |
29 | // Sort keys to ensure whitespace keys come first
30 | const sortedKeys = keys.sort((a, b) => {
31 | // If both keys are whitespace or both are non-whitespace, maintain stable order
32 | const aIsWhitespace = /^\s*$/.test(a);
33 | const bIsWhitespace = /^\s*$/.test(b);
34 |
35 | if (aIsWhitespace && !bIsWhitespace) return -1;
36 | if (!aIsWhitespace && bIsWhitespace) return 1;
37 | return a.localeCompare(b, undefined, { numeric: true });
38 | });
39 |
40 | const items = sortedKeys.map((key) => {
41 | const value = data[key];
42 | return `${nextIndent}${JSON.stringify(key)} : ${format(
43 | value,
44 | level + 1,
45 | )}`;
46 | });
47 |
48 | return `{\n${items.join(",\n")}\n${currentIndent}}`;
49 | }
50 |
51 | const result = format(jsonData);
52 | return result;
53 | }
54 |
55 | function detectIndentation(jsonStr: string): string {
56 | // Find the first indented line
57 | const match = jsonStr.match(/\n(\s+)/);
58 | return match ? match[1] : " "; // fallback to 4 spaces if no indentation found
59 | }
60 |
```
--------------------------------------------------------------------------------
/.github/workflows/lingodotdev.yml:
--------------------------------------------------------------------------------
```yaml
1 | name: "Lingo.dev"
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | version:
7 | description: "Lingo.dev CLI version"
8 | default: "latest"
9 | required: false
10 | pull-request:
11 | description: "Create a pull request with the changes"
12 | type: boolean
13 | default: false
14 | required: false
15 | commit-message:
16 | description: "Commit message"
17 | default: "feat: update translations via @LingoDotDev"
18 | required: false
19 | pull-request-title:
20 | description: "Pull request title"
21 | default: "feat: update translations via @LingoDotDev"
22 | required: false
23 | working-directory:
24 | description: "Working directory"
25 | default: "."
26 | required: false
27 | process-own-commits:
28 | description: "Process commits made by this action"
29 | type: boolean
30 | default: false
31 | required: false
32 | parallel:
33 | description: "Run in parallel mode"
34 | type: boolean
35 | default: false
36 | required: false
37 |
38 | jobs:
39 | lingodotdev:
40 | runs-on: ubuntu-latest
41 | permissions:
42 | contents: write
43 | pull-requests: write
44 | steps:
45 | - name: Checkout
46 | uses: actions/checkout@v4
47 |
48 | - name: Use Node.js
49 | uses: actions/setup-node@v2
50 | with:
51 | node-version: "20"
52 |
53 | - name: Lingo.dev
54 | uses: ./
55 | with:
56 | api-key: ${{ secrets.LINGODOTDEV_API_KEY }}
57 | version: ${{ inputs.version }}
58 | pull-request: ${{ inputs['pull-request'] }}
59 | commit-message: ${{ inputs['commit-message'] }}
60 | pull-request-title: ${{ inputs['pull-request-title'] }}
61 | working-directory: ${{ inputs['working-directory'] }}
62 | process-own-commits: ${{ inputs['process-own-commits'] }}
63 | parallel: ${{ inputs.parallel }}
64 | env:
65 | GH_TOKEN: ${{ github.token }}
66 |
```
--------------------------------------------------------------------------------
/packages/compiler/src/react-router-dictionary-loader.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { createCodeMutation } from "./_base";
2 | import { ModuleId } from "./_const";
3 | import { getModuleExecutionMode, getOrCreateImport } from "./utils";
4 | import { findInvokations } from "./utils/invokations";
5 | import * as t from "@babel/types";
6 | import { getDictionaryPath } from "./_utils";
7 | import { createLocaleImportMap } from "./utils/create-locale-import-map";
8 |
9 | export const reactRouterDictionaryLoaderMutation = createCodeMutation(
10 | (payload) => {
11 | const mode = getModuleExecutionMode(payload.ast, payload.params.rsc);
12 | if (mode === "server") {
13 | return payload;
14 | }
15 |
16 | const invokations = findInvokations(payload.ast, {
17 | moduleName: ModuleId.ReactRouter,
18 | functionName: "loadDictionary",
19 | });
20 |
21 | const allLocales = Array.from(
22 | new Set([payload.params.sourceLocale, ...payload.params.targetLocales]),
23 | );
24 |
25 | for (const invokation of invokations) {
26 | const internalDictionaryLoader = getOrCreateImport(payload.ast, {
27 | moduleName: ModuleId.ReactRouter,
28 | exportedName: "loadDictionary_internal",
29 | });
30 |
31 | // Replace the function identifier with internal version
32 | if (t.isIdentifier(invokation.callee)) {
33 | invokation.callee.name = internalDictionaryLoader.importedName;
34 | }
35 |
36 | const dictionaryPath = getDictionaryPath({
37 | sourceRoot: payload.params.sourceRoot,
38 | lingoDir: payload.params.lingoDir,
39 | relativeFilePath: payload.relativeFilePath,
40 | });
41 |
42 | // Create locale import map object
43 | const localeImportMap = createLocaleImportMap(allLocales, dictionaryPath);
44 |
45 | // Add the locale import map as the second argument
46 | invokation.arguments.push(localeImportMap);
47 | // console.log("invokation modified", JSON.stringify(invokation, null, 2));
48 | }
49 |
50 | // console.log("dictionary-loader", generate(payload.ast).code);
51 |
52 | return payload;
53 | },
54 | );
55 |
```
--------------------------------------------------------------------------------
/packages/compiler/src/jsx-attribute-scopes-export.spec.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect, vi, beforeEach } from "vitest";
2 | import { createPayload, createOutput, defaultParams } from "./_base";
3 | import { jsxAttributeScopesExportMutation } from "./jsx-attribute-scopes-export";
4 |
5 | vi.mock("./lib/lcp", () => {
6 | const instance = {
7 | resetScope: vi.fn().mockReturnThis(),
8 | setScopeType: vi.fn().mockReturnThis(),
9 | setScopeHash: vi.fn().mockReturnThis(),
10 | setScopeContext: vi.fn().mockReturnThis(),
11 | setScopeSkip: vi.fn().mockReturnThis(),
12 | setScopeOverrides: vi.fn().mockReturnThis(),
13 | setScopeContent: vi.fn().mockReturnThis(),
14 | save: vi.fn(),
15 | };
16 | const getInstance = vi.fn(() => instance);
17 | return {
18 | LCP: {
19 | getInstance,
20 | },
21 | __test__: { instance, getInstance },
22 | };
23 | });
24 | describe("jsxAttributeScopesExportMutation", () => {
25 | beforeEach(() => {
26 | // dynamic import avoids ESM mock timing issues
27 | return import("./lib/lcp").then((lcpMod) => {
28 | (lcpMod.LCP.getInstance as any).mockClear();
29 | });
30 | });
31 |
32 | it("collects attribute scopes and saves to LCP", async () => {
33 | const code = `
34 | export default function X() {
35 | return <div data-jsx-attribute-scope="title:scope-1" title="Hello"/>;
36 | }`.trim();
37 | const input = createPayload({
38 | code,
39 | params: defaultParams,
40 | relativeFilePath: "src/App.tsx",
41 | } as any);
42 | const out = jsxAttributeScopesExportMutation(input);
43 | // Not asserting output code as mutation does not change AST; assert side effects
44 | const lcpMod: any = await import("./lib/lcp");
45 | const inst = lcpMod.__test__.instance;
46 | expect(lcpMod.LCP.getInstance).toHaveBeenCalled();
47 | expect(inst.setScopeType).toHaveBeenCalledWith(
48 | "src/App.tsx",
49 | "scope-1",
50 | "attribute",
51 | );
52 | expect(inst.setScopeContent).toHaveBeenCalledWith(
53 | "src/App.tsx",
54 | "scope-1",
55 | "Hello",
56 | );
57 | expect(inst.save).toHaveBeenCalled();
58 | });
59 | });
60 |
```
--------------------------------------------------------------------------------
/.github/workflows/pr-check.yml:
--------------------------------------------------------------------------------
```yaml
1 | name: Check PR
2 |
3 | on:
4 | workflow_dispatch:
5 | pull_request:
6 | types:
7 | - opened
8 | - edited
9 | - synchronize
10 | branches:
11 | - main
12 |
13 | jobs:
14 | check:
15 | runs-on: ubuntu-latest
16 | permissions:
17 | contents: read
18 | steps:
19 | - name: Checkout
20 | uses: actions/checkout@v4
21 | with:
22 | ref: ${{github.event.pull_request.head.sha}}
23 | fetch-depth: 0
24 |
25 | - name: Check for [skip i18n]
26 | run: |
27 | COMMIT_MESSAGE=$(git log -1 --pretty=%B)
28 | if echo "$COMMIT_MESSAGE" | grep -iq '\[skip i18n\]'; then
29 | echo "Skipping i18n checks due to [skip i18n] in commit message."
30 | exit 0
31 | fi
32 |
33 | - name: Use Node.js
34 | uses: actions/setup-node@v2
35 | with:
36 | node-version: 20.12.2
37 |
38 | - name: Install pnpm
39 | uses: pnpm/action-setup@v4
40 | id: pnpm-install
41 | with:
42 | version: 9.12.3
43 | run_install: false
44 |
45 | - name: Configure pnpm cache
46 | id: pnpm-cache
47 | run: echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
48 | - uses: actions/cache@v3
49 | with:
50 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
51 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
52 | restore-keys: |
53 | ${{ runner.os }}-pnpm-store-
54 |
55 | - name: Install deps
56 | run: pnpm install
57 |
58 | - name: Setup
59 | run: |
60 | pnpm turbo telemetry disable
61 |
62 | - name: Configure Turbo cache
63 | uses: dtinth/setup-github-actions-caching-for-turbo@v1
64 |
65 | - name: Check formatting
66 | run: pnpm format:check
67 |
68 | - name: Build
69 | run: pnpm turbo build --force
70 |
71 | - name: Test
72 | run: pnpm turbo test --force
73 |
74 | - name: Require changeset to be present in PR
75 | if: github.event.pull_request.user.login != 'dependabot[bot]'
76 | run: pnpm changeset status --since origin/main
77 |
```
--------------------------------------------------------------------------------
/packages/cli/src/cli/loaders/unlocalizable.ts:
--------------------------------------------------------------------------------
```typescript
1 | import _ from "lodash";
2 | import _isUrl from "is-url";
3 | import { isValid, parseISO } from "date-fns";
4 |
5 | import { ILoader } from "./_types";
6 | import { createLoader } from "./_utils";
7 |
8 | export default function createUnlocalizableLoader(
9 | returnUnlocalizedKeys: boolean = false,
10 | ): ILoader<Record<string, any>, Record<string, any>> {
11 | return createLoader({
12 | async pull(locale, input) {
13 | const unlocalizableKeys = _getUnlocalizableKeys(input);
14 |
15 | const result = _.omitBy(input, (_, key) =>
16 | unlocalizableKeys.includes(key),
17 | );
18 |
19 | if (returnUnlocalizedKeys) {
20 | result.unlocalizable = _.omitBy(
21 | input,
22 | (_, key) => !unlocalizableKeys.includes(key),
23 | );
24 | }
25 |
26 | return result;
27 | },
28 | async push(locale, data, originalInput) {
29 | const unlocalizableKeys = _getUnlocalizableKeys(originalInput);
30 |
31 | const result = _.merge(
32 | {},
33 | data,
34 | _.omitBy(originalInput, (_, key) => !unlocalizableKeys.includes(key)),
35 | );
36 |
37 | return result;
38 | },
39 | });
40 | }
41 |
42 | function _isSystemId(v: string) {
43 | return /^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)[A-Za-z0-9]{22}$/.test(v);
44 | }
45 |
46 | function _isIsoDate(v: string) {
47 | return isValid(parseISO(v));
48 | }
49 |
50 | function _getUnlocalizableKeys(input?: Record<string, any> | null) {
51 | const rules = {
52 | isEmpty: (v: any) => _.isEmpty(v),
53 | isNumber: (v: any) => typeof v === "number" || /^[0-9]+$/.test(v),
54 | isBoolean: (v: any) => _.isBoolean(v),
55 | isIsoDate: (v: any) => _.isString(v) && _isIsoDate(v),
56 | isSystemId: (v: any) => _.isString(v) && _isSystemId(v),
57 | isUrl: (v: any) => _.isString(v) && _isUrl(v),
58 | };
59 |
60 | if (!input) {
61 | return [];
62 | }
63 |
64 | return Object.entries(input)
65 | .filter(([key, value]) => {
66 | for (const [ruleName, rule] of Object.entries(rules)) {
67 | if (rule(value)) {
68 | return true;
69 | }
70 | }
71 | return false;
72 | })
73 | .map(([key, _]) => key);
74 | }
75 |
```