This is page 6 of 16. Use http://codebase.md/lingodotdev/lingo.dev?lines=false&page={x} to view the full context.
# Directory Structure
```
├── .changeset
│ ├── config.json
│ └── README.md
├── .claude
│ ├── agents
│ │ └── code-architect-reviewer.md
│ └── commands
│ ├── analyze-bucket-type.md
│ └── create-bucket-docs.md
├── .editorconfig
├── .github
│ ├── dependabot.yml
│ └── workflows
│ ├── docker.yml
│ ├── lingodotdev.yml
│ ├── pr-check.yml
│ ├── pr-lint.yml
│ └── release.yml
├── .gitignore
├── .husky
│ └── commit-msg
├── .npmrc
├── .prettierignore
├── .prettierrc
├── .vscode
│ ├── extensions.json
│ ├── launch.json
│ └── settings.json
├── action.yml
├── CLAUDE.md
├── CODE_OF_CONDUCT.md
├── commitlint.config.js
├── composer.json
├── content
│ ├── banner.compiler.png
│ ├── banner.dark.png
│ └── banner.launch.png
├── CONTRIBUTING.md
├── DEBUGGING.md
├── demo
│ ├── adonisjs
│ │ ├── .editorconfig
│ │ ├── .env.example
│ │ ├── .gitignore
│ │ ├── ace.js
│ │ ├── adonisrc.ts
│ │ ├── app
│ │ │ ├── exceptions
│ │ │ │ └── handler.ts
│ │ │ └── middleware
│ │ │ └── container_bindings_middleware.ts
│ │ ├── bin
│ │ │ ├── console.ts
│ │ │ ├── server.ts
│ │ │ └── test.ts
│ │ ├── CHANGELOG.md
│ │ ├── config
│ │ │ ├── app.ts
│ │ │ ├── bodyparser.ts
│ │ │ ├── cors.ts
│ │ │ ├── hash.ts
│ │ │ ├── inertia.ts
│ │ │ ├── logger.ts
│ │ │ ├── session.ts
│ │ │ ├── shield.ts
│ │ │ ├── static.ts
│ │ │ └── vite.ts
│ │ ├── eslint.config.js
│ │ ├── inertia
│ │ │ ├── app
│ │ │ │ ├── app.tsx
│ │ │ │ └── ssr.tsx
│ │ │ ├── css
│ │ │ │ └── app.css
│ │ │ ├── lingo
│ │ │ │ ├── dictionary.js
│ │ │ │ └── meta.json
│ │ │ ├── pages
│ │ │ │ ├── errors
│ │ │ │ │ ├── not_found.tsx
│ │ │ │ │ └── server_error.tsx
│ │ │ │ └── home.tsx
│ │ │ └── tsconfig.json
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── resources
│ │ │ └── views
│ │ │ └── inertia_layout.edge
│ │ ├── start
│ │ │ ├── env.ts
│ │ │ ├── kernel.ts
│ │ │ └── routes.ts
│ │ ├── tests
│ │ │ └── bootstrap.ts
│ │ ├── tsconfig.json
│ │ └── vite.config.ts
│ ├── next-app
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── eslint.config.mjs
│ │ ├── next.config.ts
│ │ ├── package.json
│ │ ├── postcss.config.mjs
│ │ ├── public
│ │ │ ├── file.svg
│ │ │ ├── globe.svg
│ │ │ ├── next.svg
│ │ │ ├── vercel.svg
│ │ │ └── window.svg
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── app
│ │ │ │ ├── client-component.tsx
│ │ │ │ ├── favicon.ico
│ │ │ │ ├── globals.css
│ │ │ │ ├── layout.tsx
│ │ │ │ ├── lingo-dot-dev.tsx
│ │ │ │ ├── page.tsx
│ │ │ │ └── test
│ │ │ │ └── page.tsx
│ │ │ ├── components
│ │ │ │ ├── hero-actions.tsx
│ │ │ │ ├── hero-subtitle.tsx
│ │ │ │ ├── hero-title.tsx
│ │ │ │ └── index.ts
│ │ │ └── lingo
│ │ │ ├── dictionary.js
│ │ │ └── meta.json
│ │ └── tsconfig.json
│ ├── react-router-app
│ │ ├── .dockerignore
│ │ ├── .gitignore
│ │ ├── app
│ │ │ ├── app.css
│ │ │ ├── lingo
│ │ │ │ ├── dictionary.js
│ │ │ │ └── meta.json
│ │ │ ├── root.tsx
│ │ │ ├── routes
│ │ │ │ ├── home.tsx
│ │ │ │ └── test.tsx
│ │ │ ├── routes.ts
│ │ │ └── welcome
│ │ │ ├── lingo-dot-dev.tsx
│ │ │ ├── logo-dark.svg
│ │ │ ├── logo-light.svg
│ │ │ └── welcome.tsx
│ │ ├── Dockerfile
│ │ ├── package.json
│ │ ├── public
│ │ │ └── favicon.ico
│ │ ├── react-router.config.ts
│ │ ├── README.md
│ │ ├── tsconfig.json
│ │ └── vite.config.ts
│ └── vite-project
│ ├── .gitignore
│ ├── CHANGELOG.md
│ ├── eslint.config.js
│ ├── index.html
│ ├── package.json
│ ├── public
│ │ └── vite.svg
│ ├── README.md
│ ├── src
│ │ ├── App.css
│ │ ├── App.tsx
│ │ ├── assets
│ │ │ └── react.svg
│ │ ├── components
│ │ │ └── test.tsx
│ │ ├── index.css
│ │ ├── lingo
│ │ │ ├── dictionary.js
│ │ │ └── meta.json
│ │ ├── lingo-dot-dev.tsx
│ │ ├── main.tsx
│ │ └── vite-env.d.ts
│ ├── tsconfig.app.json
│ ├── tsconfig.json
│ ├── tsconfig.node.json
│ └── vite.config.ts
├── Dockerfile
├── i18n.json
├── i18n.lock
├── integrations
│ └── directus
│ ├── .gitignore
│ ├── CHANGELOG.md
│ ├── docker-compose.yml
│ ├── Dockerfile
│ ├── package.json
│ ├── README.md
│ ├── src
│ │ ├── api.ts
│ │ ├── app.ts
│ │ └── index.spec.ts
│ ├── tsconfig.json
│ ├── tsconfig.test.json
│ └── tsup.config.ts
├── ISSUE_TEMPLATE.md
├── legacy
│ ├── cli
│ │ ├── bin
│ │ │ └── cli.mjs
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ └── readme.md
│ └── sdk
│ ├── CHANGELOG.md
│ ├── index.d.ts
│ ├── index.js
│ ├── package.json
│ └── README.md
├── LICENSE.md
├── mcp.md
├── package.json
├── packages
│ ├── cli
│ │ ├── assets
│ │ │ ├── failure.mp3
│ │ │ └── success.mp3
│ │ ├── bin
│ │ │ └── cli.mjs
│ │ ├── CHANGELOG.md
│ │ ├── demo
│ │ │ ├── android
│ │ │ │ ├── en
│ │ │ │ │ └── example.xml
│ │ │ │ ├── es
│ │ │ │ │ └── example.xml
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── csv
│ │ │ │ ├── example.csv
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── demo.spec.ts
│ │ │ ├── ejs
│ │ │ │ ├── en
│ │ │ │ │ └── example.ejs
│ │ │ │ ├── es
│ │ │ │ │ └── example.ejs
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── flutter
│ │ │ │ ├── en
│ │ │ │ │ └── example.arb
│ │ │ │ ├── es
│ │ │ │ │ └── example.arb
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── html
│ │ │ │ ├── en
│ │ │ │ │ └── example.html
│ │ │ │ ├── es
│ │ │ │ │ └── example.html
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── json
│ │ │ │ ├── en
│ │ │ │ │ └── example.json
│ │ │ │ ├── es
│ │ │ │ │ └── example.json
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── json-dictionary
│ │ │ │ ├── example.json
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── json5
│ │ │ │ ├── en
│ │ │ │ │ └── example.json5
│ │ │ │ ├── es
│ │ │ │ │ └── example.json5
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── jsonc
│ │ │ │ ├── en
│ │ │ │ │ └── example.jsonc
│ │ │ │ ├── es
│ │ │ │ │ └── example.jsonc
│ │ │ │ ├── i18n.json
│ │ │ │ ├── i18n.lock
│ │ │ │ └── ru
│ │ │ │ └── example.jsonc
│ │ │ ├── markdoc
│ │ │ │ ├── en
│ │ │ │ │ └── example.markdoc
│ │ │ │ ├── es
│ │ │ │ │ └── example.markdoc
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── markdown
│ │ │ │ ├── en
│ │ │ │ │ └── example.md
│ │ │ │ ├── es
│ │ │ │ │ └── example.md
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── mdx
│ │ │ │ ├── en
│ │ │ │ │ └── example.mdx
│ │ │ │ ├── es
│ │ │ │ │ └── example.mdx
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── php
│ │ │ │ ├── en
│ │ │ │ │ └── example.php
│ │ │ │ ├── es
│ │ │ │ │ └── example.php
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── po
│ │ │ │ ├── en
│ │ │ │ │ └── example.po
│ │ │ │ ├── es
│ │ │ │ │ └── example.po
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── properties
│ │ │ │ ├── en
│ │ │ │ │ └── example.properties
│ │ │ │ ├── es
│ │ │ │ │ └── example.properties
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── run_i18n.sh
│ │ │ ├── srt
│ │ │ │ ├── en
│ │ │ │ │ └── example.srt
│ │ │ │ ├── es
│ │ │ │ │ └── example.srt
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── txt
│ │ │ │ ├── en
│ │ │ │ │ └── example.txt
│ │ │ │ ├── es
│ │ │ │ │ └── example.txt
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── typescript
│ │ │ │ ├── en
│ │ │ │ │ └── example.ts
│ │ │ │ ├── es
│ │ │ │ │ └── example.ts
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── vtt
│ │ │ │ ├── en
│ │ │ │ │ └── example.vtt
│ │ │ │ ├── es
│ │ │ │ │ └── example.vtt
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── vue-json
│ │ │ │ ├── example.vue
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── xcode-strings
│ │ │ │ ├── en
│ │ │ │ │ └── example.strings
│ │ │ │ ├── es
│ │ │ │ │ └── example.strings
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── xcode-stringsdict
│ │ │ │ ├── en
│ │ │ │ │ └── example.stringsdict
│ │ │ │ ├── es
│ │ │ │ │ └── example.stringsdict
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── xcode-xcstrings
│ │ │ │ ├── example.xcstrings
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── xcode-xcstrings-v2
│ │ │ │ ├── complex-example.xcstrings
│ │ │ │ ├── example.xcstrings
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── xliff
│ │ │ │ ├── en
│ │ │ │ │ ├── example-v1.2.xliff
│ │ │ │ │ └── example-v2.xliff
│ │ │ │ ├── es
│ │ │ │ │ ├── example-v1.2.xliff
│ │ │ │ │ ├── example-v2.xliff
│ │ │ │ │ └── example.xliff
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── xml
│ │ │ │ ├── en
│ │ │ │ │ └── example.xml
│ │ │ │ ├── es
│ │ │ │ │ └── example.xml
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── yaml
│ │ │ │ ├── en
│ │ │ │ │ └── example.yml
│ │ │ │ ├── es
│ │ │ │ │ └── example.yml
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ └── yaml-root-key
│ │ │ ├── en
│ │ │ │ └── example.yml
│ │ │ ├── es
│ │ │ │ └── example.yml
│ │ │ ├── i18n.json
│ │ │ └── i18n.lock
│ │ ├── i18n.json
│ │ ├── i18n.lock
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── cli
│ │ │ │ ├── cmd
│ │ │ │ │ ├── auth.ts
│ │ │ │ │ ├── ci
│ │ │ │ │ │ ├── flows
│ │ │ │ │ │ │ ├── _base.ts
│ │ │ │ │ │ │ ├── in-branch.ts
│ │ │ │ │ │ │ └── pull-request.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── platforms
│ │ │ │ │ │ ├── _base.ts
│ │ │ │ │ │ ├── bitbucket.ts
│ │ │ │ │ │ ├── github.ts
│ │ │ │ │ │ ├── gitlab.ts
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── cleanup.ts
│ │ │ │ │ ├── config
│ │ │ │ │ │ ├── get.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── set.ts
│ │ │ │ │ │ └── unset.ts
│ │ │ │ │ ├── i18n.ts
│ │ │ │ │ ├── init.ts
│ │ │ │ │ ├── lockfile.ts
│ │ │ │ │ ├── login.ts
│ │ │ │ │ ├── logout.ts
│ │ │ │ │ ├── may-the-fourth.ts
│ │ │ │ │ ├── mcp.ts
│ │ │ │ │ ├── purge.ts
│ │ │ │ │ ├── run
│ │ │ │ │ │ ├── _const.ts
│ │ │ │ │ │ ├── _types.ts
│ │ │ │ │ │ ├── _utils.ts
│ │ │ │ │ │ ├── execute.spec.ts
│ │ │ │ │ │ ├── execute.ts
│ │ │ │ │ │ ├── frozen.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── plan.ts
│ │ │ │ │ │ ├── setup.ts
│ │ │ │ │ │ └── watch.ts
│ │ │ │ │ ├── show
│ │ │ │ │ │ ├── _shared-key-command.ts
│ │ │ │ │ │ ├── config.ts
│ │ │ │ │ │ ├── files.ts
│ │ │ │ │ │ ├── ignored-keys.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── locale.ts
│ │ │ │ │ │ └── locked-keys.ts
│ │ │ │ │ └── status.ts
│ │ │ │ ├── constants.ts
│ │ │ │ ├── index.spec.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── loaders
│ │ │ │ │ ├── _types.ts
│ │ │ │ │ ├── _utils.ts
│ │ │ │ │ ├── android.spec.ts
│ │ │ │ │ ├── android.ts
│ │ │ │ │ ├── csv.spec.ts
│ │ │ │ │ ├── csv.ts
│ │ │ │ │ ├── dato
│ │ │ │ │ │ ├── _base.ts
│ │ │ │ │ │ ├── _utils.ts
│ │ │ │ │ │ ├── api.ts
│ │ │ │ │ │ ├── extract.ts
│ │ │ │ │ │ ├── filter.ts
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── ejs.spec.ts
│ │ │ │ │ ├── ejs.ts
│ │ │ │ │ ├── ensure-key-order.spec.ts
│ │ │ │ │ ├── ensure-key-order.ts
│ │ │ │ │ ├── flat.spec.ts
│ │ │ │ │ ├── flat.ts
│ │ │ │ │ ├── flutter.spec.ts
│ │ │ │ │ ├── flutter.ts
│ │ │ │ │ ├── formatters
│ │ │ │ │ │ ├── _base.ts
│ │ │ │ │ │ ├── biome.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── prettier.ts
│ │ │ │ │ ├── html.ts
│ │ │ │ │ ├── icu-safety.spec.ts
│ │ │ │ │ ├── ignored-keys-buckets.spec.ts
│ │ │ │ │ ├── ignored-keys.spec.ts
│ │ │ │ │ ├── ignored-keys.ts
│ │ │ │ │ ├── index.spec.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── inject-locale.spec.ts
│ │ │ │ │ ├── inject-locale.ts
│ │ │ │ │ ├── json-dictionary.spec.ts
│ │ │ │ │ ├── json-dictionary.ts
│ │ │ │ │ ├── json-sorting.test.ts
│ │ │ │ │ ├── json-sorting.ts
│ │ │ │ │ ├── json.ts
│ │ │ │ │ ├── json5.spec.ts
│ │ │ │ │ ├── json5.ts
│ │ │ │ │ ├── jsonc.spec.ts
│ │ │ │ │ ├── jsonc.ts
│ │ │ │ │ ├── locked-keys.spec.ts
│ │ │ │ │ ├── locked-keys.ts
│ │ │ │ │ ├── locked-patterns.spec.ts
│ │ │ │ │ ├── locked-patterns.ts
│ │ │ │ │ ├── markdoc.spec.ts
│ │ │ │ │ ├── markdoc.ts
│ │ │ │ │ ├── markdown.ts
│ │ │ │ │ ├── mdx.spec.ts
│ │ │ │ │ ├── mdx.ts
│ │ │ │ │ ├── mdx2
│ │ │ │ │ │ ├── _types.ts
│ │ │ │ │ │ ├── _utils.ts
│ │ │ │ │ │ ├── code-placeholder.spec.ts
│ │ │ │ │ │ ├── code-placeholder.ts
│ │ │ │ │ │ ├── frontmatter-split.spec.ts
│ │ │ │ │ │ ├── frontmatter-split.ts
│ │ │ │ │ │ ├── localizable-document.spec.ts
│ │ │ │ │ │ ├── localizable-document.ts
│ │ │ │ │ │ ├── section-split.spec.ts
│ │ │ │ │ │ ├── section-split.ts
│ │ │ │ │ │ └── sections-split-2.ts
│ │ │ │ │ ├── passthrough.ts
│ │ │ │ │ ├── php.ts
│ │ │ │ │ ├── plutil-json-loader.ts
│ │ │ │ │ ├── po
│ │ │ │ │ │ ├── _types.ts
│ │ │ │ │ │ ├── index.spec.ts
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── properties.ts
│ │ │ │ │ ├── root-key.ts
│ │ │ │ │ ├── srt.ts
│ │ │ │ │ ├── sync.ts
│ │ │ │ │ ├── text-file.ts
│ │ │ │ │ ├── txt.ts
│ │ │ │ │ ├── typescript
│ │ │ │ │ │ ├── cjs-interop.ts
│ │ │ │ │ │ ├── index.spec.ts
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── unlocalizable.spec.ts
│ │ │ │ │ ├── unlocalizable.ts
│ │ │ │ │ ├── variable
│ │ │ │ │ │ ├── index.spec.ts
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── vtt.ts
│ │ │ │ │ ├── vue-json.ts
│ │ │ │ │ ├── xcode-strings
│ │ │ │ │ │ ├── escape.ts
│ │ │ │ │ │ ├── parser.ts
│ │ │ │ │ │ ├── tokenizer.ts
│ │ │ │ │ │ └── types.ts
│ │ │ │ │ ├── xcode-strings.spec.ts
│ │ │ │ │ ├── xcode-strings.ts
│ │ │ │ │ ├── xcode-stringsdict.ts
│ │ │ │ │ ├── xcode-xcstrings-icu.spec.ts
│ │ │ │ │ ├── xcode-xcstrings-icu.ts
│ │ │ │ │ ├── xcode-xcstrings-lock-compatibility.spec.ts
│ │ │ │ │ ├── xcode-xcstrings-v2-loader.ts
│ │ │ │ │ ├── xcode-xcstrings.spec.ts
│ │ │ │ │ ├── xcode-xcstrings.ts
│ │ │ │ │ ├── xliff.spec.ts
│ │ │ │ │ ├── xliff.ts
│ │ │ │ │ ├── xml.ts
│ │ │ │ │ └── yaml.ts
│ │ │ │ ├── localizer
│ │ │ │ │ ├── _types.ts
│ │ │ │ │ ├── explicit.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── lingodotdev.ts
│ │ │ │ ├── processor
│ │ │ │ │ ├── _base.ts
│ │ │ │ │ ├── basic.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── lingo.ts
│ │ │ │ └── utils
│ │ │ │ ├── auth.ts
│ │ │ │ ├── buckets.spec.ts
│ │ │ │ ├── buckets.ts
│ │ │ │ ├── cache.ts
│ │ │ │ ├── cloudflare-status.ts
│ │ │ │ ├── config.ts
│ │ │ │ ├── delta.spec.ts
│ │ │ │ ├── delta.ts
│ │ │ │ ├── ensure-patterns.ts
│ │ │ │ ├── errors.ts
│ │ │ │ ├── exec.spec.ts
│ │ │ │ ├── exec.ts
│ │ │ │ ├── exit-gracefully.spec.ts
│ │ │ │ ├── exit-gracefully.ts
│ │ │ │ ├── exp-backoff.ts
│ │ │ │ ├── find-locale-paths.spec.ts
│ │ │ │ ├── find-locale-paths.ts
│ │ │ │ ├── fs.ts
│ │ │ │ ├── init-ci-cd.ts
│ │ │ │ ├── key-matching.spec.ts
│ │ │ │ ├── key-matching.ts
│ │ │ │ ├── lockfile.ts
│ │ │ │ ├── md5.ts
│ │ │ │ ├── observability.ts
│ │ │ │ ├── plutil-formatter.spec.ts
│ │ │ │ ├── plutil-formatter.ts
│ │ │ │ ├── settings.ts
│ │ │ │ ├── ui.ts
│ │ │ │ └── update-gitignore.ts
│ │ │ ├── compiler
│ │ │ │ └── index.ts
│ │ │ ├── locale-codes
│ │ │ │ └── index.ts
│ │ │ ├── react
│ │ │ │ ├── client.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── react-router.ts
│ │ │ │ └── rsc.ts
│ │ │ ├── sdk
│ │ │ │ └── index.ts
│ │ │ └── spec
│ │ │ └── index.ts
│ │ ├── tests
│ │ │ └── mock-storage.ts
│ │ ├── troubleshooting.md
│ │ ├── tsconfig.json
│ │ ├── tsconfig.test.json
│ │ ├── tsup.config.ts
│ │ ├── types
│ │ │ ├── vtt.d.ts
│ │ │ └── xliff.d.ts
│ │ ├── vitest.config.ts
│ │ └── WATCH_MODE.md
│ ├── compiler
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── _base.ts
│ │ │ ├── _const.ts
│ │ │ ├── _loader-utils.spec.ts
│ │ │ ├── _loader-utils.ts
│ │ │ ├── _utils.spec.ts
│ │ │ ├── _utils.ts
│ │ │ ├── client-dictionary-loader.ts
│ │ │ ├── i18n-directive.spec.ts
│ │ │ ├── i18n-directive.ts
│ │ │ ├── index.spec.ts
│ │ │ ├── index.ts
│ │ │ ├── jsx-attribute-flag.spec.ts
│ │ │ ├── jsx-attribute-flag.ts
│ │ │ ├── jsx-attribute-scope-inject.spec.ts
│ │ │ ├── jsx-attribute-scope-inject.ts
│ │ │ ├── jsx-attribute-scopes-export.spec.ts
│ │ │ ├── jsx-attribute-scopes-export.ts
│ │ │ ├── jsx-attribute.spec.ts
│ │ │ ├── jsx-attribute.ts
│ │ │ ├── jsx-fragment.spec.ts
│ │ │ ├── jsx-fragment.ts
│ │ │ ├── jsx-html-lang.spec.ts
│ │ │ ├── jsx-html-lang.ts
│ │ │ ├── jsx-provider.spec.ts
│ │ │ ├── jsx-provider.ts
│ │ │ ├── jsx-remove-attributes.spec.ts
│ │ │ ├── jsx-remove-attributes.ts
│ │ │ ├── jsx-root-flag.spec.ts
│ │ │ ├── jsx-root-flag.ts
│ │ │ ├── jsx-scope-flag.spec.ts
│ │ │ ├── jsx-scope-flag.ts
│ │ │ ├── jsx-scope-inject.spec.ts
│ │ │ ├── jsx-scope-inject.ts
│ │ │ ├── jsx-scopes-export.spec.ts
│ │ │ ├── jsx-scopes-export.ts
│ │ │ ├── lib
│ │ │ │ └── lcp
│ │ │ │ ├── api
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── prompt.spec.ts
│ │ │ │ │ ├── prompt.ts
│ │ │ │ │ ├── provider-details.spec.ts
│ │ │ │ │ ├── provider-details.ts
│ │ │ │ │ ├── shots.ts
│ │ │ │ │ ├── xml2obj.spec.ts
│ │ │ │ │ └── xml2obj.ts
│ │ │ │ ├── api.spec.ts
│ │ │ │ ├── cache.spec.ts
│ │ │ │ ├── cache.ts
│ │ │ │ ├── index.spec.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── schema.ts
│ │ │ │ ├── server.spec.ts
│ │ │ │ └── server.ts
│ │ │ ├── lingo-turbopack-loader.ts
│ │ │ ├── react-router-dictionary-loader.ts
│ │ │ ├── rsc-dictionary-loader.ts
│ │ │ └── utils
│ │ │ ├── ast-key.spec.ts
│ │ │ ├── ast-key.ts
│ │ │ ├── create-locale-import-map.spec.ts
│ │ │ ├── create-locale-import-map.ts
│ │ │ ├── env.spec.ts
│ │ │ ├── env.ts
│ │ │ ├── hash.spec.ts
│ │ │ ├── hash.ts
│ │ │ ├── index.spec.ts
│ │ │ ├── index.ts
│ │ │ ├── invokations.spec.ts
│ │ │ ├── invokations.ts
│ │ │ ├── jsx-attribute-scope.ts
│ │ │ ├── jsx-attribute.spec.ts
│ │ │ ├── jsx-attribute.ts
│ │ │ ├── jsx-content-whitespace.spec.ts
│ │ │ ├── jsx-content.spec.ts
│ │ │ ├── jsx-content.ts
│ │ │ ├── jsx-element.spec.ts
│ │ │ ├── jsx-element.ts
│ │ │ ├── jsx-expressions.test.ts
│ │ │ ├── jsx-expressions.ts
│ │ │ ├── jsx-functions.spec.ts
│ │ │ ├── jsx-functions.ts
│ │ │ ├── jsx-scope.spec.ts
│ │ │ ├── jsx-scope.ts
│ │ │ ├── jsx-variables.spec.ts
│ │ │ ├── jsx-variables.ts
│ │ │ ├── llm-api-key.ts
│ │ │ ├── llm-api-keys.spec.ts
│ │ │ ├── locales.spec.ts
│ │ │ ├── locales.ts
│ │ │ ├── module-params.spec.ts
│ │ │ ├── module-params.ts
│ │ │ ├── observability.spec.ts
│ │ │ ├── observability.ts
│ │ │ ├── rc.spec.ts
│ │ │ └── rc.ts
│ │ ├── tsconfig.json
│ │ ├── tsup.config.ts
│ │ └── vitest.config.ts
│ ├── locales
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── constants.ts
│ │ │ ├── index.ts
│ │ │ ├── names
│ │ │ │ ├── index.spec.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── integration.spec.ts
│ │ │ │ └── loader.ts
│ │ │ ├── parser.spec.ts
│ │ │ ├── parser.ts
│ │ │ ├── types.ts
│ │ │ ├── validation.spec.ts
│ │ │ └── validation.ts
│ │ ├── tsconfig.json
│ │ └── tsup.config.ts
│ ├── react
│ │ ├── build.config.ts
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── client
│ │ │ │ ├── attribute-component.spec.tsx
│ │ │ │ ├── attribute-component.tsx
│ │ │ │ ├── component.lingo-component.spec.tsx
│ │ │ │ ├── component.spec.tsx
│ │ │ │ ├── component.tsx
│ │ │ │ ├── context.spec.tsx
│ │ │ │ ├── context.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── loader.spec.ts
│ │ │ │ ├── loader.ts
│ │ │ │ ├── locale-switcher.spec.tsx
│ │ │ │ ├── locale-switcher.tsx
│ │ │ │ ├── locale.spec.ts
│ │ │ │ ├── locale.ts
│ │ │ │ ├── provider.spec.tsx
│ │ │ │ ├── provider.tsx
│ │ │ │ ├── utils.spec.ts
│ │ │ │ └── utils.ts
│ │ │ ├── core
│ │ │ │ ├── attribute-component.spec.tsx
│ │ │ │ ├── attribute-component.tsx
│ │ │ │ ├── component.spec.tsx
│ │ │ │ ├── component.tsx
│ │ │ │ ├── const.ts
│ │ │ │ ├── get-dictionary.spec.ts
│ │ │ │ ├── get-dictionary.ts
│ │ │ │ └── index.ts
│ │ │ ├── react-router
│ │ │ │ ├── index.ts
│ │ │ │ ├── loader.spec.ts
│ │ │ │ └── loader.ts
│ │ │ ├── rsc
│ │ │ │ ├── attribute-component.tsx
│ │ │ │ ├── component.lingo-component.spec.tsx
│ │ │ │ ├── component.spec.tsx
│ │ │ │ ├── component.tsx
│ │ │ │ ├── index.ts
│ │ │ │ ├── loader.spec.ts
│ │ │ │ ├── loader.ts
│ │ │ │ ├── provider.spec.tsx
│ │ │ │ ├── provider.tsx
│ │ │ │ ├── utils.spec.ts
│ │ │ │ └── utils.ts
│ │ │ └── test
│ │ │ └── setup.ts
│ │ ├── tsconfig.json
│ │ └── vitest.config.ts
│ ├── sdk
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── abort-controller.specs.ts
│ │ │ ├── index.spec.ts
│ │ │ └── index.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.test.json
│ │ └── tsup.config.ts
│ └── spec
│ ├── CHANGELOG.md
│ ├── package.json
│ ├── README.md
│ ├── src
│ │ ├── config.spec.ts
│ │ ├── config.ts
│ │ ├── formats.ts
│ │ ├── index.spec.ts
│ │ ├── index.ts
│ │ ├── json-schema.ts
│ │ ├── locales.spec.ts
│ │ └── locales.ts
│ ├── tsconfig.json
│ ├── tsconfig.test.json
│ └── tsup.config.ts
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── readme
│ ├── ar.md
│ ├── bn.md
│ ├── de.md
│ ├── en.md
│ ├── es.md
│ ├── fa.md
│ ├── fr.md
│ ├── he.md
│ ├── hi.md
│ ├── it.md
│ ├── ja.md
│ ├── ko.md
│ ├── pl.md
│ ├── pt-BR.md
│ ├── ru.md
│ ├── tr.md
│ ├── uk-UA.md
│ └── zh-Hans.md
├── readme.md
├── scripts
│ ├── docs
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── generate-cli-docs.ts
│ │ │ ├── generate-config-docs.ts
│ │ │ ├── json-schema
│ │ │ │ ├── markdown-renderer.test.ts
│ │ │ │ ├── markdown-renderer.ts
│ │ │ │ ├── parser.test.ts
│ │ │ │ ├── parser.ts
│ │ │ │ └── types.ts
│ │ │ ├── utils.test.ts
│ │ │ └── utils.ts
│ │ ├── tsconfig.json
│ │ └── vitest.config.ts
│ └── packagist-publish.php
└── turbo.json
```
# Files
--------------------------------------------------------------------------------
/packages/cli/src/cli/loaders/xcode-xcstrings.ts:
--------------------------------------------------------------------------------
```typescript
import { ILoader } from "./_types";
import { createLoader } from "./_utils";
import _ from "lodash";
export default function createXcodeXcstringsLoader(
defaultLocale: string,
): ILoader<Record<string, any>, Record<string, any>> {
return createLoader({
async pull(locale, input, initCtx) {
const resultData: Record<string, any> = {};
const isSourceLanguage = locale === defaultLocale;
for (const [translationKey, _translationEntity] of Object.entries(
(input as any).strings,
)) {
const rootTranslationEntity = _translationEntity as any;
if (rootTranslationEntity.shouldTranslate === false) {
continue;
}
const langTranslationEntity =
rootTranslationEntity?.localizations?.[locale];
if (langTranslationEntity) {
if ("stringUnit" in langTranslationEntity) {
resultData[translationKey] = langTranslationEntity.stringUnit.value;
} else if ("variations" in langTranslationEntity) {
if ("plural" in langTranslationEntity.variations) {
resultData[translationKey] = {};
const pluralForms = langTranslationEntity.variations.plural;
for (const form in pluralForms) {
if (pluralForms[form]?.stringUnit?.value) {
resultData[translationKey][form] =
pluralForms[form].stringUnit.value;
}
}
}
}
} else if (isSourceLanguage) {
resultData[translationKey] = translationKey;
}
}
return resultData;
},
async push(locale, payload, originalInput) {
const langDataToMerge: any = {};
langDataToMerge.strings = {};
const input = _.cloneDeep(originalInput) || {
sourceLanguage: locale,
strings: {},
};
for (const [key, value] of Object.entries(payload)) {
if (value === null || value === undefined) {
continue;
}
const hasDoNotTranslateFlag =
originalInput &&
(originalInput as any).strings &&
(originalInput as any).strings[key] &&
(originalInput as any).strings[key].shouldTranslate === false;
if (typeof value === "string") {
langDataToMerge.strings[key] = {
extractionState: originalInput?.strings?.[key]?.extractionState,
localizations: {
[locale]: {
stringUnit: {
state: "translated",
value,
},
},
},
};
if (hasDoNotTranslateFlag) {
langDataToMerge.strings[key].shouldTranslate = false;
}
} else {
const updatedVariations: any = {};
for (const form in value) {
updatedVariations[form] = {
stringUnit: {
state: "translated",
value: value[form],
},
};
}
langDataToMerge.strings[key] = {
extractionState: "manual",
localizations: {
[locale]: {
variations: {
plural: updatedVariations,
},
},
},
};
if (hasDoNotTranslateFlag) {
langDataToMerge.strings[key].shouldTranslate = false;
}
}
}
const originalInputWithoutLocale = originalInput
? _removeLocale(originalInput, locale)
: {};
const result = _.merge({}, originalInputWithoutLocale, langDataToMerge);
return result;
},
async pullHints(originalInput) {
if (!originalInput || !originalInput.strings) {
return {};
}
const hints: Record<string, any> = {};
for (const [translationKey, translationEntity] of Object.entries(
originalInput.strings,
)) {
const entity = translationEntity as any;
// Extract comment field if it exists
if (entity.comment && typeof entity.comment === "string") {
hints[translationKey] = { hint: entity.comment };
}
// For plural forms, we might want to include the base comment for all variants
if (entity.localizations) {
for (const [locale, localization] of Object.entries(
entity.localizations,
)) {
if ((localization as any).variations?.plural) {
const pluralForms = (localization as any).variations.plural;
for (const form in pluralForms) {
const pluralKey = `${translationKey}/${form}`;
if (entity.comment && typeof entity.comment === "string") {
hints[pluralKey] = { hint: entity.comment };
}
}
}
}
}
}
return hints;
},
});
}
export function _removeLocale(input: Record<string, any>, locale: string) {
const { strings } = input;
const newStrings = _.cloneDeep(strings);
for (const [key, value] of Object.entries(newStrings)) {
if ((value as any).localizations?.[locale]) {
delete (value as any).localizations[locale];
}
}
return { ...input, strings: newStrings };
}
```
--------------------------------------------------------------------------------
/packages/locales/src/parser.ts:
--------------------------------------------------------------------------------
```typescript
import type { LocaleComponents, LocaleDelimiter, ParseResult } from "./types";
import { LOCALE_REGEX } from "./constants";
/**
* Normalizes the case of locale components before parsing
*
* @param locale - The locale string to normalize
* @returns The normalized locale string
*
* @example
* normalizeLocaleCase("EN-US") // "en-US"
* normalizeLocaleCase("en-us") // "en-US"
* normalizeLocaleCase("zh-hans-cn") // "zh-Hans-CN"
*/
function normalizeLocaleCase(locale: string): string {
// Split by either hyphen or underscore
const parts = locale.split(/[-_]/);
if (parts.length === 1) {
// Language only: normalize to lowercase
return parts[0].toLowerCase();
}
if (parts.length === 2) {
// Language-region: normalize language to lowercase, region to uppercase
const language = parts[0].toLowerCase();
const region = parts[1].toUpperCase();
return `${language}-${region}`;
}
if (parts.length === 3) {
// Language-script-region: normalize language to lowercase, preserve script case, region to uppercase
const language = parts[0].toLowerCase();
const script = parts[1]; // Preserve original case as-is
const region = parts[2].toUpperCase();
return `${language}-${script}-${region}`;
}
// For any other number of parts, return as-is
return locale;
}
/**
* Breaks apart a locale string into its components
*
* @param locale - The locale string to parse
* @returns LocaleComponents object with language, script, and region
*
* @example
* ```typescript
* parseLocale("en-US"); // { language: "en", region: "US" }
* parseLocale("en_US"); // { language: "en", region: "US" }
* parseLocale("zh-Hans-CN"); // { language: "zh", script: "Hans", region: "CN" }
* parseLocale("zh_Hans_CN"); // { language: "zh", script: "Hans", region: "CN" }
* parseLocale("es"); // { language: "es" }
* parseLocale("sr-Cyrl-RS"); // { language: "sr", script: "Cyrl", region: "RS" }
* ```
*/
export function parseLocale(locale: string): LocaleComponents {
if (typeof locale !== "string") {
throw new Error("Locale must be a string");
}
if (!locale.trim()) {
throw new Error("Locale cannot be empty");
}
// Normalize case before parsing:
// - Language: convert to lowercase (e.g., "EN" -> "en")
// - Script: preserve case (e.g., "Hans", "hans" -> "Hans")
// - Region: convert to uppercase (e.g., "us" -> "US")
const normalizedLocale = normalizeLocaleCase(locale);
const match = normalizedLocale.match(LOCALE_REGEX);
if (!match) {
throw new Error(`Invalid locale format: ${locale}`);
}
const [, language, script, region] = match;
const components: LocaleComponents = {
language: language.toLowerCase(),
};
// Add script if present
if (script) {
components.script = script;
}
// Add region if present
if (region) {
components.region = region.toUpperCase();
}
return components;
}
/**
* Parses a locale string and returns detailed information about the parsing result
*
* @param locale - The locale string to parse
* @returns ParseResult with components, delimiter, and validation info
*/
export function parseLocaleWithDetails(locale: string): ParseResult {
try {
const components = parseLocale(locale);
// Determine the delimiter used
let delimiter: LocaleDelimiter | null = null;
if (locale.includes("-")) {
delimiter = "-";
} else if (locale.includes("_")) {
delimiter = "_";
}
return {
components,
delimiter,
isValid: true,
};
} catch (error) {
return {
components: { language: "" },
delimiter: null,
isValid: false,
error: error instanceof Error ? error.message : "Unknown parsing error",
};
}
}
/**
* Extracts just the language code from a locale string
*
* @param locale - The locale string to parse
* @returns The language code
*
* @example
* ```typescript
* getLanguageCode("en-US"); // "en"
* getLanguageCode("zh-Hans-CN"); // "zh"
* getLanguageCode("es-MX"); // "es"
* getLanguageCode("fr_CA"); // "fr"
* ```
*/
export function getLanguageCode(locale: string): string {
return parseLocale(locale).language;
}
/**
* Extracts the script code from a locale string
*
* @param locale - The locale string to parse
* @returns The script code or null if not present
*
* @example
* ```typescript
* getScriptCode("zh-Hans-CN"); // "Hans"
* getScriptCode("zh-Hant-TW"); // "Hant"
* getScriptCode("sr-Cyrl-RS"); // "Cyrl"
* getScriptCode("en-US"); // null
* getScriptCode("es"); // null
* ```
*/
export function getScriptCode(locale: string): string | null {
const components = parseLocale(locale);
return components.script || null;
}
/**
* Extracts the region/country code from a locale string
*
* @param locale - The locale string to parse
* @returns The region code or null if not present
*
* @example
* ```typescript
* getRegionCode("en-US"); // "US"
* getRegionCode("zh-Hans-CN"); // "CN"
* getRegionCode("es"); // null
* getRegionCode("fr_CA"); // "CA"
* ```
*/
export function getRegionCode(locale: string): string | null {
const components = parseLocale(locale);
return components.region || null;
}
```
--------------------------------------------------------------------------------
/packages/cli/src/cli/cmd/cleanup.ts:
--------------------------------------------------------------------------------
```typescript
import { I18nConfig, resolveOverriddenLocale } from "@lingo.dev/_spec";
import { Command } from "interactive-commander";
import _ from "lodash";
import { getConfig } from "../utils/config";
import { CLIError } from "../utils/errors";
import Ora from "ora";
import createBucketLoader from "../loaders";
import { getBuckets } from "../utils/buckets";
export default new Command()
.command("cleanup")
.description(
"Remove translation keys from target locales that no longer exist in the source locale",
)
.helpOption("-h, --help", "Show help")
.option(
"--locale <locale>",
"Limit cleanup to a specific target locale from i18n.json. Defaults to all configured target locales",
)
.option(
"--bucket <bucket>",
"Limit cleanup to a specific bucket type defined under `buckets` in i18n.json",
)
.option(
"--dry-run",
"Preview which keys would be deleted without making any changes",
)
.option(
"--verbose",
"Print detailed output showing the specific keys to be removed for each locale",
)
.action(async function (options) {
const ora = Ora();
const results: any = [];
try {
ora.start("Loading configuration...");
const i18nConfig = getConfig();
validateConfig(i18nConfig);
ora.succeed("Configuration loaded");
let buckets = getBuckets(i18nConfig!);
if (options.bucket) {
buckets = buckets.filter(
(bucket: any) => bucket.type === options.bucket,
);
}
const targetLocales = options.locale
? [options.locale]
: i18nConfig!.locale.targets;
// Process each bucket
for (const bucket of buckets) {
console.log();
ora.info(`Processing bucket: ${bucket.type}`);
for (const bucketConfig of bucket.paths) {
const sourceLocale = resolveOverriddenLocale(
i18nConfig!.locale.source,
bucketConfig.delimiter,
);
const bucketOra = Ora({ indent: 2 }).info(
`Processing path: ${bucketConfig.pathPattern}`,
);
const bucketLoader = createBucketLoader(
bucket.type,
bucketConfig.pathPattern,
{
defaultLocale: sourceLocale,
formatter: i18nConfig!.formatter,
},
bucket.lockedKeys,
bucket.lockedPatterns,
bucket.ignoredKeys,
);
bucketLoader.setDefaultLocale(sourceLocale);
// Load source data
const sourceData = await bucketLoader.pull(sourceLocale);
const sourceKeys = Object.keys(sourceData);
for (const _targetLocale of targetLocales) {
const targetLocale = resolveOverriddenLocale(
_targetLocale,
bucketConfig.delimiter,
);
try {
const targetData = await bucketLoader.pull(targetLocale);
const targetKeys = Object.keys(targetData);
const keysToRemove = _.difference(targetKeys, sourceKeys);
if (keysToRemove.length === 0) {
bucketOra.succeed(`[${targetLocale}] No keys to remove`);
continue;
}
if (options.verbose) {
bucketOra.info(
`[${targetLocale}] Keys to remove: ${JSON.stringify(
keysToRemove,
null,
2,
)}`,
);
}
if (!options.dryRun) {
const cleanedData = _.pick(targetData, sourceKeys);
await bucketLoader.push(targetLocale, cleanedData);
bucketOra.succeed(
`[${targetLocale}] Removed ${keysToRemove.length} keys`,
);
} else {
bucketOra.succeed(
`[${targetLocale}] Would remove ${keysToRemove.length} keys (dry run)`,
);
}
} catch (error: any) {
bucketOra.fail(
`[${targetLocale}] Failed to cleanup: ${error.message}`,
);
results.push({
step: `Cleanup ${bucket.type}/${bucketConfig} for ${targetLocale}`,
status: "Failed",
error: error.message,
});
}
}
}
}
console.log();
ora.succeed("Cleanup completed!");
} catch (error: any) {
ora.fail(error.message);
process.exit(1);
} finally {
displaySummary(results);
}
});
function validateConfig(i18nConfig: I18nConfig | null) {
if (!i18nConfig) {
throw new CLIError({
message:
"i18n.json not found. Please run `lingo.dev init` to initialize the project.",
docUrl: "i18nNotFound",
});
}
if (!i18nConfig.buckets || !Object.keys(i18nConfig.buckets).length) {
throw new CLIError({
message:
"No buckets found in i18n.json. Please add at least one bucket containing i18n content.",
docUrl: "bucketNotFound",
});
}
}
function displaySummary(results: any[]) {
if (results.length === 0) return;
console.log("\nProcess Summary:");
results.forEach((result) => {
console.log(`${result.step}: ${result.status}`);
if (result.error) console.log(` - Error: ${result.error}`);
});
}
```
--------------------------------------------------------------------------------
/packages/cli/src/cli/loaders/ejs.ts:
--------------------------------------------------------------------------------
```typescript
import * as ejs from "ejs";
import { ILoader } from "./_types";
import { createLoader } from "./_utils";
interface EjsParseResult {
content: string;
translatable: Record<string, string>;
}
function parseEjsForTranslation(input: string): EjsParseResult {
const translatable: Record<string, string> = {};
let counter = 0;
// Regular expression for all EJS tags
const ejsTagRegex = /<%[\s\S]*?%>/g;
// Split content by EJS tags, preserving both text and EJS parts
const parts: Array<{ type: "text" | "ejs"; content: string }> = [];
let lastIndex = 0;
let match;
while ((match = ejsTagRegex.exec(input)) !== null) {
// Add text before the tag
if (match.index > lastIndex) {
parts.push({
type: "text",
content: input.slice(lastIndex, match.index),
});
}
// Add the EJS tag
parts.push({
type: "ejs",
content: match[0],
});
lastIndex = match.index + match[0].length;
}
// Add remaining text after the last tag
if (lastIndex < input.length) {
parts.push({
type: "text",
content: input.slice(lastIndex),
});
}
// Build the template and extract translatable content
let template = "";
for (const part of parts) {
if (part.type === "ejs") {
// Keep EJS tags as-is
template += part.content;
} else {
// For text content, extract translatable parts while preserving HTML structure
const textContent = part.content;
// Extract text content from HTML tags while preserving structure
const htmlTagRegex = /<[^>]+>/g;
const textParts: Array<{ type: "html" | "text"; content: string }> = [];
let lastTextIndex = 0;
let htmlMatch;
while ((htmlMatch = htmlTagRegex.exec(textContent)) !== null) {
// Add text before the HTML tag
if (htmlMatch.index > lastTextIndex) {
const textBefore = textContent.slice(lastTextIndex, htmlMatch.index);
if (textBefore.trim()) {
textParts.push({ type: "text", content: textBefore });
} else {
textParts.push({ type: "html", content: textBefore });
}
}
// Add the HTML tag
textParts.push({ type: "html", content: htmlMatch[0] });
lastTextIndex = htmlMatch.index + htmlMatch[0].length;
}
// Add remaining text after the last HTML tag
if (lastTextIndex < textContent.length) {
const remainingText = textContent.slice(lastTextIndex);
if (remainingText.trim()) {
textParts.push({ type: "text", content: remainingText });
} else {
textParts.push({ type: "html", content: remainingText });
}
}
// If no HTML tags found, treat entire content as text
if (textParts.length === 0) {
const trimmedContent = textContent.trim();
if (trimmedContent) {
textParts.push({ type: "text", content: textContent });
} else {
textParts.push({ type: "html", content: textContent });
}
}
// Process text parts
for (const textPart of textParts) {
if (textPart.type === "text") {
const trimmedContent = textPart.content.trim();
if (trimmedContent) {
const key = `text_${counter++}`;
translatable[key] = trimmedContent;
template += textPart.content.replace(
trimmedContent,
`__LINGO_PLACEHOLDER_${key}__`,
);
} else {
template += textPart.content;
}
} else {
template += textPart.content;
}
}
}
}
return { content: template, translatable };
}
function reconstructEjsWithTranslation(
template: string,
translatable: Record<string, string>,
): string {
let result = template;
// Replace placeholders with translated content
for (const [key, value] of Object.entries(translatable)) {
const placeholder = `__LINGO_PLACEHOLDER_${key}__`;
result = result.replace(new RegExp(placeholder, "g"), value);
}
return result;
}
export default function createEjsLoader(): ILoader<
string,
Record<string, any>
> {
return createLoader({
async pull(locale, input) {
if (!input || input.trim() === "") {
return {};
}
try {
const parseResult = parseEjsForTranslation(input);
return parseResult.translatable;
} catch (error) {
console.warn(
"Warning: Could not parse EJS template, treating as plain text",
);
// Fallback: treat entire input as translatable content
return { content: input.trim() };
}
},
async push(locale, data, originalInput) {
if (!originalInput) {
// If no original input, reconstruct from data
return Object.values(data).join("\n");
}
try {
const parseResult = parseEjsForTranslation(originalInput);
// Merge original translatable content with new translations
const mergedTranslatable = { ...parseResult.translatable, ...data };
return reconstructEjsWithTranslation(
parseResult.content,
mergedTranslatable,
);
} catch (error) {
console.warn(
"Warning: Could not reconstruct EJS template, returning translated data",
);
return Object.values(data).join("\n");
}
},
});
}
```
--------------------------------------------------------------------------------
/packages/cli/src/cli/loaders/locked-keys.spec.ts:
--------------------------------------------------------------------------------
```typescript
import { describe, it, expect } from "vitest";
import createLockedKeysLoader from "./locked-keys";
describe("createLockedKeysLoader", () => {
const lockedKeys = ["common.locked", "feature.settings"];
const locale = "en";
describe("pull", () => {
it("should remove locked keys from the data", async () => {
const loader = createLockedKeysLoader(lockedKeys);
loader.setDefaultLocale(locale);
const data = {
"common.title": "Title",
"common.locked.label": "Locked Label",
"feature.settings.title": "Settings",
"feature.enabled": true,
};
const result = await loader.pull(locale, data);
expect(result).toEqual({
"common.title": "Title",
"feature.enabled": true,
});
});
it("should remove locked keys with wildcard from the data", async () => {
const loader = createLockedKeysLoader(["settings/*/locked"]);
loader.setDefaultLocale(locale);
const data = {
"common.title": "Title",
"settings/default/locked": "Foo",
"settings/default/notifications": "Enabled",
"settings/global/locked": "Bar",
"settings/global/notifications": "Disabled",
"settings/user/locked": "Baz",
"settings/user/notifications": "Enabled",
};
const result = await loader.pull(locale, data);
expect(result).toEqual({
"common.title": "Title",
"settings/default/notifications": "Enabled",
"settings/global/notifications": "Disabled",
"settings/user/notifications": "Enabled",
});
});
it("should return the same data if no keys are locked", async () => {
const loader = createLockedKeysLoader([]);
loader.setDefaultLocale(locale);
const data = {
"common.title": "Title",
"feature.enabled": true,
};
const result = await loader.pull(locale, data);
expect(result).toEqual(data);
});
it("should handle empty data object", async () => {
const loader = createLockedKeysLoader(lockedKeys);
loader.setDefaultLocale(locale);
const data = {};
const result = await loader.pull(locale, data);
expect(result).toEqual({});
});
it("should not remove keys that partially match but do not start with a locked key", async () => {
const loader = createLockedKeysLoader(["locked"]);
loader.setDefaultLocale(locale);
const data = {
"locked.a": 1,
"is.locked.b": 1,
"notlocked.c": 1,
};
const result = await loader.pull("en", data);
expect(result).toEqual({
"is.locked.b": 1,
"notlocked.c": 1,
});
});
});
describe("push", () => {
const originalInput = {
"common.title": "Original Title",
"common.locked.label": "Original Locked Label",
"feature.settings.title": "Original Settings",
"feature.enabled": false,
};
it("should merge new data with original, preserving locked keys from original", async () => {
const loader = createLockedKeysLoader(lockedKeys);
loader.setDefaultLocale(locale);
await loader.pull(locale, originalInput);
const data = {
"common.title": "New Title",
"common.locked.label": "New Locked Label",
"feature.enabled": true,
"new.feature": "hello",
};
const result = await loader.push(locale, data);
expect(result).toEqual({
"common.title": "New Title",
"common.locked.label": "Original Locked Label",
"feature.settings.title": "Original Settings",
"feature.enabled": true,
"new.feature": "hello",
});
});
it("should merge new data with original, preserving wildcard locked keys from original", async () => {
const loader = createLockedKeysLoader(["settings/*/locked"]);
loader.setDefaultLocale(locale);
const originalInputWithWildcardKeys = {
"common.title": "Some Title",
"settings/default/locked": "Foo",
"settings/default/notifications": "Enabled",
"settings/global/locked": "Bar",
"settings/global/notifications": "Disabled",
"settings/user/locked": "Baz",
"settings/user/notifications": "Enabled",
};
await loader.pull(locale, originalInputWithWildcardKeys);
const data = {
"common.title": "Better Title",
"settings/default/notifications": "Maybe",
"settings/global/notifications": "Perhaps",
"settings/user/notifications": "Unknown",
};
const result = await loader.push(locale, data);
expect(result).toEqual({
"common.title": "Better Title",
"settings/default/locked": "Foo",
"settings/default/notifications": "Maybe",
"settings/global/locked": "Bar",
"settings/global/notifications": "Perhaps",
"settings/user/locked": "Baz",
"settings/user/notifications": "Unknown",
});
});
it("should handle undefined original input", async () => {
const loader = createLockedKeysLoader(lockedKeys);
loader.setDefaultLocale(locale);
await loader.pull(locale, undefined as any);
const data = {
"common.title": "New Title",
"new.feature": "hello",
};
const result = await loader.push(locale, data);
expect(result).toEqual({
"common.title": "New Title",
"new.feature": "hello",
});
});
});
});
```
--------------------------------------------------------------------------------
/packages/cli/WATCH_MODE.md:
--------------------------------------------------------------------------------
```markdown
# Watch Mode Implementation
This document describes the implementation of the watch mode feature for the Lingo.dev CLI.
## Overview
The watch mode (`--watch` flag) automatically monitors source files for changes and triggers retranslation when modifications are detected. This eliminates the need for manual retranslation after each edit and keeps target language files in sync with source file changes.
## Usage
```bash
# Start watch mode
lingo.dev run --watch
# Watch with custom debounce timing (7 seconds)
lingo.dev run --watch --debounce 7000
# Watch with faster debounce for development (2 seconds)
lingo.dev run --watch --debounce 2000
# Watch with additional filters
lingo.dev run --watch --locale es --bucket json
lingo.dev run --watch --file "src/locales/*.json" --debounce 1000
```
## Features
### 1. Automatic File Monitoring
- Watches all source locale files based on your `i18n.json` configuration
- Monitors file changes, additions, and deletions
- Uses stable file watching to avoid false triggers
### 2. Debounced Processing
- Implements configurable debounce mechanism to avoid excessive retranslations
- Default: 5 seconds, customizable with `--debounce` flag
- Groups rapid changes into single translation batches
- Prevents resource waste from frequent file saves
### 3. Intelligent Pattern Detection
- Automatically determines which files to watch based on bucket patterns
- Replaces `[locale]` placeholders with source locale
- Respects filtering options (`--bucket`, `--file`, etc.)
### 4. Real-time Feedback
- Shows which files are being watched on startup
- Displays file change notifications
- Provides translation progress updates
- Shows completion status for each batch
### 5. Graceful Error Handling
- Continues watching even if individual translations fail
- Reports errors without stopping the watch process
- Maintains watch state across translation cycles
## Implementation Details
### File Structure
- `src/cli/cmd/run/watch.ts` - Main watch implementation
- `src/cli/cmd/run/_types.ts` - Updated to include watch flag
- `src/cli/cmd/run/index.ts` - Integration with main run command
### Key Components
#### Watch State Management
```typescript
interface WatchState {
isRunning: boolean;
pendingChanges: Set<string>;
debounceTimer?: NodeJS.Timeout;
}
```
#### File Pattern Resolution
The watch mode automatically determines which files to monitor by:
1. Getting buckets from `i18n.json`
2. Applying user filters (`--bucket`, `--file`)
3. Replacing `[locale]` with source locale
4. Creating file patterns for chokidar
#### Debounce Logic
- Uses configurable debounce timer (default: 5000ms)
- Resets timer on each file change
- Only triggers translation when timer expires
- Prevents overlapping translation runs
- Customizable via `--debounce <milliseconds>` flag
### Dependencies
- `chokidar` - Robust file watching library
- Existing Lingo.dev pipeline (setup, plan, execute)
## Example Workflow
1. **Start Watch Mode**
```bash
lingo.dev run --watch
```
2. **Initial Setup**
- Performs normal translation setup
- Runs initial planning and execution
- Shows summary of completed translations
- Starts file watching
3. **File Change Detection**
```
📝 File changed: locales/en.json
⏳ Debouncing... (5000ms)
```
4. **Automatic Retranslation**
```
🔄 Triggering retranslation...
Changed files: locales/en.json
[Planning] Found 2 translation task(s)
[Localization] Processing tasks...
✅ Retranslation completed
👀 Continuing to watch for changes...
```
## Error Handling
The watch mode is designed to be resilient:
- **Translation Errors**: Reports errors but continues watching
- **File System Errors**: Logs watch errors but maintains process
- **Invalid Files**: Skips problematic files and continues
- **Interrupt Handling**: Gracefully shuts down on Ctrl+C
## Performance Considerations
- **Efficient Pattern Matching**: Only watches relevant source files
- **Debounced Processing**: Prevents excessive API calls
- **Memory Management**: Clears completed change sets
- **Process Isolation**: Each translation runs in isolated context
## Testing
Use the provided demo setup script:
```bash
./demo-watch-setup.sh
cd /tmp/lingo-watch-demo
lingo.dev run --watch
```
Then in another terminal:
```bash
# Add a new translation key
echo '{"hello": "Hello", "world": "World", "welcome": "Welcome to Lingo.dev", "goodbye": "Goodbye"}' > locales/en.json
# Watch as translations are automatically updated
```
## Integration with Existing Features
The watch mode works seamlessly with all existing run command options:
- `--locale` - Watch only affects specified locales
- `--bucket` - Watch only monitors specified bucket types
- `--file` - Watch only monitors matching file patterns
- `--key` - Post-change filtering applies to specific keys
- `--force` - Forces full retranslation on each change
- `--api-key` - Uses specified API key for all operations
- `--concurrency` - Controls translation parallelism
- `--debounce` - Configures debounce delay in milliseconds (default: 5000ms)
## Future Enhancements
Potential improvements for future versions:
1. **Watch Exclusions**: Ignore specific files or patterns
2. **Selective Translation**: Only translate changed keys
3. **Change Summaries**: Show detailed change reports
4. **Multi-project Support**: Watch multiple i18n configurations
5. **Advanced Debounce Modes**: Per-file or per-bucket debouncing
```
--------------------------------------------------------------------------------
/packages/cli/src/cli/utils/exit-gracefully.spec.ts:
--------------------------------------------------------------------------------
```typescript
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { exitGracefully } from "./exit-gracefully";
// Mock process.exit
const mockExit: any = vi.fn();
const originalProcess = global.process;
describe("exitGracefully", () => {
beforeEach(() => {
// Mock process.exit
vi.spyOn(process, "exit").mockImplementation(mockExit);
// Mock process._getActiveHandles and _getActiveRequests
Object.defineProperty(process, "_getActiveHandles", {
value: vi.fn(),
writable: true,
});
Object.defineProperty(process, "_getActiveRequests", {
value: vi.fn(),
writable: true,
});
});
afterEach(() => {
vi.restoreAllMocks();
vi.clearAllTimers();
mockExit.mockClear();
});
it("should exit immediately when no pending operations", () => {
// Mock no pending operations
(process as any)._getActiveHandles.mockReturnValue([]);
(process as any)._getActiveRequests.mockReturnValue([]);
exitGracefully();
expect(mockExit).toHaveBeenCalledWith(0);
});
it("should wait and retry when there are pending operations", () => {
vi.useFakeTimers();
// Mock pending operations
(process as any)._getActiveHandles.mockReturnValue([
{ hasRef: () => true, close: () => {} },
]);
(process as any)._getActiveRequests.mockReturnValue([]);
exitGracefully();
// Should not exit immediately
expect(mockExit).not.toHaveBeenCalled();
// Fast-forward time to trigger retry
vi.advanceTimersByTime(250);
// Should still not exit if operations are pending
expect(mockExit).not.toHaveBeenCalled();
// Fast-forward to max wait time
vi.advanceTimersByTime(1750);
// Should exit after max wait time
expect(mockExit).toHaveBeenCalledWith(0);
});
it("should exit after max wait interval even with pending operations", () => {
vi.useFakeTimers();
// Mock persistent pending operations
(process as any)._getActiveHandles.mockReturnValue([
{ hasRef: () => true, close: () => {} },
]);
(process as any)._getActiveRequests.mockReturnValue([]);
exitGracefully();
// Fast-forward to max wait time (2000ms)
vi.advanceTimersByTime(2000);
expect(mockExit).toHaveBeenCalledWith(0);
});
it("should handle standard process handles correctly", () => {
// Mock only standard handles
(process as any)._getActiveHandles.mockReturnValue([
process.stdin,
process.stdout,
process.stderr,
]);
(process as any)._getActiveRequests.mockReturnValue([]);
exitGracefully();
// Should exit immediately as standard handles are filtered out
expect(mockExit).toHaveBeenCalledWith(0);
});
it("should handle timers without ref correctly", () => {
// Mock timers without ref
(process as any)._getActiveHandles.mockReturnValue([
{ hasRef: () => false },
]);
(process as any)._getActiveRequests.mockReturnValue([]);
exitGracefully();
// Should exit immediately as timers without ref are filtered out
expect(mockExit).toHaveBeenCalledWith(0);
});
it("should detect file watchers correctly", () => {
// Mock file watcher handles
(process as any)._getActiveHandles.mockReturnValue([{ close: () => {} }]);
(process as any)._getActiveRequests.mockReturnValue([]);
exitGracefully();
// Should not exit immediately due to file watcher
expect(mockExit).not.toHaveBeenCalled();
});
it("should detect pending requests correctly", () => {
// Mock pending requests
(process as any)._getActiveHandles.mockReturnValue([]);
(process as any)._getActiveRequests.mockReturnValue([
{ someRequest: true },
]);
exitGracefully();
// Should not exit immediately due to pending requests
expect(mockExit).not.toHaveBeenCalled();
});
it("should handle elapsed time parameter correctly", () => {
vi.useFakeTimers();
// Mock pending operations
(process as any)._getActiveHandles.mockReturnValue([
{ hasRef: () => true, close: () => {} },
]);
(process as any)._getActiveRequests.mockReturnValue([]);
// Start with 1500ms already elapsed
exitGracefully(1500);
// Should exit after 500ms more (reaching 2000ms max)
vi.advanceTimersByTime(500);
expect(mockExit).toHaveBeenCalledWith(0);
});
it("should exit immediately when elapsed time exceeds max wait interval", () => {
// Mock pending operations but start with elapsed time > 2000ms
(process as any)._getActiveHandles.mockReturnValue([
{ hasRef: () => true, close: () => {} },
]);
(process as any)._getActiveRequests.mockReturnValue([]);
exitGracefully(2500);
// Should exit immediately as elapsed time exceeds max wait interval
expect(mockExit).toHaveBeenCalledWith(0);
});
it("should handle mixed types of pending operations", () => {
vi.useFakeTimers();
// Mock mixed pending operations
(process as any)._getActiveHandles.mockReturnValue([
{ hasRef: () => true, close: () => {} },
{ hasRef: () => false }, // Timer without ref
process.stdin, // Standard handle
]);
(process as any)._getActiveRequests.mockReturnValue([
{ someRequest: true },
]);
exitGracefully();
// Should not exit immediately due to mixed pending operations
expect(mockExit).not.toHaveBeenCalled();
// Fast-forward to max wait time
vi.advanceTimersByTime(2000);
expect(mockExit).toHaveBeenCalledWith(0);
});
});
```
--------------------------------------------------------------------------------
/packages/compiler/src/utils/jsx-variables.spec.ts:
--------------------------------------------------------------------------------
```typescript
import * as t from "@babel/types";
import traverse, { NodePath } from "@babel/traverse";
import { parse } from "@babel/parser";
import { getJsxVariables } from "./jsx-variables";
import { describe, it, expect } from "vitest";
describe("JSX Variables Utils", () => {
function parseJSX(code: string): t.File {
return parse(code, {
sourceType: "module",
plugins: ["jsx", "typescript"],
});
}
function getJSXElementPath(code: string): NodePath<t.JSXElement> {
const ast = parseJSX(code);
let elementPath: NodePath<t.JSXElement> | null = null;
traverse(ast, {
JSXElement(path) {
elementPath = path;
path.stop();
},
});
if (!elementPath) {
throw new Error("No JSX element found in the code");
}
return elementPath;
}
describe("getJsxVariables", () => {
it("should extract single variable from JSX element", () => {
const path = getJSXElementPath(
"<div>You have {count} new messages.</div>",
);
const result = getJsxVariables(path);
expect(result.type).toBe("ObjectExpression");
expect(result.properties).toHaveLength(1);
const property = result.properties[0] as t.ObjectProperty;
expect((property.key as t.StringLiteral).value).toBe("count");
expect((property.value as t.Identifier).name).toBe("count");
});
it("should extract multiple variables from JSX element", () => {
const path = getJSXElementPath("<div>{count} items in {category}</div>");
const result = getJsxVariables(path);
expect(result.type).toBe("ObjectExpression");
expect(result.properties).toHaveLength(2);
const propertyNames = result.properties
.map((prop) => (prop as t.ObjectProperty).key as t.StringLiteral)
.map((key) => key.value);
expect(propertyNames).toContain("count");
expect(propertyNames).toContain("category");
});
it("should extract variables from nested elements", () => {
const path = getJSXElementPath(
"<div>Total: <strong>{count}</strong> in <span>{category}</span></div>",
);
const result = getJsxVariables(path);
expect(result.type).toBe("ObjectExpression");
expect(result.properties).toHaveLength(2);
const propertyNames = result.properties
.map((prop) => (prop as t.ObjectProperty).key as t.StringLiteral)
.map((key) => key.value);
expect(propertyNames).toContain("count");
expect(propertyNames).toContain("category");
});
it("should return empty object expression when no variables present", () => {
const path = getJSXElementPath("<div>Hello world</div>");
const result = getJsxVariables(path);
expect(result.type).toBe("ObjectExpression");
expect(result.properties).toHaveLength(0);
});
it("should handle duplicate variables by including them only once", () => {
const path = getJSXElementPath(
"<div>{count} items ({count} total)</div>",
);
const result = getJsxVariables(path);
expect(result.type).toBe("ObjectExpression");
expect(result.properties).toHaveLength(1);
const property = result.properties[0] as t.ObjectProperty;
expect((property.key as t.StringLiteral).value).toBe("count");
expect((property.value as t.Identifier).name).toBe("count");
});
it("should handle variables from objects", () => {
const path = getJSXElementPath(
"<div>user {user.name} has {user.profile.details.private.items.count} items</div>",
);
const result = getJsxVariables(path);
expect(result.type).toBe("ObjectExpression");
expect(result.properties).toHaveLength(2);
const userNameProperty = result.properties[0] as t.ObjectProperty;
expect((userNameProperty.key as t.StringLiteral).value).toBe("user.name");
expect((userNameProperty.value as t.Identifier).name).toBe("user.name");
const countProperty = result.properties[1] as t.ObjectProperty;
expect((countProperty.key as t.StringLiteral).value).toBe(
"user.profile.details.private.items.count",
);
expect((countProperty.value as t.Identifier).name).toBe(
"user.profile.details.private.items.count",
);
});
it("should handle nested dynamic vatiables", () => {
const path = getJSXElementPath(
"<div>User {data[currentUserType][currentUserIndex].name} has {items.counts[type]} items of type {typeNames[type]}</div>",
);
const result = getJsxVariables(path);
expect(result.type).toBe("ObjectExpression");
expect(result.properties).toHaveLength(3);
const userNameProperty = result.properties[0] as t.ObjectProperty;
expect((userNameProperty.key as t.StringLiteral).value).toBe(
"data[currentUserType][currentUserIndex].name",
);
expect((userNameProperty.value as t.Identifier).name).toBe(
"data[currentUserType][currentUserIndex].name",
);
const countProperty = result.properties[1] as t.ObjectProperty;
expect((countProperty.key as t.StringLiteral).value).toBe(
"items.counts[type]",
);
expect((countProperty.value as t.Identifier).name).toBe(
"items.counts[type]",
);
const typeProperty = result.properties[2] as t.ObjectProperty;
expect((typeProperty.key as t.StringLiteral).value).toBe(
"typeNames[type]",
);
expect((typeProperty.value as t.Identifier).name).toBe("typeNames[type]");
});
});
});
```
--------------------------------------------------------------------------------
/packages/compiler/src/lib/lcp/api.spec.ts:
--------------------------------------------------------------------------------
```typescript
import { describe, expect, it, vi, afterEach } from "vitest";
import { LCPAPI } from "./api";
import _ = require("lodash");
describe("LCPAPI", () => {
afterEach(() => {
vi.restoreAllMocks();
});
describe("translate", () => {
// very abstract test to make sure the translate function calls private functions of the class
it("should chunk, translate and merge", async () => {
const modelsMock = {};
const chunkSpy = vi
.spyOn(LCPAPI as any, "_chunkDictionary")
.mockReturnValue([1, 2, 3]);
const translateSpy = vi
.spyOn(LCPAPI as any, "_translateChunk")
.mockImplementation((_: any, param: number) => param * 10);
const mergeSpy = vi
.spyOn(LCPAPI as any, "_mergeDictionaries")
.mockReturnValue(100);
const result = await LCPAPI.translate(modelsMock, 0 as any, "en", "es");
expect(chunkSpy).toHaveBeenCalledWith(0);
expect(translateSpy).toHaveBeenCalledTimes(3);
expect(translateSpy).toHaveBeenCalledWith(
modelsMock,
1,
"en",
"es",
undefined,
);
expect(translateSpy).toHaveBeenCalledWith(
modelsMock,
2,
"en",
"es",
undefined,
);
expect(translateSpy).toHaveBeenCalledWith(
modelsMock,
3,
"en",
"es",
undefined,
);
expect(mergeSpy).toHaveBeenCalledWith([10, 20, 30]);
expect(result).toEqual(100);
});
});
describe("_chunkDictionary", () => {
it("should split dictionary into chunks of maximum 100 entries", () => {
const result = (LCPAPI as any)._chunkDictionary({
$schema: "https://lcp.dev/schema/v1/dictionary.json",
version: 0.1,
locale: "en",
files: {
"test1.json": {
entries: _.fromPairs(
_.times(230, (i) => [`entry${i}`, `value${i}`]),
),
},
"test2.json": {
entries: _.fromPairs(
_.times(90, (i) => [`entry${i}`, `value${i}`]),
),
},
"test3.json": {
entries: _.fromPairs(
_.times(130, (i) => [`entry${i}`, `value${i}`]),
),
},
},
});
expect(result.length).toEqual(5);
expect(Object.keys(result[0].files["test1.json"].entries).length).toEqual(
100,
);
expect(Object.keys(result[1].files["test1.json"].entries).length).toEqual(
100,
);
expect(Object.keys(result[2].files["test1.json"].entries).length).toEqual(
30,
);
expect(Object.keys(result[2].files["test2.json"].entries).length).toEqual(
70,
);
expect(Object.keys(result[3].files["test2.json"].entries).length).toEqual(
20,
);
expect(Object.keys(result[3].files["test3.json"].entries).length).toEqual(
80,
);
expect(Object.keys(result[4].files["test3.json"].entries).length).toEqual(
50,
);
});
});
describe("_mergeDictionaries", () => {
it("should merge dictionaries into one", () => {
const dictionaries = [
{
$schema: "https://lcp.dev/schema/v1/dictionary.json",
version: 0.1,
locale: "en",
files: {
"test1.json": {
entries: _.fromPairs(
_.times(10, (i) => [`a-entry${i}`, `value${i}`]),
),
},
},
},
{
$schema: "https://lcp.dev/schema/v1/dictionary.json",
version: 0.1,
locale: "en",
files: {
"test1.json": {
entries: _.fromPairs(
_.times(10, (i) => [`b-entry${i}`, `value${i}`]),
),
},
},
},
{
$schema: "https://lcp.dev/schema/v1/dictionary.json",
version: 0.1,
locale: "en",
files: {
"test1.json": {
entries: _.fromPairs(
_.times(5, (i) => [`c-entry${i}`, `value${i}`]),
),
},
"test2.json": {
entries: _.fromPairs(
_.times(5, (i) => [`a-entry${i}`, `value${i}`]),
),
},
},
},
{
$schema: "https://lcp.dev/schema/v1/dictionary.json",
version: 0.1,
locale: "en",
files: {
"test2.json": {
entries: _.fromPairs(
_.times(3, (i) => [`b-entry${i}`, `value${i}`]),
),
},
"test3.json": {
entries: _.fromPairs(
_.times(7, (i) => [`a-entry${i}`, `value${i}`]),
),
},
},
},
{
$schema: "https://lcp.dev/schema/v1/dictionary.json",
version: 0.1,
locale: "en",
files: {
"test3.json": {
entries: _.fromPairs(
_.times(6, (i) => [`b-entry${i}`, `value${i}`]),
),
},
},
},
];
const result = (LCPAPI as any)._mergeDictionaries(dictionaries);
expect(Object.keys(result.files).length).toEqual(3);
expect(Object.keys(result.files["test1.json"].entries).length).toEqual(
25,
);
expect(Object.keys(result.files["test2.json"].entries).length).toEqual(8);
expect(Object.keys(result.files["test3.json"].entries).length).toEqual(
13,
);
});
});
});
```
--------------------------------------------------------------------------------
/packages/compiler/src/_base.ts:
--------------------------------------------------------------------------------
```typescript
import generate, { GeneratorResult } from "@babel/generator";
import * as t from "@babel/types";
import * as parser from "@babel/parser";
import { LocaleCode } from "@lingo.dev/_spec";
/**
* Options for configuring Lingo.dev Compiler.
*/
export type CompilerParams = {
/**
* The locale to translate from.
*
* This must match one of the following formats:
*
* - [ISO 639-1 language code](https://en.wikipedia.org/wiki/ISO_639-1) (e.g., `"en"`)
* - [IETF BCP 47 language tag](https://en.wikipedia.org/wiki/IETF_language_tag) (e.g., `"en-US"`)
*
* @default "en"
*/
sourceLocale: LocaleCode;
/**
* The locale(s) to translate to.
*
* Each locale must match one of the following formats:
*
* - [ISO 639-1 language code](https://en.wikipedia.org/wiki/ISO_639-1) (e.g., `"en"`)
* - [IETF BCP 47 language tag](https://en.wikipedia.org/wiki/IETF_language_tag) (e.g., `"en-US"`)
*
* @default ["es"]
*/
targetLocales: LocaleCode[];
/**
* The name of the directory where translation files will be stored, relative to `sourceRoot`.
*
* @default "lingo"
*/
lingoDir: string;
/**
* The directory of the source code that will be translated, relative to the current working directory.
*
* @default "src"
*/
sourceRoot: string;
/**
* If `true`, the compiler will generate code for React Server Components (RSC).
*
* When using Vite, this value is always `false`.
*
* When using Next.js, this value is always `true`.
*
* @default false
*/
rsc: boolean;
/**
* If `true`, the compiler will only localize files that use the `"use i18n";` directive.
*
* @default false
*/
useDirective: boolean;
/**
* If `true`, the compiler will log additional information to the console.
*
* @default false
*/
debug: boolean;
/**
* The model(s) to use for translation.
*
* If set to `"lingo.dev"`, the compiler will use Lingo.dev Engine.
*
* If set to an object, the compiler will use the model(s) specified in the object:
*
* - The key is a string that represents the source and target locales, separated by a colon (e.g., `"en:es"`).
* - The value is a string that represents the LLM provider and model, separated by a colon (e.g., `"google:gemini-2.0-flash"`).
*
* You can use `*` as a wildcard to match any locale.
*
* If a model is not specified, an error will be thrown.
*
* @default {}
*/
models: "lingo.dev" | ModelMap;
/**
* Custom system prompt for the translation engine. If set, this prompt will override the default system prompt defined in Compiler.
* Only works with custom models, not with Lingo.dev Engine.
*
* Example: "You are a helpful assistant that translates {SOURCE_LOCALE} to {TARGET_LOCALE}."
*
* @default null
*/
prompt?: string | null;
};
/**
* A mapping between locale pairings and the model to use to translate that pairing.
*/
export type ModelMap = {
[key in SourceTargetLocale]?: ModelIdentifier;
};
/**
* A pairing of a source and target locale.
*/
export type SourceTargetLocale =
| LocalePair
| AnyTargetLocale
| AnySourceLocale
| AnyLocale;
/**
* A translation from a specific source locale to a specific target locale.
*/
export type LocalePair = `${LocaleCode}:${LocaleCode}`;
/**
* A translation from a specific source locale to any target locale.
*/
export type AnyTargetLocale = `${LocaleCode}:${LocaleWildcard}`;
/**
* A translation from any source locale to a specific target locale.
*/
export type AnySourceLocale = `${LocaleWildcard}:${LocaleCode}`;
/**
* A translation from any source locale to any target locale.
*/
export type AnyLocale = `${LocaleWildcard}:${LocaleWildcard}`;
/**
* A wildcard symbol that matches any locale.
*/
export type LocaleWildcard = "*";
/**
* The colon-separated identifier of a model to use for translation.
*/
export type ModelIdentifier = `${string}:${string}`;
export type CompilerInput = {
relativeFilePath: string;
code: string;
params: CompilerParams;
};
export type CompilerPayload = CompilerInput & {
ast: t.File;
};
export type CompilerOutput = {
code: string;
map: GeneratorResult["map"];
};
export type CodeMutation = (payload: CompilerPayload) => CompilerPayload | null;
export type CodeMutationDefinition = CodeMutation;
export function createCodeMutation(spec: CodeMutationDefinition): CodeMutation {
return (payload: CompilerPayload) => {
const result = spec(payload);
return result;
};
}
export function createPayload(input: CompilerInput): CompilerPayload {
const ast = parser.parse(input.code, {
sourceType: "module",
plugins: ["jsx", "typescript"],
});
return {
...input,
ast,
};
}
export function createOutput(payload: CompilerPayload): CompilerOutput {
const generationResult = generate(payload.ast, {}, payload.code);
return {
code: generationResult.code,
map: generationResult.map,
};
}
export function composeMutations(...mutations: CodeMutation[]) {
return (input: CompilerPayload) => {
let result = input;
for (const mutate of mutations) {
const intermediateResult = mutate(result);
if (!intermediateResult) {
break;
} else {
result = intermediateResult;
}
}
return result;
};
}
export const defaultParams: CompilerParams = {
sourceRoot: "src",
lingoDir: "lingo",
sourceLocale: "en",
targetLocales: ["es"],
rsc: false,
useDirective: false,
debug: false,
models: {},
prompt: null,
};
```
--------------------------------------------------------------------------------
/packages/cli/src/cli/loaders/ignored-keys.spec.ts:
--------------------------------------------------------------------------------
```typescript
import { describe, it, expect } from "vitest";
import createIgnoredKeysLoader from "./ignored-keys";
// Helper values
const defaultLocale = "en";
const targetLocale = "es";
// Common ignored keys list used across tests
const IGNORED_KEYS = ["meta", "todo", "pages/*/title"];
/**
* Creates a fresh loader instance with the default locale already set.
*/
function createLoader() {
const loader = createIgnoredKeysLoader(IGNORED_KEYS);
loader.setDefaultLocale(defaultLocale);
return loader;
}
describe("ignored-keys loader", () => {
it("should omit the ignored keys when pulling the default locale", async () => {
const loader = createLoader();
const input = {
greeting: "hello",
meta: "some meta information",
todo: "translation pending",
};
const result = await loader.pull(defaultLocale, input);
expect(result).toEqual({ greeting: "hello" });
});
it("should omit the ignored keys when pulling a target locale", async () => {
const loader = createLoader();
// First pull for the default locale (required by createLoader)
await loader.pull(defaultLocale, {
greeting: "hello",
meta: "meta en",
});
// Now pull the target locale
const targetInput = {
greeting: "hola",
meta: "meta es",
todo: "todo es",
};
const result = await loader.pull(targetLocale, targetInput);
expect(result).toEqual({ greeting: "hola" });
});
it("should remove ignored keys when pushing a target locale", async () => {
const loader = createLoader();
// Initial pull for the default locale
await loader.pull(defaultLocale, {
greeting: "hello",
meta: "meta en",
todo: "todo en",
});
// Pull for the target locale (simulating a translator editing the file)
const targetInput = {
greeting: "hola",
meta: "meta es",
todo: "todo es",
};
await loader.pull(targetLocale, targetInput);
// Data that will be pushed (may still contain ignored keys from translation)
const dataToPush = {
greeting: "hola",
meta: "should be removed",
todo: "should be removed",
};
const pushResult = await loader.push(targetLocale, dataToPush);
// The loader should have removed the ignored keys completely.
expect(pushResult).toEqual({
greeting: "hola",
});
});
it("should omit keys matching wildcard patterns when pulling the default locale", async () => {
const loader = createLoader();
const input = {
greeting: "hello",
meta: "some meta information",
"pages/0/title": "Title 0",
"pages/0/content": "Content 0",
"pages/foo/title": "Foo Title",
"pages/foo/content": "Foo Content",
"pages/bar/notitle": "No Title",
"pages/bar/content": "No Content",
};
const result = await loader.pull(defaultLocale, input);
expect(result).toEqual({
greeting: "hello",
"pages/0/content": "Content 0",
"pages/foo/content": "Foo Content",
"pages/bar/notitle": "No Title",
"pages/bar/content": "No Content",
});
});
it("should omit keys matching wildcard patterns when pulling a target locale", async () => {
const loader = createLoader();
await loader.pull(defaultLocale, {
greeting: "hello",
meta: "meta en",
"pages/0/title": "Title 0",
"pages/0/content": "Content 0",
"pages/foo/title": "Foo Title",
"pages/foo/content": "Foo Content",
"pages/bar/notitle": "No Title",
"pages/bar/content": "No Content",
});
const targetInput = {
greeting: "hola",
meta: "meta es",
"pages/0/title": "Title 0",
"pages/0/content": "Contenido 0",
"pages/foo/title": "Foo Title",
"pages/foo/content": "Contenido Foo",
"pages/bar/notitle": "No Title",
"pages/bar/content": "No Content",
};
const result = await loader.pull(targetLocale, targetInput);
expect(result).toEqual({
greeting: "hola",
"pages/0/content": "Contenido 0",
"pages/foo/content": "Contenido Foo",
"pages/bar/notitle": "No Title",
"pages/bar/content": "No Content",
});
});
it("should remove wildcard-ignored keys when pushing a target locale", async () => {
const loader = createLoader();
await loader.pull(defaultLocale, {
greeting: "hello",
meta: "meta en",
"pages/0/title": "Title 0",
"pages/0/content": "Content 0",
"pages/foo/title": "Foo Title",
"pages/foo/content": "Foo Content",
"pages/bar/notitle": "No Title",
"pages/bar/content": "No Content",
});
await loader.pull(targetLocale, {
greeting: "hola",
meta: "meta es",
"pages/0/title": "Título 0",
"pages/0/content": "Contenido 0",
"pages/foo/title": "Título Foo",
"pages/foo/content": "Contenido Foo",
"pages/bar/notitle": "No Título",
"pages/bar/content": "Contenido Bar",
});
const dataToPush = {
greeting: "hola",
meta: "should be removed",
"pages/0/title": "should be removed",
"pages/0/content": "Contenido Nuveo",
"pages/foo/title": "should be removed",
"pages/foo/content": "Contenido Nuevo Foo",
"pages/bar/notitle": "No Título",
"pages/bar/content": "Contenido Nuevo Bar",
};
const pushResult = await loader.push(targetLocale, dataToPush);
expect(pushResult).toEqual({
greeting: "hola",
"pages/0/content": "Contenido Nuveo",
"pages/foo/content": "Contenido Nuevo Foo",
"pages/bar/notitle": "No Título",
"pages/bar/content": "Contenido Nuevo Bar",
});
});
});
```
--------------------------------------------------------------------------------
/scripts/docs/src/json-schema/markdown-renderer.ts:
--------------------------------------------------------------------------------
```typescript
import type { ListItem, Root, RootContent } from "mdast";
import { unified } from "unified";
import remarkStringify from "remark-stringify";
import type { PropertyInfo } from "./types";
export function makeHeadingNode(fullName: string): RootContent {
const headingDepth = Math.min(6, 2 + (fullName.split(".").length - 1));
return {
type: "heading",
depth: headingDepth as 1 | 2 | 3 | 4 | 5 | 6,
children: [{ type: "inlineCode", value: fullName }],
};
}
export function makeDescriptionNode(description?: string): RootContent | null {
if (!description) return null;
return {
type: "paragraph",
children: [{ type: "text", value: description }],
};
}
export function makeTypeBulletNode(type: string): ListItem {
return {
type: "listItem",
children: [
{
type: "paragraph",
children: [
{ type: "text", value: "Type: " },
{ type: "inlineCode", value: type },
],
},
],
};
}
export function makeRequiredBulletNode(required: boolean): ListItem {
return {
type: "listItem",
children: [
{
type: "paragraph",
children: [
{ type: "text", value: "Required: " },
{ type: "inlineCode", value: required ? "yes" : "no" },
],
},
],
};
}
export function makeDefaultBulletNode(defaultValue?: unknown): ListItem | null {
if (defaultValue === undefined) return null;
return {
type: "listItem",
children: [
{
type: "paragraph",
children: [
{ type: "text", value: "Default: " },
{ type: "inlineCode", value: JSON.stringify(defaultValue) },
],
},
],
};
}
export function makeEnumBulletNode(allowedValues?: unknown[]): ListItem | null {
if (!allowedValues || allowedValues.length === 0) return null;
return {
type: "listItem",
children: [
{
type: "paragraph",
children: [{ type: "text", value: "Allowed values:" }],
},
{
type: "list",
ordered: false,
spread: false,
children: allowedValues.map((v) => ({
type: "listItem",
children: [
{
type: "paragraph",
children: [{ type: "inlineCode", value: String(v) }],
},
],
})),
},
],
};
}
export function makeAllowedKeysBulletNode(
allowedKeys?: string[],
): ListItem | null {
if (!allowedKeys || allowedKeys.length === 0) return null;
return {
type: "listItem",
children: [
{
type: "paragraph",
children: [{ type: "text", value: "Allowed keys:" }],
},
{
type: "list",
ordered: false,
spread: false,
children: allowedKeys.map((v) => ({
type: "listItem",
children: [
{
type: "paragraph",
children: [{ type: "inlineCode", value: v }],
},
],
})),
},
],
};
}
export function makeBullets(property: PropertyInfo): ListItem[] {
const bullets: ListItem[] = [
makeTypeBulletNode(property.type),
makeRequiredBulletNode(property.required),
];
const defaultNode = makeDefaultBulletNode(property.defaultValue);
if (defaultNode) bullets.push(defaultNode);
const enumNode = makeEnumBulletNode(property.allowedValues);
if (enumNode) bullets.push(enumNode);
const allowedKeysNode = makeAllowedKeysBulletNode(property.allowedKeys);
if (allowedKeysNode) bullets.push(allowedKeysNode);
return bullets;
}
export function renderPropertyToMarkdown(
property: PropertyInfo,
): RootContent[] {
const nodes: RootContent[] = [makeHeadingNode(property.fullPath)];
// Description node
const descNode = makeDescriptionNode(property.description);
if (descNode) nodes.push(descNode);
// Bullet list node (with all bullets)
const bulletItems = makeBullets(property);
nodes.push({
type: "list",
ordered: false,
spread: false,
children: bulletItems,
});
// Recurse for nested properties
if (property.children) {
for (const child of property.children) {
nodes.push(...renderPropertyToMarkdown(child));
}
}
return nodes;
}
export function renderPropertiesToMarkdown(
properties: PropertyInfo[],
): RootContent[] {
const children: RootContent[] = [
{
type: "paragraph",
children: [
{
type: "text",
value:
"This page describes the complete list of properties that are available within the ",
},
{ type: "inlineCode", value: "i18n.json" },
{
type: "text",
value: " configuration file. This file is used by ",
},
{
type: "strong",
children: [{ type: "text", value: "Lingo.dev CLI" }],
},
{
type: "text",
value: " to configure the behavior of the translation pipeline.",
},
],
},
];
for (const property of properties) {
children.push(...renderPropertyToMarkdown(property));
// Add spacing between top-level sections
children.push({
type: "paragraph",
children: [{ type: "text", value: "" }],
});
}
return children;
}
export function renderMarkdown(properties: PropertyInfo[]): string {
const children = renderPropertiesToMarkdown(properties);
const root: Root = { type: "root", children };
const markdownContent = unified()
.use(remarkStringify, { fences: true, listItemIndent: "one" })
.stringify(root);
// Add YAML frontmatter
const frontmatter = `---
title: i18n.json properties
---
`;
return frontmatter + markdownContent;
}
```
--------------------------------------------------------------------------------
/demo/adonisjs/CHANGELOG.md:
--------------------------------------------------------------------------------
```markdown
# adonis
## 0.0.29
### Patch Changes
- Updated dependencies [[`68fb3ea`](https://github.com/lingodotdev/lingo.dev/commit/68fb3ea64fc0191ecee66403432e0c8efabab2b9)]:
- [email protected]
## 0.0.28
### Patch Changes
- Updated dependencies [[`e70385b`](https://github.com/lingodotdev/lingo.dev/commit/e70385bd1ac676bf5bd31b212d8510e6b7ebf793)]:
- [email protected]
## 0.0.27
### Patch Changes
- Updated dependencies [[`f7215c1`](https://github.com/lingodotdev/lingo.dev/commit/f7215c1e435378aac8fc953765335cd478cbf507)]:
- [email protected]
## 0.0.26
### Patch Changes
- Updated dependencies [[`898bd36`](https://github.com/lingodotdev/lingo.dev/commit/898bd36cc2e444641560d2ad2b28065a57072183)]:
- [email protected]
## 0.0.25
### Patch Changes
- Updated dependencies [[`060680c`](https://github.com/lingodotdev/lingo.dev/commit/060680cd13c05dd77dd9d5447c064d948bd21cb0), [`f102356`](https://github.com/lingodotdev/lingo.dev/commit/f102356e1ea12c800399ac11f074c42708c304b1), [`a956e53`](https://github.com/lingodotdev/lingo.dev/commit/a956e537d0d45565c3243dd0c5ba4eec8bed69c6), [`3fd38c2`](https://github.com/lingodotdev/lingo.dev/commit/3fd38c2d38e4b22dcd824c865fe31abbc56bc862)]:
- [email protected]
## 0.0.24
### Patch Changes
- Updated dependencies [[`03671f7`](https://github.com/lingodotdev/lingo.dev/commit/03671f7cb252d6bee3debce2f4a4eb989dc0050b)]:
- [email protected]
## 0.0.23
### Patch Changes
- Updated dependencies [[`4f5ffe6`](https://github.com/lingodotdev/lingo.dev/commit/4f5ffe62189949bb26a6c7825cb72c217aefa32f)]:
- [email protected]
## 0.0.22
### Patch Changes
- Updated dependencies [[`be8de32`](https://github.com/lingodotdev/lingo.dev/commit/be8de3280bb5dc5f409fc7680c0e5ff6a53e2fe5)]:
- [email protected]
## 0.0.21
### Patch Changes
- Updated dependencies [[`79c4c00`](https://github.com/lingodotdev/lingo.dev/commit/79c4c00108b9c102cf53e1c090b286070a43e3d5)]:
- [email protected]
## 0.0.20
### Patch Changes
- Updated dependencies [[`b45347c`](https://github.com/lingodotdev/lingo.dev/commit/b45347c38572ee371b2bc494261b7e3e90c4aed1)]:
- [email protected]
## 0.0.19
### Patch Changes
- Updated dependencies [[`74d8efe`](https://github.com/lingodotdev/lingo.dev/commit/74d8efef8d4789f9baa5b7837e053c2571df0308)]:
- [email protected]
## 0.0.18
### Patch Changes
- Updated dependencies [[`3d3c3d7`](https://github.com/lingodotdev/lingo.dev/commit/3d3c3d783a61443da50a5d182391db33a0d29c84)]:
- [email protected]
## 0.0.17
### Patch Changes
- Updated dependencies [[`38139c8`](https://github.com/lingodotdev/lingo.dev/commit/38139c81a85001739cece60873c0c6ad711327a4)]:
- [email protected]
## 0.0.16
### Patch Changes
- Updated dependencies [[`3413dad`](https://github.com/lingodotdev/lingo.dev/commit/3413dad22af688a6d26649c4f25e18304b3caee6)]:
- [email protected]
## 0.0.15
### Patch Changes
- Updated dependencies [[`26d2ec1`](https://github.com/lingodotdev/lingo.dev/commit/26d2ec155c5868a5bdce1027cd76a5a2d4f8f2b1)]:
- [email protected]
## 0.0.14
### Patch Changes
- Updated dependencies [[`82f5e7c`](https://github.com/lingodotdev/lingo.dev/commit/82f5e7cdde9a2a15b4c2a7fcb8c67ed64eab596b), [`e858174`](https://github.com/lingodotdev/lingo.dev/commit/e858174fd5165e0ea3e3f25fa1fc3edb292bc58f)]:
- [email protected]
## 0.0.13
### Patch Changes
- Updated dependencies [[`f3d4987`](https://github.com/lingodotdev/lingo.dev/commit/f3d4987ddc393c28d488f030c087f3e99a667975), [`a933b81`](https://github.com/lingodotdev/lingo.dev/commit/a933b8102763e0481f088c847da53e0eee3f0617)]:
- [email protected]
## 0.0.12
### Patch Changes
- Updated dependencies []:
- [email protected]
## 0.0.11
### Patch Changes
- Updated dependencies [[`dd0663f`](https://github.com/lingodotdev/lingo.dev/commit/dd0663fdcdd0ff4fd5748386758a8c20f9e52a4b)]:
- [email protected]
## 0.0.10
### Patch Changes
- Updated dependencies [[`762396b`](https://github.com/lingodotdev/lingo.dev/commit/762396bb37110dbe3e4e000edb27892b318aa3ef)]:
- [email protected]
## 0.0.9
### Patch Changes
- Updated dependencies [[`468a59b`](https://github.com/lingodotdev/lingo.dev/commit/468a59b89736c72253b1f32abbf30a950e5434ec)]:
- [email protected]
## 0.0.8
### Patch Changes
- Updated dependencies [[`bbc71b9`](https://github.com/lingodotdev/lingo.dev/commit/bbc71b9948ccc289c9669d8b0c276c9596f6a5e7)]:
- [email protected]
## 0.0.7
### Patch Changes
- Updated dependencies [[`0e6d605`](https://github.com/lingodotdev/lingo.dev/commit/0e6d605a9ad6835bef26c40895760c652a69b7a2)]:
- [email protected]
## 0.0.6
### Patch Changes
- Updated dependencies [[`03138da`](https://github.com/lingodotdev/lingo.dev/commit/03138dac37e869e2e99702ffd3c76532f1c58aa6), [`9557fe5`](https://github.com/lingodotdev/lingo.dev/commit/9557fe572d3e4a1a4d8c1e35417fe3b7531c3d52)]:
- [email protected]
## 0.0.5
### Patch Changes
- Updated dependencies [[`64225d0`](https://github.com/lingodotdev/lingo.dev/commit/64225d073999d599ba86f65fee8e08e3e5f2800b)]:
- [email protected]
## 0.0.4
### Patch Changes
- Updated dependencies []:
- [email protected]
## 0.0.3
### Patch Changes
- Updated dependencies [[`88b7e31`](https://github.com/lingodotdev/lingo.dev/commit/88b7e3132c77d0a1e823de4ee6ef5a96a3098b97)]:
- [email protected]
## 0.0.2
### Patch Changes
- Updated dependencies [[`d9294c0`](https://github.com/lingodotdev/lingo.dev/commit/d9294c0bbb993454ad3654f77dd48d82211e0465)]:
- [email protected]
## 0.0.1
### Patch Changes
- Updated dependencies [[`100b141`](https://github.com/lingodotdev/lingo.dev/commit/100b141d2143e33b603830475ba55089dc421e3d)]:
- [email protected]
```
--------------------------------------------------------------------------------
/readme/tr.md:
--------------------------------------------------------------------------------
```markdown
<p align="center">
<a href="https://lingo.dev">
<img
src="https://raw.githubusercontent.com/lingodotdev/lingo.dev/main/content/banner.compiler.png"
width="100%"
alt="Lingo.dev"
/>
</a>
</p>
<p align="center">
<strong>
⚡ Lingo.dev - LLM'ler ile anında yerelleştirme için açık kaynaklı, yapay
zeka destekli i18n araç seti.
</strong>
</p>
<br />
<p align="center">
<a href="https://lingo.dev/compiler">Lingo.dev Derleyici</a> •
<a href="https://lingo.dev/cli">Lingo.dev CLI</a> •
<a href="https://lingo.dev/ci">Lingo.dev CI/CD</a> •
<a href="https://lingo.dev/sdk">Lingo.dev SDK</a>
</p>
<p align="center">
<a href="https://github.com/lingodotdev/lingo.dev/actions/workflows/release.yml">
<img
src="https://github.com/lingodotdev/lingo.dev/actions/workflows/release.yml/badge.svg"
alt="Sürüm"
/>
</a>
<a href="https://github.com/lingodotdev/lingo.dev/blob/main/LICENSE.md">
<img
src="https://img.shields.io/github/license/lingodotdev/lingo.dev"
alt="Lisans"
/>
</a>
<a href="https://github.com/lingodotdev/lingo.dev/commits/main">
<img
src="https://img.shields.io/github/last-commit/lingodotdev/lingo.dev"
alt="Son Değişiklik"
/>
</a>
</p>
---
## Derleyici ile tanışın 🆕
**Lingo.dev Derleyici**, mevcut React bileşenlerinde herhangi bir değişiklik gerektirmeden, derleme zamanında herhangi bir React uygulamasını çok dilli hale getirmek için tasarlanmış ücretsiz, açık kaynaklı bir derleyici ara yazılımıdır.
Bir kez kurun:
```bash
npm install lingo.dev
```
Derleme yapılandırmanızda etkinleştirin:
```js
import lingoCompiler from "lingo.dev/compiler";
const existingNextConfig = {};
export default lingoCompiler.next({
sourceLocale: "en",
targetLocales: ["es", "fr"],
})(existingNextConfig);
```
`next build` komutunu çalıştırın ve İspanyolca ve Fransızca paketlerin ortaya çıkışını izleyin ✨
Tam kılavuz için [belgeleri okuyun →](https://lingo.dev/compiler) ve kurulumunuzla ilgili yardım almak için [Discord'umuza katılın](https://lingo.dev/go/discord).
---
### Bu depoda neler var?
| Alet | Özet | Dokümanlar |
| ------------- | --------------------------------------------------------------------------------------------- | --------------------------------------- |
| **Derleyici** | Derleme zamanında React yerelleştirme | [/compiler](https://lingo.dev/compiler) |
| **CLI** | Web ve mobil uygulamalar, JSON, YAML, markdown ve daha fazlası için tek komutla yerelleştirme | [/cli](https://lingo.dev/cli) |
| **CI/CD** | Her push'ta otomatik çeviri commit'leri + gerekirse pull request oluşturma | [/ci](https://lingo.dev/ci) |
| **SDK** | Kullanıcı tarafından oluşturulan içerik için gerçek zamanlı çeviri | [/sdk](https://lingo.dev/sdk) |
Aşağıda her biri için hızlı bilgiler bulunmaktadır 👇
---
### ⚡️ Lingo.dev CLI
Kod ve içeriği doğrudan terminalinizden çevirin.
```bash
npx lingo.dev@latest run
```
Her dizeyi parmak iziyle işaretler, sonuçları önbelleğe alır ve yalnızca değişen kısımları yeniden çevirir.
Nasıl kurulacağını öğrenmek için [dokümanları takip edin →](https://lingo.dev/cli).
---
### 🔄 Lingo.dev CI/CD
Otomatik olarak mükemmel çeviriler yayınlayın.
```yaml
# .github/workflows/i18n.yml
name: Lingo.dev i18n
on: [push]
jobs:
i18n:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: lingodotdev/lingo.dev@main
with:
api-key: ${{ secrets.LINGODOTDEV_API_KEY }}
```
Deponuzu yeşil tutar ve manuel adımlar olmadan ürününüzü çok dilli hale getirir.
[Belgeleri oku →](https://lingo.dev/ci)
---
### 🧩 Lingo.dev SDK
Dinamik içerik için anında istek başına çeviri.
```ts
import { LingoDotDevEngine } from "lingo.dev/sdk";
const lingoDotDev = new LingoDotDevEngine({
apiKey: "your-api-key-here",
});
const content = {
greeting: "Hello",
farewell: "Goodbye",
message: "Welcome to our platform",
};
const translated = await lingoDotDev.localizeObject(content, {
sourceLocale: "en",
targetLocale: "es",
});
// Returns: { greeting: "Hola", farewell: "Adiós", message: "Bienvenido a nuestra plataforma" }
```
Sohbet, kullanıcı yorumları ve diğer gerçek zamanlı akışlar için mükemmel.
[Belgeleri oku →](https://lingo.dev/sdk)
---
## 🤝 Topluluk
Topluluk odaklıyız ve katkıları seviyoruz!
- Bir fikriniz mi var? [Bir sorun açın](https://github.com/lingodotdev/lingo.dev/issues)
- Bir şeyi düzeltmek mi istiyorsunuz? [PR gönderin](https://github.com/lingodotdev/lingo.dev/pulls)
- Yardıma mı ihtiyacınız var? [Discord'umuza katılın](https://lingo.dev/go/discord)
## ⭐ Yıldız Tarihi
Yaptıklarımızı beğeniyorsanız, bize bir ⭐ verin ve 3.000 yıldıza ulaşmamıza yardımcı olun! 🌟
[

](https://www.star-history.com/#lingodotdev/lingo.dev&Date)
## 🌐 Diğer dillerde Readme
[English](https://github.com/lingodotdev/lingo.dev) • [中文](/readme/zh-Hans.md) • [日本語](/readme/ja.md) • [한국어](/readme/ko.md) • [Español](/readme/es.md) • [Français](/readme/fr.md) • [Русский](/readme/ru.md) • [Українська](/readme/uk-UA.md) • [Deutsch](/readme/de.md) • [Italiano](/readme/it.md) • [العربية](/readme/ar.md) • [עברית](/readme/he.md) • [हिन्दी](/readme/hi.md) • [বাংলা](/readme/bn.md) • [فارسی](/readme/fa.md)
Dilinizi görmüyor musunuz? [`i18n.json`](./i18n.json) dosyasına ekleyin ve bir PR açın!
```
--------------------------------------------------------------------------------
/packages/cli/src/cli/loaders/variable/index.spec.ts:
--------------------------------------------------------------------------------
```typescript
import { describe, it, expect } from "vitest";
import createVariableLoader, { VariableLoaderParams } from "./index";
describe("createVariableLoader", () => {
describe("ieee format", () => {
it("extracts variables during pull", async () => {
const loader = createLoader("ieee");
const input = {
simple: "Hello %s!",
multiple: "Value: %d and %f",
complex: "Precision %.2f with position %1$d",
};
const result = await loader.pull("en", input);
expect(result).toEqual({
simple: "Hello {variable:0}!",
multiple: "Value: {variable:0} and {variable:1}",
complex: "Precision {variable:0} with position {variable:1}",
});
});
it("restores variables during push", async () => {
const loader = createLoader("ieee");
const input = {
simple: "Hello %s!",
multiple: "Value: %d and %f",
complex: "Precision %.2f with position %1$d",
};
const payload = {
simple: "[updated] Hello {variable:0}!",
multiple: "[updated] Value: {variable:0} and {variable:1}",
complex: "[updated] Precision {variable:0} with position {variable:1}",
};
await loader.pull("en", input);
const result = await loader.push("en", payload);
expect(result).toEqual({
simple: "[updated] Hello %s!",
multiple: "[updated] Value: %d and %f",
complex: "[updated] Precision %.2f with position %1$d",
});
});
it("handles empty input", async () => {
const loader = createLoader("ieee");
const result = await loader.pull("en", {});
expect(result).toEqual({});
});
it("preserves variable order for target locale during push", async () => {
const loader = createLoader("ieee");
const sourceInput = {
message: "Value: %d and %f",
};
// Pull the default (source) locale first
await loader.pull("en", sourceInput);
// Target locale has variables in different order due to linguistic specifics
const targetInput = {
message: "Wert: %f und %d",
};
// Pull the target locale to capture its variable ordering
await loader.pull("de", targetInput);
// Translator updates the string while keeping placeholders
const payload = {
message: "[aktualisiert] Wert: {variable:1} und {variable:0}",
};
// Push the updated translation back
const result = await loader.push("de", payload);
expect(result).toEqual({
message: "[aktualisiert] Wert: %f und %d",
});
});
it("extracts variables with positional specifiers during pull", async () => {
const loader = createLoader("ieee");
const input = {
message: "You have %2$d new items and %1$s.",
};
const result = await loader.pull("en", input);
expect(result).toEqual({
message: "You have {variable:0} new items and {variable:1}.",
});
});
it("restores variables with positional specifiers during push", async () => {
const loader = createLoader("ieee");
const input = {
message: "You have %2$d new items and %1$s.",
};
const payload = {
message: "[updated] You have {variable:0} new items and {variable:1}.",
};
await loader.pull("en", input);
const result = await loader.push("en", payload);
expect(result).toEqual({
message: "[updated] You have %2$d new items and %1$s.",
});
});
});
describe("python format", () => {
it("extracts python variables during pull", async () => {
const loader = createLoader("python");
const input = {
simple: "Hello %(name)s!",
multiple: "Value: %(num)d and %(float)f",
};
const result = await loader.pull("en", input);
expect(result).toEqual({
simple: "Hello {variable:0}!",
multiple: "Value: {variable:0} and {variable:1}",
});
});
it("restores python variables during push", async () => {
const loader = createLoader("python");
const input = {
simple: "Hello %(name)s!",
multiple: "Value: %(num)d and %(float)f",
};
const payload = {
simple: "[updated] Hello {variable:0}!",
multiple: "[updated] Value: {variable:0} and {variable:1}",
};
await loader.pull("en", input);
const result = await loader.push("en", input);
expect(result).toEqual({
simple: "Hello %(name)s!",
multiple: "Value: %(num)d and %(float)f",
});
});
it("preserves variable order for target locale during push", async () => {
const loader = createLoader("python");
const sourceInput = {
message: "Hello %(name)s, you have %(count)d items.",
};
// Pull default locale first
await loader.pull("en", sourceInput);
// Target locale with reversed variable order
const targetInput = {
message: "Du hast %(count)d Artikel, %(name)s.",
};
await loader.pull("de", targetInput);
const payload = {
message: "[aktualisiert] Du hast {variable:1} Artikel, {variable:0}.",
};
const result = await loader.push("de", payload);
expect(result).toEqual({
message: "[aktualisiert] Du hast %(count)d Artikel, %(name)s.",
});
});
});
it("throws error for unsupported format type", () => {
expect(() => {
// @ts-expect-error Testing invalid type
createVariableLoader({ type: "invalid" });
}).toThrow("Unsupported variable format type: invalid");
});
});
function createLoader(type: VariableLoaderParams["type"]) {
return createVariableLoader({ type }).setDefaultLocale("en");
}
```
--------------------------------------------------------------------------------
/packages/cli/src/cli/loaders/ejs.spec.ts:
--------------------------------------------------------------------------------
```typescript
import { describe, it, expect } from "vitest";
import createEjsLoader from "./ejs";
describe("EJS Loader", () => {
const loader = createEjsLoader().setDefaultLocale("en");
describe("pull", () => {
it("should extract translatable text from simple EJS template", async () => {
const input = `
<h1>Welcome to our website</h1>
<p>Hello <%= name %>, you have <%= messages.length %> messages.</p>
<footer>© 2024 Our Company</footer>
`;
const result = await loader.pull("en", input);
// Check that we have extracted some translatable content
expect(Object.keys(result).length).toBeGreaterThan(0);
// Check that the EJS variables are not included in the translatable text
const allValues = Object.values(result).join(" ");
expect(allValues).not.toContain("<%= name %>");
expect(allValues).not.toContain("<%= messages.length %>");
// Check that we have the main content
expect(allValues).toContain("Welcome to our website");
expect(allValues).toContain("Hello");
expect(allValues).toContain("messages");
expect(allValues).toContain("© 2024 Our Company");
});
it("should handle EJS templates with various tag types", async () => {
const input = `
<div>
<h2>User Dashboard</h2>
<% if (user.isAdmin) { %>
<p>Admin Panel</p>
<% } %>
<%# This is a comment %>
<p>Welcome back, <%- user.name %></p>
<span>Last login: <%= formatDate(user.lastLogin) %></span>
</div>
`;
const result = await loader.pull("en", input);
expect(result).toHaveProperty("text_0");
expect(result).toHaveProperty("text_1");
expect(Object.keys(result).length).toBeGreaterThan(0);
});
it("should handle empty input", async () => {
const result = await loader.pull("en", "");
expect(result).toEqual({});
});
it("should handle input with only EJS tags", async () => {
const input = "<%= variable %><% if (condition) { %><% } %>";
const result = await loader.pull("en", input);
expect(result).toEqual({});
});
it("should handle mixed content", async () => {
const input = `
Welcome <%= user.name %>!
<% for (let i = 0; i < items.length; i++) { %>
Item: <%= items[i].name %>
<% } %>
Thank you for visiting.
`;
const result = await loader.pull("en", input);
expect(Object.keys(result).length).toBeGreaterThan(0);
expect(
Object.values(result).some((text) => text.includes("Welcome")),
).toBe(true);
expect(
Object.values(result).some((text) => text.includes("Thank you")),
).toBe(true);
});
});
describe("push", () => {
it("should reconstruct EJS template with translated content", async () => {
const originalInput = `<h1>Welcome</h1><p>Hello <%= name %></p>`;
// First pull to get the structure
const pulled = await loader.pull("en", originalInput);
// Static translated data object based on actual loader behavior
const translated = {
text_0: "Bienvenido",
text_1: "Hola",
};
const result = await loader.push("es", translated);
// Test against the expected reconstructed string
const expectedOutput = `<h1>Bienvenido</h1><p>Hola <%= name %></p>`;
expect(result).toBe(expectedOutput);
});
it("should handle complex EJS templates", async () => {
const originalInput = `<h2>Dashboard</h2><% if (user) { %><p>Welcome</p><% } %>`;
const pulled = await loader.pull("en", originalInput);
// Static translated data object
const translated = {
text_0: "Tablero",
text_1: "Bienvenido",
};
const result = await loader.push("es", translated);
// Test against the expected reconstructed string
const expectedOutput = `<h2>Tablero</h2><% if (user) { %><p>Bienvenido</p><% } %>`;
expect(result).toBe(expectedOutput);
});
it("should handle missing original input", async () => {
const translated = {
text_0: "Hello World",
text_1: "This is a test",
};
const result = await loader.push("es", translated);
expect(result).toContain("Hello World");
expect(result).toContain("This is a test");
});
});
describe("round trip", () => {
it("should maintain EJS functionality after round trip", async () => {
const originalInput = `
<h1>Welcome <%= title %></h1>
<% if (showMessage) { %>
<p>Hello <%= user.name %>, you have <%= count %> new messages.</p>
<% } %>
<ul>
<% items.forEach(function(item) { %>
<li><%= item.name %> - $<%= item.price %></li>
<% }); %>
</ul>
<footer>Contact us at [email protected]</footer>
`;
// Pull original content
const pulled = await loader.pull("en", originalInput);
// Push back without translation (should be identical)
const reconstructed = await loader.push("en", pulled);
// Verify EJS tags are preserved
expect(reconstructed).toContain("<%= title %>");
expect(reconstructed).toContain("<% if (showMessage) { %>");
expect(reconstructed).toContain("<%= user.name %>");
expect(reconstructed).toContain("<%= count %>");
expect(reconstructed).toContain("<% items.forEach(function(item) { %>");
expect(reconstructed).toContain("<%= item.name %>");
expect(reconstructed).toContain("<%= item.price %>");
expect(reconstructed).toContain("<% }); %>");
expect(reconstructed).toContain("Contact us at [email protected]");
});
});
});
```
--------------------------------------------------------------------------------
/packages/cli/src/cli/loaders/mdx2/code-placeholder.ts:
--------------------------------------------------------------------------------
```typescript
import { ILoader } from "../_types";
import { createLoader } from "../_utils";
import { md5 } from "../../utils/md5";
import _ from "lodash";
const fenceRegex = /([ \t]*)(^>\s*)?```([\s\S]*?)```/gm;
const inlineCodeRegex = /(?<!`)`([^`\r\n]+?)`(?!`)/g;
// Matches markdown image tags, with optional alt text & parenthesis URL, possibly inside blockquotes
// Captures patterns like  or , with optional leading '> ' for blockquotes
const imageRegex =
/([ \t]*)(^>\s*)?!\[[^\]]*?\]\(([^()]*(\([^()]*\)[^()]*)*)\)/gm;
/**
* Ensures that markdown image tags are surrounded by blank lines (\n\n) so that they are properly
* treated as separate blocks during subsequent processing and serialization.
*
* Behaviour mirrors `ensureTrailingFenceNewline` logic for code fences:
* • If an image tag is already inside a blockquote (starts with `>` after trimming) we leave it untouched.
* • Otherwise we add two newlines before and after the image tag, then later collapse multiple
* consecutive blank lines back to exactly one separation using lodash chain logic.
*/
function ensureSurroundingImageNewlines(_content: string) {
let found = false;
let content = _content;
let workingContent = content;
do {
found = false;
const matches = workingContent.match(imageRegex);
if (matches) {
const match = matches[0];
const replacement = match.trim().startsWith(">")
? match
: `\n\n${match}\n\n`;
content = content.replaceAll(match, () => replacement);
workingContent = workingContent.replaceAll(match, "");
found = true;
}
} while (found);
content = _.chain(content)
.split("\n\n")
.map((section) => _.trim(section, "\n"))
.filter(Boolean)
.join("\n\n")
.value();
return content;
}
function ensureTrailingFenceNewline(_content: string) {
let found = false;
let content = _content;
let workingContent = content;
do {
found = false;
const matches = workingContent.match(fenceRegex);
if (matches) {
const match = matches[0];
const replacement = match.trim().startsWith(">")
? match
: `\n\n${match}\n\n`;
content = content.replaceAll(match, () => replacement);
workingContent = workingContent.replaceAll(match, "");
found = true;
}
} while (found);
content = _.chain(content)
.split("\n\n")
.map((section) => _.trim(section, "\n"))
.filter(Boolean)
.join("\n\n")
.value();
return content;
}
// Helper that replaces code (block & inline) with stable placeholders and returns
// both the transformed content and the placeholder → original mapping so it can
// later be restored. Extracted so that we can reuse the exact same logic in both
// `pull` and `push` phases (e.g. to recreate the mapping from `originalInput`).
function extractCodePlaceholders(content: string): {
content: string;
codePlaceholders: Record<string, string>;
} {
let finalContent = content;
finalContent = ensureTrailingFenceNewline(finalContent);
finalContent = ensureSurroundingImageNewlines(finalContent);
const codePlaceholders: Record<string, string> = {};
const codeBlockMatches = finalContent.matchAll(fenceRegex);
for (const match of codeBlockMatches) {
const codeBlock = match[0];
const codeBlockHash = md5(codeBlock);
const placeholder = `---CODE-PLACEHOLDER-${codeBlockHash}---`;
codePlaceholders[placeholder] = codeBlock;
const replacement = codeBlock.trim().startsWith(">")
? `> ${placeholder}`
: `${placeholder}`;
finalContent = finalContent.replace(codeBlock, () => replacement);
}
const inlineCodeMatches = finalContent.matchAll(inlineCodeRegex);
for (const match of inlineCodeMatches) {
const inlineCode = match[0];
const inlineCodeHash = md5(inlineCode);
const placeholder = `---INLINE-CODE-PLACEHOLDER-${inlineCodeHash}---`;
codePlaceholders[placeholder] = inlineCode;
const replacement = placeholder;
finalContent = finalContent.replace(inlineCode, () => replacement);
}
return {
content: finalContent,
codePlaceholders,
};
}
export default function createMdxCodePlaceholderLoader(): ILoader<
string,
string
> {
// Keep a global registry of all placeholders we've ever created
// This solves the state synchronization issue
const globalPlaceholderRegistry: Record<string, string> = {};
return createLoader({
async pull(locale, input) {
const response = extractCodePlaceholders(input);
// Register all placeholders we create so we can use them later
Object.assign(globalPlaceholderRegistry, response.codePlaceholders);
return response.content;
},
async push(locale, data, originalInput, originalLocale, pullInput) {
const sourceInfo = extractCodePlaceholders(originalInput ?? "");
const currentInfo = extractCodePlaceholders(pullInput ?? "");
// Use the global registry to ensure all placeholders can be replaced,
// including those from previous pulls that are no longer in current state
const codePlaceholders = _.merge(
sourceInfo.codePlaceholders,
currentInfo.codePlaceholders,
globalPlaceholderRegistry, // Include ALL placeholders ever created
);
let result = data;
for (const [placeholder, original] of Object.entries(codePlaceholders)) {
const replacement = original.startsWith(">")
? _.trimStart(original, "> ")
: original;
// Use function replacer to avoid special $ character handling
// When using a string, $ has special meaning (e.g., $` inserts text before match)
result = result.replaceAll(placeholder, () => replacement);
}
return result;
},
});
}
```
--------------------------------------------------------------------------------
/readme/pt-BR.md:
--------------------------------------------------------------------------------
```markdown
<p align="center">
<a href="https://lingo.dev">
<img
src="https://raw.githubusercontent.com/lingodotdev/lingo.dev/main/content/banner.compiler.png"
width="100%"
alt="Lingo.dev"
/>
</a>
</p>
<p align="center">
<strong>
⚡ Lingo.dev - kit de ferramentas i18n de código aberto, alimentado por IA
para localização instantânea com LLMs.
</strong>
</p>
<br />
<p align="center">
<a href="https://lingo.dev/compiler">Lingo.dev Compiler</a> •
<a href="https://lingo.dev/cli">Lingo.dev CLI</a> •
<a href="https://lingo.dev/ci">Lingo.dev CI/CD</a> •
<a href="https://lingo.dev/sdk">Lingo.dev SDK</a>
</p>
<p align="center">
<a href="https://github.com/lingodotdev/lingo.dev/actions/workflows/release.yml">
<img
src="https://github.com/lingodotdev/lingo.dev/actions/workflows/release.yml/badge.svg"
alt="Release"
/>
</a>
<a href="https://github.com/lingodotdev/lingo.dev/blob/main/LICENSE.md">
<img
src="https://img.shields.io/github/license/lingodotdev/lingo.dev"
alt="Licença"
/>
</a>
<a href="https://github.com/lingodotdev/lingo.dev/commits/main">
<img
src="https://img.shields.io/github/last-commit/lingodotdev/lingo.dev"
alt="Último Commit"
/>
</a>
</p>
---
## Conheça o Compiler 🆕
**Lingo.dev Compiler** é um middleware compilador gratuito e de código aberto, projetado para tornar qualquer aplicativo React multilíngue durante o tempo de compilação sem exigir alterações nos componentes React existentes.
---CODE-PLACEHOLDER-f159f7253d409892d00e70ee045902a5---
Execute `next build` e veja os pacotes em espanhol e francês surgirem ✨
[Leia a documentação →](https://lingo.dev/compiler) para o guia completo.
---
### O que há neste repositório?
| Ferramenta | Resumo | Documentação |
| ------------ | ------------------------------------------------------------------------------------------- | --------------------------------------- |
| **Compiler** | Localização React em tempo de compilação | [/compiler](https://lingo.dev/compiler) |
| **CLI** | Localização com um único comando para aplicativos web e mobile, JSON, YAML, markdown e mais | [/cli](https://lingo.dev/cli) |
| **CI/CD** | Auto-commit de traduções a cada push + criação de pull requests se necessário | [/ci](https://lingo.dev/ci) |
| **SDK** | Tradução em tempo real para conteúdo gerado pelo usuário | [/sdk](https://lingo.dev/sdk) |
Abaixo estão os destaques de cada um 👇
---
### ⚡️ Lingo.dev CLI
Traduza código e conteúdo diretamente do seu terminal.
---CODE-PLACEHOLDER-a4836309dda7477e1ba399e340828247---
Ele cria uma impressão digital de cada string, armazena resultados em cache e apenas retraduz o que foi alterado.
[Leia a documentação →](https://lingo.dev/cli)
---
### 🔄 Lingo.dev CI/CD
Entregue traduções perfeitas automaticamente.
```yaml
# .github/workflows/i18n.yml
name: Lingo.dev i18n
on: [push]
jobs:
i18n:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: lingodotdev/lingo.dev@main
with:
api-key: ${{ secrets.LINGODOTDEV_API_KEY }}
```
Mantém seu repositório verde e seu produto multilíngue sem etapas manuais.
[Leia a documentação →](https://lingo.dev/ci)
---
### 🧩 SDK Lingo.dev
Tradução instantânea por requisição para conteúdo dinâmico.
---CODE-PLACEHOLDER-c50e1e589a70e31dd2dde95be8da6ddf---
Perfeito para chat, comentários de usuários e outros fluxos em tempo real.
[Leia a documentação →](https://lingo.dev/sdk)
```ts
import { LingoDotDevEngine } from "lingo.dev/sdk";
const lingoDotDev = new LingoDotDevEngine({
apiKey: "your-api-key-here",
});
const content = {
greeting: "Hello",
farewell: "Goodbye",
message: "Welcome to our platform",
};
const translated = await lingoDotDev.localizeObject(content, {
sourceLocale: "en",
targetLocale: "es",
});
// Returns: { greeting: "Hola", farewell: "Adiós", message: "Bienvenido a nuestra plataforma" }
```
## 🤝 Comunidade
Somos orientados pela comunidade e adoramos contribuições!
- Tem uma ideia? [Abra uma issue](https://github.com/lingodotdev/lingo.dev/issues)
- Quer corrigir algo? [Envie um PR](https://github.com/lingodotdev/lingo.dev/pulls)
- Precisa de ajuda? [Entre no nosso Discord](https://lingo.dev/go/discord)
## ⭐ Histórico de Estrelas
Se você gosta do que estamos fazendo, dê-nos uma ⭐ e ajude-nos a alcançar 3.000 estrelas! 🌟
[

](https://www.star-history.com/#lingodotdev/lingo.dev&Date)
## 🌐 Readme em outros idiomas
[English](https://github.com/lingodotdev/lingo.dev) • [中文](/readme/zh-Hans.md) • [日本語](/readme/ja.md) • [한국어](/readme/ko.md) • [Español](/readme/es.md) • [Français](/readme/fr.md) • [Русский](/readme/ru.md) • [Deutsch](/readme/de.md) • [Italiano](/readme/it.md) • [العربية](/readme/ar.md) • [हिन्दी](/readme/hi.md) • [বাংলা](/readme/bn.md) • [فارسی](/readme/fa.md)
Não vê seu idioma? Adicione-o ao [`i18n.json`](./i18n.json) e abra um PR!
## 🌐 Readme em outros idiomas
[English](https://github.com/lingodotdev/lingo.dev) • [中文](/readme/zh-Hans.md) • [日本語](/readme/ja.md) • [한국어](/readme/ko.md) • [Español](/readme/es.md) • [Français](/readme/fr.md) • [Русский](/readme/ru.md) • [Українська](/readme/uk-UA.md) • [Deutsch](/readme/de.md) • [Italiano](/readme/it.md) • [العربية](/readme/ar.md) • [עברית](/readme/he.md) • [हिन्दी](/readme/hi.md) • [বাংলা](/readme/bn.md) • [فارسی](/readme/fa.md)
Não vê seu idioma? Adicione-o ao [`i18n.json`](./i18n.json) e abra um PR!
```
--------------------------------------------------------------------------------
/readme/pl.md:
--------------------------------------------------------------------------------
```markdown
<p align="center">
<a href="https://lingo.dev">
<img
src="https://raw.githubusercontent.com/lingodotdev/lingo.dev/main/content/banner.compiler.png"
width="100%"
alt="Lingo.dev"
/>
</a>
</p>
<p align="center">
<strong>
⚡ Lingo.dev - otwartoźródłowe, wspierane przez AI narzędzie i18n do
natychmiastowej lokalizacji z wykorzystaniem LLM.
</strong>
</p>
<br />
<p align="center">
<a href="https://lingo.dev/compiler">Lingo.dev Compiler</a> •
<a href="https://lingo.dev/cli">Lingo.dev CLI</a> •
<a href="https://lingo.dev/ci">Lingo.dev CI/CD</a> •
<a href="https://lingo.dev/sdk">Lingo.dev SDK</a>
</p>
<p align="center">
<a href="https://github.com/lingodotdev/lingo.dev/actions/workflows/release.yml">
<img
src="https://github.com/lingodotdev/lingo.dev/actions/workflows/release.yml/badge.svg"
alt="Release"
/>
</a>
<a href="https://github.com/lingodotdev/lingo.dev/blob/main/LICENSE.md">
<img
src="https://img.shields.io/github/license/lingodotdev/lingo.dev"
alt="Licencja"
/>
</a>
<a href="https://github.com/lingodotdev/lingo.dev/commits/main">
<img
src="https://img.shields.io/github/last-commit/lingodotdev/lingo.dev"
alt="Ostatni commit"
/>
</a>
</p>
---
## Poznaj Compiler 🆕
**Lingo.dev Compiler** to darmowe, otwartoźródłowe oprogramowanie pośredniczące (middleware), zaprojektowane, aby uczynić każdą aplikację React wielojęzyczną na etapie budowania, bez konieczności wprowadzania zmian w istniejących komponentach React.
Zainstaluj raz:
```bash
npm install lingo.dev
```
Włącz w swojej konfiguracji budowania:
```js
import lingoCompiler from "lingo.dev/compiler";
const existingNextConfig = {};
export default lingoCompiler.next({
sourceLocale: "en",
targetLocales: ["es", "fr"],
})(existingNextConfig);
```
Uruchom `next build` i zobacz, jak pojawiają się pakiety w języku hiszpańskim i francuskim ✨
[Przeczytaj dokumentację →](https://lingo.dev/compiler), aby uzyskać pełny przewodnik, oraz [Dołącz do naszego Discorda](https://lingo.dev/go/discord), aby uzyskać pomoc w konfiguracji.
---
### Co zawiera to repozytorium?
| Narzędzie | TL;DR | Dokumentacja |
| ------------ | ----------------------------------------------------------------------------------------------------- | --------------------------------------- |
| **Compiler** | Lokalizacja React na etapie budowania | [/compiler](https://lingo.dev/compiler) |
| **CLI** | Lokalizacja aplikacji webowych i mobilnych, JSON, YAML, markdown i więcej | [/cli](https://lingo.dev/cli) |
| **CI/CD** | Automatyczne zatwierdzanie tłumaczeń przy każdym pushu + tworzenie pull requestów, jeśli to konieczne | [/ci](https://lingo.dev/ci) |
| **SDK** | Tłumaczenie w czasie rzeczywistym dla treści generowanych przez użytkowników | [/sdk](https://lingo.dev/sdk) |
Poniżej znajdziesz szybkie informacje o każdym z nich 👇
---
### ⚡️ Lingo.dev CLI
Tłumacz kod i treści bezpośrednio z terminala.
```bash
npx lingo.dev@latest run
```
Odciska każdy ciąg znaków, zapisuje wyniki w pamięci podręcznej i tłumaczy ponownie tylko to, co się zmieniło.
[Przejdź do dokumentacji →](https://lingo.dev/cli), aby dowiedzieć się, jak to skonfigurować.
---
### 🔄 Lingo.dev CI/CD
Automatyczne dostarczanie perfekcyjnych tłumaczeń.
```yaml
# .github/workflows/i18n.yml
name: Lingo.dev i18n
on: [push]
jobs:
i18n:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: lingodotdev/lingo.dev@main
with:
api-key: ${{ secrets.LINGODOTDEV_API_KEY }}
```
Utrzymuje repozytorium w dobrym stanie i produkt wielojęzyczny bez ręcznych kroków.
[Przeczytaj dokumentację →](https://lingo.dev/ci)
---
### 🧩 Lingo.dev SDK
Natychmiastowe tłumaczenie na żądanie dla dynamicznych treści.
```ts
import { LingoDotDevEngine } from "lingo.dev/sdk";
const lingoDotDev = new LingoDotDevEngine({
apiKey: "your-api-key-here",
});
const content = {
greeting: "Hello",
farewell: "Goodbye",
message: "Welcome to our platform",
};
const translated = await lingoDotDev.localizeObject(content, {
sourceLocale: "en",
targetLocale: "es",
});
// Returns: { greeting: "Hola", farewell: "Adiós", message: "Bienvenido a nuestra plataforma" }
```
Idealne do czatów, komentarzy użytkowników i innych procesów w czasie rzeczywistym.
[Przeczytaj dokumentację →](https://lingo.dev/sdk)
---
## 🤝 Społeczność
Jesteśmy napędzani przez społeczność i uwielbiamy wkład innych!
- Masz pomysł? [Otwórz zgłoszenie](https://github.com/lingodotdev/lingo.dev/issues)
- Chcesz coś naprawić? [Wyślij PR](https://github.com/lingodotdev/lingo.dev/pulls)
- Potrzebujesz pomocy? [Dołącz do naszego Discorda](https://lingo.dev/go/discord)
## ⭐ Historia gwiazdek
Jeśli podoba Ci się to, co robimy, daj nam ⭐ i pomóż nam osiągnąć 3 000 gwiazdek! 🌟
[

](https://www.star-history.com/#lingodotdev/lingo.dev&Date)
## 🌐 Readme w innych językach
[English](https://github.com/lingodotdev/lingo.dev) • [中文](/readme/zh-Hans.md) • [日本語](/readme/ja.md) • [한국어](/readme/ko.md) • [Español](/readme/es.md) • [Français](/readme/fr.md) • [Русский](/readme/ru.md) • [Українська](/readme/uk-UA.md) • [Deutsch](/readme/de.md) • [Italiano](/readme/it.md) • [العربية](/readme/ar.md) • [עברית](/readme/he.md) • [हिन्दी](/readme/hi.md) • [বাংলা](/readme/bn.md) • [فارسی](/readme/fa.md)
Nie widzisz swojego języka? Dodaj go do [`i18n.json`](./i18n.json) i otwórz PR!
```
--------------------------------------------------------------------------------
/packages/compiler/src/lib/lcp/index.ts:
--------------------------------------------------------------------------------
```typescript
import * as fs from "fs";
import _ from "lodash";
import { LCPFile, LCPSchema, LCPScope } from "./schema";
import * as path from "path";
import { LCP_DICTIONARY_FILE_NAME } from "../../_const";
import dedent from "dedent";
const LCP_FILE_NAME = "meta.json";
export class LCP {
private constructor(
private readonly filePath: string,
public readonly data: LCPSchema = {
version: 0.1,
},
) {}
public static ensureFile(params: { sourceRoot: string; lingoDir: string }) {
const filePath = path.resolve(
process.cwd(),
params.sourceRoot,
params.lingoDir,
LCP_FILE_NAME,
);
if (!fs.existsSync(filePath)) {
const dir = path.dirname(filePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(filePath, "{}");
try {
fs.rmdirSync(path.resolve(process.cwd(), ".next"), {
recursive: true,
});
} catch (error) {
// Ignore errors if directory doesn't exist
}
throw new Error(dedent`
⚠️ Lingo.dev Compiler detected missing meta.json file in lingo directory.
Please restart the build / watch command to regenerate all Lingo.dev Compiler files.
`);
}
}
public static getInstance(params: {
sourceRoot: string;
lingoDir: string;
}): LCP {
const filePath = path.resolve(
process.cwd(),
params.sourceRoot,
params.lingoDir,
LCP_FILE_NAME,
);
if (fs.existsSync(filePath)) {
return new LCP(filePath, JSON.parse(fs.readFileSync(filePath, "utf8")));
}
return new LCP(filePath);
}
// wait until LCP file stops updating
// this ensures all files were transformed before loading / translating dictionaries
public static async ready(params: {
sourceRoot: string;
lingoDir: string;
isDev: boolean;
}): Promise<void> {
if (params.isDev) {
LCP.ensureFile(params);
}
const filePath = path.resolve(
process.cwd(),
params.sourceRoot,
params.lingoDir,
LCP_FILE_NAME,
);
if (fs.existsSync(filePath)) {
const stats = fs.statSync(filePath);
if (Date.now() - stats.mtimeMs > 1500) {
return;
}
}
return new Promise((resolve) => {
setTimeout(() => {
LCP.ready(params).then(resolve);
}, 750);
});
}
resetScope(fileKey: string, scopeKey: string): this {
if (
!_.isObject(
_.get(this.data, ["files" satisfies keyof LCPSchema, fileKey]),
)
) {
_.set(this.data, ["files" satisfies keyof LCPSchema, fileKey], {});
}
_.set(
this.data,
[
"files" satisfies keyof LCPSchema,
fileKey,
"scopes" satisfies keyof LCPFile,
scopeKey,
],
{},
);
return this;
}
setScopeType(
fileKey: string,
scopeKey: string,
type: "element" | "attribute",
): this {
return this._setScopeField(fileKey, scopeKey, "type", type);
}
setScopeContext(fileKey: string, scopeKey: string, context: string): this {
return this._setScopeField(fileKey, scopeKey, "context", context);
}
setScopeHash(fileKey: string, scopeKey: string, hash: string): this {
return this._setScopeField(fileKey, scopeKey, "hash", hash);
}
setScopeSkip(fileKey: string, scopeKey: string, skip: boolean): this {
return this._setScopeField(fileKey, scopeKey, "skip", skip);
}
setScopeOverrides(
fileKey: string,
scopeKey: string,
overrides: Record<string, string>,
): this {
return this._setScopeField(fileKey, scopeKey, "overrides", overrides);
}
setScopeContent(fileKey: string, scopeKey: string, content: string): this {
return this._setScopeField(fileKey, scopeKey, "content", content);
}
toJSON() {
const files = _(this.data?.files)
.mapValues((file: any, fileName: string) => {
return {
...file,
scopes: _(file?.scopes).toPairs().sortBy([0]).fromPairs().value(),
};
})
.toPairs()
.sortBy([0])
.fromPairs()
.value();
return { ...this.data, files };
}
toString() {
return JSON.stringify(this.toJSON(), null, 2) + "\n";
}
save() {
const hasChanges =
!fs.existsSync(this.filePath) ||
fs.readFileSync(this.filePath, "utf8") !== this.toString();
if (hasChanges) {
const dir = path.dirname(this.filePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(this.filePath, this.toString());
this._triggerLCPReload();
}
}
private _triggerLCPReload() {
const dir = path.dirname(this.filePath);
const filePath = path.resolve(dir, LCP_DICTIONARY_FILE_NAME);
if (fs.existsSync(filePath)) {
try {
const now = Math.floor(Date.now() / 1000); // Convert to seconds
fs.utimesSync(filePath, now, now);
} catch (error: any) {
// Non-critical operation - timestamp update is just for triggering reload
if (error?.code === "EINVAL") {
console.warn(
dedent`
⚠️ Lingo: Auto-reload disabled - system blocks Node.js timestamp updates.
💡 Fix: Adjust security settings to allow Node.js file modifications.
⚡ Workaround: Manually refresh browser after translation changes.
💬 Need help? Join our Discord: https://lingo.dev/go/discord.
`,
);
}
}
}
}
private _setScopeField<K extends keyof LCPScope>(
fileKey: string,
scopeKey: string,
field: K,
value: LCPScope[K],
): this {
_.set(
this.data,
[
"files" satisfies keyof LCPSchema,
fileKey,
"scopes" satisfies keyof LCPFile,
scopeKey,
field,
],
value,
);
return this;
}
}
```
--------------------------------------------------------------------------------
/packages/cli/src/cli/cmd/run/index.ts:
--------------------------------------------------------------------------------
```typescript
import { Command } from "interactive-commander";
import { exec } from "child_process";
import path from "path";
import { fileURLToPath } from "url";
import os from "os";
import setup from "./setup";
import plan from "./plan";
import execute from "./execute";
import watch from "./watch";
import { CmdRunContext, flagsSchema } from "./_types";
import frozen from "./frozen";
import {
renderClear,
renderSpacer,
renderBanner,
renderHero,
pauseIfDebug,
renderSummary,
} from "../../utils/ui";
import trackEvent from "../../utils/observability";
import { determineAuthId } from "./_utils";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
function playSound(type: "success" | "failure") {
const platform = os.platform();
return new Promise<void>((resolve) => {
const assetDir = path.join(__dirname, "../assets");
const soundFiles = [path.join(assetDir, `${type}.mp3`)];
let command = "";
if (platform === "linux") {
command = soundFiles
.map(
(file) =>
`mpg123 -q "${file}" 2>/dev/null || aplay "${file}" 2>/dev/null`,
)
.join(" || ");
} else if (platform === "darwin") {
command = soundFiles.map((file) => `afplay "${file}"`).join(" || ");
} else if (platform === "win32") {
command = `powershell -c "try { (New-Object Media.SoundPlayer '${soundFiles[1]}').PlaySync() } catch { Start-Process -FilePath '${soundFiles[0]}' -WindowStyle Hidden -Wait }"`;
} else {
command = soundFiles
.map(
(file) =>
`aplay "${file}" 2>/dev/null || afplay "${file}" 2>/dev/null`,
)
.join(" || ");
}
exec(command, () => {
resolve();
});
setTimeout(resolve, 3000);
});
}
export default new Command()
.command("run")
.description("Run localization pipeline")
.helpOption("-h, --help", "Show help")
.option(
"--source-locale <source-locale>",
"Override the source locale from i18n.json for this run",
)
.option(
"--target-locale <target-locale>",
"Limit processing to the listed target locale codes from i18n.json. Repeat the flag to include multiple locales. Defaults to all configured target locales",
(val: string, prev: string[]) => (prev ? [...prev, val] : [val]),
)
.option(
"--bucket <bucket>",
"Limit processing to specific bucket types defined in i18n.json (e.g., json, yaml, android). Repeat the flag to include multiple bucket types. Defaults to all configured buckets",
(val: string, prev: string[]) => (prev ? [...prev, val] : [val]),
)
.option(
"--file <file>",
"Filter bucket path pattern values by substring match. Examples: messages.json or locale/. Repeat to add multiple filters",
(val: string, prev: string[]) => (prev ? [...prev, val] : [val]),
)
.option(
"--key <key>",
"Filter keys by prefix matching on dot-separated paths. Example: auth.login to match all keys starting with auth.login. Repeat for multiple patterns",
(val: string, prev: string[]) =>
prev ? [...prev, encodeURIComponent(val)] : [encodeURIComponent(val)],
)
.option(
"--force",
"Force re-translation of all keys, bypassing change detection. Useful when you want to regenerate translations with updated AI models or translation settings",
)
.option(
"--frozen",
"Validate translations are up-to-date without making changes - fails if source files, target files, or lockfile are out of sync. Ideal for CI/CD to ensure translation consistency before deployment",
)
.option(
"--api-key <api-key>",
"Override API key from settings or environment variables",
)
.option("--debug", "Pause before processing to allow attaching a debugger.")
.option(
"--concurrency <concurrency>",
"Number of translation jobs to run concurrently. Higher values can speed up large translation batches but may increase memory usage. Defaults to 10 (maximum 10)",
(val: string) => parseInt(val),
)
.option(
"--watch",
"Watch source locale files continuously and retranslate automatically when files change",
)
.option(
"--debounce <milliseconds>",
"Delay in milliseconds after file changes before retranslating in watch mode. Defaults to 5000",
(val: string) => parseInt(val),
)
.option(
"--sound",
"Play audio feedback when translations complete (success or failure sounds)",
)
.action(async (args) => {
let authId: string | null = null;
try {
const ctx: CmdRunContext = {
flags: flagsSchema.parse(args),
config: null,
results: new Map(),
tasks: [],
localizer: null,
};
await pauseIfDebug(ctx.flags.debug);
await renderClear();
await renderSpacer();
await renderBanner();
await renderHero();
await renderSpacer();
await setup(ctx);
authId = await determineAuthId(ctx);
await trackEvent(authId, "cmd.run.start", {
config: ctx.config,
flags: ctx.flags,
});
await renderSpacer();
await plan(ctx);
await renderSpacer();
await frozen(ctx);
await renderSpacer();
await execute(ctx);
await renderSpacer();
await renderSummary(ctx.results);
await renderSpacer();
// Play sound after main tasks complete if sound flag is enabled
if (ctx.flags.sound) {
await playSound("success");
}
// If watch mode is enabled, start watching for changes
if (ctx.flags.watch) {
await watch(ctx);
}
await trackEvent(authId, "cmd.run.success", {
config: ctx.config,
flags: ctx.flags,
});
} catch (error: any) {
await trackEvent(authId || "unknown", "cmd.run.error", {});
// Play sad sound if sound flag is enabled
if (args.sound) {
await playSound("failure");
}
throw error;
}
});
```
--------------------------------------------------------------------------------
/packages/cli/src/cli/loaders/jsonc.spec.ts:
--------------------------------------------------------------------------------
```typescript
import { describe, expect, it } from "vitest";
import createJsoncLoader from "./jsonc";
describe("jsonc loader", () => {
it("pull should parse valid JSONC format with comments", async () => {
const loader = createJsoncLoader();
loader.setDefaultLocale("en");
const jsoncInput = `{
// Comments are allowed in JSONC
"hello": "Hello",
"world": "World", // Trailing comment
/* Block comment */
"nested": {
"key": "value"
}
}`;
const result = await loader.pull("en", jsoncInput);
expect(result).toEqual({
hello: "Hello",
world: "World",
nested: {
key: "value",
},
});
});
it("pull should parse JSONC with trailing commas", async () => {
const loader = createJsoncLoader();
loader.setDefaultLocale("en");
const jsoncInput = `{
"hello": "Hello",
"world": "World",
"array": [
"item1",
"item2",
],
}`;
const result = await loader.pull("en", jsoncInput);
expect(result).toEqual({
hello: "Hello",
world: "World",
array: ["item1", "item2"],
});
});
it("pull should parse regular JSON as valid JSONC", async () => {
const loader = createJsoncLoader();
loader.setDefaultLocale("en");
const jsonInput = '{"hello": "Hello", "world": "World"}';
const result = await loader.pull("en", jsonInput);
expect(result).toEqual({
hello: "Hello",
world: "World",
});
});
it("pull should handle empty input", async () => {
const loader = createJsoncLoader();
loader.setDefaultLocale("en");
const result = await loader.pull("en", "");
expect(result).toEqual({});
});
it("pull should handle null/undefined input", async () => {
const loader = createJsoncLoader();
loader.setDefaultLocale("en");
const result = await loader.pull("en", null as any);
expect(result).toEqual({});
});
it("pull should handle JSONC with mixed comment styles", async () => {
const loader = createJsoncLoader();
loader.setDefaultLocale("en");
const jsoncInput = `{
// Line comment
"title": "Hello",
/*
* Multi-line
* block comment
*/
"description": "World",
"version": "1.0.0" // Another line comment
}`;
const result = await loader.pull("en", jsoncInput);
expect(result).toEqual({
title: "Hello",
description: "World",
version: "1.0.0",
});
});
it("pull should throw error for invalid JSONC", async () => {
const loader = createJsoncLoader();
loader.setDefaultLocale("en");
const invalidInput = `{
"hello": "Hello"
"world": "World" // missing comma
invalid: syntax
}`;
await expect(loader.pull("en", invalidInput)).rejects.toThrow(
"Failed to parse JSONC",
);
});
it("push should serialize data to JSON format", async () => {
const loader = createJsoncLoader();
loader.setDefaultLocale("en");
// Need to call pull first to initialize the loader state
await loader.pull("en", "{}");
const data = {
hello: "Hello",
world: "World",
nested: {
key: "value",
},
};
const result = await loader.push("en", data);
const expectedOutput = `{
"hello": "Hello",
"world": "World",
"nested": {
"key": "value"
}
}`;
expect(result).toBe(expectedOutput);
});
it("push should handle empty object", async () => {
const loader = createJsoncLoader();
loader.setDefaultLocale("en");
// Need to call pull first to initialize the loader state
await loader.pull("en", "{}");
const result = await loader.push("en", {});
expect(result).toBe("{}");
});
it("push should handle complex nested data", async () => {
const loader = createJsoncLoader();
loader.setDefaultLocale("en");
// Need to call pull first to initialize the loader state
await loader.pull("en", "{}");
const data = {
strings: ["hello", "world"],
numbers: [1, 2, 3],
nested: {
deep: {
key: "value",
},
},
};
const result = await loader.push("en", data);
// Parse the result back to verify it's valid JSON
const parsed = JSON.parse(result);
expect(parsed).toEqual(data);
});
it("pull should handle JSONC with Unicode escape sequences", async () => {
const loader = createJsoncLoader();
loader.setDefaultLocale("en");
const jsoncInput = `{
// Unicode characters
"unicode": "\\u0048\\u0065\\u006c\\u006c\\u006f",
"emoji": "🚀"
}`;
const result = await loader.pull("en", jsoncInput);
expect(result).toEqual({
unicode: "Hello",
emoji: "🚀",
});
});
it("pullHints should extract comments from JSONC", async () => {
const loader = createJsoncLoader();
loader.setDefaultLocale("en");
const jsoncInput = `{
"key1": "value1", // This is a comment for key1
"key2": "value2" /* This is a comment for key2 */,
// This is a comment for key3
"key3": "value3",
/* This is a block comment for key4 */
"key4": "value4",
/*
This is a comment for key5
*/
"key5": "value5",
// This is a comment for key6
"key6": {
// This is a comment for key7
"key7": "value7"
}
}`;
// First call pull to initialize the loader state
await loader.pull("en", jsoncInput);
const comments = await loader.pullHints(jsoncInput);
expect(comments).toEqual({
key1: { hint: "This is a comment for key1" },
key2: { hint: "This is a comment for key2" },
key3: { hint: "This is a comment for key3" },
key4: { hint: "This is a block comment for key4" },
key5: { hint: "This is a comment for key5" },
key6: {
hint: "This is a comment for key6",
key7: { hint: "This is a comment for key7" },
},
});
});
});
```
--------------------------------------------------------------------------------
/packages/cli/src/cli/utils/buckets.ts:
--------------------------------------------------------------------------------
```typescript
import _ from "lodash";
import path from "path";
import { glob } from "glob";
import { CLIError } from "./errors";
import {
I18nConfig,
resolveOverriddenLocale,
BucketItem,
LocaleDelimiter,
} from "@lingo.dev/_spec";
import { bucketTypeSchema } from "@lingo.dev/_spec";
import Z from "zod";
type BucketConfig = {
type: Z.infer<typeof bucketTypeSchema>;
paths: Array<{ pathPattern: string; delimiter?: LocaleDelimiter }>;
injectLocale?: string[];
lockedKeys?: string[];
lockedPatterns?: string[];
ignoredKeys?: string[];
};
export function getBuckets(i18nConfig: I18nConfig) {
const result = Object.entries(i18nConfig.buckets).map(
([bucketType, bucketEntry]) => {
const includeItems = bucketEntry.include.map((item) =>
resolveBucketItem(item),
);
const excludeItems = bucketEntry.exclude?.map((item) =>
resolveBucketItem(item),
);
const config: BucketConfig = {
type: bucketType as Z.infer<typeof bucketTypeSchema>,
paths: extractPathPatterns(
i18nConfig.locale.source,
includeItems,
excludeItems,
),
};
if (bucketEntry.injectLocale) {
config.injectLocale = bucketEntry.injectLocale;
}
if (bucketEntry.lockedKeys) {
config.lockedKeys = bucketEntry.lockedKeys;
}
if (bucketEntry.lockedPatterns) {
config.lockedPatterns = bucketEntry.lockedPatterns;
}
if (bucketEntry.ignoredKeys) {
config.ignoredKeys = bucketEntry.ignoredKeys;
}
return config;
},
);
return result;
}
function extractPathPatterns(
sourceLocale: string,
include: BucketItem[],
exclude?: BucketItem[],
) {
const includedPatterns = include.flatMap((pattern) =>
expandPlaceholderedGlob(
pattern.path,
resolveOverriddenLocale(sourceLocale, pattern.delimiter),
).map((pathPattern) => ({
pathPattern,
delimiter: pattern.delimiter,
})),
);
const excludedPatterns = exclude?.flatMap((pattern) =>
expandPlaceholderedGlob(
pattern.path,
resolveOverriddenLocale(sourceLocale, pattern.delimiter),
).map((pathPattern) => ({
pathPattern,
delimiter: pattern.delimiter,
})),
);
const result = _.differenceBy(
includedPatterns,
excludedPatterns ?? [],
(item) => item.pathPattern,
);
return result;
}
// Windows path normalization helper function
function normalizePath(filepath: string): string {
const normalized = path.normalize(filepath);
// Ensure case consistency on Windows
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
}
// Path expansion
function expandPlaceholderedGlob(
_pathPattern: string,
sourceLocale: string,
): string[] {
const absolutePathPattern = path.resolve(_pathPattern);
const pathPattern = normalizePath(
path.relative(process.cwd(), absolutePathPattern),
);
if (pathPattern.startsWith("..")) {
throw new CLIError({
message: `Invalid path pattern: ${pathPattern}. Path pattern must be within the current working directory.`,
docUrl: "invalidPathPattern",
});
}
// Throw error if pathPattern contains "**" – we don't support recursive path patterns
if (pathPattern.includes("**")) {
throw new CLIError({
message: `Invalid path pattern: ${pathPattern}. Recursive path patterns are not supported.`,
docUrl: "invalidPathPattern",
});
}
// Break down path pattern into parts
const pathPatternChunks = pathPattern.split(path.sep);
// Find the index of the segment containing "[locale]"
const localeSegmentIndexes = pathPatternChunks.reduce(
(indexes, segment, index) => {
if (segment.includes("[locale]")) {
indexes.push(index);
}
return indexes;
},
[] as number[],
);
// substitute [locale] in pathPattern with sourceLocale
const sourcePathPattern = pathPattern.replaceAll(/\[locale\]/g, sourceLocale);
// Convert to Unix-style for Windows compatibility
const unixStylePattern = sourcePathPattern.replace(/\\/g, "/");
// get all files that match the sourcePathPattern
const sourcePaths = glob
.sync(unixStylePattern, {
follow: true,
withFileTypes: true,
windowsPathsNoEscape: true, // Windows path support
})
.filter((file) => file.isFile() || file.isSymbolicLink())
.map((file) => file.fullpath())
.map((fullpath) => normalizePath(path.relative(process.cwd(), fullpath)));
// transform each source file path back to [locale] placeholder paths
const placeholderedPaths = sourcePaths.map((sourcePath) => {
// Normalize path returned by glob for platform compatibility
const normalizedSourcePath = normalizePath(
sourcePath.replace(/\//g, path.sep),
);
const sourcePathChunks = normalizedSourcePath.split(path.sep);
localeSegmentIndexes.forEach((localeSegmentIndex) => {
// Find the position of the "[locale]" placeholder within the segment
const pathPatternChunk = pathPatternChunks[localeSegmentIndex];
const sourcePathChunk = sourcePathChunks[localeSegmentIndex];
const regexp = new RegExp(
"(" +
pathPatternChunk
.replaceAll(".", "\\.")
.replaceAll("*", ".*")
.replace("[locale]", `)${sourceLocale}(`) +
")",
);
const match = sourcePathChunk.match(regexp);
if (match) {
const [, prefix, suffix] = match;
const placeholderedSegment = prefix + "[locale]" + suffix;
sourcePathChunks[localeSegmentIndex] = placeholderedSegment;
}
});
const placeholderedPath = sourcePathChunks.join(path.sep);
return placeholderedPath;
});
// return the placeholdered paths
return placeholderedPaths;
}
function resolveBucketItem(bucketItem: string | BucketItem): BucketItem {
if (typeof bucketItem === "string") {
return { path: bucketItem, delimiter: null };
}
return bucketItem;
}
```
--------------------------------------------------------------------------------
/packages/compiler/src/lib/lcp/cache.ts:
--------------------------------------------------------------------------------
```typescript
import * as fs from "fs";
import * as path from "path";
import * as prettier from "prettier";
import { DictionaryCacheSchema, DictionarySchema, LCPSchema } from "./schema";
import _ from "lodash";
import { LCP_DICTIONARY_FILE_NAME } from "../../_const";
export interface LCPCacheParams {
sourceRoot: string;
lingoDir: string;
lcp: LCPSchema;
}
export class LCPCache {
// make sure the cache file exists, otherwise imports will fail
static ensureDictionaryFile(params: {
sourceRoot: string;
lingoDir: string;
}) {
const cachePath = this._getCachePath(params);
if (!fs.existsSync(cachePath)) {
const dir = path.dirname(cachePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(cachePath, "export default {};");
}
}
// read cache entries for given locale, validate entry hash from LCP schema
static readLocaleDictionary(
locale: string,
params: LCPCacheParams,
): DictionarySchema {
const cache = this._read(params);
const dictionary = this._extractLocaleDictionary(cache, locale, params.lcp);
return dictionary;
}
// write cache entries for given locale to existing cache file, use hash from LCP schema
static async writeLocaleDictionary(
dictionary: DictionarySchema,
params: LCPCacheParams,
): Promise<void> {
const currentCache = this._read(params);
const newCache = this._mergeLocaleDictionary(
currentCache,
dictionary,
params.lcp,
);
await this._write(newCache, params);
}
// merge dictionary with current cache, sort files, entries and locales to minimize diffs
private static _mergeLocaleDictionary(
currentCache: DictionaryCacheSchema,
dictionary: DictionarySchema,
lcp: LCPSchema,
): DictionaryCacheSchema {
const files = _(dictionary.files)
.mapValues((file, fileName) => ({
...file,
entries: _(file.entries)
.mapValues((entry, entryName) => {
// find if entry exists in current cache, it might contain some locales already
const cachedEntry =
_.get(currentCache, ["files", fileName, "entries", entryName]) ??
{};
const hash = _.get(lcp, [
"files",
fileName,
"scopes",
entryName,
"hash",
]);
// reuse existing cache entry if its hash matches LCP schema, ensures the cache is up to date
const cachedEntryContent =
cachedEntry.hash === hash ? cachedEntry.content : {};
// sorted by keys (locales) to minimize diffs
const content = _({
...cachedEntryContent,
[dictionary.locale]: entry,
})
.toPairs()
.sortBy([0])
.fromPairs()
.value();
return { content, hash };
})
.toPairs()
.sortBy([0])
.fromPairs()
.value(),
}))
.toPairs()
.sortBy([0])
.fromPairs()
.value();
const newCache = {
version: dictionary.version,
files,
};
return newCache;
}
// extract dictionary from cache for given locale, validate entry hash from LCP schema
private static _extractLocaleDictionary(
cache: DictionaryCacheSchema,
locale: string,
lcp: LCPSchema,
): DictionarySchema {
const findCachedEntry = (hash: string) => {
const cachedEntry = _(cache.files)
.flatMap((file) => _.values(file.entries))
.find((entry) => entry.hash === hash);
if (cachedEntry) {
return cachedEntry.content[locale];
}
return undefined;
};
const files = _(lcp.files)
.mapValues((file) => {
return {
entries: _(file.scopes)
.mapValues((entry) => {
return findCachedEntry(entry.hash);
})
.pickBy((value) => value !== undefined)
.value(),
};
})
.pickBy((file) => !_.isEmpty(file.entries))
.value();
const dictionary = {
version: cache.version,
locale,
files,
};
return dictionary;
}
// format with prettier
private static async _format(
cachedContent: string,
cachePath: string,
): Promise<string> {
try {
const config = await prettier.resolveConfig(cachePath);
const prettierOptions = {
...(config ?? {}),
parser: config?.parser ? config.parser : "typescript",
};
return await prettier.format(cachedContent, prettierOptions);
} catch (error) {
// prettier not configured or formatting failed
}
return cachedContent;
}
// write cache to file as JSON
private static async _write(
dictionaryCache: DictionaryCacheSchema,
params: LCPCacheParams,
) {
const cachePath = this._getCachePath(params);
const cache = `export default ${JSON.stringify(dictionaryCache, null, 2)};`;
const formattedCache = await this._format(cache, cachePath);
fs.writeFileSync(cachePath, formattedCache);
}
// read cache from file as JSON
private static _read(params: LCPCacheParams): DictionaryCacheSchema {
const cachePath = this._getCachePath(params);
if (!fs.existsSync(cachePath)) {
return {
version: 0.1,
files: {},
};
}
const jsObjectString = fs.readFileSync(cachePath, "utf8");
// Remove 'export default' and trailing semicolon before parsing
const cache = jsObjectString
.replace(/^export default/, "")
.replace(/;\s*$/, "");
// Use Function constructor to safely evaluate the object
// eslint-disable-next-line no-new-func
const obj = new Function(`return (${cache})`)();
return obj;
}
// get cache file path
private static _getCachePath(params: {
sourceRoot: string;
lingoDir: string;
}) {
return path.resolve(
process.cwd(),
params.sourceRoot,
params.lingoDir,
LCP_DICTIONARY_FILE_NAME,
);
}
}
```
--------------------------------------------------------------------------------
/demo/react-router-app/app/welcome/logo-dark.svg:
--------------------------------------------------------------------------------
```
<svg width="1080" height="174" viewBox="0 0 1080 174" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M231.527 86.9999C231.527 94.9642 228.297 102.173 223.067 107.387C217.837 112.606 210.614 115.835 202.634 115.835C194.654 115.835 187.43 119.059 182.206 124.278C176.977 129.498 173.741 136.707 173.741 144.671C173.741 152.635 170.51 159.844 165.281 165.058C160.051 170.277 152.828 173.507 144.847 173.507C136.867 173.507 129.644 170.277 124.42 165.058C119.19 159.844 115.954 152.635 115.954 144.671C115.954 136.707 119.19 129.498 124.42 124.278C129.644 119.059 136.867 115.835 144.847 115.835C152.828 115.835 160.051 112.606 165.281 107.387C170.51 102.173 173.741 94.9642 173.741 86.9999C173.741 71.0711 160.808 58.1643 144.847 58.1643C136.867 58.1643 129.644 54.9347 124.42 49.7155C119.19 44.502 115.954 37.2931 115.954 29.3287C115.954 21.3643 119.19 14.1555 124.42 8.93622C129.644 3.71698 136.867 0.493164 144.847 0.493164C160.808 0.493164 173.741 13.4 173.741 29.3287C173.741 37.2931 176.977 44.502 182.206 49.7155C187.43 54.9347 194.654 58.1643 202.634 58.1643C218.594 58.1643 231.527 71.0711 231.527 86.9999Z" fill="#F44250"/>
<path d="M115.954 86.9996C115.954 71.0742 103.018 58.1641 87.061 58.1641C71.1037 58.1641 58.1677 71.0742 58.1677 86.9996C58.1677 102.925 71.1037 115.835 87.061 115.835C103.018 115.835 115.954 102.925 115.954 86.9996Z" fill="white"/>
<path d="M58.1676 144.671C58.1676 128.745 45.2316 115.835 29.2743 115.835C13.317 115.835 0.381104 128.745 0.381104 144.671C0.381104 160.596 13.317 173.506 29.2743 173.506C45.2316 173.506 58.1676 160.596 58.1676 144.671Z" fill="white"/>
<path d="M289.314 144.671C289.314 128.745 276.378 115.835 260.42 115.835C244.463 115.835 231.527 128.745 231.527 144.671C231.527 160.596 244.463 173.506 260.42 173.506C276.378 173.506 289.314 160.596 289.314 144.671Z" fill="white"/>
<g clip-path="url(#clip0_202_2131)">
<path d="M562.482 173.247C524.388 173.247 498.363 147.49 498.363 110.468C498.363 73.4455 524.388 47.6885 562.482 47.6885C600.576 47.6885 626.869 73.7135 626.869 110.468C626.869 147.222 600.576 173.247 562.482 173.247ZM562.482 144.007C579.385 144.007 587.703 130.319 587.703 110.468C587.703 90.6168 579.385 76.9289 562.482 76.9289C545.579 76.9289 537.529 90.6168 537.529 110.468C537.529 130.319 545.311 144.007 562.482 144.007Z" fill="white"/>
<path d="M833.64 141.116C824.217 141.116 819.237 136.684 819.237 126.156V74.8983H851.928V47.7792H819.237V1.15527H791.75L786.1 26.1978C783.343 36.4805 780.82 42.822 773.897 46.0821C773.105 46.4506 771.129 46.9976 769.409 47.3884C768.014 47.701 766.596 47.8573 765.167 47.8573H752.338V47.9243H734.832C723.578 47.9243 714.445 57.0459 714.445 68.3111V111.552C714.445 130.599 707.199 142.668 692.719 142.668C678.238 142.668 672.868 133.279 672.868 116.375V47.9243H634.249V125.765C634.249 151.254 644.442 173.248 676.63 173.248C691.915 173.248 703.895 167.231 711.096 157.182C712.145 155.72 714.445 156.49 714.445 158.276V170.022H753.332V83.8412C753.332 78.8953 757.34 74.8871 762.286 74.8871H779.882V136.952C779.882 164.663 797.89 173.248 817.842 173.248C833.908 173.248 844.436 169.374 853.58 162.441V136.126C846.1 139.453 839.725 141.116 833.629 141.116H833.64Z" fill="white"/>
<path d="M981.561 130.865C975.387 157.962 954.197 173.258 923.07 173.258C885.243 173.258 858.415 150.18 858.415 112.354C858.415 74.5281 885.779 47.6992 922.266 47.6992C961.699 47.6992 982.365 74.796 982.365 107.263V113.884H896.509C894.555 135.711 909.382 144.017 924.409 144.017C937.829 144.017 946.136 138.915 950.434 127.918L981.561 130.865ZM945.075 94.9372C944.271 83.1361 936.757 75.8567 921.998 75.8567C906.434 75.8567 899.188 82.321 897.045 94.9372H945.064H945.075Z" fill="white"/>
<path d="M1076.24 85.7486C1070.06 82.2652 1064.17 80.9142 1055.85 80.9142C1039.75 80.9142 1029.02 90.0358 1029.02 110.691V170.02H990.393V47.9225H1029.02V64.3235C1029.02 65.4623 1030.54 65.8195 1031.05 64.8035C1036.68 53.5718 1047.91 44.707 1062.03 44.707C1069.27 44.707 1075.45 46.8507 1078.66 49.5414L1076.25 85.7597L1076.24 85.7486Z" fill="white"/>
<path d="M547.32 31.5345V23.9983H522.457V31.5345H515.378V2.23828H542.14C553.562 2.23828 554.365 2.95282 554.365 13.1239C554.365 17.4111 553.472 18.5611 551.329 19.6553L549.408 20.6378L551.317 21.6426C553.595 22.8372 554.365 23.2391 554.365 30.0273V31.5345H547.332H547.32ZM522.457 18.3601H547.32V7.88763H522.457V18.349V18.3601Z" fill="white"/>
<path d="M578.493 2.23828H610.826V7.90996H580.067V14.5083H610.011V19.2868H580.067V25.8963H610.837V31.501L578.504 31.5345C575.344 31.5345 572.787 28.9778 572.787 25.8293V7.95462C572.787 4.80617 575.344 2.24945 578.493 2.24945V2.23828Z" fill="white"/>
<path d="M655.562 31.5345L653.151 26.3429H633.746L631.335 31.5345H624.58L637.006 4.75034C637.71 3.22078 639.262 2.23828 640.936 2.23828H645.927C647.613 2.23828 649.154 3.22078 649.857 4.75034L662.283 31.5345H655.529H655.562ZM643.46 8.06627C642.712 8.06627 642.053 8.49053 641.729 9.17158L635.968 21.5756H650.94L645.19 9.17158C644.878 8.49053 644.208 8.06627 643.46 8.06627Z" fill="white"/>
<path d="M694.862 32.4153C676.05 32.4153 675.313 32.4153 675.313 16.8852C675.313 1.35505 676.05 1.36621 694.862 1.36621C711.721 1.36621 713.764 2.06959 714.244 10.5325H707.333V7.01556H682.168V26.766H707.333V23.2714H714.244C713.775 31.7119 711.721 32.4153 694.862 32.4153Z" fill="white"/>
<path d="M745.282 31.5345V7.02795H729.16V2.23828H768.147V7.02795H752.025V31.5345H745.282Z" fill="white"/>
<path d="M454.419 169.819C450.935 165.264 448.792 154.814 447.452 137.397C446.112 118.104 437.806 113.817 422.532 113.817H392.254V169.83H347.494V0.986328H432.715C476.391 0.986328 498.106 21.6187 498.106 54.5882C498.106 79.2399 482.833 95.3171 462.201 98.0078C479.618 101.491 489.8 111.405 491.675 130.966C494.087 156.154 494.891 163.656 500.518 169.819H454.419ZM424.676 78.704C443.969 78.704 453.615 73.8808 453.615 58.3395C453.615 44.6739 443.969 37.4392 424.676 37.4392H392.254V78.7152H424.676V78.704Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_202_2131">
<rect width="731.156" height="172.261" fill="white" transform="translate(347.494 0.986328)"/>
</clipPath>
</defs>
</svg>
```