This is page 8 of 20. Use http://codebase.md/lingodotdev/lingo.dev?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .changeset
│ ├── config.json
│ └── README.md
├── .claude
│ ├── agents
│ │ └── code-architect-reviewer.md
│ └── commands
│ ├── analyze-bucket-type.md
│ └── create-bucket-docs.md
├── .editorconfig
├── .github
│ ├── dependabot.yml
│ └── workflows
│ ├── docker.yml
│ ├── lingodotdev.yml
│ ├── pr-check.yml
│ ├── pr-lint.yml
│ └── release.yml
├── .gitignore
├── .husky
│ └── commit-msg
├── .npmrc
├── .prettierignore
├── .prettierrc
├── .vscode
│ ├── extensions.json
│ ├── launch.json
│ └── settings.json
├── action.yml
├── CLAUDE.md
├── CODE_OF_CONDUCT.md
├── commitlint.config.js
├── composer.json
├── content
│ ├── banner.compiler.png
│ ├── banner.dark.png
│ └── banner.launch.png
├── CONTRIBUTING.md
├── DEBUGGING.md
├── demo
│ ├── adonisjs
│ │ ├── .editorconfig
│ │ ├── .env.example
│ │ ├── .gitignore
│ │ ├── ace.js
│ │ ├── adonisrc.ts
│ │ ├── app
│ │ │ ├── exceptions
│ │ │ │ └── handler.ts
│ │ │ └── middleware
│ │ │ └── container_bindings_middleware.ts
│ │ ├── bin
│ │ │ ├── console.ts
│ │ │ ├── server.ts
│ │ │ └── test.ts
│ │ ├── CHANGELOG.md
│ │ ├── config
│ │ │ ├── app.ts
│ │ │ ├── bodyparser.ts
│ │ │ ├── cors.ts
│ │ │ ├── hash.ts
│ │ │ ├── inertia.ts
│ │ │ ├── logger.ts
│ │ │ ├── session.ts
│ │ │ ├── shield.ts
│ │ │ ├── static.ts
│ │ │ └── vite.ts
│ │ ├── eslint.config.js
│ │ ├── inertia
│ │ │ ├── app
│ │ │ │ ├── app.tsx
│ │ │ │ └── ssr.tsx
│ │ │ ├── css
│ │ │ │ └── app.css
│ │ │ ├── lingo
│ │ │ │ ├── dictionary.js
│ │ │ │ └── meta.json
│ │ │ ├── pages
│ │ │ │ ├── errors
│ │ │ │ │ ├── not_found.tsx
│ │ │ │ │ └── server_error.tsx
│ │ │ │ └── home.tsx
│ │ │ └── tsconfig.json
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── resources
│ │ │ └── views
│ │ │ └── inertia_layout.edge
│ │ ├── start
│ │ │ ├── env.ts
│ │ │ ├── kernel.ts
│ │ │ └── routes.ts
│ │ ├── tests
│ │ │ └── bootstrap.ts
│ │ ├── tsconfig.json
│ │ └── vite.config.ts
│ ├── next-app
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── eslint.config.mjs
│ │ ├── next.config.ts
│ │ ├── package.json
│ │ ├── postcss.config.mjs
│ │ ├── public
│ │ │ ├── file.svg
│ │ │ ├── globe.svg
│ │ │ ├── next.svg
│ │ │ ├── vercel.svg
│ │ │ └── window.svg
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── app
│ │ │ │ ├── client-component.tsx
│ │ │ │ ├── favicon.ico
│ │ │ │ ├── globals.css
│ │ │ │ ├── layout.tsx
│ │ │ │ ├── lingo-dot-dev.tsx
│ │ │ │ ├── page.tsx
│ │ │ │ └── test
│ │ │ │ └── page.tsx
│ │ │ ├── components
│ │ │ │ ├── hero-actions.tsx
│ │ │ │ ├── hero-subtitle.tsx
│ │ │ │ ├── hero-title.tsx
│ │ │ │ └── index.ts
│ │ │ └── lingo
│ │ │ ├── dictionary.js
│ │ │ └── meta.json
│ │ └── tsconfig.json
│ ├── react-router-app
│ │ ├── .dockerignore
│ │ ├── .gitignore
│ │ ├── app
│ │ │ ├── app.css
│ │ │ ├── lingo
│ │ │ │ ├── dictionary.js
│ │ │ │ └── meta.json
│ │ │ ├── root.tsx
│ │ │ ├── routes
│ │ │ │ ├── home.tsx
│ │ │ │ └── test.tsx
│ │ │ ├── routes.ts
│ │ │ └── welcome
│ │ │ ├── lingo-dot-dev.tsx
│ │ │ ├── logo-dark.svg
│ │ │ ├── logo-light.svg
│ │ │ └── welcome.tsx
│ │ ├── Dockerfile
│ │ ├── package.json
│ │ ├── public
│ │ │ └── favicon.ico
│ │ ├── react-router.config.ts
│ │ ├── README.md
│ │ ├── tsconfig.json
│ │ └── vite.config.ts
│ └── vite-project
│ ├── .gitignore
│ ├── CHANGELOG.md
│ ├── eslint.config.js
│ ├── index.html
│ ├── package.json
│ ├── public
│ │ └── vite.svg
│ ├── README.md
│ ├── src
│ │ ├── App.css
│ │ ├── App.tsx
│ │ ├── assets
│ │ │ └── react.svg
│ │ ├── components
│ │ │ └── test.tsx
│ │ ├── index.css
│ │ ├── lingo
│ │ │ ├── dictionary.js
│ │ │ └── meta.json
│ │ ├── lingo-dot-dev.tsx
│ │ ├── main.tsx
│ │ └── vite-env.d.ts
│ ├── tsconfig.app.json
│ ├── tsconfig.json
│ ├── tsconfig.node.json
│ └── vite.config.ts
├── Dockerfile
├── i18n.json
├── i18n.lock
├── integrations
│ └── directus
│ ├── .gitignore
│ ├── CHANGELOG.md
│ ├── docker-compose.yml
│ ├── Dockerfile
│ ├── package.json
│ ├── README.md
│ ├── src
│ │ ├── api.ts
│ │ ├── app.ts
│ │ └── index.spec.ts
│ ├── tsconfig.json
│ ├── tsconfig.test.json
│ └── tsup.config.ts
├── ISSUE_TEMPLATE.md
├── legacy
│ ├── cli
│ │ ├── bin
│ │ │ └── cli.mjs
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ └── readme.md
│ └── sdk
│ ├── CHANGELOG.md
│ ├── index.d.ts
│ ├── index.js
│ ├── package.json
│ └── README.md
├── LICENSE.md
├── mcp.md
├── package.json
├── packages
│ ├── cli
│ │ ├── assets
│ │ │ ├── failure.mp3
│ │ │ └── success.mp3
│ │ ├── bin
│ │ │ └── cli.mjs
│ │ ├── CHANGELOG.md
│ │ ├── demo
│ │ │ ├── android
│ │ │ │ ├── en
│ │ │ │ │ └── example.xml
│ │ │ │ ├── es
│ │ │ │ │ └── example.xml
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── csv
│ │ │ │ ├── example.csv
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── demo.spec.ts
│ │ │ ├── ejs
│ │ │ │ ├── en
│ │ │ │ │ └── example.ejs
│ │ │ │ ├── es
│ │ │ │ │ └── example.ejs
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── flutter
│ │ │ │ ├── en
│ │ │ │ │ └── example.arb
│ │ │ │ ├── es
│ │ │ │ │ └── example.arb
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── html
│ │ │ │ ├── en
│ │ │ │ │ └── example.html
│ │ │ │ ├── es
│ │ │ │ │ └── example.html
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── json
│ │ │ │ ├── en
│ │ │ │ │ └── example.json
│ │ │ │ ├── es
│ │ │ │ │ └── example.json
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── json-dictionary
│ │ │ │ ├── example.json
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── json5
│ │ │ │ ├── en
│ │ │ │ │ └── example.json5
│ │ │ │ ├── es
│ │ │ │ │ └── example.json5
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── jsonc
│ │ │ │ ├── en
│ │ │ │ │ └── example.jsonc
│ │ │ │ ├── es
│ │ │ │ │ └── example.jsonc
│ │ │ │ ├── i18n.json
│ │ │ │ ├── i18n.lock
│ │ │ │ └── ru
│ │ │ │ └── example.jsonc
│ │ │ ├── markdoc
│ │ │ │ ├── en
│ │ │ │ │ └── example.markdoc
│ │ │ │ ├── es
│ │ │ │ │ └── example.markdoc
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── markdown
│ │ │ │ ├── en
│ │ │ │ │ └── example.md
│ │ │ │ ├── es
│ │ │ │ │ └── example.md
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── mdx
│ │ │ │ ├── en
│ │ │ │ │ └── example.mdx
│ │ │ │ ├── es
│ │ │ │ │ └── example.mdx
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── php
│ │ │ │ ├── en
│ │ │ │ │ └── example.php
│ │ │ │ ├── es
│ │ │ │ │ └── example.php
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── po
│ │ │ │ ├── en
│ │ │ │ │ └── example.po
│ │ │ │ ├── es
│ │ │ │ │ └── example.po
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── properties
│ │ │ │ ├── en
│ │ │ │ │ └── example.properties
│ │ │ │ ├── es
│ │ │ │ │ └── example.properties
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── run_i18n.sh
│ │ │ ├── srt
│ │ │ │ ├── en
│ │ │ │ │ └── example.srt
│ │ │ │ ├── es
│ │ │ │ │ └── example.srt
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── txt
│ │ │ │ ├── en
│ │ │ │ │ └── example.txt
│ │ │ │ ├── es
│ │ │ │ │ └── example.txt
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── typescript
│ │ │ │ ├── en
│ │ │ │ │ └── example.ts
│ │ │ │ ├── es
│ │ │ │ │ └── example.ts
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── vtt
│ │ │ │ ├── en
│ │ │ │ │ └── example.vtt
│ │ │ │ ├── es
│ │ │ │ │ └── example.vtt
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── vue-json
│ │ │ │ ├── example.vue
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── xcode-strings
│ │ │ │ ├── en
│ │ │ │ │ └── example.strings
│ │ │ │ ├── es
│ │ │ │ │ └── example.strings
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── xcode-stringsdict
│ │ │ │ ├── en
│ │ │ │ │ └── example.stringsdict
│ │ │ │ ├── es
│ │ │ │ │ └── example.stringsdict
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── xcode-xcstrings
│ │ │ │ ├── example.xcstrings
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── xcode-xcstrings-v2
│ │ │ │ ├── complex-example.xcstrings
│ │ │ │ ├── example.xcstrings
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── xliff
│ │ │ │ ├── en
│ │ │ │ │ ├── example-v1.2.xliff
│ │ │ │ │ └── example-v2.xliff
│ │ │ │ ├── es
│ │ │ │ │ ├── example-v1.2.xliff
│ │ │ │ │ ├── example-v2.xliff
│ │ │ │ │ └── example.xliff
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── xml
│ │ │ │ ├── en
│ │ │ │ │ └── example.xml
│ │ │ │ ├── es
│ │ │ │ │ └── example.xml
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ ├── yaml
│ │ │ │ ├── en
│ │ │ │ │ └── example.yml
│ │ │ │ ├── es
│ │ │ │ │ └── example.yml
│ │ │ │ ├── i18n.json
│ │ │ │ └── i18n.lock
│ │ │ └── yaml-root-key
│ │ │ ├── en
│ │ │ │ └── example.yml
│ │ │ ├── es
│ │ │ │ └── example.yml
│ │ │ ├── i18n.json
│ │ │ └── i18n.lock
│ │ ├── i18n.json
│ │ ├── i18n.lock
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── cli
│ │ │ │ ├── cmd
│ │ │ │ │ ├── auth.ts
│ │ │ │ │ ├── ci
│ │ │ │ │ │ ├── flows
│ │ │ │ │ │ │ ├── _base.ts
│ │ │ │ │ │ │ ├── in-branch.ts
│ │ │ │ │ │ │ └── pull-request.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── platforms
│ │ │ │ │ │ ├── _base.ts
│ │ │ │ │ │ ├── bitbucket.ts
│ │ │ │ │ │ ├── github.ts
│ │ │ │ │ │ ├── gitlab.ts
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── cleanup.ts
│ │ │ │ │ ├── config
│ │ │ │ │ │ ├── get.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── set.ts
│ │ │ │ │ │ └── unset.ts
│ │ │ │ │ ├── i18n.ts
│ │ │ │ │ ├── init.ts
│ │ │ │ │ ├── lockfile.ts
│ │ │ │ │ ├── login.ts
│ │ │ │ │ ├── logout.ts
│ │ │ │ │ ├── may-the-fourth.ts
│ │ │ │ │ ├── mcp.ts
│ │ │ │ │ ├── purge.ts
│ │ │ │ │ ├── run
│ │ │ │ │ │ ├── _const.ts
│ │ │ │ │ │ ├── _types.ts
│ │ │ │ │ │ ├── _utils.ts
│ │ │ │ │ │ ├── execute.spec.ts
│ │ │ │ │ │ ├── execute.ts
│ │ │ │ │ │ ├── frozen.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── plan.ts
│ │ │ │ │ │ ├── setup.ts
│ │ │ │ │ │ └── watch.ts
│ │ │ │ │ ├── show
│ │ │ │ │ │ ├── _shared-key-command.ts
│ │ │ │ │ │ ├── config.ts
│ │ │ │ │ │ ├── files.ts
│ │ │ │ │ │ ├── ignored-keys.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── locale.ts
│ │ │ │ │ │ └── locked-keys.ts
│ │ │ │ │ └── status.ts
│ │ │ │ ├── constants.ts
│ │ │ │ ├── index.spec.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── loaders
│ │ │ │ │ ├── _types.ts
│ │ │ │ │ ├── _utils.ts
│ │ │ │ │ ├── android.spec.ts
│ │ │ │ │ ├── android.ts
│ │ │ │ │ ├── csv.spec.ts
│ │ │ │ │ ├── csv.ts
│ │ │ │ │ ├── dato
│ │ │ │ │ │ ├── _base.ts
│ │ │ │ │ │ ├── _utils.ts
│ │ │ │ │ │ ├── api.ts
│ │ │ │ │ │ ├── extract.ts
│ │ │ │ │ │ ├── filter.ts
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── ejs.spec.ts
│ │ │ │ │ ├── ejs.ts
│ │ │ │ │ ├── ensure-key-order.spec.ts
│ │ │ │ │ ├── ensure-key-order.ts
│ │ │ │ │ ├── flat.spec.ts
│ │ │ │ │ ├── flat.ts
│ │ │ │ │ ├── flutter.spec.ts
│ │ │ │ │ ├── flutter.ts
│ │ │ │ │ ├── formatters
│ │ │ │ │ │ ├── _base.ts
│ │ │ │ │ │ ├── biome.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── prettier.ts
│ │ │ │ │ ├── html.ts
│ │ │ │ │ ├── icu-safety.spec.ts
│ │ │ │ │ ├── ignored-keys-buckets.spec.ts
│ │ │ │ │ ├── ignored-keys.spec.ts
│ │ │ │ │ ├── ignored-keys.ts
│ │ │ │ │ ├── index.spec.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── inject-locale.spec.ts
│ │ │ │ │ ├── inject-locale.ts
│ │ │ │ │ ├── json-dictionary.spec.ts
│ │ │ │ │ ├── json-dictionary.ts
│ │ │ │ │ ├── json-sorting.test.ts
│ │ │ │ │ ├── json-sorting.ts
│ │ │ │ │ ├── json.ts
│ │ │ │ │ ├── json5.spec.ts
│ │ │ │ │ ├── json5.ts
│ │ │ │ │ ├── jsonc.spec.ts
│ │ │ │ │ ├── jsonc.ts
│ │ │ │ │ ├── locked-keys.spec.ts
│ │ │ │ │ ├── locked-keys.ts
│ │ │ │ │ ├── locked-patterns.spec.ts
│ │ │ │ │ ├── locked-patterns.ts
│ │ │ │ │ ├── markdoc.spec.ts
│ │ │ │ │ ├── markdoc.ts
│ │ │ │ │ ├── markdown.ts
│ │ │ │ │ ├── mdx.spec.ts
│ │ │ │ │ ├── mdx.ts
│ │ │ │ │ ├── mdx2
│ │ │ │ │ │ ├── _types.ts
│ │ │ │ │ │ ├── _utils.ts
│ │ │ │ │ │ ├── code-placeholder.spec.ts
│ │ │ │ │ │ ├── code-placeholder.ts
│ │ │ │ │ │ ├── frontmatter-split.spec.ts
│ │ │ │ │ │ ├── frontmatter-split.ts
│ │ │ │ │ │ ├── localizable-document.spec.ts
│ │ │ │ │ │ ├── localizable-document.ts
│ │ │ │ │ │ ├── section-split.spec.ts
│ │ │ │ │ │ ├── section-split.ts
│ │ │ │ │ │ └── sections-split-2.ts
│ │ │ │ │ ├── passthrough.ts
│ │ │ │ │ ├── php.ts
│ │ │ │ │ ├── plutil-json-loader.ts
│ │ │ │ │ ├── po
│ │ │ │ │ │ ├── _types.ts
│ │ │ │ │ │ ├── index.spec.ts
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── properties.ts
│ │ │ │ │ ├── root-key.ts
│ │ │ │ │ ├── srt.ts
│ │ │ │ │ ├── sync.ts
│ │ │ │ │ ├── text-file.ts
│ │ │ │ │ ├── txt.ts
│ │ │ │ │ ├── typescript
│ │ │ │ │ │ ├── cjs-interop.ts
│ │ │ │ │ │ ├── index.spec.ts
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── unlocalizable.spec.ts
│ │ │ │ │ ├── unlocalizable.ts
│ │ │ │ │ ├── variable
│ │ │ │ │ │ ├── index.spec.ts
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── vtt.ts
│ │ │ │ │ ├── vue-json.ts
│ │ │ │ │ ├── xcode-strings
│ │ │ │ │ │ ├── escape.ts
│ │ │ │ │ │ ├── parser.ts
│ │ │ │ │ │ ├── tokenizer.ts
│ │ │ │ │ │ └── types.ts
│ │ │ │ │ ├── xcode-strings.spec.ts
│ │ │ │ │ ├── xcode-strings.ts
│ │ │ │ │ ├── xcode-stringsdict.ts
│ │ │ │ │ ├── xcode-xcstrings-icu.spec.ts
│ │ │ │ │ ├── xcode-xcstrings-icu.ts
│ │ │ │ │ ├── xcode-xcstrings-lock-compatibility.spec.ts
│ │ │ │ │ ├── xcode-xcstrings-v2-loader.ts
│ │ │ │ │ ├── xcode-xcstrings.spec.ts
│ │ │ │ │ ├── xcode-xcstrings.ts
│ │ │ │ │ ├── xliff.spec.ts
│ │ │ │ │ ├── xliff.ts
│ │ │ │ │ ├── xml.ts
│ │ │ │ │ └── yaml.ts
│ │ │ │ ├── localizer
│ │ │ │ │ ├── _types.ts
│ │ │ │ │ ├── explicit.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── lingodotdev.ts
│ │ │ │ ├── processor
│ │ │ │ │ ├── _base.ts
│ │ │ │ │ ├── basic.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── lingo.ts
│ │ │ │ └── utils
│ │ │ │ ├── auth.ts
│ │ │ │ ├── buckets.spec.ts
│ │ │ │ ├── buckets.ts
│ │ │ │ ├── cache.ts
│ │ │ │ ├── cloudflare-status.ts
│ │ │ │ ├── config.ts
│ │ │ │ ├── delta.spec.ts
│ │ │ │ ├── delta.ts
│ │ │ │ ├── ensure-patterns.ts
│ │ │ │ ├── errors.ts
│ │ │ │ ├── exec.spec.ts
│ │ │ │ ├── exec.ts
│ │ │ │ ├── exit-gracefully.spec.ts
│ │ │ │ ├── exit-gracefully.ts
│ │ │ │ ├── exp-backoff.ts
│ │ │ │ ├── find-locale-paths.spec.ts
│ │ │ │ ├── find-locale-paths.ts
│ │ │ │ ├── fs.ts
│ │ │ │ ├── init-ci-cd.ts
│ │ │ │ ├── key-matching.spec.ts
│ │ │ │ ├── key-matching.ts
│ │ │ │ ├── lockfile.ts
│ │ │ │ ├── md5.ts
│ │ │ │ ├── observability.ts
│ │ │ │ ├── plutil-formatter.spec.ts
│ │ │ │ ├── plutil-formatter.ts
│ │ │ │ ├── settings.ts
│ │ │ │ ├── ui.ts
│ │ │ │ └── update-gitignore.ts
│ │ │ ├── compiler
│ │ │ │ └── index.ts
│ │ │ ├── locale-codes
│ │ │ │ └── index.ts
│ │ │ ├── react
│ │ │ │ ├── client.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── react-router.ts
│ │ │ │ └── rsc.ts
│ │ │ ├── sdk
│ │ │ │ └── index.ts
│ │ │ └── spec
│ │ │ └── index.ts
│ │ ├── tests
│ │ │ └── mock-storage.ts
│ │ ├── troubleshooting.md
│ │ ├── tsconfig.json
│ │ ├── tsconfig.test.json
│ │ ├── tsup.config.ts
│ │ ├── types
│ │ │ ├── vtt.d.ts
│ │ │ └── xliff.d.ts
│ │ ├── vitest.config.ts
│ │ └── WATCH_MODE.md
│ ├── compiler
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── _base.ts
│ │ │ ├── _const.ts
│ │ │ ├── _loader-utils.spec.ts
│ │ │ ├── _loader-utils.ts
│ │ │ ├── _utils.spec.ts
│ │ │ ├── _utils.ts
│ │ │ ├── client-dictionary-loader.ts
│ │ │ ├── i18n-directive.spec.ts
│ │ │ ├── i18n-directive.ts
│ │ │ ├── index.spec.ts
│ │ │ ├── index.ts
│ │ │ ├── jsx-attribute-flag.spec.ts
│ │ │ ├── jsx-attribute-flag.ts
│ │ │ ├── jsx-attribute-scope-inject.spec.ts
│ │ │ ├── jsx-attribute-scope-inject.ts
│ │ │ ├── jsx-attribute-scopes-export.spec.ts
│ │ │ ├── jsx-attribute-scopes-export.ts
│ │ │ ├── jsx-attribute.spec.ts
│ │ │ ├── jsx-attribute.ts
│ │ │ ├── jsx-fragment.spec.ts
│ │ │ ├── jsx-fragment.ts
│ │ │ ├── jsx-html-lang.spec.ts
│ │ │ ├── jsx-html-lang.ts
│ │ │ ├── jsx-provider.spec.ts
│ │ │ ├── jsx-provider.ts
│ │ │ ├── jsx-remove-attributes.spec.ts
│ │ │ ├── jsx-remove-attributes.ts
│ │ │ ├── jsx-root-flag.spec.ts
│ │ │ ├── jsx-root-flag.ts
│ │ │ ├── jsx-scope-flag.spec.ts
│ │ │ ├── jsx-scope-flag.ts
│ │ │ ├── jsx-scope-inject.spec.ts
│ │ │ ├── jsx-scope-inject.ts
│ │ │ ├── jsx-scopes-export.spec.ts
│ │ │ ├── jsx-scopes-export.ts
│ │ │ ├── lib
│ │ │ │ └── lcp
│ │ │ │ ├── api
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── prompt.spec.ts
│ │ │ │ │ ├── prompt.ts
│ │ │ │ │ ├── provider-details.spec.ts
│ │ │ │ │ ├── provider-details.ts
│ │ │ │ │ ├── shots.ts
│ │ │ │ │ ├── xml2obj.spec.ts
│ │ │ │ │ └── xml2obj.ts
│ │ │ │ ├── api.spec.ts
│ │ │ │ ├── cache.spec.ts
│ │ │ │ ├── cache.ts
│ │ │ │ ├── index.spec.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── schema.ts
│ │ │ │ ├── server.spec.ts
│ │ │ │ └── server.ts
│ │ │ ├── lingo-turbopack-loader.ts
│ │ │ ├── react-router-dictionary-loader.ts
│ │ │ ├── rsc-dictionary-loader.ts
│ │ │ └── utils
│ │ │ ├── ast-key.spec.ts
│ │ │ ├── ast-key.ts
│ │ │ ├── create-locale-import-map.spec.ts
│ │ │ ├── create-locale-import-map.ts
│ │ │ ├── env.spec.ts
│ │ │ ├── env.ts
│ │ │ ├── hash.spec.ts
│ │ │ ├── hash.ts
│ │ │ ├── index.spec.ts
│ │ │ ├── index.ts
│ │ │ ├── invokations.spec.ts
│ │ │ ├── invokations.ts
│ │ │ ├── jsx-attribute-scope.ts
│ │ │ ├── jsx-attribute.spec.ts
│ │ │ ├── jsx-attribute.ts
│ │ │ ├── jsx-content-whitespace.spec.ts
│ │ │ ├── jsx-content.spec.ts
│ │ │ ├── jsx-content.ts
│ │ │ ├── jsx-element.spec.ts
│ │ │ ├── jsx-element.ts
│ │ │ ├── jsx-expressions.test.ts
│ │ │ ├── jsx-expressions.ts
│ │ │ ├── jsx-functions.spec.ts
│ │ │ ├── jsx-functions.ts
│ │ │ ├── jsx-scope.spec.ts
│ │ │ ├── jsx-scope.ts
│ │ │ ├── jsx-variables.spec.ts
│ │ │ ├── jsx-variables.ts
│ │ │ ├── llm-api-key.ts
│ │ │ ├── llm-api-keys.spec.ts
│ │ │ ├── locales.spec.ts
│ │ │ ├── locales.ts
│ │ │ ├── module-params.spec.ts
│ │ │ ├── module-params.ts
│ │ │ ├── observability.spec.ts
│ │ │ ├── observability.ts
│ │ │ ├── rc.spec.ts
│ │ │ └── rc.ts
│ │ ├── tsconfig.json
│ │ ├── tsup.config.ts
│ │ └── vitest.config.ts
│ ├── locales
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── constants.ts
│ │ │ ├── index.ts
│ │ │ ├── names
│ │ │ │ ├── index.spec.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── integration.spec.ts
│ │ │ │ └── loader.ts
│ │ │ ├── parser.spec.ts
│ │ │ ├── parser.ts
│ │ │ ├── types.ts
│ │ │ ├── validation.spec.ts
│ │ │ └── validation.ts
│ │ ├── tsconfig.json
│ │ └── tsup.config.ts
│ ├── react
│ │ ├── build.config.ts
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── client
│ │ │ │ ├── attribute-component.spec.tsx
│ │ │ │ ├── attribute-component.tsx
│ │ │ │ ├── component.lingo-component.spec.tsx
│ │ │ │ ├── component.spec.tsx
│ │ │ │ ├── component.tsx
│ │ │ │ ├── context.spec.tsx
│ │ │ │ ├── context.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── loader.spec.ts
│ │ │ │ ├── loader.ts
│ │ │ │ ├── locale-switcher.spec.tsx
│ │ │ │ ├── locale-switcher.tsx
│ │ │ │ ├── locale.spec.ts
│ │ │ │ ├── locale.ts
│ │ │ │ ├── provider.spec.tsx
│ │ │ │ ├── provider.tsx
│ │ │ │ ├── utils.spec.ts
│ │ │ │ └── utils.ts
│ │ │ ├── core
│ │ │ │ ├── attribute-component.spec.tsx
│ │ │ │ ├── attribute-component.tsx
│ │ │ │ ├── component.spec.tsx
│ │ │ │ ├── component.tsx
│ │ │ │ ├── const.ts
│ │ │ │ ├── get-dictionary.spec.ts
│ │ │ │ ├── get-dictionary.ts
│ │ │ │ └── index.ts
│ │ │ ├── react-router
│ │ │ │ ├── index.ts
│ │ │ │ ├── loader.spec.ts
│ │ │ │ └── loader.ts
│ │ │ ├── rsc
│ │ │ │ ├── attribute-component.tsx
│ │ │ │ ├── component.lingo-component.spec.tsx
│ │ │ │ ├── component.spec.tsx
│ │ │ │ ├── component.tsx
│ │ │ │ ├── index.ts
│ │ │ │ ├── loader.spec.ts
│ │ │ │ ├── loader.ts
│ │ │ │ ├── provider.spec.tsx
│ │ │ │ ├── provider.tsx
│ │ │ │ ├── utils.spec.ts
│ │ │ │ └── utils.ts
│ │ │ └── test
│ │ │ └── setup.ts
│ │ ├── tsconfig.json
│ │ └── vitest.config.ts
│ ├── sdk
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── abort-controller.specs.ts
│ │ │ ├── index.spec.ts
│ │ │ └── index.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.test.json
│ │ └── tsup.config.ts
│ └── spec
│ ├── CHANGELOG.md
│ ├── package.json
│ ├── README.md
│ ├── src
│ │ ├── config.spec.ts
│ │ ├── config.ts
│ │ ├── formats.ts
│ │ ├── index.spec.ts
│ │ ├── index.ts
│ │ ├── json-schema.ts
│ │ ├── locales.spec.ts
│ │ └── locales.ts
│ ├── tsconfig.json
│ ├── tsconfig.test.json
│ └── tsup.config.ts
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── readme
│ ├── ar.md
│ ├── bn.md
│ ├── de.md
│ ├── en.md
│ ├── es.md
│ ├── fa.md
│ ├── fr.md
│ ├── he.md
│ ├── hi.md
│ ├── it.md
│ ├── ja.md
│ ├── ko.md
│ ├── pl.md
│ ├── pt-BR.md
│ ├── ru.md
│ ├── tr.md
│ ├── uk-UA.md
│ └── zh-Hans.md
├── readme.md
├── scripts
│ ├── docs
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── generate-cli-docs.ts
│ │ │ ├── generate-config-docs.ts
│ │ │ ├── json-schema
│ │ │ │ ├── markdown-renderer.test.ts
│ │ │ │ ├── markdown-renderer.ts
│ │ │ │ ├── parser.test.ts
│ │ │ │ ├── parser.ts
│ │ │ │ └── types.ts
│ │ │ ├── utils.test.ts
│ │ │ └── utils.ts
│ │ ├── tsconfig.json
│ │ └── vitest.config.ts
│ └── packagist-publish.php
└── turbo.json
```
# Files
--------------------------------------------------------------------------------
/packages/cli/WATCH_MODE.md:
--------------------------------------------------------------------------------
```markdown
1 | # Watch Mode Implementation
2 |
3 | This document describes the implementation of the watch mode feature for the Lingo.dev CLI.
4 |
5 | ## Overview
6 |
7 | 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.
8 |
9 | ## Usage
10 |
11 | ```bash
12 | # Start watch mode
13 | lingo.dev run --watch
14 |
15 | # Watch with custom debounce timing (7 seconds)
16 | lingo.dev run --watch --debounce 7000
17 |
18 | # Watch with faster debounce for development (2 seconds)
19 | lingo.dev run --watch --debounce 2000
20 |
21 | # Watch with additional filters
22 | lingo.dev run --watch --locale es --bucket json
23 | lingo.dev run --watch --file "src/locales/*.json" --debounce 1000
24 | ```
25 |
26 | ## Features
27 |
28 | ### 1. Automatic File Monitoring
29 |
30 | - Watches all source locale files based on your `i18n.json` configuration
31 | - Monitors file changes, additions, and deletions
32 | - Uses stable file watching to avoid false triggers
33 |
34 | ### 2. Debounced Processing
35 |
36 | - Implements configurable debounce mechanism to avoid excessive retranslations
37 | - Default: 5 seconds, customizable with `--debounce` flag
38 | - Groups rapid changes into single translation batches
39 | - Prevents resource waste from frequent file saves
40 |
41 | ### 3. Intelligent Pattern Detection
42 |
43 | - Automatically determines which files to watch based on bucket patterns
44 | - Replaces `[locale]` placeholders with source locale
45 | - Respects filtering options (`--bucket`, `--file`, etc.)
46 |
47 | ### 4. Real-time Feedback
48 |
49 | - Shows which files are being watched on startup
50 | - Displays file change notifications
51 | - Provides translation progress updates
52 | - Shows completion status for each batch
53 |
54 | ### 5. Graceful Error Handling
55 |
56 | - Continues watching even if individual translations fail
57 | - Reports errors without stopping the watch process
58 | - Maintains watch state across translation cycles
59 |
60 | ## Implementation Details
61 |
62 | ### File Structure
63 |
64 | - `src/cli/cmd/run/watch.ts` - Main watch implementation
65 | - `src/cli/cmd/run/_types.ts` - Updated to include watch flag
66 | - `src/cli/cmd/run/index.ts` - Integration with main run command
67 |
68 | ### Key Components
69 |
70 | #### Watch State Management
71 |
72 | ```typescript
73 | interface WatchState {
74 | isRunning: boolean;
75 | pendingChanges: Set<string>;
76 | debounceTimer?: NodeJS.Timeout;
77 | }
78 | ```
79 |
80 | #### File Pattern Resolution
81 |
82 | The watch mode automatically determines which files to monitor by:
83 |
84 | 1. Getting buckets from `i18n.json`
85 | 2. Applying user filters (`--bucket`, `--file`)
86 | 3. Replacing `[locale]` with source locale
87 | 4. Creating file patterns for chokidar
88 |
89 | #### Debounce Logic
90 |
91 | - Uses configurable debounce timer (default: 5000ms)
92 | - Resets timer on each file change
93 | - Only triggers translation when timer expires
94 | - Prevents overlapping translation runs
95 | - Customizable via `--debounce <milliseconds>` flag
96 |
97 | ### Dependencies
98 |
99 | - `chokidar` - Robust file watching library
100 | - Existing Lingo.dev pipeline (setup, plan, execute)
101 |
102 | ## Example Workflow
103 |
104 | 1. **Start Watch Mode**
105 |
106 | ```bash
107 | lingo.dev run --watch
108 | ```
109 |
110 | 2. **Initial Setup**
111 |
112 | - Performs normal translation setup
113 | - Runs initial planning and execution
114 | - Shows summary of completed translations
115 | - Starts file watching
116 |
117 | 3. **File Change Detection**
118 |
119 | ```
120 | 📝 File changed: locales/en.json
121 | ⏳ Debouncing... (5000ms)
122 | ```
123 |
124 | 4. **Automatic Retranslation**
125 |
126 | ```
127 | 🔄 Triggering retranslation...
128 | Changed files: locales/en.json
129 |
130 | [Planning] Found 2 translation task(s)
131 | [Localization] Processing tasks...
132 | ✅ Retranslation completed
133 | 👀 Continuing to watch for changes...
134 | ```
135 |
136 | ## Error Handling
137 |
138 | The watch mode is designed to be resilient:
139 |
140 | - **Translation Errors**: Reports errors but continues watching
141 | - **File System Errors**: Logs watch errors but maintains process
142 | - **Invalid Files**: Skips problematic files and continues
143 | - **Interrupt Handling**: Gracefully shuts down on Ctrl+C
144 |
145 | ## Performance Considerations
146 |
147 | - **Efficient Pattern Matching**: Only watches relevant source files
148 | - **Debounced Processing**: Prevents excessive API calls
149 | - **Memory Management**: Clears completed change sets
150 | - **Process Isolation**: Each translation runs in isolated context
151 |
152 | ## Testing
153 |
154 | Use the provided demo setup script:
155 |
156 | ```bash
157 | ./demo-watch-setup.sh
158 | cd /tmp/lingo-watch-demo
159 | lingo.dev run --watch
160 | ```
161 |
162 | Then in another terminal:
163 |
164 | ```bash
165 | # Add a new translation key
166 | echo '{"hello": "Hello", "world": "World", "welcome": "Welcome to Lingo.dev", "goodbye": "Goodbye"}' > locales/en.json
167 |
168 | # Watch as translations are automatically updated
169 | ```
170 |
171 | ## Integration with Existing Features
172 |
173 | The watch mode works seamlessly with all existing run command options:
174 |
175 | - `--locale` - Watch only affects specified locales
176 | - `--bucket` - Watch only monitors specified bucket types
177 | - `--file` - Watch only monitors matching file patterns
178 | - `--key` - Post-change filtering applies to specific keys
179 | - `--force` - Forces full retranslation on each change
180 | - `--api-key` - Uses specified API key for all operations
181 | - `--concurrency` - Controls translation parallelism
182 | - `--debounce` - Configures debounce delay in milliseconds (default: 5000ms)
183 |
184 | ## Future Enhancements
185 |
186 | Potential improvements for future versions:
187 |
188 | 1. **Watch Exclusions**: Ignore specific files or patterns
189 | 2. **Selective Translation**: Only translate changed keys
190 | 3. **Change Summaries**: Show detailed change reports
191 | 4. **Multi-project Support**: Watch multiple i18n configurations
192 | 5. **Advanced Debounce Modes**: Per-file or per-bucket debouncing
193 |
```
--------------------------------------------------------------------------------
/packages/cli/src/cli/utils/exit-gracefully.spec.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2 | import { exitGracefully } from "./exit-gracefully";
3 |
4 | // Mock process.exit
5 | const mockExit: any = vi.fn();
6 | const originalProcess = global.process;
7 |
8 | describe("exitGracefully", () => {
9 | beforeEach(() => {
10 | // Mock process.exit
11 | vi.spyOn(process, "exit").mockImplementation(mockExit);
12 |
13 | // Mock process._getActiveHandles and _getActiveRequests
14 | Object.defineProperty(process, "_getActiveHandles", {
15 | value: vi.fn(),
16 | writable: true,
17 | });
18 | Object.defineProperty(process, "_getActiveRequests", {
19 | value: vi.fn(),
20 | writable: true,
21 | });
22 | });
23 |
24 | afterEach(() => {
25 | vi.restoreAllMocks();
26 | vi.clearAllTimers();
27 | mockExit.mockClear();
28 | });
29 |
30 | it("should exit immediately when no pending operations", () => {
31 | // Mock no pending operations
32 | (process as any)._getActiveHandles.mockReturnValue([]);
33 | (process as any)._getActiveRequests.mockReturnValue([]);
34 |
35 | exitGracefully();
36 |
37 | expect(mockExit).toHaveBeenCalledWith(0);
38 | });
39 |
40 | it("should wait and retry when there are pending operations", () => {
41 | vi.useFakeTimers();
42 |
43 | // Mock pending operations
44 | (process as any)._getActiveHandles.mockReturnValue([
45 | { hasRef: () => true, close: () => {} },
46 | ]);
47 | (process as any)._getActiveRequests.mockReturnValue([]);
48 |
49 | exitGracefully();
50 |
51 | // Should not exit immediately
52 | expect(mockExit).not.toHaveBeenCalled();
53 |
54 | // Fast-forward time to trigger retry
55 | vi.advanceTimersByTime(250);
56 |
57 | // Should still not exit if operations are pending
58 | expect(mockExit).not.toHaveBeenCalled();
59 |
60 | // Fast-forward to max wait time
61 | vi.advanceTimersByTime(1750);
62 |
63 | // Should exit after max wait time
64 | expect(mockExit).toHaveBeenCalledWith(0);
65 | });
66 |
67 | it("should exit after max wait interval even with pending operations", () => {
68 | vi.useFakeTimers();
69 |
70 | // Mock persistent pending operations
71 | (process as any)._getActiveHandles.mockReturnValue([
72 | { hasRef: () => true, close: () => {} },
73 | ]);
74 | (process as any)._getActiveRequests.mockReturnValue([]);
75 |
76 | exitGracefully();
77 |
78 | // Fast-forward to max wait time (2000ms)
79 | vi.advanceTimersByTime(2000);
80 |
81 | expect(mockExit).toHaveBeenCalledWith(0);
82 | });
83 |
84 | it("should handle standard process handles correctly", () => {
85 | // Mock only standard handles
86 | (process as any)._getActiveHandles.mockReturnValue([
87 | process.stdin,
88 | process.stdout,
89 | process.stderr,
90 | ]);
91 | (process as any)._getActiveRequests.mockReturnValue([]);
92 |
93 | exitGracefully();
94 |
95 | // Should exit immediately as standard handles are filtered out
96 | expect(mockExit).toHaveBeenCalledWith(0);
97 | });
98 |
99 | it("should handle timers without ref correctly", () => {
100 | // Mock timers without ref
101 | (process as any)._getActiveHandles.mockReturnValue([
102 | { hasRef: () => false },
103 | ]);
104 | (process as any)._getActiveRequests.mockReturnValue([]);
105 |
106 | exitGracefully();
107 |
108 | // Should exit immediately as timers without ref are filtered out
109 | expect(mockExit).toHaveBeenCalledWith(0);
110 | });
111 |
112 | it("should detect file watchers correctly", () => {
113 | // Mock file watcher handles
114 | (process as any)._getActiveHandles.mockReturnValue([{ close: () => {} }]);
115 | (process as any)._getActiveRequests.mockReturnValue([]);
116 |
117 | exitGracefully();
118 |
119 | // Should not exit immediately due to file watcher
120 | expect(mockExit).not.toHaveBeenCalled();
121 | });
122 |
123 | it("should detect pending requests correctly", () => {
124 | // Mock pending requests
125 | (process as any)._getActiveHandles.mockReturnValue([]);
126 | (process as any)._getActiveRequests.mockReturnValue([
127 | { someRequest: true },
128 | ]);
129 |
130 | exitGracefully();
131 |
132 | // Should not exit immediately due to pending requests
133 | expect(mockExit).not.toHaveBeenCalled();
134 | });
135 |
136 | it("should handle elapsed time parameter correctly", () => {
137 | vi.useFakeTimers();
138 |
139 | // Mock pending operations
140 | (process as any)._getActiveHandles.mockReturnValue([
141 | { hasRef: () => true, close: () => {} },
142 | ]);
143 | (process as any)._getActiveRequests.mockReturnValue([]);
144 |
145 | // Start with 1500ms already elapsed
146 | exitGracefully(1500);
147 |
148 | // Should exit after 500ms more (reaching 2000ms max)
149 | vi.advanceTimersByTime(500);
150 |
151 | expect(mockExit).toHaveBeenCalledWith(0);
152 | });
153 |
154 | it("should exit immediately when elapsed time exceeds max wait interval", () => {
155 | // Mock pending operations but start with elapsed time > 2000ms
156 | (process as any)._getActiveHandles.mockReturnValue([
157 | { hasRef: () => true, close: () => {} },
158 | ]);
159 | (process as any)._getActiveRequests.mockReturnValue([]);
160 |
161 | exitGracefully(2500);
162 |
163 | // Should exit immediately as elapsed time exceeds max wait interval
164 | expect(mockExit).toHaveBeenCalledWith(0);
165 | });
166 |
167 | it("should handle mixed types of pending operations", () => {
168 | vi.useFakeTimers();
169 |
170 | // Mock mixed pending operations
171 | (process as any)._getActiveHandles.mockReturnValue([
172 | { hasRef: () => true, close: () => {} },
173 | { hasRef: () => false }, // Timer without ref
174 | process.stdin, // Standard handle
175 | ]);
176 | (process as any)._getActiveRequests.mockReturnValue([
177 | { someRequest: true },
178 | ]);
179 |
180 | exitGracefully();
181 |
182 | // Should not exit immediately due to mixed pending operations
183 | expect(mockExit).not.toHaveBeenCalled();
184 |
185 | // Fast-forward to max wait time
186 | vi.advanceTimersByTime(2000);
187 |
188 | expect(mockExit).toHaveBeenCalledWith(0);
189 | });
190 | });
191 |
```
--------------------------------------------------------------------------------
/packages/compiler/src/utils/jsx-variables.spec.ts:
--------------------------------------------------------------------------------
```typescript
1 | import * as t from "@babel/types";
2 | import traverse, { NodePath } from "@babel/traverse";
3 | import { parse } from "@babel/parser";
4 | import { getJsxVariables } from "./jsx-variables";
5 | import { describe, it, expect } from "vitest";
6 |
7 | describe("JSX Variables Utils", () => {
8 | function parseJSX(code: string): t.File {
9 | return parse(code, {
10 | sourceType: "module",
11 | plugins: ["jsx", "typescript"],
12 | });
13 | }
14 |
15 | function getJSXElementPath(code: string): NodePath<t.JSXElement> {
16 | const ast = parseJSX(code);
17 | let elementPath: NodePath<t.JSXElement> | null = null;
18 |
19 | traverse(ast, {
20 | JSXElement(path) {
21 | elementPath = path;
22 | path.stop();
23 | },
24 | });
25 |
26 | if (!elementPath) {
27 | throw new Error("No JSX element found in the code");
28 | }
29 |
30 | return elementPath;
31 | }
32 |
33 | describe("getJsxVariables", () => {
34 | it("should extract single variable from JSX element", () => {
35 | const path = getJSXElementPath(
36 | "<div>You have {count} new messages.</div>",
37 | );
38 | const result = getJsxVariables(path);
39 |
40 | expect(result.type).toBe("ObjectExpression");
41 | expect(result.properties).toHaveLength(1);
42 |
43 | const property = result.properties[0] as t.ObjectProperty;
44 | expect((property.key as t.StringLiteral).value).toBe("count");
45 | expect((property.value as t.Identifier).name).toBe("count");
46 | });
47 |
48 | it("should extract multiple variables from JSX element", () => {
49 | const path = getJSXElementPath("<div>{count} items in {category}</div>");
50 | const result = getJsxVariables(path);
51 |
52 | expect(result.type).toBe("ObjectExpression");
53 | expect(result.properties).toHaveLength(2);
54 |
55 | const propertyNames = result.properties
56 | .map((prop) => (prop as t.ObjectProperty).key as t.StringLiteral)
57 | .map((key) => key.value);
58 |
59 | expect(propertyNames).toContain("count");
60 | expect(propertyNames).toContain("category");
61 | });
62 |
63 | it("should extract variables from nested elements", () => {
64 | const path = getJSXElementPath(
65 | "<div>Total: <strong>{count}</strong> in <span>{category}</span></div>",
66 | );
67 | const result = getJsxVariables(path);
68 |
69 | expect(result.type).toBe("ObjectExpression");
70 | expect(result.properties).toHaveLength(2);
71 |
72 | const propertyNames = result.properties
73 | .map((prop) => (prop as t.ObjectProperty).key as t.StringLiteral)
74 | .map((key) => key.value);
75 |
76 | expect(propertyNames).toContain("count");
77 | expect(propertyNames).toContain("category");
78 | });
79 |
80 | it("should return empty object expression when no variables present", () => {
81 | const path = getJSXElementPath("<div>Hello world</div>");
82 | const result = getJsxVariables(path);
83 |
84 | expect(result.type).toBe("ObjectExpression");
85 | expect(result.properties).toHaveLength(0);
86 | });
87 |
88 | it("should handle duplicate variables by including them only once", () => {
89 | const path = getJSXElementPath(
90 | "<div>{count} items ({count} total)</div>",
91 | );
92 | const result = getJsxVariables(path);
93 |
94 | expect(result.type).toBe("ObjectExpression");
95 | expect(result.properties).toHaveLength(1);
96 |
97 | const property = result.properties[0] as t.ObjectProperty;
98 | expect((property.key as t.StringLiteral).value).toBe("count");
99 | expect((property.value as t.Identifier).name).toBe("count");
100 | });
101 |
102 | it("should handle variables from objects", () => {
103 | const path = getJSXElementPath(
104 | "<div>user {user.name} has {user.profile.details.private.items.count} items</div>",
105 | );
106 | const result = getJsxVariables(path);
107 |
108 | expect(result.type).toBe("ObjectExpression");
109 | expect(result.properties).toHaveLength(2);
110 |
111 | const userNameProperty = result.properties[0] as t.ObjectProperty;
112 | expect((userNameProperty.key as t.StringLiteral).value).toBe("user.name");
113 | expect((userNameProperty.value as t.Identifier).name).toBe("user.name");
114 |
115 | const countProperty = result.properties[1] as t.ObjectProperty;
116 | expect((countProperty.key as t.StringLiteral).value).toBe(
117 | "user.profile.details.private.items.count",
118 | );
119 | expect((countProperty.value as t.Identifier).name).toBe(
120 | "user.profile.details.private.items.count",
121 | );
122 | });
123 |
124 | it("should handle nested dynamic vatiables", () => {
125 | const path = getJSXElementPath(
126 | "<div>User {data[currentUserType][currentUserIndex].name} has {items.counts[type]} items of type {typeNames[type]}</div>",
127 | );
128 | const result = getJsxVariables(path);
129 |
130 | expect(result.type).toBe("ObjectExpression");
131 | expect(result.properties).toHaveLength(3);
132 |
133 | const userNameProperty = result.properties[0] as t.ObjectProperty;
134 | expect((userNameProperty.key as t.StringLiteral).value).toBe(
135 | "data[currentUserType][currentUserIndex].name",
136 | );
137 | expect((userNameProperty.value as t.Identifier).name).toBe(
138 | "data[currentUserType][currentUserIndex].name",
139 | );
140 |
141 | const countProperty = result.properties[1] as t.ObjectProperty;
142 | expect((countProperty.key as t.StringLiteral).value).toBe(
143 | "items.counts[type]",
144 | );
145 | expect((countProperty.value as t.Identifier).name).toBe(
146 | "items.counts[type]",
147 | );
148 |
149 | const typeProperty = result.properties[2] as t.ObjectProperty;
150 | expect((typeProperty.key as t.StringLiteral).value).toBe(
151 | "typeNames[type]",
152 | );
153 | expect((typeProperty.value as t.Identifier).name).toBe("typeNames[type]");
154 | });
155 | });
156 | });
157 |
```
--------------------------------------------------------------------------------
/packages/compiler/src/lib/lcp/api.spec.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, expect, it, vi, afterEach } from "vitest";
2 | import { LCPAPI } from "./api";
3 | import _ = require("lodash");
4 |
5 | describe("LCPAPI", () => {
6 | afterEach(() => {
7 | vi.restoreAllMocks();
8 | });
9 |
10 | describe("translate", () => {
11 | // very abstract test to make sure the translate function calls private functions of the class
12 | it("should chunk, translate and merge", async () => {
13 | const modelsMock = {};
14 | const chunkSpy = vi
15 | .spyOn(LCPAPI as any, "_chunkDictionary")
16 | .mockReturnValue([1, 2, 3]);
17 | const translateSpy = vi
18 | .spyOn(LCPAPI as any, "_translateChunk")
19 | .mockImplementation((_: any, param: number) => param * 10);
20 | const mergeSpy = vi
21 | .spyOn(LCPAPI as any, "_mergeDictionaries")
22 | .mockReturnValue(100);
23 |
24 | const result = await LCPAPI.translate(modelsMock, 0 as any, "en", "es");
25 |
26 | expect(chunkSpy).toHaveBeenCalledWith(0);
27 | expect(translateSpy).toHaveBeenCalledTimes(3);
28 | expect(translateSpy).toHaveBeenCalledWith(
29 | modelsMock,
30 | 1,
31 | "en",
32 | "es",
33 | undefined,
34 | );
35 | expect(translateSpy).toHaveBeenCalledWith(
36 | modelsMock,
37 | 2,
38 | "en",
39 | "es",
40 | undefined,
41 | );
42 | expect(translateSpy).toHaveBeenCalledWith(
43 | modelsMock,
44 | 3,
45 | "en",
46 | "es",
47 | undefined,
48 | );
49 | expect(mergeSpy).toHaveBeenCalledWith([10, 20, 30]);
50 | expect(result).toEqual(100);
51 | });
52 | });
53 |
54 | describe("_chunkDictionary", () => {
55 | it("should split dictionary into chunks of maximum 100 entries", () => {
56 | const result = (LCPAPI as any)._chunkDictionary({
57 | $schema: "https://lcp.dev/schema/v1/dictionary.json",
58 | version: 0.1,
59 | locale: "en",
60 | files: {
61 | "test1.json": {
62 | entries: _.fromPairs(
63 | _.times(230, (i) => [`entry${i}`, `value${i}`]),
64 | ),
65 | },
66 | "test2.json": {
67 | entries: _.fromPairs(
68 | _.times(90, (i) => [`entry${i}`, `value${i}`]),
69 | ),
70 | },
71 | "test3.json": {
72 | entries: _.fromPairs(
73 | _.times(130, (i) => [`entry${i}`, `value${i}`]),
74 | ),
75 | },
76 | },
77 | });
78 |
79 | expect(result.length).toEqual(5);
80 | expect(Object.keys(result[0].files["test1.json"].entries).length).toEqual(
81 | 100,
82 | );
83 | expect(Object.keys(result[1].files["test1.json"].entries).length).toEqual(
84 | 100,
85 | );
86 | expect(Object.keys(result[2].files["test1.json"].entries).length).toEqual(
87 | 30,
88 | );
89 | expect(Object.keys(result[2].files["test2.json"].entries).length).toEqual(
90 | 70,
91 | );
92 | expect(Object.keys(result[3].files["test2.json"].entries).length).toEqual(
93 | 20,
94 | );
95 | expect(Object.keys(result[3].files["test3.json"].entries).length).toEqual(
96 | 80,
97 | );
98 | expect(Object.keys(result[4].files["test3.json"].entries).length).toEqual(
99 | 50,
100 | );
101 | });
102 | });
103 |
104 | describe("_mergeDictionaries", () => {
105 | it("should merge dictionaries into one", () => {
106 | const dictionaries = [
107 | {
108 | $schema: "https://lcp.dev/schema/v1/dictionary.json",
109 | version: 0.1,
110 | locale: "en",
111 | files: {
112 | "test1.json": {
113 | entries: _.fromPairs(
114 | _.times(10, (i) => [`a-entry${i}`, `value${i}`]),
115 | ),
116 | },
117 | },
118 | },
119 | {
120 | $schema: "https://lcp.dev/schema/v1/dictionary.json",
121 | version: 0.1,
122 | locale: "en",
123 | files: {
124 | "test1.json": {
125 | entries: _.fromPairs(
126 | _.times(10, (i) => [`b-entry${i}`, `value${i}`]),
127 | ),
128 | },
129 | },
130 | },
131 | {
132 | $schema: "https://lcp.dev/schema/v1/dictionary.json",
133 | version: 0.1,
134 | locale: "en",
135 | files: {
136 | "test1.json": {
137 | entries: _.fromPairs(
138 | _.times(5, (i) => [`c-entry${i}`, `value${i}`]),
139 | ),
140 | },
141 | "test2.json": {
142 | entries: _.fromPairs(
143 | _.times(5, (i) => [`a-entry${i}`, `value${i}`]),
144 | ),
145 | },
146 | },
147 | },
148 | {
149 | $schema: "https://lcp.dev/schema/v1/dictionary.json",
150 | version: 0.1,
151 | locale: "en",
152 | files: {
153 | "test2.json": {
154 | entries: _.fromPairs(
155 | _.times(3, (i) => [`b-entry${i}`, `value${i}`]),
156 | ),
157 | },
158 | "test3.json": {
159 | entries: _.fromPairs(
160 | _.times(7, (i) => [`a-entry${i}`, `value${i}`]),
161 | ),
162 | },
163 | },
164 | },
165 | {
166 | $schema: "https://lcp.dev/schema/v1/dictionary.json",
167 | version: 0.1,
168 | locale: "en",
169 | files: {
170 | "test3.json": {
171 | entries: _.fromPairs(
172 | _.times(6, (i) => [`b-entry${i}`, `value${i}`]),
173 | ),
174 | },
175 | },
176 | },
177 | ];
178 |
179 | const result = (LCPAPI as any)._mergeDictionaries(dictionaries);
180 | expect(Object.keys(result.files).length).toEqual(3);
181 | expect(Object.keys(result.files["test1.json"].entries).length).toEqual(
182 | 25,
183 | );
184 | expect(Object.keys(result.files["test2.json"].entries).length).toEqual(8);
185 | expect(Object.keys(result.files["test3.json"].entries).length).toEqual(
186 | 13,
187 | );
188 | });
189 | });
190 | });
191 |
```
--------------------------------------------------------------------------------
/packages/compiler/src/_base.ts:
--------------------------------------------------------------------------------
```typescript
1 | import generate, { GeneratorResult } from "@babel/generator";
2 | import * as t from "@babel/types";
3 | import * as parser from "@babel/parser";
4 | import { LocaleCode } from "@lingo.dev/_spec";
5 |
6 | /**
7 | * Options for configuring Lingo.dev Compiler.
8 | */
9 | export type CompilerParams = {
10 | /**
11 | * The locale to translate from.
12 | *
13 | * This must match one of the following formats:
14 | *
15 | * - [ISO 639-1 language code](https://en.wikipedia.org/wiki/ISO_639-1) (e.g., `"en"`)
16 | * - [IETF BCP 47 language tag](https://en.wikipedia.org/wiki/IETF_language_tag) (e.g., `"en-US"`)
17 | *
18 | * @default "en"
19 | */
20 | sourceLocale: LocaleCode;
21 | /**
22 | * The locale(s) to translate to.
23 | *
24 | * Each locale must match one of the following formats:
25 | *
26 | * - [ISO 639-1 language code](https://en.wikipedia.org/wiki/ISO_639-1) (e.g., `"en"`)
27 | * - [IETF BCP 47 language tag](https://en.wikipedia.org/wiki/IETF_language_tag) (e.g., `"en-US"`)
28 | *
29 | * @default ["es"]
30 | */
31 | targetLocales: LocaleCode[];
32 | /**
33 | * The name of the directory where translation files will be stored, relative to `sourceRoot`.
34 | *
35 | * @default "lingo"
36 | */
37 | lingoDir: string;
38 | /**
39 | * The directory of the source code that will be translated, relative to the current working directory.
40 | *
41 | * @default "src"
42 | */
43 | sourceRoot: string;
44 | /**
45 | * If `true`, the compiler will generate code for React Server Components (RSC).
46 | *
47 | * When using Vite, this value is always `false`.
48 | *
49 | * When using Next.js, this value is always `true`.
50 | *
51 | * @default false
52 | */
53 | rsc: boolean;
54 | /**
55 | * If `true`, the compiler will only localize files that use the `"use i18n";` directive.
56 | *
57 | * @default false
58 | */
59 | useDirective: boolean;
60 | /**
61 | * If `true`, the compiler will log additional information to the console.
62 | *
63 | * @default false
64 | */
65 | debug: boolean;
66 | /**
67 | * The model(s) to use for translation.
68 | *
69 | * If set to `"lingo.dev"`, the compiler will use Lingo.dev Engine.
70 | *
71 | * If set to an object, the compiler will use the model(s) specified in the object:
72 | *
73 | * - The key is a string that represents the source and target locales, separated by a colon (e.g., `"en:es"`).
74 | * - The value is a string that represents the LLM provider and model, separated by a colon (e.g., `"google:gemini-2.0-flash"`).
75 | *
76 | * You can use `*` as a wildcard to match any locale.
77 | *
78 | * If a model is not specified, an error will be thrown.
79 | *
80 | * @default {}
81 | */
82 | models: "lingo.dev" | ModelMap;
83 | /**
84 | * Custom system prompt for the translation engine. If set, this prompt will override the default system prompt defined in Compiler.
85 | * Only works with custom models, not with Lingo.dev Engine.
86 | *
87 | * Example: "You are a helpful assistant that translates {SOURCE_LOCALE} to {TARGET_LOCALE}."
88 | *
89 | * @default null
90 | */
91 | prompt?: string | null;
92 | };
93 |
94 | /**
95 | * A mapping between locale pairings and the model to use to translate that pairing.
96 | */
97 | export type ModelMap = {
98 | [key in SourceTargetLocale]?: ModelIdentifier;
99 | };
100 |
101 | /**
102 | * A pairing of a source and target locale.
103 | */
104 | export type SourceTargetLocale =
105 | | LocalePair
106 | | AnyTargetLocale
107 | | AnySourceLocale
108 | | AnyLocale;
109 |
110 | /**
111 | * A translation from a specific source locale to a specific target locale.
112 | */
113 | export type LocalePair = `${LocaleCode}:${LocaleCode}`;
114 |
115 | /**
116 | * A translation from a specific source locale to any target locale.
117 | */
118 | export type AnyTargetLocale = `${LocaleCode}:${LocaleWildcard}`;
119 |
120 | /**
121 | * A translation from any source locale to a specific target locale.
122 | */
123 | export type AnySourceLocale = `${LocaleWildcard}:${LocaleCode}`;
124 |
125 | /**
126 | * A translation from any source locale to any target locale.
127 | */
128 | export type AnyLocale = `${LocaleWildcard}:${LocaleWildcard}`;
129 |
130 | /**
131 | * A wildcard symbol that matches any locale.
132 | */
133 | export type LocaleWildcard = "*";
134 |
135 | /**
136 | * The colon-separated identifier of a model to use for translation.
137 | */
138 | export type ModelIdentifier = `${string}:${string}`;
139 |
140 | export type CompilerInput = {
141 | relativeFilePath: string;
142 | code: string;
143 | params: CompilerParams;
144 | };
145 |
146 | export type CompilerPayload = CompilerInput & {
147 | ast: t.File;
148 | };
149 | export type CompilerOutput = {
150 | code: string;
151 | map: GeneratorResult["map"];
152 | };
153 |
154 | export type CodeMutation = (payload: CompilerPayload) => CompilerPayload | null;
155 | export type CodeMutationDefinition = CodeMutation;
156 | export function createCodeMutation(spec: CodeMutationDefinition): CodeMutation {
157 | return (payload: CompilerPayload) => {
158 | const result = spec(payload);
159 | return result;
160 | };
161 | }
162 |
163 | export function createPayload(input: CompilerInput): CompilerPayload {
164 | const ast = parser.parse(input.code, {
165 | sourceType: "module",
166 | plugins: ["jsx", "typescript"],
167 | });
168 | return {
169 | ...input,
170 | ast,
171 | };
172 | }
173 |
174 | export function createOutput(payload: CompilerPayload): CompilerOutput {
175 | const generationResult = generate(payload.ast, {}, payload.code);
176 | return {
177 | code: generationResult.code,
178 | map: generationResult.map,
179 | };
180 | }
181 |
182 | export function composeMutations(...mutations: CodeMutation[]) {
183 | return (input: CompilerPayload) => {
184 | let result = input;
185 | for (const mutate of mutations) {
186 | const intermediateResult = mutate(result);
187 | if (!intermediateResult) {
188 | break;
189 | } else {
190 | result = intermediateResult;
191 | }
192 | }
193 | return result;
194 | };
195 | }
196 |
197 | export const defaultParams: CompilerParams = {
198 | sourceRoot: "src",
199 | lingoDir: "lingo",
200 | sourceLocale: "en",
201 | targetLocales: ["es"],
202 | rsc: false,
203 | useDirective: false,
204 | debug: false,
205 | models: {},
206 | prompt: null,
207 | };
208 |
```
--------------------------------------------------------------------------------
/packages/cli/src/cli/loaders/ignored-keys.spec.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect } from "vitest";
2 | import createIgnoredKeysLoader from "./ignored-keys";
3 |
4 | // Helper values
5 | const defaultLocale = "en";
6 | const targetLocale = "es";
7 |
8 | // Common ignored keys list used across tests
9 | const IGNORED_KEYS = ["meta", "todo", "pages/*/title"];
10 |
11 | /**
12 | * Creates a fresh loader instance with the default locale already set.
13 | */
14 | function createLoader() {
15 | const loader = createIgnoredKeysLoader(IGNORED_KEYS);
16 | loader.setDefaultLocale(defaultLocale);
17 | return loader;
18 | }
19 |
20 | describe("ignored-keys loader", () => {
21 | it("should omit the ignored keys when pulling the default locale", async () => {
22 | const loader = createLoader();
23 | const input = {
24 | greeting: "hello",
25 | meta: "some meta information",
26 | todo: "translation pending",
27 | };
28 |
29 | const result = await loader.pull(defaultLocale, input);
30 |
31 | expect(result).toEqual({ greeting: "hello" });
32 | });
33 |
34 | it("should omit the ignored keys when pulling a target locale", async () => {
35 | const loader = createLoader();
36 |
37 | // First pull for the default locale (required by createLoader)
38 | await loader.pull(defaultLocale, {
39 | greeting: "hello",
40 | meta: "meta en",
41 | });
42 |
43 | // Now pull the target locale
44 | const targetInput = {
45 | greeting: "hola",
46 | meta: "meta es",
47 | todo: "todo es",
48 | };
49 | const result = await loader.pull(targetLocale, targetInput);
50 |
51 | expect(result).toEqual({ greeting: "hola" });
52 | });
53 |
54 | it("should remove ignored keys when pushing a target locale", async () => {
55 | const loader = createLoader();
56 |
57 | // Initial pull for the default locale
58 | await loader.pull(defaultLocale, {
59 | greeting: "hello",
60 | meta: "meta en",
61 | todo: "todo en",
62 | });
63 |
64 | // Pull for the target locale (simulating a translator editing the file)
65 | const targetInput = {
66 | greeting: "hola",
67 | meta: "meta es",
68 | todo: "todo es",
69 | };
70 | await loader.pull(targetLocale, targetInput);
71 |
72 | // Data that will be pushed (may still contain ignored keys from translation)
73 | const dataToPush = {
74 | greeting: "hola",
75 | meta: "should be removed",
76 | todo: "should be removed",
77 | };
78 |
79 | const pushResult = await loader.push(targetLocale, dataToPush);
80 |
81 | // The loader should have removed the ignored keys completely.
82 | expect(pushResult).toEqual({
83 | greeting: "hola",
84 | });
85 | });
86 |
87 | it("should omit keys matching wildcard patterns when pulling the default locale", async () => {
88 | const loader = createLoader();
89 | const input = {
90 | greeting: "hello",
91 | meta: "some meta information",
92 | "pages/0/title": "Title 0",
93 | "pages/0/content": "Content 0",
94 | "pages/foo/title": "Foo Title",
95 | "pages/foo/content": "Foo Content",
96 | "pages/bar/notitle": "No Title",
97 | "pages/bar/content": "No Content",
98 | };
99 | const result = await loader.pull(defaultLocale, input);
100 | expect(result).toEqual({
101 | greeting: "hello",
102 | "pages/0/content": "Content 0",
103 | "pages/foo/content": "Foo Content",
104 | "pages/bar/notitle": "No Title",
105 | "pages/bar/content": "No Content",
106 | });
107 | });
108 |
109 | it("should omit keys matching wildcard patterns when pulling a target locale", async () => {
110 | const loader = createLoader();
111 | await loader.pull(defaultLocale, {
112 | greeting: "hello",
113 | meta: "meta en",
114 | "pages/0/title": "Title 0",
115 | "pages/0/content": "Content 0",
116 | "pages/foo/title": "Foo Title",
117 | "pages/foo/content": "Foo Content",
118 | "pages/bar/notitle": "No Title",
119 | "pages/bar/content": "No Content",
120 | });
121 | const targetInput = {
122 | greeting: "hola",
123 | meta: "meta es",
124 | "pages/0/title": "Title 0",
125 | "pages/0/content": "Contenido 0",
126 | "pages/foo/title": "Foo Title",
127 | "pages/foo/content": "Contenido Foo",
128 | "pages/bar/notitle": "No Title",
129 | "pages/bar/content": "No Content",
130 | };
131 | const result = await loader.pull(targetLocale, targetInput);
132 | expect(result).toEqual({
133 | greeting: "hola",
134 | "pages/0/content": "Contenido 0",
135 | "pages/foo/content": "Contenido Foo",
136 | "pages/bar/notitle": "No Title",
137 | "pages/bar/content": "No Content",
138 | });
139 | });
140 |
141 | it("should remove wildcard-ignored keys when pushing a target locale", async () => {
142 | const loader = createLoader();
143 | await loader.pull(defaultLocale, {
144 | greeting: "hello",
145 | meta: "meta en",
146 | "pages/0/title": "Title 0",
147 | "pages/0/content": "Content 0",
148 | "pages/foo/title": "Foo Title",
149 | "pages/foo/content": "Foo Content",
150 | "pages/bar/notitle": "No Title",
151 | "pages/bar/content": "No Content",
152 | });
153 | await loader.pull(targetLocale, {
154 | greeting: "hola",
155 | meta: "meta es",
156 | "pages/0/title": "Título 0",
157 | "pages/0/content": "Contenido 0",
158 | "pages/foo/title": "Título Foo",
159 | "pages/foo/content": "Contenido Foo",
160 | "pages/bar/notitle": "No Título",
161 | "pages/bar/content": "Contenido Bar",
162 | });
163 | const dataToPush = {
164 | greeting: "hola",
165 | meta: "should be removed",
166 | "pages/0/title": "should be removed",
167 | "pages/0/content": "Contenido Nuveo",
168 | "pages/foo/title": "should be removed",
169 | "pages/foo/content": "Contenido Nuevo Foo",
170 | "pages/bar/notitle": "No Título",
171 | "pages/bar/content": "Contenido Nuevo Bar",
172 | };
173 | const pushResult = await loader.push(targetLocale, dataToPush);
174 | expect(pushResult).toEqual({
175 | greeting: "hola",
176 | "pages/0/content": "Contenido Nuveo",
177 | "pages/foo/content": "Contenido Nuevo Foo",
178 | "pages/bar/notitle": "No Título",
179 | "pages/bar/content": "Contenido Nuevo Bar",
180 | });
181 | });
182 | });
183 |
```
--------------------------------------------------------------------------------
/scripts/docs/src/json-schema/markdown-renderer.ts:
--------------------------------------------------------------------------------
```typescript
1 | import type { ListItem, Root, RootContent } from "mdast";
2 | import { unified } from "unified";
3 | import remarkStringify from "remark-stringify";
4 | import type { PropertyInfo } from "./types";
5 |
6 | export function makeHeadingNode(fullName: string): RootContent {
7 | const headingDepth = Math.min(6, 2 + (fullName.split(".").length - 1));
8 | return {
9 | type: "heading",
10 | depth: headingDepth as 1 | 2 | 3 | 4 | 5 | 6,
11 | children: [{ type: "inlineCode", value: fullName }],
12 | };
13 | }
14 |
15 | export function makeDescriptionNode(description?: string): RootContent | null {
16 | if (!description) return null;
17 | return {
18 | type: "paragraph",
19 | children: [{ type: "text", value: description }],
20 | };
21 | }
22 |
23 | export function makeTypeBulletNode(type: string): ListItem {
24 | return {
25 | type: "listItem",
26 | children: [
27 | {
28 | type: "paragraph",
29 | children: [
30 | { type: "text", value: "Type: " },
31 | { type: "inlineCode", value: type },
32 | ],
33 | },
34 | ],
35 | };
36 | }
37 |
38 | export function makeRequiredBulletNode(required: boolean): ListItem {
39 | return {
40 | type: "listItem",
41 | children: [
42 | {
43 | type: "paragraph",
44 | children: [
45 | { type: "text", value: "Required: " },
46 | { type: "inlineCode", value: required ? "yes" : "no" },
47 | ],
48 | },
49 | ],
50 | };
51 | }
52 |
53 | export function makeDefaultBulletNode(defaultValue?: unknown): ListItem | null {
54 | if (defaultValue === undefined) return null;
55 | return {
56 | type: "listItem",
57 | children: [
58 | {
59 | type: "paragraph",
60 | children: [
61 | { type: "text", value: "Default: " },
62 | { type: "inlineCode", value: JSON.stringify(defaultValue) },
63 | ],
64 | },
65 | ],
66 | };
67 | }
68 |
69 | export function makeEnumBulletNode(allowedValues?: unknown[]): ListItem | null {
70 | if (!allowedValues || allowedValues.length === 0) return null;
71 | return {
72 | type: "listItem",
73 | children: [
74 | {
75 | type: "paragraph",
76 | children: [{ type: "text", value: "Allowed values:" }],
77 | },
78 | {
79 | type: "list",
80 | ordered: false,
81 | spread: false,
82 | children: allowedValues.map((v) => ({
83 | type: "listItem",
84 | children: [
85 | {
86 | type: "paragraph",
87 | children: [{ type: "inlineCode", value: String(v) }],
88 | },
89 | ],
90 | })),
91 | },
92 | ],
93 | };
94 | }
95 |
96 | export function makeAllowedKeysBulletNode(
97 | allowedKeys?: string[],
98 | ): ListItem | null {
99 | if (!allowedKeys || allowedKeys.length === 0) return null;
100 | return {
101 | type: "listItem",
102 | children: [
103 | {
104 | type: "paragraph",
105 | children: [{ type: "text", value: "Allowed keys:" }],
106 | },
107 | {
108 | type: "list",
109 | ordered: false,
110 | spread: false,
111 | children: allowedKeys.map((v) => ({
112 | type: "listItem",
113 | children: [
114 | {
115 | type: "paragraph",
116 | children: [{ type: "inlineCode", value: v }],
117 | },
118 | ],
119 | })),
120 | },
121 | ],
122 | };
123 | }
124 |
125 | export function makeBullets(property: PropertyInfo): ListItem[] {
126 | const bullets: ListItem[] = [
127 | makeTypeBulletNode(property.type),
128 | makeRequiredBulletNode(property.required),
129 | ];
130 |
131 | const defaultNode = makeDefaultBulletNode(property.defaultValue);
132 | if (defaultNode) bullets.push(defaultNode);
133 |
134 | const enumNode = makeEnumBulletNode(property.allowedValues);
135 | if (enumNode) bullets.push(enumNode);
136 |
137 | const allowedKeysNode = makeAllowedKeysBulletNode(property.allowedKeys);
138 | if (allowedKeysNode) bullets.push(allowedKeysNode);
139 |
140 | return bullets;
141 | }
142 |
143 | export function renderPropertyToMarkdown(
144 | property: PropertyInfo,
145 | ): RootContent[] {
146 | const nodes: RootContent[] = [makeHeadingNode(property.fullPath)];
147 |
148 | // Description node
149 | const descNode = makeDescriptionNode(property.description);
150 | if (descNode) nodes.push(descNode);
151 |
152 | // Bullet list node (with all bullets)
153 | const bulletItems = makeBullets(property);
154 | nodes.push({
155 | type: "list",
156 | ordered: false,
157 | spread: false,
158 | children: bulletItems,
159 | });
160 |
161 | // Recurse for nested properties
162 | if (property.children) {
163 | for (const child of property.children) {
164 | nodes.push(...renderPropertyToMarkdown(child));
165 | }
166 | }
167 |
168 | return nodes;
169 | }
170 |
171 | export function renderPropertiesToMarkdown(
172 | properties: PropertyInfo[],
173 | ): RootContent[] {
174 | const children: RootContent[] = [
175 | {
176 | type: "paragraph",
177 | children: [
178 | {
179 | type: "text",
180 | value:
181 | "This page describes the complete list of properties that are available within the ",
182 | },
183 | { type: "inlineCode", value: "i18n.json" },
184 | {
185 | type: "text",
186 | value: " configuration file. This file is used by ",
187 | },
188 | {
189 | type: "strong",
190 | children: [{ type: "text", value: "Lingo.dev CLI" }],
191 | },
192 | {
193 | type: "text",
194 | value: " to configure the behavior of the translation pipeline.",
195 | },
196 | ],
197 | },
198 | ];
199 |
200 | for (const property of properties) {
201 | children.push(...renderPropertyToMarkdown(property));
202 |
203 | // Add spacing between top-level sections
204 | children.push({
205 | type: "paragraph",
206 | children: [{ type: "text", value: "" }],
207 | });
208 | }
209 |
210 | return children;
211 | }
212 |
213 | export function renderMarkdown(properties: PropertyInfo[]): string {
214 | const children = renderPropertiesToMarkdown(properties);
215 | const root: Root = { type: "root", children };
216 | const markdownContent = unified()
217 | .use(remarkStringify, { fences: true, listItemIndent: "one" })
218 | .stringify(root);
219 |
220 | // Add YAML frontmatter
221 | const frontmatter = `---
222 | title: i18n.json properties
223 | ---
224 |
225 | `;
226 |
227 | return frontmatter + markdownContent;
228 | }
229 |
```
--------------------------------------------------------------------------------
/demo/adonisjs/CHANGELOG.md:
--------------------------------------------------------------------------------
```markdown
1 | # adonis
2 |
3 | ## 0.0.29
4 |
5 | ### Patch Changes
6 |
7 | - Updated dependencies [[`68fb3ea`](https://github.com/lingodotdev/lingo.dev/commit/68fb3ea64fc0191ecee66403432e0c8efabab2b9)]:
8 | - [email protected]
9 |
10 | ## 0.0.28
11 |
12 | ### Patch Changes
13 |
14 | - Updated dependencies [[`e70385b`](https://github.com/lingodotdev/lingo.dev/commit/e70385bd1ac676bf5bd31b212d8510e6b7ebf793)]:
15 | - [email protected]
16 |
17 | ## 0.0.27
18 |
19 | ### Patch Changes
20 |
21 | - Updated dependencies [[`f7215c1`](https://github.com/lingodotdev/lingo.dev/commit/f7215c1e435378aac8fc953765335cd478cbf507)]:
22 | - [email protected]
23 |
24 | ## 0.0.26
25 |
26 | ### Patch Changes
27 |
28 | - Updated dependencies [[`898bd36`](https://github.com/lingodotdev/lingo.dev/commit/898bd36cc2e444641560d2ad2b28065a57072183)]:
29 | - [email protected]
30 |
31 | ## 0.0.25
32 |
33 | ### Patch Changes
34 |
35 | - 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)]:
36 | - [email protected]
37 |
38 | ## 0.0.24
39 |
40 | ### Patch Changes
41 |
42 | - Updated dependencies [[`03671f7`](https://github.com/lingodotdev/lingo.dev/commit/03671f7cb252d6bee3debce2f4a4eb989dc0050b)]:
43 | - [email protected]
44 |
45 | ## 0.0.23
46 |
47 | ### Patch Changes
48 |
49 | - Updated dependencies [[`4f5ffe6`](https://github.com/lingodotdev/lingo.dev/commit/4f5ffe62189949bb26a6c7825cb72c217aefa32f)]:
50 | - [email protected]
51 |
52 | ## 0.0.22
53 |
54 | ### Patch Changes
55 |
56 | - Updated dependencies [[`be8de32`](https://github.com/lingodotdev/lingo.dev/commit/be8de3280bb5dc5f409fc7680c0e5ff6a53e2fe5)]:
57 | - [email protected]
58 |
59 | ## 0.0.21
60 |
61 | ### Patch Changes
62 |
63 | - Updated dependencies [[`79c4c00`](https://github.com/lingodotdev/lingo.dev/commit/79c4c00108b9c102cf53e1c090b286070a43e3d5)]:
64 | - [email protected]
65 |
66 | ## 0.0.20
67 |
68 | ### Patch Changes
69 |
70 | - Updated dependencies [[`b45347c`](https://github.com/lingodotdev/lingo.dev/commit/b45347c38572ee371b2bc494261b7e3e90c4aed1)]:
71 | - [email protected]
72 |
73 | ## 0.0.19
74 |
75 | ### Patch Changes
76 |
77 | - Updated dependencies [[`74d8efe`](https://github.com/lingodotdev/lingo.dev/commit/74d8efef8d4789f9baa5b7837e053c2571df0308)]:
78 | - [email protected]
79 |
80 | ## 0.0.18
81 |
82 | ### Patch Changes
83 |
84 | - Updated dependencies [[`3d3c3d7`](https://github.com/lingodotdev/lingo.dev/commit/3d3c3d783a61443da50a5d182391db33a0d29c84)]:
85 | - [email protected]
86 |
87 | ## 0.0.17
88 |
89 | ### Patch Changes
90 |
91 | - Updated dependencies [[`38139c8`](https://github.com/lingodotdev/lingo.dev/commit/38139c81a85001739cece60873c0c6ad711327a4)]:
92 | - [email protected]
93 |
94 | ## 0.0.16
95 |
96 | ### Patch Changes
97 |
98 | - Updated dependencies [[`3413dad`](https://github.com/lingodotdev/lingo.dev/commit/3413dad22af688a6d26649c4f25e18304b3caee6)]:
99 | - [email protected]
100 |
101 | ## 0.0.15
102 |
103 | ### Patch Changes
104 |
105 | - Updated dependencies [[`26d2ec1`](https://github.com/lingodotdev/lingo.dev/commit/26d2ec155c5868a5bdce1027cd76a5a2d4f8f2b1)]:
106 | - [email protected]
107 |
108 | ## 0.0.14
109 |
110 | ### Patch Changes
111 |
112 | - Updated dependencies [[`82f5e7c`](https://github.com/lingodotdev/lingo.dev/commit/82f5e7cdde9a2a15b4c2a7fcb8c67ed64eab596b), [`e858174`](https://github.com/lingodotdev/lingo.dev/commit/e858174fd5165e0ea3e3f25fa1fc3edb292bc58f)]:
113 | - [email protected]
114 |
115 | ## 0.0.13
116 |
117 | ### Patch Changes
118 |
119 | - Updated dependencies [[`f3d4987`](https://github.com/lingodotdev/lingo.dev/commit/f3d4987ddc393c28d488f030c087f3e99a667975), [`a933b81`](https://github.com/lingodotdev/lingo.dev/commit/a933b8102763e0481f088c847da53e0eee3f0617)]:
120 | - [email protected]
121 |
122 | ## 0.0.12
123 |
124 | ### Patch Changes
125 |
126 | - Updated dependencies []:
127 | - [email protected]
128 |
129 | ## 0.0.11
130 |
131 | ### Patch Changes
132 |
133 | - Updated dependencies [[`dd0663f`](https://github.com/lingodotdev/lingo.dev/commit/dd0663fdcdd0ff4fd5748386758a8c20f9e52a4b)]:
134 | - [email protected]
135 |
136 | ## 0.0.10
137 |
138 | ### Patch Changes
139 |
140 | - Updated dependencies [[`762396b`](https://github.com/lingodotdev/lingo.dev/commit/762396bb37110dbe3e4e000edb27892b318aa3ef)]:
141 | - [email protected]
142 |
143 | ## 0.0.9
144 |
145 | ### Patch Changes
146 |
147 | - Updated dependencies [[`468a59b`](https://github.com/lingodotdev/lingo.dev/commit/468a59b89736c72253b1f32abbf30a950e5434ec)]:
148 | - [email protected]
149 |
150 | ## 0.0.8
151 |
152 | ### Patch Changes
153 |
154 | - Updated dependencies [[`bbc71b9`](https://github.com/lingodotdev/lingo.dev/commit/bbc71b9948ccc289c9669d8b0c276c9596f6a5e7)]:
155 | - [email protected]
156 |
157 | ## 0.0.7
158 |
159 | ### Patch Changes
160 |
161 | - Updated dependencies [[`0e6d605`](https://github.com/lingodotdev/lingo.dev/commit/0e6d605a9ad6835bef26c40895760c652a69b7a2)]:
162 | - [email protected]
163 |
164 | ## 0.0.6
165 |
166 | ### Patch Changes
167 |
168 | - Updated dependencies [[`03138da`](https://github.com/lingodotdev/lingo.dev/commit/03138dac37e869e2e99702ffd3c76532f1c58aa6), [`9557fe5`](https://github.com/lingodotdev/lingo.dev/commit/9557fe572d3e4a1a4d8c1e35417fe3b7531c3d52)]:
169 | - [email protected]
170 |
171 | ## 0.0.5
172 |
173 | ### Patch Changes
174 |
175 | - Updated dependencies [[`64225d0`](https://github.com/lingodotdev/lingo.dev/commit/64225d073999d599ba86f65fee8e08e3e5f2800b)]:
176 | - [email protected]
177 |
178 | ## 0.0.4
179 |
180 | ### Patch Changes
181 |
182 | - Updated dependencies []:
183 | - [email protected]
184 |
185 | ## 0.0.3
186 |
187 | ### Patch Changes
188 |
189 | - Updated dependencies [[`88b7e31`](https://github.com/lingodotdev/lingo.dev/commit/88b7e3132c77d0a1e823de4ee6ef5a96a3098b97)]:
190 | - [email protected]
191 |
192 | ## 0.0.2
193 |
194 | ### Patch Changes
195 |
196 | - Updated dependencies [[`d9294c0`](https://github.com/lingodotdev/lingo.dev/commit/d9294c0bbb993454ad3654f77dd48d82211e0465)]:
197 | - [email protected]
198 |
199 | ## 0.0.1
200 |
201 | ### Patch Changes
202 |
203 | - Updated dependencies [[`100b141`](https://github.com/lingodotdev/lingo.dev/commit/100b141d2143e33b603830475ba55089dc421e3d)]:
204 | - [email protected]
205 |
```
--------------------------------------------------------------------------------
/readme/tr.md:
--------------------------------------------------------------------------------
```markdown
1 | <p align="center">
2 | <a href="https://lingo.dev">
3 | <img
4 | src="https://raw.githubusercontent.com/lingodotdev/lingo.dev/main/content/banner.compiler.png"
5 | width="100%"
6 | alt="Lingo.dev"
7 | />
8 | </a>
9 | </p>
10 |
11 | <p align="center">
12 | <strong>
13 | ⚡ Lingo.dev - LLM'ler ile anında yerelleştirme için açık kaynaklı, yapay
14 | zeka destekli i18n araç seti.
15 | </strong>
16 | </p>
17 |
18 | <br />
19 |
20 | <p align="center">
21 | <a href="https://lingo.dev/compiler">Lingo.dev Derleyici</a> •
22 | <a href="https://lingo.dev/cli">Lingo.dev CLI</a> •
23 | <a href="https://lingo.dev/ci">Lingo.dev CI/CD</a> •
24 | <a href="https://lingo.dev/sdk">Lingo.dev SDK</a>
25 | </p>
26 |
27 | <p align="center">
28 | <a href="https://github.com/lingodotdev/lingo.dev/actions/workflows/release.yml">
29 | <img
30 | src="https://github.com/lingodotdev/lingo.dev/actions/workflows/release.yml/badge.svg"
31 | alt="Sürüm"
32 | />
33 | </a>
34 | <a href="https://github.com/lingodotdev/lingo.dev/blob/main/LICENSE.md">
35 | <img
36 | src="https://img.shields.io/github/license/lingodotdev/lingo.dev"
37 | alt="Lisans"
38 | />
39 | </a>
40 | <a href="https://github.com/lingodotdev/lingo.dev/commits/main">
41 | <img
42 | src="https://img.shields.io/github/last-commit/lingodotdev/lingo.dev"
43 | alt="Son Değişiklik"
44 | />
45 | </a>
46 | </p>
47 |
48 | ---
49 |
50 | ## Derleyici ile tanışın 🆕
51 |
52 | **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.
53 |
54 | Bir kez kurun:
55 |
56 | ```bash
57 | npm install lingo.dev
58 | ```
59 |
60 | Derleme yapılandırmanızda etkinleştirin:
61 |
62 | ```js
63 | import lingoCompiler from "lingo.dev/compiler";
64 |
65 | const existingNextConfig = {};
66 |
67 | export default lingoCompiler.next({
68 | sourceLocale: "en",
69 | targetLocales: ["es", "fr"],
70 | })(existingNextConfig);
71 | ```
72 |
73 | `next build` komutunu çalıştırın ve İspanyolca ve Fransızca paketlerin ortaya çıkışını izleyin ✨
74 |
75 | 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).
76 |
77 | ---
78 |
79 | ### Bu depoda neler var?
80 |
81 | | Alet | Özet | Dokümanlar |
82 | | ------------- | --------------------------------------------------------------------------------------------- | --------------------------------------- |
83 | | **Derleyici** | Derleme zamanında React yerelleştirme | [/compiler](https://lingo.dev/compiler) |
84 | | **CLI** | Web ve mobil uygulamalar, JSON, YAML, markdown ve daha fazlası için tek komutla yerelleştirme | [/cli](https://lingo.dev/cli) |
85 | | **CI/CD** | Her push'ta otomatik çeviri commit'leri + gerekirse pull request oluşturma | [/ci](https://lingo.dev/ci) |
86 | | **SDK** | Kullanıcı tarafından oluşturulan içerik için gerçek zamanlı çeviri | [/sdk](https://lingo.dev/sdk) |
87 |
88 | Aşağıda her biri için hızlı bilgiler bulunmaktadır 👇
89 |
90 | ---
91 |
92 | ### ⚡️ Lingo.dev CLI
93 |
94 | Kod ve içeriği doğrudan terminalinizden çevirin.
95 |
96 | ```bash
97 | npx lingo.dev@latest run
98 | ```
99 |
100 | Her dizeyi parmak iziyle işaretler, sonuçları önbelleğe alır ve yalnızca değişen kısımları yeniden çevirir.
101 |
102 | Nasıl kurulacağını öğrenmek için [dokümanları takip edin →](https://lingo.dev/cli).
103 |
104 | ---
105 |
106 | ### 🔄 Lingo.dev CI/CD
107 |
108 | Otomatik olarak mükemmel çeviriler yayınlayın.
109 |
110 | ```yaml
111 | # .github/workflows/i18n.yml
112 | name: Lingo.dev i18n
113 | on: [push]
114 |
115 | jobs:
116 | i18n:
117 | runs-on: ubuntu-latest
118 | steps:
119 | - uses: actions/checkout@v4
120 | - uses: lingodotdev/lingo.dev@main
121 | with:
122 | api-key: ${{ secrets.LINGODOTDEV_API_KEY }}
123 | ```
124 |
125 | Deponuzu yeşil tutar ve manuel adımlar olmadan ürününüzü çok dilli hale getirir.
126 |
127 | [Belgeleri oku →](https://lingo.dev/ci)
128 |
129 | ---
130 |
131 | ### 🧩 Lingo.dev SDK
132 |
133 | Dinamik içerik için anında istek başına çeviri.
134 |
135 | ```ts
136 | import { LingoDotDevEngine } from "lingo.dev/sdk";
137 |
138 | const lingoDotDev = new LingoDotDevEngine({
139 | apiKey: "your-api-key-here",
140 | });
141 |
142 | const content = {
143 | greeting: "Hello",
144 | farewell: "Goodbye",
145 | message: "Welcome to our platform",
146 | };
147 |
148 | const translated = await lingoDotDev.localizeObject(content, {
149 | sourceLocale: "en",
150 | targetLocale: "es",
151 | });
152 | // Returns: { greeting: "Hola", farewell: "Adiós", message: "Bienvenido a nuestra plataforma" }
153 | ```
154 |
155 | Sohbet, kullanıcı yorumları ve diğer gerçek zamanlı akışlar için mükemmel.
156 |
157 | [Belgeleri oku →](https://lingo.dev/sdk)
158 |
159 | ---
160 |
161 | ## 🤝 Topluluk
162 |
163 | Topluluk odaklıyız ve katkıları seviyoruz!
164 |
165 | - Bir fikriniz mi var? [Bir sorun açın](https://github.com/lingodotdev/lingo.dev/issues)
166 | - Bir şeyi düzeltmek mi istiyorsunuz? [PR gönderin](https://github.com/lingodotdev/lingo.dev/pulls)
167 | - Yardıma mı ihtiyacınız var? [Discord'umuza katılın](https://lingo.dev/go/discord)
168 |
169 | ## ⭐ Yıldız Tarihi
170 |
171 | Yaptıklarımızı beğeniyorsanız, bize bir ⭐ verin ve 3.000 yıldıza ulaşmamıza yardımcı olun! 🌟
172 |
173 | [
174 |
175 | 
176 |
177 | ](https://www.star-history.com/#lingodotdev/lingo.dev&Date)
178 |
179 | ## 🌐 Diğer dillerde Readme
180 |
181 | [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)
182 |
183 | Dilinizi görmüyor musunuz? [`i18n.json`](./i18n.json) dosyasına ekleyin ve bir PR açın!
184 |
```
--------------------------------------------------------------------------------
/packages/cli/src/cli/loaders/variable/index.spec.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect } from "vitest";
2 | import createVariableLoader, { VariableLoaderParams } from "./index";
3 |
4 | describe("createVariableLoader", () => {
5 | describe("ieee format", () => {
6 | it("extracts variables during pull", async () => {
7 | const loader = createLoader("ieee");
8 | const input = {
9 | simple: "Hello %s!",
10 | multiple: "Value: %d and %f",
11 | complex: "Precision %.2f with position %1$d",
12 | };
13 |
14 | const result = await loader.pull("en", input);
15 | expect(result).toEqual({
16 | simple: "Hello {variable:0}!",
17 | multiple: "Value: {variable:0} and {variable:1}",
18 | complex: "Precision {variable:0} with position {variable:1}",
19 | });
20 | });
21 |
22 | it("restores variables during push", async () => {
23 | const loader = createLoader("ieee");
24 | const input = {
25 | simple: "Hello %s!",
26 | multiple: "Value: %d and %f",
27 | complex: "Precision %.2f with position %1$d",
28 | };
29 |
30 | const payload = {
31 | simple: "[updated] Hello {variable:0}!",
32 | multiple: "[updated] Value: {variable:0} and {variable:1}",
33 | complex: "[updated] Precision {variable:0} with position {variable:1}",
34 | };
35 |
36 | await loader.pull("en", input);
37 | const result = await loader.push("en", payload);
38 |
39 | expect(result).toEqual({
40 | simple: "[updated] Hello %s!",
41 | multiple: "[updated] Value: %d and %f",
42 | complex: "[updated] Precision %.2f with position %1$d",
43 | });
44 | });
45 |
46 | it("handles empty input", async () => {
47 | const loader = createLoader("ieee");
48 | const result = await loader.pull("en", {});
49 | expect(result).toEqual({});
50 | });
51 |
52 | it("preserves variable order for target locale during push", async () => {
53 | const loader = createLoader("ieee");
54 |
55 | const sourceInput = {
56 | message: "Value: %d and %f",
57 | };
58 |
59 | // Pull the default (source) locale first
60 | await loader.pull("en", sourceInput);
61 |
62 | // Target locale has variables in different order due to linguistic specifics
63 | const targetInput = {
64 | message: "Wert: %f und %d",
65 | };
66 |
67 | // Pull the target locale to capture its variable ordering
68 | await loader.pull("de", targetInput);
69 |
70 | // Translator updates the string while keeping placeholders
71 | const payload = {
72 | message: "[aktualisiert] Wert: {variable:1} und {variable:0}",
73 | };
74 |
75 | // Push the updated translation back
76 | const result = await loader.push("de", payload);
77 |
78 | expect(result).toEqual({
79 | message: "[aktualisiert] Wert: %f und %d",
80 | });
81 | });
82 |
83 | it("extracts variables with positional specifiers during pull", async () => {
84 | const loader = createLoader("ieee");
85 | const input = {
86 | message: "You have %2$d new items and %1$s.",
87 | };
88 |
89 | const result = await loader.pull("en", input);
90 | expect(result).toEqual({
91 | message: "You have {variable:0} new items and {variable:1}.",
92 | });
93 | });
94 |
95 | it("restores variables with positional specifiers during push", async () => {
96 | const loader = createLoader("ieee");
97 | const input = {
98 | message: "You have %2$d new items and %1$s.",
99 | };
100 |
101 | const payload = {
102 | message: "[updated] You have {variable:0} new items and {variable:1}.",
103 | };
104 |
105 | await loader.pull("en", input);
106 | const result = await loader.push("en", payload);
107 |
108 | expect(result).toEqual({
109 | message: "[updated] You have %2$d new items and %1$s.",
110 | });
111 | });
112 | });
113 |
114 | describe("python format", () => {
115 | it("extracts python variables during pull", async () => {
116 | const loader = createLoader("python");
117 | const input = {
118 | simple: "Hello %(name)s!",
119 | multiple: "Value: %(num)d and %(float)f",
120 | };
121 |
122 | const result = await loader.pull("en", input);
123 | expect(result).toEqual({
124 | simple: "Hello {variable:0}!",
125 | multiple: "Value: {variable:0} and {variable:1}",
126 | });
127 | });
128 |
129 | it("restores python variables during push", async () => {
130 | const loader = createLoader("python");
131 | const input = {
132 | simple: "Hello %(name)s!",
133 | multiple: "Value: %(num)d and %(float)f",
134 | };
135 |
136 | const payload = {
137 | simple: "[updated] Hello {variable:0}!",
138 | multiple: "[updated] Value: {variable:0} and {variable:1}",
139 | };
140 |
141 | await loader.pull("en", input);
142 | const result = await loader.push("en", input);
143 | expect(result).toEqual({
144 | simple: "Hello %(name)s!",
145 | multiple: "Value: %(num)d and %(float)f",
146 | });
147 | });
148 |
149 | it("preserves variable order for target locale during push", async () => {
150 | const loader = createLoader("python");
151 |
152 | const sourceInput = {
153 | message: "Hello %(name)s, you have %(count)d items.",
154 | };
155 |
156 | // Pull default locale first
157 | await loader.pull("en", sourceInput);
158 |
159 | // Target locale with reversed variable order
160 | const targetInput = {
161 | message: "Du hast %(count)d Artikel, %(name)s.",
162 | };
163 | await loader.pull("de", targetInput);
164 |
165 | const payload = {
166 | message: "[aktualisiert] Du hast {variable:1} Artikel, {variable:0}.",
167 | };
168 |
169 | const result = await loader.push("de", payload);
170 |
171 | expect(result).toEqual({
172 | message: "[aktualisiert] Du hast %(count)d Artikel, %(name)s.",
173 | });
174 | });
175 | });
176 |
177 | it("throws error for unsupported format type", () => {
178 | expect(() => {
179 | // @ts-expect-error Testing invalid type
180 | createVariableLoader({ type: "invalid" });
181 | }).toThrow("Unsupported variable format type: invalid");
182 | });
183 | });
184 |
185 | function createLoader(type: VariableLoaderParams["type"]) {
186 | return createVariableLoader({ type }).setDefaultLocale("en");
187 | }
188 |
```
--------------------------------------------------------------------------------
/packages/cli/src/cli/loaders/ejs.spec.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect } from "vitest";
2 | import createEjsLoader from "./ejs";
3 |
4 | describe("EJS Loader", () => {
5 | const loader = createEjsLoader().setDefaultLocale("en");
6 |
7 | describe("pull", () => {
8 | it("should extract translatable text from simple EJS template", async () => {
9 | const input = `
10 | <h1>Welcome to our website</h1>
11 | <p>Hello <%= name %>, you have <%= messages.length %> messages.</p>
12 | <footer>© 2024 Our Company</footer>
13 | `;
14 |
15 | const result = await loader.pull("en", input);
16 |
17 | // Check that we have extracted some translatable content
18 | expect(Object.keys(result).length).toBeGreaterThan(0);
19 |
20 | // Check that the EJS variables are not included in the translatable text
21 | const allValues = Object.values(result).join(" ");
22 | expect(allValues).not.toContain("<%= name %>");
23 | expect(allValues).not.toContain("<%= messages.length %>");
24 |
25 | // Check that we have the main content
26 | expect(allValues).toContain("Welcome to our website");
27 | expect(allValues).toContain("Hello");
28 | expect(allValues).toContain("messages");
29 | expect(allValues).toContain("© 2024 Our Company");
30 | });
31 |
32 | it("should handle EJS templates with various tag types", async () => {
33 | const input = `
34 | <div>
35 | <h2>User Dashboard</h2>
36 | <% if (user.isAdmin) { %>
37 | <p>Admin Panel</p>
38 | <% } %>
39 | <%# This is a comment %>
40 | <p>Welcome back, <%- user.name %></p>
41 | <span>Last login: <%= formatDate(user.lastLogin) %></span>
42 | </div>
43 | `;
44 |
45 | const result = await loader.pull("en", input);
46 |
47 | expect(result).toHaveProperty("text_0");
48 | expect(result).toHaveProperty("text_1");
49 | expect(Object.keys(result).length).toBeGreaterThan(0);
50 | });
51 |
52 | it("should handle empty input", async () => {
53 | const result = await loader.pull("en", "");
54 | expect(result).toEqual({});
55 | });
56 |
57 | it("should handle input with only EJS tags", async () => {
58 | const input = "<%= variable %><% if (condition) { %><% } %>";
59 | const result = await loader.pull("en", input);
60 | expect(result).toEqual({});
61 | });
62 |
63 | it("should handle mixed content", async () => {
64 | const input = `
65 | Welcome <%= user.name %>!
66 | <% for (let i = 0; i < items.length; i++) { %>
67 | Item: <%= items[i].name %>
68 | <% } %>
69 | Thank you for visiting.
70 | `;
71 |
72 | const result = await loader.pull("en", input);
73 | expect(Object.keys(result).length).toBeGreaterThan(0);
74 | expect(
75 | Object.values(result).some((text) => text.includes("Welcome")),
76 | ).toBe(true);
77 | expect(
78 | Object.values(result).some((text) => text.includes("Thank you")),
79 | ).toBe(true);
80 | });
81 | });
82 |
83 | describe("push", () => {
84 | it("should reconstruct EJS template with translated content", async () => {
85 | const originalInput = `<h1>Welcome</h1><p>Hello <%= name %></p>`;
86 |
87 | // First pull to get the structure
88 | const pulled = await loader.pull("en", originalInput);
89 |
90 | // Static translated data object based on actual loader behavior
91 | const translated = {
92 | text_0: "Bienvenido",
93 | text_1: "Hola",
94 | };
95 |
96 | const result = await loader.push("es", translated);
97 |
98 | // Test against the expected reconstructed string
99 | const expectedOutput = `<h1>Bienvenido</h1><p>Hola <%= name %></p>`;
100 |
101 | expect(result).toBe(expectedOutput);
102 | });
103 |
104 | it("should handle complex EJS templates", async () => {
105 | const originalInput = `<h2>Dashboard</h2><% if (user) { %><p>Welcome</p><% } %>`;
106 |
107 | const pulled = await loader.pull("en", originalInput);
108 |
109 | // Static translated data object
110 | const translated = {
111 | text_0: "Tablero",
112 | text_1: "Bienvenido",
113 | };
114 |
115 | const result = await loader.push("es", translated);
116 |
117 | // Test against the expected reconstructed string
118 | const expectedOutput = `<h2>Tablero</h2><% if (user) { %><p>Bienvenido</p><% } %>`;
119 |
120 | expect(result).toBe(expectedOutput);
121 | });
122 |
123 | it("should handle missing original input", async () => {
124 | const translated = {
125 | text_0: "Hello World",
126 | text_1: "This is a test",
127 | };
128 |
129 | const result = await loader.push("es", translated);
130 |
131 | expect(result).toContain("Hello World");
132 | expect(result).toContain("This is a test");
133 | });
134 | });
135 |
136 | describe("round trip", () => {
137 | it("should maintain EJS functionality after round trip", async () => {
138 | const originalInput = `
139 | <h1>Welcome <%= title %></h1>
140 | <% if (showMessage) { %>
141 | <p>Hello <%= user.name %>, you have <%= count %> new messages.</p>
142 | <% } %>
143 | <ul>
144 | <% items.forEach(function(item) { %>
145 | <li><%= item.name %> - $<%= item.price %></li>
146 | <% }); %>
147 | </ul>
148 | <footer>Contact us at [email protected]</footer>
149 | `;
150 |
151 | // Pull original content
152 | const pulled = await loader.pull("en", originalInput);
153 |
154 | // Push back without translation (should be identical)
155 | const reconstructed = await loader.push("en", pulled);
156 |
157 | // Verify EJS tags are preserved
158 | expect(reconstructed).toContain("<%= title %>");
159 | expect(reconstructed).toContain("<% if (showMessage) { %>");
160 | expect(reconstructed).toContain("<%= user.name %>");
161 | expect(reconstructed).toContain("<%= count %>");
162 | expect(reconstructed).toContain("<% items.forEach(function(item) { %>");
163 | expect(reconstructed).toContain("<%= item.name %>");
164 | expect(reconstructed).toContain("<%= item.price %>");
165 | expect(reconstructed).toContain("<% }); %>");
166 | expect(reconstructed).toContain("Contact us at [email protected]");
167 | });
168 | });
169 | });
170 |
```
--------------------------------------------------------------------------------
/packages/cli/src/cli/loaders/mdx2/code-placeholder.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { ILoader } from "../_types";
2 | import { createLoader } from "../_utils";
3 | import { md5 } from "../../utils/md5";
4 | import _ from "lodash";
5 |
6 | const fenceRegex = /([ \t]*)(^>\s*)?```([\s\S]*?)```/gm;
7 | const inlineCodeRegex = /(?<!`)`([^`\r\n]+?)`(?!`)/g;
8 |
9 | // Matches markdown image tags, with optional alt text & parenthesis URL, possibly inside blockquotes
10 | // Captures patterns like  or , with optional leading '> ' for blockquotes
11 | const imageRegex =
12 | /([ \t]*)(^>\s*)?!\[[^\]]*?\]\(([^()]*(\([^()]*\)[^()]*)*)\)/gm;
13 |
14 | /**
15 | * Ensures that markdown image tags are surrounded by blank lines (\n\n) so that they are properly
16 | * treated as separate blocks during subsequent processing and serialization.
17 | *
18 | * Behaviour mirrors `ensureTrailingFenceNewline` logic for code fences:
19 | * • If an image tag is already inside a blockquote (starts with `>` after trimming) we leave it untouched.
20 | * • Otherwise we add two newlines before and after the image tag, then later collapse multiple
21 | * consecutive blank lines back to exactly one separation using lodash chain logic.
22 | */
23 | function ensureSurroundingImageNewlines(_content: string) {
24 | let found = false;
25 | let content = _content;
26 | let workingContent = content;
27 |
28 | do {
29 | found = false;
30 | const matches = workingContent.match(imageRegex);
31 | if (matches) {
32 | const match = matches[0];
33 |
34 | const replacement = match.trim().startsWith(">")
35 | ? match
36 | : `\n\n${match}\n\n`;
37 |
38 | content = content.replaceAll(match, () => replacement);
39 | workingContent = workingContent.replaceAll(match, "");
40 | found = true;
41 | }
42 | } while (found);
43 |
44 | content = _.chain(content)
45 | .split("\n\n")
46 | .map((section) => _.trim(section, "\n"))
47 | .filter(Boolean)
48 | .join("\n\n")
49 | .value();
50 |
51 | return content;
52 | }
53 |
54 | function ensureTrailingFenceNewline(_content: string) {
55 | let found = false;
56 | let content = _content;
57 | let workingContent = content;
58 |
59 | do {
60 | found = false;
61 | const matches = workingContent.match(fenceRegex);
62 | if (matches) {
63 | const match = matches[0];
64 |
65 | const replacement = match.trim().startsWith(">")
66 | ? match
67 | : `\n\n${match}\n\n`;
68 | content = content.replaceAll(match, () => replacement);
69 | workingContent = workingContent.replaceAll(match, "");
70 | found = true;
71 | }
72 | } while (found);
73 |
74 | content = _.chain(content)
75 | .split("\n\n")
76 | .map((section) => _.trim(section, "\n"))
77 | .filter(Boolean)
78 | .join("\n\n")
79 | .value();
80 |
81 | return content;
82 | }
83 |
84 | // Helper that replaces code (block & inline) with stable placeholders and returns
85 | // both the transformed content and the placeholder → original mapping so it can
86 | // later be restored. Extracted so that we can reuse the exact same logic in both
87 | // `pull` and `push` phases (e.g. to recreate the mapping from `originalInput`).
88 | function extractCodePlaceholders(content: string): {
89 | content: string;
90 | codePlaceholders: Record<string, string>;
91 | } {
92 | let finalContent = content;
93 | finalContent = ensureTrailingFenceNewline(finalContent);
94 | finalContent = ensureSurroundingImageNewlines(finalContent);
95 |
96 | const codePlaceholders: Record<string, string> = {};
97 |
98 | const codeBlockMatches = finalContent.matchAll(fenceRegex);
99 | for (const match of codeBlockMatches) {
100 | const codeBlock = match[0];
101 | const codeBlockHash = md5(codeBlock);
102 | const placeholder = `---CODE-PLACEHOLDER-${codeBlockHash}---`;
103 |
104 | codePlaceholders[placeholder] = codeBlock;
105 |
106 | const replacement = codeBlock.trim().startsWith(">")
107 | ? `> ${placeholder}`
108 | : `${placeholder}`;
109 | finalContent = finalContent.replace(codeBlock, () => replacement);
110 | }
111 |
112 | const inlineCodeMatches = finalContent.matchAll(inlineCodeRegex);
113 | for (const match of inlineCodeMatches) {
114 | const inlineCode = match[0];
115 | const inlineCodeHash = md5(inlineCode);
116 | const placeholder = `---INLINE-CODE-PLACEHOLDER-${inlineCodeHash}---`;
117 | codePlaceholders[placeholder] = inlineCode;
118 | const replacement = placeholder;
119 | finalContent = finalContent.replace(inlineCode, () => replacement);
120 | }
121 |
122 | return {
123 | content: finalContent,
124 | codePlaceholders,
125 | };
126 | }
127 |
128 | export default function createMdxCodePlaceholderLoader(): ILoader<
129 | string,
130 | string
131 | > {
132 | // Keep a global registry of all placeholders we've ever created
133 | // This solves the state synchronization issue
134 | const globalPlaceholderRegistry: Record<string, string> = {};
135 |
136 | return createLoader({
137 | async pull(locale, input) {
138 | const response = extractCodePlaceholders(input);
139 |
140 | // Register all placeholders we create so we can use them later
141 | Object.assign(globalPlaceholderRegistry, response.codePlaceholders);
142 |
143 | return response.content;
144 | },
145 |
146 | async push(locale, data, originalInput, originalLocale, pullInput) {
147 | const sourceInfo = extractCodePlaceholders(originalInput ?? "");
148 | const currentInfo = extractCodePlaceholders(pullInput ?? "");
149 |
150 | // Use the global registry to ensure all placeholders can be replaced,
151 | // including those from previous pulls that are no longer in current state
152 | const codePlaceholders = _.merge(
153 | sourceInfo.codePlaceholders,
154 | currentInfo.codePlaceholders,
155 | globalPlaceholderRegistry, // Include ALL placeholders ever created
156 | );
157 |
158 | let result = data;
159 | for (const [placeholder, original] of Object.entries(codePlaceholders)) {
160 | const replacement = original.startsWith(">")
161 | ? _.trimStart(original, "> ")
162 | : original;
163 |
164 | // Use function replacer to avoid special $ character handling
165 | // When using a string, $ has special meaning (e.g., $` inserts text before match)
166 | result = result.replaceAll(placeholder, () => replacement);
167 | }
168 |
169 | return result;
170 | },
171 | });
172 | }
173 |
```
--------------------------------------------------------------------------------
/readme/pt-BR.md:
--------------------------------------------------------------------------------
```markdown
1 | <p align="center">
2 | <a href="https://lingo.dev">
3 | <img
4 | src="https://raw.githubusercontent.com/lingodotdev/lingo.dev/main/content/banner.compiler.png"
5 | width="100%"
6 | alt="Lingo.dev"
7 | />
8 | </a>
9 | </p>
10 |
11 | <p align="center">
12 | <strong>
13 | ⚡ Lingo.dev - kit de ferramentas i18n de código aberto, alimentado por IA
14 | para localização instantânea com LLMs.
15 | </strong>
16 | </p>
17 |
18 | <br />
19 |
20 | <p align="center">
21 | <a href="https://lingo.dev/compiler">Lingo.dev Compiler</a> •
22 | <a href="https://lingo.dev/cli">Lingo.dev CLI</a> •
23 | <a href="https://lingo.dev/ci">Lingo.dev CI/CD</a> •
24 | <a href="https://lingo.dev/sdk">Lingo.dev SDK</a>
25 | </p>
26 |
27 | <p align="center">
28 | <a href="https://github.com/lingodotdev/lingo.dev/actions/workflows/release.yml">
29 | <img
30 | src="https://github.com/lingodotdev/lingo.dev/actions/workflows/release.yml/badge.svg"
31 | alt="Release"
32 | />
33 | </a>
34 | <a href="https://github.com/lingodotdev/lingo.dev/blob/main/LICENSE.md">
35 | <img
36 | src="https://img.shields.io/github/license/lingodotdev/lingo.dev"
37 | alt="Licença"
38 | />
39 | </a>
40 | <a href="https://github.com/lingodotdev/lingo.dev/commits/main">
41 | <img
42 | src="https://img.shields.io/github/last-commit/lingodotdev/lingo.dev"
43 | alt="Último Commit"
44 | />
45 | </a>
46 | </p>
47 |
48 | ---
49 |
50 | ## Conheça o Compiler 🆕
51 |
52 | **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.
53 |
54 | ---CODE-PLACEHOLDER-f159f7253d409892d00e70ee045902a5---
55 |
56 | Execute `next build` e veja os pacotes em espanhol e francês surgirem ✨
57 |
58 | [Leia a documentação →](https://lingo.dev/compiler) para o guia completo.
59 |
60 | ---
61 |
62 | ### O que há neste repositório?
63 |
64 | | Ferramenta | Resumo | Documentação |
65 | | ------------ | ------------------------------------------------------------------------------------------- | --------------------------------------- |
66 | | **Compiler** | Localização React em tempo de compilação | [/compiler](https://lingo.dev/compiler) |
67 | | **CLI** | Localização com um único comando para aplicativos web e mobile, JSON, YAML, markdown e mais | [/cli](https://lingo.dev/cli) |
68 | | **CI/CD** | Auto-commit de traduções a cada push + criação de pull requests se necessário | [/ci](https://lingo.dev/ci) |
69 | | **SDK** | Tradução em tempo real para conteúdo gerado pelo usuário | [/sdk](https://lingo.dev/sdk) |
70 |
71 | Abaixo estão os destaques de cada um 👇
72 |
73 | ---
74 |
75 | ### ⚡️ Lingo.dev CLI
76 |
77 | Traduza código e conteúdo diretamente do seu terminal.
78 |
79 | ---CODE-PLACEHOLDER-a4836309dda7477e1ba399e340828247---
80 |
81 | Ele cria uma impressão digital de cada string, armazena resultados em cache e apenas retraduz o que foi alterado.
82 |
83 | [Leia a documentação →](https://lingo.dev/cli)
84 |
85 | ---
86 |
87 | ### 🔄 Lingo.dev CI/CD
88 |
89 | Entregue traduções perfeitas automaticamente.
90 |
91 | ```yaml
92 | # .github/workflows/i18n.yml
93 | name: Lingo.dev i18n
94 | on: [push]
95 |
96 | jobs:
97 | i18n:
98 | runs-on: ubuntu-latest
99 | steps:
100 | - uses: actions/checkout@v4
101 | - uses: lingodotdev/lingo.dev@main
102 | with:
103 | api-key: ${{ secrets.LINGODOTDEV_API_KEY }}
104 | ```
105 |
106 | Mantém seu repositório verde e seu produto multilíngue sem etapas manuais.
107 |
108 | [Leia a documentação →](https://lingo.dev/ci)
109 |
110 | ---
111 |
112 | ### 🧩 SDK Lingo.dev
113 |
114 | Tradução instantânea por requisição para conteúdo dinâmico.
115 |
116 | ---CODE-PLACEHOLDER-c50e1e589a70e31dd2dde95be8da6ddf---
117 |
118 | Perfeito para chat, comentários de usuários e outros fluxos em tempo real.
119 |
120 | [Leia a documentação →](https://lingo.dev/sdk)
121 |
122 | ```ts
123 | import { LingoDotDevEngine } from "lingo.dev/sdk";
124 |
125 | const lingoDotDev = new LingoDotDevEngine({
126 | apiKey: "your-api-key-here",
127 | });
128 |
129 | const content = {
130 | greeting: "Hello",
131 | farewell: "Goodbye",
132 | message: "Welcome to our platform",
133 | };
134 |
135 | const translated = await lingoDotDev.localizeObject(content, {
136 | sourceLocale: "en",
137 | targetLocale: "es",
138 | });
139 | // Returns: { greeting: "Hola", farewell: "Adiós", message: "Bienvenido a nuestra plataforma" }
140 | ```
141 |
142 | ## 🤝 Comunidade
143 |
144 | Somos orientados pela comunidade e adoramos contribuições!
145 |
146 | - Tem uma ideia? [Abra uma issue](https://github.com/lingodotdev/lingo.dev/issues)
147 | - Quer corrigir algo? [Envie um PR](https://github.com/lingodotdev/lingo.dev/pulls)
148 | - Precisa de ajuda? [Entre no nosso Discord](https://lingo.dev/go/discord)
149 |
150 | ## ⭐ Histórico de Estrelas
151 |
152 | Se você gosta do que estamos fazendo, dê-nos uma ⭐ e ajude-nos a alcançar 3.000 estrelas! 🌟
153 |
154 | [
155 |
156 | 
157 |
158 | ](https://www.star-history.com/#lingodotdev/lingo.dev&Date)
159 |
160 | ## 🌐 Readme em outros idiomas
161 |
162 | [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)
163 |
164 | Não vê seu idioma? Adicione-o ao [`i18n.json`](./i18n.json) e abra um PR!
165 |
166 | ## 🌐 Readme em outros idiomas
167 |
168 | [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)
169 |
170 | Não vê seu idioma? Adicione-o ao [`i18n.json`](./i18n.json) e abra um PR!
171 |
```
--------------------------------------------------------------------------------
/readme/pl.md:
--------------------------------------------------------------------------------
```markdown
1 | <p align="center">
2 | <a href="https://lingo.dev">
3 | <img
4 | src="https://raw.githubusercontent.com/lingodotdev/lingo.dev/main/content/banner.compiler.png"
5 | width="100%"
6 | alt="Lingo.dev"
7 | />
8 | </a>
9 | </p>
10 |
11 | <p align="center">
12 | <strong>
13 | ⚡ Lingo.dev - otwartoźródłowe, wspierane przez AI narzędzie i18n do
14 | natychmiastowej lokalizacji z wykorzystaniem LLM.
15 | </strong>
16 | </p>
17 |
18 | <br />
19 |
20 | <p align="center">
21 | <a href="https://lingo.dev/compiler">Lingo.dev Compiler</a> •
22 | <a href="https://lingo.dev/cli">Lingo.dev CLI</a> •
23 | <a href="https://lingo.dev/ci">Lingo.dev CI/CD</a> •
24 | <a href="https://lingo.dev/sdk">Lingo.dev SDK</a>
25 | </p>
26 |
27 | <p align="center">
28 | <a href="https://github.com/lingodotdev/lingo.dev/actions/workflows/release.yml">
29 | <img
30 | src="https://github.com/lingodotdev/lingo.dev/actions/workflows/release.yml/badge.svg"
31 | alt="Release"
32 | />
33 | </a>
34 | <a href="https://github.com/lingodotdev/lingo.dev/blob/main/LICENSE.md">
35 | <img
36 | src="https://img.shields.io/github/license/lingodotdev/lingo.dev"
37 | alt="Licencja"
38 | />
39 | </a>
40 | <a href="https://github.com/lingodotdev/lingo.dev/commits/main">
41 | <img
42 | src="https://img.shields.io/github/last-commit/lingodotdev/lingo.dev"
43 | alt="Ostatni commit"
44 | />
45 | </a>
46 | </p>
47 |
48 | ---
49 |
50 | ## Poznaj Compiler 🆕
51 |
52 | **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.
53 |
54 | Zainstaluj raz:
55 |
56 | ```bash
57 | npm install lingo.dev
58 | ```
59 |
60 | Włącz w swojej konfiguracji budowania:
61 |
62 | ```js
63 | import lingoCompiler from "lingo.dev/compiler";
64 |
65 | const existingNextConfig = {};
66 |
67 | export default lingoCompiler.next({
68 | sourceLocale: "en",
69 | targetLocales: ["es", "fr"],
70 | })(existingNextConfig);
71 | ```
72 |
73 | Uruchom `next build` i zobacz, jak pojawiają się pakiety w języku hiszpańskim i francuskim ✨
74 |
75 | [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.
76 |
77 | ---
78 |
79 | ### Co zawiera to repozytorium?
80 |
81 | | Narzędzie | TL;DR | Dokumentacja |
82 | | ------------ | ----------------------------------------------------------------------------------------------------- | --------------------------------------- |
83 | | **Compiler** | Lokalizacja React na etapie budowania | [/compiler](https://lingo.dev/compiler) |
84 | | **CLI** | Lokalizacja aplikacji webowych i mobilnych, JSON, YAML, markdown i więcej | [/cli](https://lingo.dev/cli) |
85 | | **CI/CD** | Automatyczne zatwierdzanie tłumaczeń przy każdym pushu + tworzenie pull requestów, jeśli to konieczne | [/ci](https://lingo.dev/ci) |
86 | | **SDK** | Tłumaczenie w czasie rzeczywistym dla treści generowanych przez użytkowników | [/sdk](https://lingo.dev/sdk) |
87 |
88 | Poniżej znajdziesz szybkie informacje o każdym z nich 👇
89 |
90 | ---
91 |
92 | ### ⚡️ Lingo.dev CLI
93 |
94 | Tłumacz kod i treści bezpośrednio z terminala.
95 |
96 | ```bash
97 | npx lingo.dev@latest run
98 | ```
99 |
100 | Odciska każdy ciąg znaków, zapisuje wyniki w pamięci podręcznej i tłumaczy ponownie tylko to, co się zmieniło.
101 |
102 | [Przejdź do dokumentacji →](https://lingo.dev/cli), aby dowiedzieć się, jak to skonfigurować.
103 |
104 | ---
105 |
106 | ### 🔄 Lingo.dev CI/CD
107 |
108 | Automatyczne dostarczanie perfekcyjnych tłumaczeń.
109 |
110 | ```yaml
111 | # .github/workflows/i18n.yml
112 | name: Lingo.dev i18n
113 | on: [push]
114 |
115 | jobs:
116 | i18n:
117 | runs-on: ubuntu-latest
118 | steps:
119 | - uses: actions/checkout@v4
120 | - uses: lingodotdev/lingo.dev@main
121 | with:
122 | api-key: ${{ secrets.LINGODOTDEV_API_KEY }}
123 | ```
124 |
125 | Utrzymuje repozytorium w dobrym stanie i produkt wielojęzyczny bez ręcznych kroków.
126 |
127 | [Przeczytaj dokumentację →](https://lingo.dev/ci)
128 |
129 | ---
130 |
131 | ### 🧩 Lingo.dev SDK
132 |
133 | Natychmiastowe tłumaczenie na żądanie dla dynamicznych treści.
134 |
135 | ```ts
136 | import { LingoDotDevEngine } from "lingo.dev/sdk";
137 |
138 | const lingoDotDev = new LingoDotDevEngine({
139 | apiKey: "your-api-key-here",
140 | });
141 |
142 | const content = {
143 | greeting: "Hello",
144 | farewell: "Goodbye",
145 | message: "Welcome to our platform",
146 | };
147 |
148 | const translated = await lingoDotDev.localizeObject(content, {
149 | sourceLocale: "en",
150 | targetLocale: "es",
151 | });
152 | // Returns: { greeting: "Hola", farewell: "Adiós", message: "Bienvenido a nuestra plataforma" }
153 | ```
154 |
155 | Idealne do czatów, komentarzy użytkowników i innych procesów w czasie rzeczywistym.
156 |
157 | [Przeczytaj dokumentację →](https://lingo.dev/sdk)
158 |
159 | ---
160 |
161 | ## 🤝 Społeczność
162 |
163 | Jesteśmy napędzani przez społeczność i uwielbiamy wkład innych!
164 |
165 | - Masz pomysł? [Otwórz zgłoszenie](https://github.com/lingodotdev/lingo.dev/issues)
166 | - Chcesz coś naprawić? [Wyślij PR](https://github.com/lingodotdev/lingo.dev/pulls)
167 | - Potrzebujesz pomocy? [Dołącz do naszego Discorda](https://lingo.dev/go/discord)
168 |
169 | ## ⭐ Historia gwiazdek
170 |
171 | Jeśli podoba Ci się to, co robimy, daj nam ⭐ i pomóż nam osiągnąć 3 000 gwiazdek! 🌟
172 |
173 | [
174 |
175 | 
176 |
177 | ](https://www.star-history.com/#lingodotdev/lingo.dev&Date)
178 |
179 | ## 🌐 Readme w innych językach
180 |
181 | [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)
182 |
183 | Nie widzisz swojego języka? Dodaj go do [`i18n.json`](./i18n.json) i otwórz PR!
184 |
```
--------------------------------------------------------------------------------
/packages/compiler/src/lib/lcp/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | import * as fs from "fs";
2 | import _ from "lodash";
3 | import { LCPFile, LCPSchema, LCPScope } from "./schema";
4 | import * as path from "path";
5 | import { LCP_DICTIONARY_FILE_NAME } from "../../_const";
6 | import dedent from "dedent";
7 |
8 | const LCP_FILE_NAME = "meta.json";
9 |
10 | export class LCP {
11 | private constructor(
12 | private readonly filePath: string,
13 | public readonly data: LCPSchema = {
14 | version: 0.1,
15 | },
16 | ) {}
17 |
18 | public static ensureFile(params: { sourceRoot: string; lingoDir: string }) {
19 | const filePath = path.resolve(
20 | process.cwd(),
21 | params.sourceRoot,
22 | params.lingoDir,
23 | LCP_FILE_NAME,
24 | );
25 | if (!fs.existsSync(filePath)) {
26 | const dir = path.dirname(filePath);
27 | if (!fs.existsSync(dir)) {
28 | fs.mkdirSync(dir, { recursive: true });
29 | }
30 | fs.writeFileSync(filePath, "{}");
31 |
32 | try {
33 | fs.rmdirSync(path.resolve(process.cwd(), ".next"), {
34 | recursive: true,
35 | });
36 | } catch (error) {
37 | // Ignore errors if directory doesn't exist
38 | }
39 | throw new Error(dedent`
40 | ⚠️ Lingo.dev Compiler detected missing meta.json file in lingo directory.
41 | Please restart the build / watch command to regenerate all Lingo.dev Compiler files.
42 | `);
43 | }
44 | }
45 |
46 | public static getInstance(params: {
47 | sourceRoot: string;
48 | lingoDir: string;
49 | }): LCP {
50 | const filePath = path.resolve(
51 | process.cwd(),
52 | params.sourceRoot,
53 | params.lingoDir,
54 | LCP_FILE_NAME,
55 | );
56 | if (fs.existsSync(filePath)) {
57 | return new LCP(filePath, JSON.parse(fs.readFileSync(filePath, "utf8")));
58 | }
59 | return new LCP(filePath);
60 | }
61 |
62 | // wait until LCP file stops updating
63 | // this ensures all files were transformed before loading / translating dictionaries
64 | public static async ready(params: {
65 | sourceRoot: string;
66 | lingoDir: string;
67 | isDev: boolean;
68 | }): Promise<void> {
69 | if (params.isDev) {
70 | LCP.ensureFile(params);
71 | }
72 |
73 | const filePath = path.resolve(
74 | process.cwd(),
75 | params.sourceRoot,
76 | params.lingoDir,
77 | LCP_FILE_NAME,
78 | );
79 | if (fs.existsSync(filePath)) {
80 | const stats = fs.statSync(filePath);
81 | if (Date.now() - stats.mtimeMs > 1500) {
82 | return;
83 | }
84 | }
85 | return new Promise((resolve) => {
86 | setTimeout(() => {
87 | LCP.ready(params).then(resolve);
88 | }, 750);
89 | });
90 | }
91 |
92 | resetScope(fileKey: string, scopeKey: string): this {
93 | if (
94 | !_.isObject(
95 | _.get(this.data, ["files" satisfies keyof LCPSchema, fileKey]),
96 | )
97 | ) {
98 | _.set(this.data, ["files" satisfies keyof LCPSchema, fileKey], {});
99 | }
100 |
101 | _.set(
102 | this.data,
103 | [
104 | "files" satisfies keyof LCPSchema,
105 | fileKey,
106 | "scopes" satisfies keyof LCPFile,
107 | scopeKey,
108 | ],
109 | {},
110 | );
111 |
112 | return this;
113 | }
114 |
115 | setScopeType(
116 | fileKey: string,
117 | scopeKey: string,
118 | type: "element" | "attribute",
119 | ): this {
120 | return this._setScopeField(fileKey, scopeKey, "type", type);
121 | }
122 |
123 | setScopeContext(fileKey: string, scopeKey: string, context: string): this {
124 | return this._setScopeField(fileKey, scopeKey, "context", context);
125 | }
126 |
127 | setScopeHash(fileKey: string, scopeKey: string, hash: string): this {
128 | return this._setScopeField(fileKey, scopeKey, "hash", hash);
129 | }
130 |
131 | setScopeSkip(fileKey: string, scopeKey: string, skip: boolean): this {
132 | return this._setScopeField(fileKey, scopeKey, "skip", skip);
133 | }
134 |
135 | setScopeOverrides(
136 | fileKey: string,
137 | scopeKey: string,
138 | overrides: Record<string, string>,
139 | ): this {
140 | return this._setScopeField(fileKey, scopeKey, "overrides", overrides);
141 | }
142 |
143 | setScopeContent(fileKey: string, scopeKey: string, content: string): this {
144 | return this._setScopeField(fileKey, scopeKey, "content", content);
145 | }
146 |
147 | toJSON() {
148 | const files = _(this.data?.files)
149 | .mapValues((file: any, fileName: string) => {
150 | return {
151 | ...file,
152 | scopes: _(file?.scopes).toPairs().sortBy([0]).fromPairs().value(),
153 | };
154 | })
155 | .toPairs()
156 | .sortBy([0])
157 | .fromPairs()
158 | .value();
159 | return { ...this.data, files };
160 | }
161 |
162 | toString() {
163 | return JSON.stringify(this.toJSON(), null, 2) + "\n";
164 | }
165 |
166 | save() {
167 | const hasChanges =
168 | !fs.existsSync(this.filePath) ||
169 | fs.readFileSync(this.filePath, "utf8") !== this.toString();
170 |
171 | if (hasChanges) {
172 | const dir = path.dirname(this.filePath);
173 | if (!fs.existsSync(dir)) {
174 | fs.mkdirSync(dir, { recursive: true });
175 | }
176 | fs.writeFileSync(this.filePath, this.toString());
177 |
178 | this._triggerLCPReload();
179 | }
180 | }
181 |
182 | private _triggerLCPReload() {
183 | const dir = path.dirname(this.filePath);
184 | const filePath = path.resolve(dir, LCP_DICTIONARY_FILE_NAME);
185 | if (fs.existsSync(filePath)) {
186 | try {
187 | const now = Math.floor(Date.now() / 1000); // Convert to seconds
188 | fs.utimesSync(filePath, now, now);
189 | } catch (error: any) {
190 | // Non-critical operation - timestamp update is just for triggering reload
191 | if (error?.code === "EINVAL") {
192 | console.warn(
193 | dedent`
194 | ⚠️ Lingo: Auto-reload disabled - system blocks Node.js timestamp updates.
195 | 💡 Fix: Adjust security settings to allow Node.js file modifications.
196 | ⚡ Workaround: Manually refresh browser after translation changes.
197 | 💬 Need help? Join our Discord: https://lingo.dev/go/discord.
198 | `,
199 | );
200 | }
201 | }
202 | }
203 | }
204 |
205 | private _setScopeField<K extends keyof LCPScope>(
206 | fileKey: string,
207 | scopeKey: string,
208 | field: K,
209 | value: LCPScope[K],
210 | ): this {
211 | _.set(
212 | this.data,
213 | [
214 | "files" satisfies keyof LCPSchema,
215 | fileKey,
216 | "scopes" satisfies keyof LCPFile,
217 | scopeKey,
218 | field,
219 | ],
220 | value,
221 | );
222 | return this;
223 | }
224 | }
225 |
```
--------------------------------------------------------------------------------
/packages/cli/src/cli/cmd/run/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Command } from "interactive-commander";
2 | import { exec } from "child_process";
3 | import path from "path";
4 | import { fileURLToPath } from "url";
5 | import os from "os";
6 | import setup from "./setup";
7 | import plan from "./plan";
8 | import execute from "./execute";
9 | import watch from "./watch";
10 | import { CmdRunContext, flagsSchema } from "./_types";
11 | import frozen from "./frozen";
12 | import {
13 | renderClear,
14 | renderSpacer,
15 | renderBanner,
16 | renderHero,
17 | pauseIfDebug,
18 | renderSummary,
19 | } from "../../utils/ui";
20 | import trackEvent from "../../utils/observability";
21 | import { determineAuthId } from "./_utils";
22 |
23 | const __dirname = path.dirname(fileURLToPath(import.meta.url));
24 |
25 | function playSound(type: "success" | "failure") {
26 | const platform = os.platform();
27 |
28 | return new Promise<void>((resolve) => {
29 | const assetDir = path.join(__dirname, "../assets");
30 | const soundFiles = [path.join(assetDir, `${type}.mp3`)];
31 |
32 | let command = "";
33 |
34 | if (platform === "linux") {
35 | command = soundFiles
36 | .map(
37 | (file) =>
38 | `mpg123 -q "${file}" 2>/dev/null || aplay "${file}" 2>/dev/null`,
39 | )
40 | .join(" || ");
41 | } else if (platform === "darwin") {
42 | command = soundFiles.map((file) => `afplay "${file}"`).join(" || ");
43 | } else if (platform === "win32") {
44 | command = `powershell -c "try { (New-Object Media.SoundPlayer '${soundFiles[1]}').PlaySync() } catch { Start-Process -FilePath '${soundFiles[0]}' -WindowStyle Hidden -Wait }"`;
45 | } else {
46 | command = soundFiles
47 | .map(
48 | (file) =>
49 | `aplay "${file}" 2>/dev/null || afplay "${file}" 2>/dev/null`,
50 | )
51 | .join(" || ");
52 | }
53 |
54 | exec(command, () => {
55 | resolve();
56 | });
57 | setTimeout(resolve, 3000);
58 | });
59 | }
60 |
61 | export default new Command()
62 | .command("run")
63 | .description("Run localization pipeline")
64 | .helpOption("-h, --help", "Show help")
65 | .option(
66 | "--source-locale <source-locale>",
67 | "Override the source locale from i18n.json for this run",
68 | )
69 | .option(
70 | "--target-locale <target-locale>",
71 | "Limit processing to the listed target locale codes from i18n.json. Repeat the flag to include multiple locales. Defaults to all configured target locales",
72 | (val: string, prev: string[]) => (prev ? [...prev, val] : [val]),
73 | )
74 | .option(
75 | "--bucket <bucket>",
76 | "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",
77 | (val: string, prev: string[]) => (prev ? [...prev, val] : [val]),
78 | )
79 | .option(
80 | "--file <file>",
81 | "Filter bucket path pattern values by substring match. Examples: messages.json or locale/. Repeat to add multiple filters",
82 | (val: string, prev: string[]) => (prev ? [...prev, val] : [val]),
83 | )
84 | .option(
85 | "--key <key>",
86 | "Filter keys by prefix matching on dot-separated paths. Example: auth.login to match all keys starting with auth.login. Repeat for multiple patterns",
87 | (val: string, prev: string[]) =>
88 | prev ? [...prev, encodeURIComponent(val)] : [encodeURIComponent(val)],
89 | )
90 | .option(
91 | "--force",
92 | "Force re-translation of all keys, bypassing change detection. Useful when you want to regenerate translations with updated AI models or translation settings",
93 | )
94 | .option(
95 | "--frozen",
96 | "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",
97 | )
98 | .option(
99 | "--api-key <api-key>",
100 | "Override API key from settings or environment variables",
101 | )
102 | .option("--debug", "Pause before processing to allow attaching a debugger.")
103 | .option(
104 | "--concurrency <concurrency>",
105 | "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)",
106 | (val: string) => parseInt(val),
107 | )
108 | .option(
109 | "--watch",
110 | "Watch source locale files continuously and retranslate automatically when files change",
111 | )
112 | .option(
113 | "--debounce <milliseconds>",
114 | "Delay in milliseconds after file changes before retranslating in watch mode. Defaults to 5000",
115 | (val: string) => parseInt(val),
116 | )
117 | .option(
118 | "--sound",
119 | "Play audio feedback when translations complete (success or failure sounds)",
120 | )
121 | .action(async (args) => {
122 | let authId: string | null = null;
123 | try {
124 | const ctx: CmdRunContext = {
125 | flags: flagsSchema.parse(args),
126 | config: null,
127 | results: new Map(),
128 | tasks: [],
129 | localizer: null,
130 | };
131 |
132 | await pauseIfDebug(ctx.flags.debug);
133 | await renderClear();
134 | await renderSpacer();
135 | await renderBanner();
136 | await renderHero();
137 | await renderSpacer();
138 |
139 | await setup(ctx);
140 |
141 | authId = await determineAuthId(ctx);
142 |
143 | await trackEvent(authId, "cmd.run.start", {
144 | config: ctx.config,
145 | flags: ctx.flags,
146 | });
147 |
148 | await renderSpacer();
149 |
150 | await plan(ctx);
151 | await renderSpacer();
152 |
153 | await frozen(ctx);
154 | await renderSpacer();
155 |
156 | await execute(ctx);
157 | await renderSpacer();
158 |
159 | await renderSummary(ctx.results);
160 | await renderSpacer();
161 |
162 | // Play sound after main tasks complete if sound flag is enabled
163 | if (ctx.flags.sound) {
164 | await playSound("success");
165 | }
166 |
167 | // If watch mode is enabled, start watching for changes
168 | if (ctx.flags.watch) {
169 | await watch(ctx);
170 | }
171 |
172 | await trackEvent(authId, "cmd.run.success", {
173 | config: ctx.config,
174 | flags: ctx.flags,
175 | });
176 | } catch (error: any) {
177 | await trackEvent(authId || "unknown", "cmd.run.error", {});
178 | // Play sad sound if sound flag is enabled
179 | if (args.sound) {
180 | await playSound("failure");
181 | }
182 | throw error;
183 | }
184 | });
185 |
```
--------------------------------------------------------------------------------
/packages/cli/src/cli/loaders/jsonc.spec.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, expect, it } from "vitest";
2 | import createJsoncLoader from "./jsonc";
3 |
4 | describe("jsonc loader", () => {
5 | it("pull should parse valid JSONC format with comments", async () => {
6 | const loader = createJsoncLoader();
7 | loader.setDefaultLocale("en");
8 | const jsoncInput = `{
9 | // Comments are allowed in JSONC
10 | "hello": "Hello",
11 | "world": "World", // Trailing comment
12 | /* Block comment */
13 | "nested": {
14 | "key": "value"
15 | }
16 | }`;
17 |
18 | const result = await loader.pull("en", jsoncInput);
19 | expect(result).toEqual({
20 | hello: "Hello",
21 | world: "World",
22 | nested: {
23 | key: "value",
24 | },
25 | });
26 | });
27 |
28 | it("pull should parse JSONC with trailing commas", async () => {
29 | const loader = createJsoncLoader();
30 | loader.setDefaultLocale("en");
31 | const jsoncInput = `{
32 | "hello": "Hello",
33 | "world": "World",
34 | "array": [
35 | "item1",
36 | "item2",
37 | ],
38 | }`;
39 |
40 | const result = await loader.pull("en", jsoncInput);
41 | expect(result).toEqual({
42 | hello: "Hello",
43 | world: "World",
44 | array: ["item1", "item2"],
45 | });
46 | });
47 |
48 | it("pull should parse regular JSON as valid JSONC", async () => {
49 | const loader = createJsoncLoader();
50 | loader.setDefaultLocale("en");
51 | const jsonInput = '{"hello": "Hello", "world": "World"}';
52 |
53 | const result = await loader.pull("en", jsonInput);
54 | expect(result).toEqual({
55 | hello: "Hello",
56 | world: "World",
57 | });
58 | });
59 |
60 | it("pull should handle empty input", async () => {
61 | const loader = createJsoncLoader();
62 | loader.setDefaultLocale("en");
63 | const result = await loader.pull("en", "");
64 | expect(result).toEqual({});
65 | });
66 |
67 | it("pull should handle null/undefined input", async () => {
68 | const loader = createJsoncLoader();
69 | loader.setDefaultLocale("en");
70 | const result = await loader.pull("en", null as any);
71 | expect(result).toEqual({});
72 | });
73 |
74 | it("pull should handle JSONC with mixed comment styles", async () => {
75 | const loader = createJsoncLoader();
76 | loader.setDefaultLocale("en");
77 | const jsoncInput = `{
78 | // Line comment
79 | "title": "Hello",
80 | /*
81 | * Multi-line
82 | * block comment
83 | */
84 | "description": "World",
85 | "version": "1.0.0" // Another line comment
86 | }`;
87 |
88 | const result = await loader.pull("en", jsoncInput);
89 | expect(result).toEqual({
90 | title: "Hello",
91 | description: "World",
92 | version: "1.0.0",
93 | });
94 | });
95 |
96 | it("pull should throw error for invalid JSONC", async () => {
97 | const loader = createJsoncLoader();
98 | loader.setDefaultLocale("en");
99 | const invalidInput = `{
100 | "hello": "Hello"
101 | "world": "World" // missing comma
102 | invalid: syntax
103 | }`;
104 |
105 | await expect(loader.pull("en", invalidInput)).rejects.toThrow(
106 | "Failed to parse JSONC",
107 | );
108 | });
109 |
110 | it("push should serialize data to JSON format", async () => {
111 | const loader = createJsoncLoader();
112 | loader.setDefaultLocale("en");
113 | // Need to call pull first to initialize the loader state
114 | await loader.pull("en", "{}");
115 |
116 | const data = {
117 | hello: "Hello",
118 | world: "World",
119 | nested: {
120 | key: "value",
121 | },
122 | };
123 |
124 | const result = await loader.push("en", data);
125 | const expectedOutput = `{
126 | "hello": "Hello",
127 | "world": "World",
128 | "nested": {
129 | "key": "value"
130 | }
131 | }`;
132 |
133 | expect(result).toBe(expectedOutput);
134 | });
135 |
136 | it("push should handle empty object", async () => {
137 | const loader = createJsoncLoader();
138 | loader.setDefaultLocale("en");
139 | // Need to call pull first to initialize the loader state
140 | await loader.pull("en", "{}");
141 |
142 | const result = await loader.push("en", {});
143 | expect(result).toBe("{}");
144 | });
145 |
146 | it("push should handle complex nested data", async () => {
147 | const loader = createJsoncLoader();
148 | loader.setDefaultLocale("en");
149 | // Need to call pull first to initialize the loader state
150 | await loader.pull("en", "{}");
151 |
152 | const data = {
153 | strings: ["hello", "world"],
154 | numbers: [1, 2, 3],
155 | nested: {
156 | deep: {
157 | key: "value",
158 | },
159 | },
160 | };
161 |
162 | const result = await loader.push("en", data);
163 |
164 | // Parse the result back to verify it's valid JSON
165 | const parsed = JSON.parse(result);
166 | expect(parsed).toEqual(data);
167 | });
168 |
169 | it("pull should handle JSONC with Unicode escape sequences", async () => {
170 | const loader = createJsoncLoader();
171 | loader.setDefaultLocale("en");
172 | const jsoncInput = `{
173 | // Unicode characters
174 | "unicode": "\\u0048\\u0065\\u006c\\u006c\\u006f",
175 | "emoji": "🚀"
176 | }`;
177 |
178 | const result = await loader.pull("en", jsoncInput);
179 | expect(result).toEqual({
180 | unicode: "Hello",
181 | emoji: "🚀",
182 | });
183 | });
184 |
185 | it("pullHints should extract comments from JSONC", async () => {
186 | const loader = createJsoncLoader();
187 | loader.setDefaultLocale("en");
188 | const jsoncInput = `{
189 | "key1": "value1", // This is a comment for key1
190 | "key2": "value2" /* This is a comment for key2 */,
191 | // This is a comment for key3
192 | "key3": "value3",
193 | /* This is a block comment for key4 */
194 | "key4": "value4",
195 | /*
196 | This is a comment for key5
197 | */
198 | "key5": "value5",
199 | // This is a comment for key6
200 | "key6": {
201 | // This is a comment for key7
202 | "key7": "value7"
203 | }
204 | }`;
205 |
206 | // First call pull to initialize the loader state
207 | await loader.pull("en", jsoncInput);
208 | const comments = await loader.pullHints(jsoncInput);
209 |
210 | expect(comments).toEqual({
211 | key1: { hint: "This is a comment for key1" },
212 | key2: { hint: "This is a comment for key2" },
213 | key3: { hint: "This is a comment for key3" },
214 | key4: { hint: "This is a block comment for key4" },
215 | key5: { hint: "This is a comment for key5" },
216 | key6: {
217 | hint: "This is a comment for key6",
218 | key7: { hint: "This is a comment for key7" },
219 | },
220 | });
221 | });
222 | });
223 |
```
--------------------------------------------------------------------------------
/packages/cli/src/cli/utils/buckets.ts:
--------------------------------------------------------------------------------
```typescript
1 | import _ from "lodash";
2 | import path from "path";
3 | import { glob } from "glob";
4 | import { CLIError } from "./errors";
5 | import {
6 | I18nConfig,
7 | resolveOverriddenLocale,
8 | BucketItem,
9 | LocaleDelimiter,
10 | } from "@lingo.dev/_spec";
11 | import { bucketTypeSchema } from "@lingo.dev/_spec";
12 | import Z from "zod";
13 |
14 | type BucketConfig = {
15 | type: Z.infer<typeof bucketTypeSchema>;
16 | paths: Array<{ pathPattern: string; delimiter?: LocaleDelimiter }>;
17 | injectLocale?: string[];
18 | lockedKeys?: string[];
19 | lockedPatterns?: string[];
20 | ignoredKeys?: string[];
21 | };
22 |
23 | export function getBuckets(i18nConfig: I18nConfig) {
24 | const result = Object.entries(i18nConfig.buckets).map(
25 | ([bucketType, bucketEntry]) => {
26 | const includeItems = bucketEntry.include.map((item) =>
27 | resolveBucketItem(item),
28 | );
29 | const excludeItems = bucketEntry.exclude?.map((item) =>
30 | resolveBucketItem(item),
31 | );
32 | const config: BucketConfig = {
33 | type: bucketType as Z.infer<typeof bucketTypeSchema>,
34 | paths: extractPathPatterns(
35 | i18nConfig.locale.source,
36 | includeItems,
37 | excludeItems,
38 | ),
39 | };
40 | if (bucketEntry.injectLocale) {
41 | config.injectLocale = bucketEntry.injectLocale;
42 | }
43 | if (bucketEntry.lockedKeys) {
44 | config.lockedKeys = bucketEntry.lockedKeys;
45 | }
46 | if (bucketEntry.lockedPatterns) {
47 | config.lockedPatterns = bucketEntry.lockedPatterns;
48 | }
49 | if (bucketEntry.ignoredKeys) {
50 | config.ignoredKeys = bucketEntry.ignoredKeys;
51 | }
52 | return config;
53 | },
54 | );
55 |
56 | return result;
57 | }
58 |
59 | function extractPathPatterns(
60 | sourceLocale: string,
61 | include: BucketItem[],
62 | exclude?: BucketItem[],
63 | ) {
64 | const includedPatterns = include.flatMap((pattern) =>
65 | expandPlaceholderedGlob(
66 | pattern.path,
67 | resolveOverriddenLocale(sourceLocale, pattern.delimiter),
68 | ).map((pathPattern) => ({
69 | pathPattern,
70 | delimiter: pattern.delimiter,
71 | })),
72 | );
73 | const excludedPatterns = exclude?.flatMap((pattern) =>
74 | expandPlaceholderedGlob(
75 | pattern.path,
76 | resolveOverriddenLocale(sourceLocale, pattern.delimiter),
77 | ).map((pathPattern) => ({
78 | pathPattern,
79 | delimiter: pattern.delimiter,
80 | })),
81 | );
82 | const result = _.differenceBy(
83 | includedPatterns,
84 | excludedPatterns ?? [],
85 | (item) => item.pathPattern,
86 | );
87 | return result;
88 | }
89 |
90 | // Windows path normalization helper function
91 | function normalizePath(filepath: string): string {
92 | const normalized = path.normalize(filepath);
93 | // Ensure case consistency on Windows
94 | return process.platform === "win32" ? normalized.toLowerCase() : normalized;
95 | }
96 |
97 | // Path expansion
98 | function expandPlaceholderedGlob(
99 | _pathPattern: string,
100 | sourceLocale: string,
101 | ): string[] {
102 | const absolutePathPattern = path.resolve(_pathPattern);
103 | const pathPattern = normalizePath(
104 | path.relative(process.cwd(), absolutePathPattern),
105 | );
106 | if (pathPattern.startsWith("..")) {
107 | throw new CLIError({
108 | message: `Invalid path pattern: ${pathPattern}. Path pattern must be within the current working directory.`,
109 | docUrl: "invalidPathPattern",
110 | });
111 | }
112 |
113 | // Throw error if pathPattern contains "**" – we don't support recursive path patterns
114 | if (pathPattern.includes("**")) {
115 | throw new CLIError({
116 | message: `Invalid path pattern: ${pathPattern}. Recursive path patterns are not supported.`,
117 | docUrl: "invalidPathPattern",
118 | });
119 | }
120 |
121 | // Break down path pattern into parts
122 | const pathPatternChunks = pathPattern.split(path.sep);
123 | // Find the index of the segment containing "[locale]"
124 | const localeSegmentIndexes = pathPatternChunks.reduce(
125 | (indexes, segment, index) => {
126 | if (segment.includes("[locale]")) {
127 | indexes.push(index);
128 | }
129 | return indexes;
130 | },
131 | [] as number[],
132 | );
133 | // substitute [locale] in pathPattern with sourceLocale
134 | const sourcePathPattern = pathPattern.replaceAll(/\[locale\]/g, sourceLocale);
135 | // Convert to Unix-style for Windows compatibility
136 | const unixStylePattern = sourcePathPattern.replace(/\\/g, "/");
137 |
138 | // get all files that match the sourcePathPattern
139 | const sourcePaths = glob
140 | .sync(unixStylePattern, {
141 | follow: true,
142 | withFileTypes: true,
143 | windowsPathsNoEscape: true, // Windows path support
144 | })
145 | .filter((file) => file.isFile() || file.isSymbolicLink())
146 | .map((file) => file.fullpath())
147 | .map((fullpath) => normalizePath(path.relative(process.cwd(), fullpath)));
148 |
149 | // transform each source file path back to [locale] placeholder paths
150 | const placeholderedPaths = sourcePaths.map((sourcePath) => {
151 | // Normalize path returned by glob for platform compatibility
152 | const normalizedSourcePath = normalizePath(
153 | sourcePath.replace(/\//g, path.sep),
154 | );
155 | const sourcePathChunks = normalizedSourcePath.split(path.sep);
156 | localeSegmentIndexes.forEach((localeSegmentIndex) => {
157 | // Find the position of the "[locale]" placeholder within the segment
158 | const pathPatternChunk = pathPatternChunks[localeSegmentIndex];
159 | const sourcePathChunk = sourcePathChunks[localeSegmentIndex];
160 | const regexp = new RegExp(
161 | "(" +
162 | pathPatternChunk
163 | .replaceAll(".", "\\.")
164 | .replaceAll("*", ".*")
165 | .replace("[locale]", `)${sourceLocale}(`) +
166 | ")",
167 | );
168 | const match = sourcePathChunk.match(regexp);
169 | if (match) {
170 | const [, prefix, suffix] = match;
171 | const placeholderedSegment = prefix + "[locale]" + suffix;
172 | sourcePathChunks[localeSegmentIndex] = placeholderedSegment;
173 | }
174 | });
175 | const placeholderedPath = sourcePathChunks.join(path.sep);
176 | return placeholderedPath;
177 | });
178 | // return the placeholdered paths
179 | return placeholderedPaths;
180 | }
181 |
182 | function resolveBucketItem(bucketItem: string | BucketItem): BucketItem {
183 | if (typeof bucketItem === "string") {
184 | return { path: bucketItem, delimiter: null };
185 | }
186 | return bucketItem;
187 | }
188 |
```
--------------------------------------------------------------------------------
/packages/compiler/src/lib/lcp/cache.ts:
--------------------------------------------------------------------------------
```typescript
1 | import * as fs from "fs";
2 | import * as path from "path";
3 | import * as prettier from "prettier";
4 | import { DictionaryCacheSchema, DictionarySchema, LCPSchema } from "./schema";
5 | import _ from "lodash";
6 | import { LCP_DICTIONARY_FILE_NAME } from "../../_const";
7 |
8 | export interface LCPCacheParams {
9 | sourceRoot: string;
10 | lingoDir: string;
11 | lcp: LCPSchema;
12 | }
13 |
14 | export class LCPCache {
15 | // make sure the cache file exists, otherwise imports will fail
16 | static ensureDictionaryFile(params: {
17 | sourceRoot: string;
18 | lingoDir: string;
19 | }) {
20 | const cachePath = this._getCachePath(params);
21 | if (!fs.existsSync(cachePath)) {
22 | const dir = path.dirname(cachePath);
23 | if (!fs.existsSync(dir)) {
24 | fs.mkdirSync(dir, { recursive: true });
25 | }
26 | fs.writeFileSync(cachePath, "export default {};");
27 | }
28 | }
29 |
30 | // read cache entries for given locale, validate entry hash from LCP schema
31 | static readLocaleDictionary(
32 | locale: string,
33 | params: LCPCacheParams,
34 | ): DictionarySchema {
35 | const cache = this._read(params);
36 | const dictionary = this._extractLocaleDictionary(cache, locale, params.lcp);
37 | return dictionary;
38 | }
39 |
40 | // write cache entries for given locale to existing cache file, use hash from LCP schema
41 | static async writeLocaleDictionary(
42 | dictionary: DictionarySchema,
43 | params: LCPCacheParams,
44 | ): Promise<void> {
45 | const currentCache = this._read(params);
46 | const newCache = this._mergeLocaleDictionary(
47 | currentCache,
48 | dictionary,
49 | params.lcp,
50 | );
51 | await this._write(newCache, params);
52 | }
53 |
54 | // merge dictionary with current cache, sort files, entries and locales to minimize diffs
55 | private static _mergeLocaleDictionary(
56 | currentCache: DictionaryCacheSchema,
57 | dictionary: DictionarySchema,
58 | lcp: LCPSchema,
59 | ): DictionaryCacheSchema {
60 | const files = _(dictionary.files)
61 | .mapValues((file, fileName) => ({
62 | ...file,
63 | entries: _(file.entries)
64 | .mapValues((entry, entryName) => {
65 | // find if entry exists in current cache, it might contain some locales already
66 | const cachedEntry =
67 | _.get(currentCache, ["files", fileName, "entries", entryName]) ??
68 | {};
69 | const hash = _.get(lcp, [
70 | "files",
71 | fileName,
72 | "scopes",
73 | entryName,
74 | "hash",
75 | ]);
76 |
77 | // reuse existing cache entry if its hash matches LCP schema, ensures the cache is up to date
78 | const cachedEntryContent =
79 | cachedEntry.hash === hash ? cachedEntry.content : {};
80 |
81 | // sorted by keys (locales) to minimize diffs
82 | const content = _({
83 | ...cachedEntryContent,
84 | [dictionary.locale]: entry,
85 | })
86 | .toPairs()
87 | .sortBy([0])
88 | .fromPairs()
89 | .value();
90 | return { content, hash };
91 | })
92 | .toPairs()
93 | .sortBy([0])
94 | .fromPairs()
95 | .value(),
96 | }))
97 | .toPairs()
98 | .sortBy([0])
99 | .fromPairs()
100 | .value();
101 |
102 | const newCache = {
103 | version: dictionary.version,
104 | files,
105 | };
106 | return newCache;
107 | }
108 |
109 | // extract dictionary from cache for given locale, validate entry hash from LCP schema
110 | private static _extractLocaleDictionary(
111 | cache: DictionaryCacheSchema,
112 | locale: string,
113 | lcp: LCPSchema,
114 | ): DictionarySchema {
115 | const findCachedEntry = (hash: string) => {
116 | const cachedEntry = _(cache.files)
117 | .flatMap((file) => _.values(file.entries))
118 | .find((entry) => entry.hash === hash);
119 | if (cachedEntry) {
120 | return cachedEntry.content[locale];
121 | }
122 | return undefined;
123 | };
124 |
125 | const files = _(lcp.files)
126 | .mapValues((file) => {
127 | return {
128 | entries: _(file.scopes)
129 | .mapValues((entry) => {
130 | return findCachedEntry(entry.hash);
131 | })
132 | .pickBy((value) => value !== undefined)
133 | .value(),
134 | };
135 | })
136 | .pickBy((file) => !_.isEmpty(file.entries))
137 | .value();
138 |
139 | const dictionary = {
140 | version: cache.version,
141 | locale,
142 | files,
143 | };
144 | return dictionary;
145 | }
146 |
147 | // format with prettier
148 | private static async _format(
149 | cachedContent: string,
150 | cachePath: string,
151 | ): Promise<string> {
152 | try {
153 | const config = await prettier.resolveConfig(cachePath);
154 | const prettierOptions = {
155 | ...(config ?? {}),
156 | parser: config?.parser ? config.parser : "typescript",
157 | };
158 | return await prettier.format(cachedContent, prettierOptions);
159 | } catch (error) {
160 | // prettier not configured or formatting failed
161 | }
162 | return cachedContent;
163 | }
164 |
165 | // write cache to file as JSON
166 | private static async _write(
167 | dictionaryCache: DictionaryCacheSchema,
168 | params: LCPCacheParams,
169 | ) {
170 | const cachePath = this._getCachePath(params);
171 | const cache = `export default ${JSON.stringify(dictionaryCache, null, 2)};`;
172 | const formattedCache = await this._format(cache, cachePath);
173 | fs.writeFileSync(cachePath, formattedCache);
174 | }
175 |
176 | // read cache from file as JSON
177 | private static _read(params: LCPCacheParams): DictionaryCacheSchema {
178 | const cachePath = this._getCachePath(params);
179 | if (!fs.existsSync(cachePath)) {
180 | return {
181 | version: 0.1,
182 | files: {},
183 | };
184 | }
185 | const jsObjectString = fs.readFileSync(cachePath, "utf8");
186 |
187 | // Remove 'export default' and trailing semicolon before parsing
188 | const cache = jsObjectString
189 | .replace(/^export default/, "")
190 | .replace(/;\s*$/, "");
191 |
192 | // Use Function constructor to safely evaluate the object
193 | // eslint-disable-next-line no-new-func
194 | const obj = new Function(`return (${cache})`)();
195 | return obj;
196 | }
197 |
198 | // get cache file path
199 | private static _getCachePath(params: {
200 | sourceRoot: string;
201 | lingoDir: string;
202 | }) {
203 | return path.resolve(
204 | process.cwd(),
205 | params.sourceRoot,
206 | params.lingoDir,
207 | LCP_DICTIONARY_FILE_NAME,
208 | );
209 | }
210 | }
211 |
```