legesher
Command-line interface for coding in any language
Status: Pre-Beta | Languages: 56 | Version: see pyproject.toml
The legesher CLI is your gateway to programming in your native language. Translate Python code between languages, manage git integration for collaborative workflows, and configure your multilingual development environment.
About
The legesher package provides command-line tools for developers who want to write code in their native language
instead of English. It handles:
- Project Setup:
legesher initscaffolds configuration, git integration, and AI context - Code Translation: Convert Python files between languages using language packs
- Execution:
legesher runexecutes a native-language file directly - Git Integration: a clean filter that keeps each file in the language its author wrote it in
- Configuration: Manage project and user settings for language preferences
- Metadata: Inspect and validate the translation history recorded for your files
This is the main user-facing package in the Legesher ecosystem, built on top of legesher-core and legesher-i18n.
Installation
pip install legesher
Or install from source in the monorepo:
cd libs/legesher
pip install -e .
Quick Start
# Initialize a new project with Spanish language pack
legesher init --language es
# Translate a Python file to Spanish
legesher translate mi_codigo.py --to es
# Set up git integration for collaborative development
legesher git setup
# View available language packs
legesher-i18n registry list
Commands
legesher init
Scaffold Legesher in a Python project: write .legesher/config.yml, optionally wire the git filters, and
optionally generate AI context files.
# Interactive setup
legesher init
# Pre-select languages and skip the prompts
legesher init --language es --language fr --git --no-ai-context
legesher translate
Translate Python source files between languages.
# Translate a single file to Spanish
legesher translate app.py --to es
# Translate to English, the run form Python executes. --from defaults to en, so name the source
legesher translate mi_app.py --from es --to en
# Between two non-English languages, go through English: direct pairs are not supported yet
legesher translate code.py --from es --to en --output code_en.py
legesher translate code_en.py --to fr --output code_fr.py
# Translate and write to different file
legesher translate input.py --to es --output salida.py
# Translate an entire directory
legesher translate src/ --to es --output traducido/
Full LLM Translation (translates identifiers, comments, strings, docstrings):
# Full translation with Anthropic Claude
legesher translate src/ --to es --full --llm-backend anthropic
# Full translation with local model (vLLM, Ollama, llama.cpp)
legesher translate src/ --to ur --full --llm-backend openai-compat \
--llm-base-url http://localhost:8000
# Batch with cross-file consistency and resume
legesher translate src/ --to zh --full \
--identifier-map .legesher/identifier_map.json \
--checkpoint .legesher/checkpoint.txt
Options:
--to LANG: Target language code (required)--from LANG: Source language code (default:en)--output PATH: Output file or directory. Omitted, a single file goes to stdout and a directory goes to a sibling<dir>_translated/. Your input file is never overwritten in place--full: Enable full LLM translation (identifiers, comments, strings, docstrings)--llm-backend: LLM backend:anthropic,cohere-aya, oropenai-compat(default: anthropic)--llm-base-url: Base URL for openai-compat backend (e.g.,http://localhost:8000)--llm-model: Model name for any backend (default: the backend's own default)--identifier-map PATH: Load/save identifier glossary for cross-file consistency (default:.legesher/identifier_map.json);--no-identifier-mapdisables it--checkpoint PATH: Resume interrupted batch jobs
Example Output:
$ legesher translate hello.py --to es
definir greet():
imprimir("hi")
$ legesher translate hello.py --to es --output hola.py
✓ Translated hello.py → hola.py
legesher run
Execute a native-language Python file.
legesher run programa.py --lang es
# Pass arguments through to the script
legesher run programa.py --lang es --args "--verbose input.txt"
--lang wins. Without it the language comes from the language: key in .legesher/config.yml,
falling back to en when that key is absent. A config written by legesher init does not have that
key (see Configuration), so dropping --lang there runs the file untranslated and
Python raises SyntaxError. Pass --lang, or write the config with legesher git setup.
legesher git
Git stores each file in its author's language. legesher git setup configures the clean filter, as one
long-running process per git command, and the guard hook for a repository.
# Set up for the current repository
legesher git setup
# Set up with a specific language
legesher git setup --language es
# Opt in to the keyword-only smudge (terminal editors)
legesher git setup --language es --keyword-smudge
# Check the configuration
legesher git status
Subcommands:
setup: Configure the clean filter, which only normalizes: a stray keyword, such as an Englishiftyped into a Spanish file, is stored in its block's language. It never refuses a file, because git stores a file unchanged when its filter fails, so setup also installs a pre-commit hook that runslegesher guard. An existing pre-commit hook is never overwritten; setup prints the line to add to it instead.--no-hookskips the hook, and removes it if setup installed it.status: Display the filter, smudge and hook configuration and the language settings
Smudge is off by default: a reader's rendering is never written into a file, so views render in the editor and
on GitHub. --keyword-smudge is the opt-in for terminal editors. It renders an English file's keywords in your
language on checkout, from the open language pack only (never builtins or library names), and only when the
clean filter can store the result back exactly. Running setup again without the flag turns it off in that
scope, and setup says so when a smudge is still configured in another, such as the global git config.
Until in-file region markers land, a file is one block. Its language is read from its committed version or, for a new file, from the file itself, among English and your configured language. A reading counts only when the file parses under it, and the language with the most keywords wins, so one slip never converts a file; with no reading that parses, or a tie, the file is stored as written. A file in a third language is left as written unless every keyword it uses is spelled the same in your language, which closely related languages can do.
One process per git command. Setup configures git's long-running filter process
(filter.legesher.process), so a git command starts the filter once instead of once per file; the single-file
clean command stays configured for git older than 2.11. The process runs through a small launcher,
.git/legesher/filter-process (~/.legesher/filter-process with --global), which names the legesher-filter
that ran setup and falls back to PATH. Git fails a whole command when a filter process ends before its handshake,
so the launcher answers the handshake itself and only then starts the filter: a filter that is missing, broken or
too old costs only that file's filtering, and git stores it as written, as it does for a missing single-file
filter. A single-file smudge command in any git config is ignored once a process is configured; the process
offers the smudge only when setup ran with --keyword-smudge. To take Legesher's filter out of a repository, run
git config --remove-section filter.legesher.
Example Output:
$ legesher git setup --language es
✓ Found git repository at: /path/to/repo
Validating language pack 'es'...
✓ Loaded language pack: es (39 keywords, 143 builtins)
Creating configuration...
✓ Saved project config to: /path/to/repo/.legesher/config.yml
Configuring git filters...
✓ Filter process: sh '/path/to/repo/.git/legesher/filter-process'
✓ Clean filter (git before 2.11): legesher-filter --clean --path %f
Configuring .gitattributes...
✓ Updated .gitattributes
Initializing metadata system...
✓ Created metadata directory: /path/to/repo/.legesher/metadata
✓ Metadata will be tracked in git
✓ Metadata system initialized successfully
Configuring the guard hook...
✓ Installed the guard hook (pre-commit: legesher guard --staged)
Verifying setup...
✓ Git filter setup verified
============================================================
✓ Git filter setup complete!
============================================================
Your repository is now configured for multilingual code.
Language: es
Storage: each file in its author's language
The trailing "Next steps" list is omitted above. The 143 builtins in that output is not the pack's
built-in count: the translator merges the pack's 70 exception names into its built-in map before
reporting, so 143 is the pack's 73 built-ins plus its 70 exceptions. The per-category counts are in
legesher-i18n.
legesher guard
Refuses what must not be committed. The pre-commit hook runs legesher guard --staged; without paths it checks the
staged files, and with paths it checks those files on disk. --diff BASE checks only what changed from BASE to
HEAD, read from HEAD and judged against each file's version at BASE: the command a pull-request check runs. It
compares the two trees, so with a pull request's merge commit checked out it covers exactly the pull request, and a
shallow clone only needs the base commit itself. --format github prints each finding as a GitHub Actions annotation.
A file is checked if .gitattributes gives it the legesher filter (staged or in the working tree for --staged; at
the base or the head for --diff); files under .legesher/ are always checked.
$ legesher guard --staged
src/app.py:2:11: unicode: U+202E RIGHT-TO-LEFT OVERRIDE: Trojan Source bidi reordering control (CVE-2021-42574)
src/app.py:3:5: keyword: `if` in a block stored in es; its stored form is `si`
legesher guard: 2 finding(s) in 1 file(s).
encoding: content that is not UTF-8, or a coding declaration other than UTF-8. Python honors the declaration, so a pure-ASCII file declaredutf-7can run code that reads as a string.unicode: bidi reordering controls, invisible characters outside string and comment prose, Arabic tatweel, and identifier characters outside every supported script. There is no override.keyword: a keyword not in its block's stored form. The clean filter normalizes these, so a finding means the file skipped it; runlegesher git setup, thengit add --renormalize <path>.artifact: a symlink marked for Legesher (the guard cannot check what it points to; replace it with the file), or a file under.legesher/that is a symlink or a submodule, that does not match its format (config.yml,metadata/*.yml,translation_memory.json,identifier_map.json), or that records a path outside the repository;metadata.directorymust stay under.legesher/, with no... The directory's name is matched in any letter case. Other files there are never loaded.
Exit status is 0 with no findings, 1 with findings, and 2 when the guard cannot run (outside a repository, or an unknown language).
The hook is a client-side check: git commit --no-verify, merges and rebases skip it, and it lets a commit through
with a notice when it cannot find legesher. A pull-request check running legesher guard --diff sees changes
made anywhere, edits on github.com included, but it checks a pull request's net change against its base: the
commits inside it land as they are under a merge commit or a rebase merge, and a push that skips pull requests is
not checked unless branch protection requires one.
In diff mode a file counts as Legesher's when the base's or the head's .gitattributes marks it, and the language
comes from the base's .legesher/config.yml (English when the base names none; the pull request's own configuration
is never read), so nothing a pull request changes in its tree relaxes what it is judged by. Diff mode also refuses
every marked symlink in the tree, and every symlink or submodule under .legesher/, not only the changed ones, since
a change elsewhere can use a link already there. A repository that already holds a marked symlink fails every pull
request until the link is replaced with the file it points to.
The check's own workflow file is outside what the guard reads: on pull_request, GitHub runs the workflow as the pull
request left it, so a pull request could change the step's inputs or remove it while the check keeps its name.
Protect .github/workflows/ as well, for example with a code owner and required code-owner review.
legesher config
Manage project and global configuration.
Project configuration is created by legesher init or legesher git setup; config reads and writes it
afterwards. get and list report the keys that are actually in the files, so what they show depends on
which of those two commands wrote the config (see Configuration).
# Set default language for project
legesher config set language es
# Get the key back
legesher config get language
# List all configuration settings (user-level merged with project)
legesher config list
# Set user-level configuration
legesher config set language es --global
config get exits 1 with Key '<key>' not found. when the key is absent from both files: it does not
fall back to the built-in defaults.
Configuration Locations:
- Project:
.legesher/config.yml(current directory) - User:
~/.legesher/config.yml(user home directory)
Available Settings (the keys the runtime reads, and the ones legesher git setup writes):
language: Default language code (e.g.,es,fr,ar)python_version: Target Python version (e.g.,3.12,3.11), ornullto auto-detectgit.canonical_language: written asenbylegesher initandlegesher git setupfor compatibility; the filter does not read it, because each file is stored in its author's languagegit.filter_enabled: Enable git filter integration (boolean)runtime.cache_size: Translation cache size. Honoured when present; neither command writes it outmetadata.enabled,metadata.directory,metadata.track_in_git,metadata.verbose_history,metadata.max_history_entries: Metadata tracking
legesher init writes a different set of keys (languages, git.enabled, ai_context.*,
translation.*, editor.*). config get and config list will report those verbatim, but nothing in
the runtime consults them.
legesher metadata
Inspect the translation metadata Legesher records for the files in your repository. Every subcommand except
list takes a file path, not a language code.
# Show metadata for one file
legesher metadata show src/calculator.py
# Show the clean/smudge history for a file
legesher metadata history src/calculator.py --limit 20
# List every file with recorded metadata
legesher metadata list
# Show the raw YAML
legesher metadata raw src/calculator.py
# Check a metadata file is present, valid YAML, and complete
legesher metadata validate src/calculator.py
Every subcommand takes --repo PATH to point at a repository other than the current directory.
To inspect a language pack rather than a file's history, use the legesher-i18n CLI:
legesher-i18n registry list
legesher-i18n validate libs/i18n/language-packs/python/es/legesher_i18n_python_es/packs/3.13.yml
Configuration
The legesher CLI uses YAML configuration files for project and user settings: .legesher/config.yml
in a project, ~/.legesher/config.yml for user-wide defaults.
Known issue:
legesher initandlegesher git setupwrite two different, incompatible shapes.git setupwrites the shape the runtime reads.initwrites its own shape, which the runtime does not understand, so afterlegesher init --language esthe working language silently falls back toen:legesher git statusreportsLanguage: en,legesher runwithout--langfails to compile the file,config get languageexits 1, and the git clean filter cannot recognize your language, so it leaves stray keywords as typed. Reconciling the two is tracked as its own issue. Until it lands, runlegesher git setup --language <code>afterlegesher init(it rewritesconfig.ymlin the shape everything else reads, and leaves the generated AI context files alone).
Project configuration written by legesher git setup
This is the shape LegesherConfig loads, and the keys legesher config documents above:
language: es
python_version: null # null means auto-detect
git:
canonical_language: en
filter_enabled: true
metadata:
enabled: true
directory: .legesher/metadata
track_in_git: true
verbose_history: false
max_history_entries: 100
Project configuration written by legesher init
Same path, different keys. Nothing in the runtime reads the keys unique to
this shape; python_version and git.canonical_language are shared with the
shape above and are read:
# Languages enabled in this project
languages: # a list, not the single `language:` string above
- es
python_version: "3.13"
git:
enabled: true # not git.filter_enabled
canonical_language: "en"
ai_context:
enabled: true
context_files:
- AI-CONTEXT-es.md
translation:
default_source: "en"
default_target: "es"
preserve_comments: true
preserve_strings: true
editor:
lsp_hover: true
inline_completion: true
bilingual_tooltips: true
User Configuration
~/.legesher/config.yml holds user-wide defaults and uses the same keys as the git setup shape.
Configuration Precedence
Settings are resolved in this order (highest to lowest priority):
- Command-line flags (e.g.,
--language es) - Project configuration (
.legesher/config.yml) - User configuration (
~/.legesher/config.yml) - Built-in defaults
Examples
Example 1: Starting a New Spanish Project
# Create project directory
mkdir mi_proyecto
cd mi_proyecto
git init
# Initialize legesher configuration (writes config.yml, git filters, AI context)
legesher init --language es --git --python-version 3.13
# Rewrite config.yml in the shape the runtime reads. Without this the filters are
# installed but inert, and your code is stored in Spanish. See Configuration above
legesher git setup --language es --python-version 3.13
# Create a Python file in Spanish. Keywords and builtins are Spanish;
# your own names, and everything inside strings, stay as you write them
cat > hola.py << 'EOF'
definir saludar(nombre):
"""Saluda a una persona por su nombre."""
imprimir(f"¡Hola, {nombre}!")
saludar("Mundo")
EOF
git add .
git commit -m "Initial commit"
# The file is stored in Spanish, as written; a stray English keyword would be normalized to Spanish
Example 2: Working in an English Project
# Clone an English Python project
git clone https://github.com/example/python-project.git
cd python-project
# Set up legesher to work in Spanish
legesher git setup --language es
# Translate a throwaway copy into Spanish for reading
legesher translate src/ --to es --output src_es/
# The project's files stay in English, the language their authors wrote them in.
# A Spanish keyword typed into one of them is normalized back to English at git add;
# a new file you write in Spanish is stored in Spanish.
Example 3: Switching Between Languages
# Currently working in Spanish (config written by `legesher git setup --language es`)
legesher config get language
# Output: es
# Translate a copy of the codebase to French, through English (direct pairs are not supported yet)
legesher translate src_es/ --from es --to en --output src_en/
legesher translate src_en/ --to fr --output src_fr/
# Update configuration
legesher config set language fr
# Now working in French!
Example 4: Checking a File's Translation Record
# Show what Legesher has recorded for a file
legesher metadata show src/calculator.py
# Check that record is present and well-formed
legesher metadata validate src/calculator.py
Development
Running Tests
# Run all tests
pytest
# Run with coverage
pytest --cov=legesher --cov-report=html
# Run specific test file
pytest tests/test_cli.py
Project Structure
libs/legesher/
├── legesher/
│ ├── __init__.py
│ ├── config.py # Configuration loading and precedence
│ ├── cli/
│ │ ├── main.py # Main CLI entry point
│ │ ├── init.py # init command
│ │ ├── translate.py # translate command
│ │ ├── run.py # run command
│ │ ├── git.py # git command group
│ │ ├── config_cmd.py # config command group
│ │ └── metadata.py # metadata command group
│ └── runtime/
│ ├── import_hook.py # Import hook for `legesher run`
│ ├── source_mapper.py
│ └── error_handler.py
├── tests/
├── pyproject.toml
└── README.md
Contributing
We welcome contributions! Please see:
- CONTRIBUTING.md in the monorepo root
- Development Guide
- Open Issues
License
Apache License 2.0 - see LICENSE for details.
Links
- Monorepo: legesher/legesher
- Documentation: docs.legesher.io
- Language Packs: legesher-i18n packages
- Changelog: CHANGELOG.md
- Issues: GitHub Issues
- Discussions: GitHub Discussions
Related Packages
- legesher-core: Core translation engine and AST utilities
- legesher-i18n: Language pack registry and translation API
- legesher-i18n-python-es: Spanish language pack for Python
- legesher-language-server: LSP implementation for IDE integration
- legesher-runtime: Localized tracebacks, used by
legesher run
Part of the Legesher Project - Empowering developers to code in their native language.
Metadata
Release files for legesher 0.24.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| legesher-0.24.0.tar.gz | 142.7 kB | With provenance | Details for legesher-0.24.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| legesher-0.24.0-py3-none-any.whl | Python 3 | none | any | Details for legesher-0.24.0-py3-none-any.whl |
Total release size: 214.0 kB
Release files / legesher-0.24.0.tar.gz
| Download URL | legesher-0.24.0.tar.gz |
|---|---|
| Size | 142.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
90688f8600c59a0af3f8d35cb6beb57f3cbba5d21de28e3c49be70d1149a4d89
|
|
BLAKE2b-256 checksum How to use checksums |
e078622e38edfb23500c41c8015110931aa3b01984de5b503558c36972225d91
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.13
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 3, 2026.
Transparency logRelease files / legesher-0.24.0-py3-none-any.whl
| Download URL | legesher-0.24.0-py3-none-any.whl |
|---|---|
| Size | 71.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
1ccee662589525a5f5a62b40dd98c9201fba485a43b513546a12cca0fe79c435
|
|
BLAKE2b-256 checksum How to use checksums |
9a843d05f947d6539e91c3c0e480693cce77dccb19bdc10dbe3e8c230a108312
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.13
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 3, 2026.
Transparency logSecurity for legesher 0.24.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for legesher-0.24.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 3, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.