lesia is a Python library for managing and automating the
translation of markup document projects (LaTeX, Markdown, Jupyter, MyST,
Typst). It preserves file structure and formatting, uses LLM services for
translation, and maintains a persistent translation cache.
For a conceptual overview of how the tool works, see the profound explanation.
Everything below is Project-centric. If you just want to chunk a document
or separate syntax from translatable text without setting up a Project
(e.g. parse_latex, parse_myst, create_translation_xml, ...), see the
Standalone API Reference instead.
Note: The library is in early development. Expect bugs and incomplete features.
Table of Contents¶
Installation¶
As a dependency¶
pip install <path_to_lesia>Or with uv:
uv add <path_to_lesia>For development¶
git clone https://github.com/DobbiKov/lesia
cd lesia
uv syncQuick start¶
import asyncio
from lesia.project_manager import init_project, load_project
from lesia.enums import Language
# Initialize a new project in an existing directory
project = init_project("my_project", "/path/to/project/root")
# Set the source directory and its language
project.set_source_directory("analysis_notes_fr", Language.FRENCH)
# Add a target language (creates the output directory automatically)
project.add_target_language(Language.ENGLISH)
# Mark a file for translation
project.set_file_translatability("analysis_notes_fr/main.tex", True)
# Copy untranslatable files (images, bibliography, etc.) to the target directory
project.sync_untranslatable_files()
# Translate (async) — returns a TranslationStats object
stats = asyncio.run(project.translate_single_file("analysis_notes_fr/main.tex", Language.ENGLISH, None))
print(f"Translated {stats.chunks_translated} chunks, {stats.chunks_from_cache} from cache")To use a custom language not in the predefined list:
# Register the custom language (optional short alias)
project.add_custom_language("American English", "_ae", short="AmEng")
# Resolve it using either the full name or the short alias
american_english = project.config.resolve_language("AmEng") # or "American English"
# Use it just like a predefined language
project.add_target_language(american_english)
total_stats = asyncio.run(project.translate_all_for_language(american_english, None))
print(f"Total chunks translated: {total_stats.chunks_translated}")To work with an existing project, load it from the current directory (searched upward, like git):
from lesia.project_manager import load_project
project = load_project(".")To supply API keys via a .env file instead of shell environment variables:
from pathlib import Path
# Store the path once — it is saved to .lesia/config.toml
project.set_env_file(Path(".env"))
# Keys are now read from .env on every translation call.
stats = asyncio.run(project.translate_single_file("notes_fr/main.tex", Language.ENGLISH, None))Precedence: shell environment variables always win over the
.envfile. IfLLM_API_KEYis set in both places, the shell value is used.
Language¶
from lesia.enums import LanguageLanguage is a str enum of supported languages.
| Member | Value | Directory suffix |
|---|---|---|
Language.FRENCH | "French" | _fr |
Language.ENGLISH | "English" | _en |
Language.GERMAN | "German" | _de |
Language.SPANISH | "Spanish" | _es |
Language.UKRAINIAN | "Ukrainian" | _ua |
Language.ARMENIAN | "Armenian" | _hy |
Language.from_str(s: str) -> Language
Case-insensitive parse from a string. Raises ValueError if the string does not match any language.
lang = Language.from_str("french") # Language.FRENCH
lang = Language.from_str("ENGLISH") # Language.ENGLISHNote: All
Projectmethods accept bothLanguageandCustomLanguage. For new code, prefer obtaining languages viaproject.config.resolve_language(name)which returns aCustomLanguageand works for both predefined and custom languages uniformly.
CustomLanguage¶
from lesia.enums import CustomLanguageCustomLanguage is the runtime representation of a language — both predefined and custom. All Project methods that accept a language argument accept either a Language enum member or a CustomLanguage instance.
Constructor¶
CustomLanguage(lang: str, suffix: str)| Parameter | Description |
|---|---|
lang | Display name, e.g. "Catalan". Used as the language identifier in cache files and config. |
suffix | Directory suffix, e.g. "_ca". Used when auto-creating target directories. |
Class method¶
CustomLanguage.from_language(lang: Language) -> CustomLanguage
Converts a predefined Language enum member to a CustomLanguage instance.
cl = CustomLanguage.from_language(Language.FRENCH)
# cl.get_lang() == "French"
# cl.get_dir_suffix() == "_fr"Instance methods¶
cl.get_lang() -> str # Returns the display name
cl.get_dir_suffix() -> str # Returns the directory suffix
str(cl) # Same as get_lang()Equality and hashing¶
Two CustomLanguage instances are equal if their names match case-insensitively. CustomLanguage is also equal to a plain str with the same name (case-insensitive). It is hashable and safe to use as a dict key.
CustomLanguage("Catalan", "_ca") == CustomLanguage("catalan", "_ca") # True
CustomLanguage("Catalan", "_ca") == "catalan" # TrueObtaining a CustomLanguage from the project¶
The recommended way to get a CustomLanguage for a registered language (predefined or custom) is via ProjectConfig.resolve_language:
catalan = project.config.resolve_language("Catalan")
english = project.config.resolve_language("English")VocabList¶
from lesia.vocab_list import VocabList, vocab_list_from_vocab_dbHolds a custom glossary that is passed to the LLM during translation to improve term consistency.
Constructor¶
VocabList(source_lang_terms: list[str], target_lang_terms: list[str])Both lists must have the same length. Each pair (source_lang_terms[i], target_lang_terms[i]) is one vocabulary entry.
vocab = VocabList(
source_lang_terms=["pomme", "ordinateur"],
target_lang_terms=["apple", "computer"],
)vocab_list_from_vocab_db¶
vocab_list_from_vocab_db(
db: list[dict],
source_lang: Language | CustomLanguage,
target_lang: Language | CustomLanguage,
) -> VocabListExtracts a VocabList from a multi-language vocabulary database. The db argument is a list of dicts where each key is a language name and each value is the term in that language — the format produced by reading a CSV file with csv.DictReader.
import csv
from lesia.vocab_list import vocab_list_from_vocab_db
from lesia.enums import Language
with open("vocab.csv") as f:
db = list(csv.DictReader(f))
# vocab.csv must have language names as column headers:
# French, English
# pomme, apple
# voiture,car
vocab = vocab_list_from_vocab_db(db, Language.FRENCH, Language.ENGLISH)If the source or target language is not found as a column header, a warning is logged and an empty VocabList is returned.
Module-level functions¶
from lesia.project_manager import init_project, load_projectinit_project¶
init_project(project_name: str, root_dir_str: str) -> ProjectCreates a new translation project by writing a .lesia/config.toml file inside root_dir_str. The directory must already exist and must not already contain a .lesia directory.
Raises: InitProjectError — if the path is invalid, does not exist, is not a directory, or a project is already initialized there.
load_project¶
load_project(path_str: str) -> ProjectLoads an existing project. Searches upward from path_str for a .lesia directory (the same strategy git uses to find .git). Can be called with "." from anywhere inside a project tree.
Raises: LoadProjectError — if no project is found or the config file cannot be parsed.
Project class¶
from lesia.project_manager import ProjectProject is the central object for all operations. Always obtain an instance via init_project or load_project; do not instantiate directly.
project.root_path # Path — absolute path to the project root
project.config # ProjectConfig — the loaded configuration modelProject setup¶
set_source_directory¶
project.set_source_directory(dir_name: str, lang: Language | CustomLanguage) -> NoneSets (or changes) the source directory and its language. dir_name is relative to project.root_path. The directory must already exist. Calling this again with a different directory replaces the previous source.
Raises: SetSourceDirError — if the directory does not exist, is not a directory, or the language is already in use as source or target.
add_target_language¶
project.add_target_language(lang: Language | CustomLanguage, tgt_dir: Path | None = None) -> PathAdds a target language. Returns the absolute path of the target directory.
If
tgt_dirisNone, a new directory is created automatically inside the project root using the naming convention<project_name><lang_suffix>(e.g.analysis_notes_en).If
tgt_diris provided, it must already exist and be located inside the project root.
Raises: AddLanguageError — if no source language is set, the language is already present, or the auto-generated directory already exists.
remove_target_language¶
project.remove_target_language(lang: Language | CustomLanguage) -> NoneRemoves a target language from the configuration and deletes its directory from disk.
Raises: RemoveLanguageError — if the language is not a configured target.
get_source_langugage¶
project.get_source_langugage() -> CustomLanguageReturns the source language as a CustomLanguage instance (works for both predefined and custom languages).
Raises: NoSourceLanguageError — if no source language is set.
Custom language management¶
Custom languages extend the fixed predefined set. Once registered, they are stored in the project config and can be used everywhere a language is accepted.
add_custom_language¶
project.add_custom_language(name: str, suffix: str, short: str | None = None) -> NoneRegisters a new custom language. name is the display name (e.g. "American English"); suffix is the directory suffix (e.g. "_ae"). The optional short parameter registers a short alias (e.g. "AmEng") that can be used in place of the full name in any call that accepts a language name string.
project.add_custom_language("Catalan", "_ca")
project.add_custom_language("American English", "_ae", short="AmEng")The short alias is looked up case-insensitively, so "ameng" resolves the same as "AmEng".
Raises: AddCustomLanguageError — if:
namematches a predefined language.nameis already registered as a custom language.shortis already used by another custom language.shortmatches a predefined language name (case-insensitive) — this would make the predefined language unreachable viaresolve_language.shortmatches an existing custom language’s full name (case-insensitive) — this would create an ambiguous alias.
remove_custom_language¶
project.remove_custom_language(name: str) -> NoneRemoves a custom language from the project config. Accepts either the full name or the registered short alias. Removing a language also removes its short alias.
project.remove_custom_language("Catalan")
project.remove_custom_language("AmEng") # same as remove_custom_language("American English")Raises: RemoveCustomLanguageError — if the name/alias matches a predefined language, is not registered, or still has an associated target directory (remove the target first with remove_target_language).
ProjectConfig.resolve_language¶
project.config.resolve_language(name: str) -> CustomLanguageResolves a language name or short alias to a CustomLanguage instance. Resolution order: predefined languages first, then the custom registry by full name, then by short alias (case-insensitive). Because predefined languages are checked first, a short alias can never shadow a predefined language — add_custom_language rejects such aliases upfront.
french = project.config.resolve_language("French") # predefined
catalan = project.config.resolve_language("Catalan") # custom full name
american_en = project.config.resolve_language("American English") # custom full name
american_en = project.config.resolve_language("AmEng") # custom short aliasRaises: ValueError — if the name is not found in either the predefined list or the custom registry (by full name or short alias).
ProjectConfig.custom_languages¶
project.config.custom_languages # dict[str, str] — full name → suffix
project.config.custom_language_shorts # dict[str, str] — short alias → full namecustom_languages maps each registered custom language’s full name to its directory suffix. custom_language_shorts maps each registered short alias to its corresponding full name. Both are persisted to config.toml.
File management¶
set_file_translatability¶
project.set_file_translatability(file_path_str: str, translatable: bool) -> NoneMarks a file in the source directory as translatable (True) or untranslatable (False).
Translatable files are processed by translation commands and ignored by
sync_untranslatable_files.Untranslatable files are copied as-is by
sync_untranslatable_filesand ignored by translation commands.
Raises: AddTranslatableFileError — if the file does not exist or no source directory is set.
get_translatable_files¶
project.get_translatable_files() -> list[Path]Returns absolute paths of all files currently marked as translatable.
Raises: GetTranslatableFilesError — if no source language is set.
sync_untranslatable_files¶
project.sync_untranslatable_files() -> NoneCopies all untranslatable files from the source directory into every configured target directory, mirroring the subdirectory structure. This makes the target directories self-contained (e.g. buildable with LaTeX).
Raises: SyncFilesError — if no source or target directories are configured, or a copy fails.
Translation¶
Translation methods are async and require the LLM_API_KEY environment variable to be set for the configured service.
Setting the API key¶
API keys are resolved at translation time (not at import time), so they can be set at any point before calling a translation method. There are three ways to supply them.
Option 1 — shell environment variable:
export LLM_API_KEY=<your_api_key>
python your_script.pyOption 2 — set it in Python at any point before translating:
import os
import lesia # order doesn't matter
os.environ["LLM_API_KEY"] = "<your_api_key>"
# key is read here, not at import time
asyncio.run(project.translate_single_file(...))Option 3 — .env file stored in the project config:
project.set_env_file(Path(".env"))The .env file is read on every translation call. Shell environment variables always take precedence over the file, so LLM_API_KEY set in the shell will override the file value.
See API key configuration for the full set_env_file / unset_env_file API.
translate_single_file¶
await project.translate_single_file(
file_path_str: str,
target_lang: Language | CustomLanguage,
vocab_list: VocabList | None,
) -> TranslationStatsTranslates one file into target_lang. The file must be marked as translatable. Returns a TranslationStats instance with per-file chunk counts.
Pass a VocabList to guide terminology. If vocab_list is None and a default vocabulary file is configured on the project (via set_vocab_file), it is loaded and used automatically. An explicit VocabList always takes precedence over the project default.
import asyncio
stats = asyncio.run(project.translate_single_file("notes_fr/main.tex", Language.ENGLISH, None))
print(stats.chunks_translated, stats.chunks_from_cache)
# With a custom language:
catalan = project.config.resolve_language("Catalan")
stats = asyncio.run(project.translate_single_file("notes_fr/main.tex", catalan, None))Raises: TranslateFileError — if the file is not marked as translatable, the language is not configured, or translation fails unrecoverably.
translate_all_for_language¶
await project.translate_all_for_language(
target_lang: Language | CustomLanguage,
vocab_list: VocabList | None,
on_file_translated: Callable[[Path, TranslationStats], None] | None = None,
) -> TranslationStatsTranslates all translatable files into target_lang. Files are processed sequentially. Individual chunk failures are logged and the chunk is left untranslated, but the run continues. Returns a TranslationStats instance that is the sum of all per-file stats (failed files are excluded).
The same vocabulary precedence applies as for translate_single_file: an explicit VocabList wins over the project default; None triggers the config lookup for every file in the run.
The optional on_file_translated callback is called after each file is successfully translated. It receives the absolute Path of the source file and its TranslationStats. This lets you react to per-file results (e.g. print progress) without putting loop logic in your own code.
import asyncio
def on_file(path, stats):
print(f"{path.name}: {stats.chunks_translated} translated, {stats.chunks_from_cache} from cache")
total = asyncio.run(project.translate_all_for_language(
Language.ENGLISH,
None,
on_file_translated=on_file,
))
print(f"Total: {total.total} chunks processed")Raises: TranslateFileError — for unrecoverable errors.
TranslationStats¶
from lesia.translator_retrieval import TranslationStatsA dataclass returned by translate_single_file and translate_all_for_language that summarises chunk-level translation activity.
@dataclass
class TranslationStats:
chunks_from_cache: int = 0 # chunks retrieved from the on-disk cache
chunks_translated: int = 0 # chunks successfully translated by the LLM
chunks_passed_to_reasoning: int = 0 # chunks where the reasoning model was attempted
chunks_failed: int = 0 # chunks that failed after all retries
@property
def total(self) -> int:
# chunks_from_cache + chunks_translated + chunks_failed
...
def __add__(self, other: TranslationStats) -> TranslationStats:
# Returns a new TranslationStats with all fields summed.
...Notes:
chunks_passed_to_reasoningcounts chunks for which the reasoning model was called at least once (whether or not translation ultimately succeeded).chunks_failedcounts only chunks that were left untranslated after exhausting all retries. A chunk counted here is never counted inchunks_translated.For
translate_all_for_language, the returned stats are the sum over all successful files; stats from files that raisedTranslateFileErrorare not included.TranslationStatssupports+for manual aggregation:combined = stats_a + stats_b.
Vocabulary configuration¶
A vocabulary file is a CSV glossary injected into every translation prompt so the LLM uses your preferred terminology. You can supply it per-call or configure a project-level default that is loaded automatically.
Precedence: explicit VocabList argument > project default > no vocabulary.
set_vocab_file¶
project.set_vocab_file(path: Path) -> NoneStores the path to a default vocabulary CSV file in the project config. On every translate_single_file or translate_all_for_language call where vocab_list=None, this file is read and used automatically.
The path is stored relative to the project root when possible, keeping the config portable across machines.
from pathlib import Path
project.set_vocab_file(Path("vocab.csv"))
project.set_vocab_file(Path("/shared/team-glossary.csv"))Raises: SetLLMServiceError — if saving the config fails.
unset_vocab_file¶
project.unset_vocab_file() -> NoneRemoves the default vocabulary file from the project config. After this call only an explicit VocabList argument supplies a glossary.
Raises: SetLLMServiceError — if saving the config fails.
ProjectConfig.get_vocab_file_path¶
project.config.get_vocab_file_path() -> Path | NoneReturns the resolved absolute path to the configured vocabulary file, or None if none is set.
ProjectConfig.vocab_file¶
project.config.vocab_file # Path | NoneThe raw stored value — relative to the project root if the file is inside it, absolute otherwise. Prefer get_vocab_file_path() in almost all cases.
Example¶
import asyncio
from pathlib import Path
# Set a project-wide glossary
project.set_vocab_file(Path("vocab.csv"))
# Used automatically — no need to pass a VocabList
stats = asyncio.run(project.translate_single_file("notes_fr/main.tex", Language.ENGLISH, None))
# Explicit VocabList takes precedence over the config default
from lesia.vocab_list import VocabList
override = VocabList(["pomme"], ["apple"])
stats = asyncio.run(project.translate_single_file("notes_fr/main.tex", Language.ENGLISH, override))Cache management¶
The translation cache stores source-to-translation pairs on disk to avoid redundant LLM calls. See the Translation Cache section of the profound explanation for a full description of the on-disk structure and algorithms.
sync_translation_cache¶
project.sync_translation_cache(target_lang: Language | CustomLanguage | None = None) -> NoneRebuilds the translation cache by scanning on-disk source and target file pairs. Run this after manually editing translated files so that future translations reuse your corrected text instead of regenerating from the LLM.
If target_lang is None, all configured target languages are synced.
Raises: TranslationCacheSyncError.
correct_translation_for_lang¶
project.correct_translation_for_lang(target_lang: Language | CustomLanguage) -> NoneReads translated files on disk for the given language and updates the cache to reflect any manual corrections.
Raises: CorrectTranslationError.
correct_translation_single_file¶
project.correct_translation_single_file(file_path_str: str) -> NoneSame as correct_translation_for_lang but limited to a single file.
Raises: CorrectTranslationError.
clear_translation_cache_missing_chunks¶
project.clear_translation_cache_missing_chunks()Removes cache entries that reference chunk files no longer present on disk. Also removes orphaned chunk files with no corresponding cache row. See the cache maintenance section of the profound explanation for the full algorithm.
clear_translation_cache_all¶
project.clear_translation_cache_all(
lang: Language | CustomLanguage | None,
file_path_str: str | None,
keyword: str | None,
)Deletes cache entries, optionally scoped to a language, a file, or a keyword substring match. Passing all three as None clears the entire cache. See the cache maintenance section of the profound explanation for details on each combination.
clear_translation_cache_by_checksum¶
project.clear_translation_cache_by_checksum(
checksum: str,
lang: Language | CustomLanguage | None,
)Deletes cache chunk files matching the given checksum. The behaviour depends on whether the checksum belongs to a source or target chunk, determined by looking it up in the correspondence CSV:
Source checksum,
lang=None: deletes the source chunk file and all associated target chunk files for that row, then removes the row from the CSV entirely.Source checksum,
lang=<language>: deletes only the target chunk file for the specified language that is associated with the given source chunk. The source chunk and all other target chunks are preserved; the row is kept with that language’s field cleared.Target checksum: deletes just the specific target chunk file and clears its field in the CSV. The source chunk and any other target chunks are unaffected. If
langis provided, the search is restricted to that language’s column — passing the wrong language with a target checksum will find nothing.
# Source checksum — delete source + all target chunks for that row
project.clear_translation_cache_by_checksum("abc123def456", None)
# Source checksum + lang — delete only the French target chunk for that row
project.clear_translation_cache_by_checksum("abc123def456", Language.FRENCH)
# Target checksum — delete just that one target chunk
fr = project.config.resolve_language("French")
project.clear_translation_cache_by_checksum("xyz789abc012", None)Raises: TranslationCacheClearError — if the source language is not set or an unexpected error occurs.
LLM configuration¶
The default LLM is google / gemini-2.0-flash.
Supported services: google, openai, anthropic, xai, aristote, ilaas.
set_llm_service_and_model¶
project.set_llm_service_and_model(service: str, model: str) -> NoneSets the primary LLM service and model used for translation.
project.set_llm_service_and_model("google", "gemini-2.0-flash")
project.set_llm_service_and_model("openai", "gpt-4o")
project.set_llm_service_and_model("anthropic", "claude-sonnet-4-5-20251001")Raises: SetLLMServiceError.
set_llm_reasoning_service_and_model¶
project.set_llm_reasoning_service_and_model(service: str, model: str) -> NoneSets an optional reasoning model for harder translation decisions. When set, the tool may use this model for chunks that require more careful handling.
Raises: SetLLMServiceError.
set_xml_retries_before_reasoning¶
project.set_xml_retries_before_reasoning(n: int) -> NoneSets how many times the standard model is retried on XML parse errors before the reasoning model is used as a fallback for that chunk. 0 means the reasoning model is always used for every chunk; the standard model is never called.
Raises: SetLLMServiceError — if n is negative.
Getters¶
project.get_llm_service() -> str
project.get_llm_model() -> str
project.get_llm_reasoning_service() -> str | None
project.get_llm_reasoning_model() -> str | None
project.get_xml_retries_before_reasoning() -> intAPI key configuration¶
API keys can be supplied as shell environment variables, as a .env file, or both. Shell values always take precedence over the file.
| Variable | Purpose |
|---|---|
LLM_API_KEY | Primary key for the standard LLM service |
LLM_REASONING_API_KEY | Key for the reasoning model; falls back to LLM_API_KEY if absent |
Keys are resolved at translation time by lesia.translator.resolve_api_keys, which checks the shell environment first and then the configured .env file.
set_env_file¶
project.set_env_file(path: Path) -> NoneStores the path to a .env file in the project config. On every translation call lesia reads LLM_API_KEY and LLM_REASONING_API_KEY from this file for any key not already present in the shell environment.
The path is stored relative to the project root when possible, so the config remains portable across machines (as long as the file exists at the same relative location).
from pathlib import Path
# File in the project root
project.set_env_file(Path(".env"))
# Absolute path (stored as-is)
project.set_env_file(Path("/home/user/secrets/lesia.env"))Expected file format:
# Lines starting with # are ignored, as are blank lines.
LLM_API_KEY=your_key_here
LLM_REASONING_API_KEY=your_reasoning_key_hereValues may optionally be surrounded by single or double quotes. Only LLM_API_KEY and LLM_REASONING_API_KEY are read; all other lines are ignored.
Raises: SetLLMServiceError — if saving the config fails.
unset_env_file¶
project.unset_env_file() -> NoneRemoves the configured .env file path from the project config. After this call only shell environment variables are consulted for API key resolution.
Raises: SetLLMServiceError — if saving the config fails.
ProjectConfig.get_env_file_path¶
project.config.get_env_file_path() -> Path | NoneReturns the resolved absolute path to the configured .env file, or None if none is set.
ProjectConfig.env_file¶
project.config.env_file # Path | NoneThe raw stored value — relative to the project root if the file is inside it, absolute otherwise. In almost all cases you should use get_env_file_path() instead, which always returns an absolute path.
Typst configuration¶
By default, string arguments of Typst functions (e.g. captions, labels) are not translated. These methods let you register specific argument names of specific functions as translatable.
set_typst_translatable_string_args_for_function¶
project.set_typst_translatable_string_args_for_function(
function_name: str,
arg_names: list[str],
) -> NoneRegisters arg_names as the translatable string arguments of the Typst function function_name.
project.set_typst_translatable_string_args_for_function("figure", ["caption"])
project.set_typst_translatable_string_args_for_function("ex", ["info", "caption"])Raises: SetTypstConfigError.
remove_typst_translatable_string_args_for_function¶
project.remove_typst_translatable_string_args_for_function(function_name: str) -> NoneRemoves the translatable-arg configuration for function_name.
Raises: SetTypstConfigError.
get_typst_translatable_string_args_by_function¶
project.get_typst_translatable_string_args_by_function() -> dict[str, list[str]]Returns the current mapping of function names to their registered translatable argument names.
These project.* methods wrap module-level, process-wide settings —
configure_typst_translatable_string_args_by_function / reset_typst_translatable_string_args_by_function
in lesia.xml_manipulator_mod.typst — that you can call directly without a
Project, e.g. when calling parse_typst from your own code. See
Typst parsing configuration
in the Standalone API Reference.
LaTeX configuration¶
The LaTeX parser has a set of hardcoded defaults — environments like verbatim and lstlisting are treated as opaque placeholders, commands like \cite and \ref are never translated, and all unrecognised environments and commands have their body/arguments walked as translatable text. These methods let you extend and fine-tune that behaviour without modifying source code.
All settings are persisted in .lesia/config.toml and applied automatically before every translation run.
Two layers: Every project method below simply stores the setting and calls
save_config(). The actual parsing logic lives inconfigure_latex_settings()inlesia.xml_manipulator_mod.latex, which can also be called directly without a project (see Standalone usage below).
add_latex_placeholder_env / remove_latex_placeholder_env¶
project.add_latex_placeholder_env(env_name: str) -> None
project.remove_latex_placeholder_env(env_name: str) -> NoneMark an environment as non-translatable — the entire \begin{env}…\end{env} block is emitted as a single opaque placeholder and its content is never sent to the LLM.
project.add_latex_placeholder_env("myverbatim")
project.add_latex_placeholder_env("algorithm")
project.remove_latex_placeholder_env("myverbatim")Raises: SetLatexConfigError — if the name is empty or (for remove) the environment is not in the list.
add_latex_math_env / remove_latex_math_env¶
project.add_latex_math_env(env_name: str) -> None
project.remove_latex_math_env(env_name: str) -> NoneMark an environment as a math environment. Its body is walked in math mode: plain text inside is treated as a placeholder and only \text{…}-style macros (known to pylatexenc) expose translatable content.
project.add_latex_math_env("myequation")
project.remove_latex_math_env("myequation")Raises: SetLatexConfigError — if the name is empty or (for remove) the environment is not in the list.
add_latex_placeholder_command / remove_latex_placeholder_command¶
project.add_latex_placeholder_command(cmd_name: str) -> None
project.remove_latex_placeholder_command(cmd_name: str) -> NoneMark a command as fully non-translatable.
For commands that pylatexenc knows (standard LaTeX commands), the command together with all its arguments becomes a single placeholder.
For unknown custom commands, only the command token itself becomes a placeholder; the {…} groups that follow are sibling nodes in the parse tree and are still walked as text. To make an unknown command’s arguments non-translatable as well, register its argument structure first with set_latex_custom_command_spec — once pylatexenc knows the spec, node.latex_verbatim() includes all arguments and the entire expression is suppressed.
# Standard usage (command is fully suppressed):
project.add_latex_placeholder_command("myref")
# Custom command — register spec first, then suppress:
project.set_latex_custom_command_spec("myfig", mandatory=2)
project.add_latex_placeholder_command("myfig")
# Now \myfig{label}{caption} becomes a single placeholder.Raises: SetLatexConfigError — if the name is empty or (for remove) the command is not in the list.
set_latex_command_translatable_args / remove_latex_command_translatable_args¶
project.set_latex_command_translatable_args(
cmd_name: str,
mandatory: list[int] | None = None,
optional: list[int] | None = None,
) -> None
project.remove_latex_command_translatable_args(cmd_name: str) -> NoneSpecify which arguments of a command are translatable. Arguments use 1-based indexing, counting {…} (mandatory) and […] (optional) separately.
mandatory: 1-based indices of{…}arguments that should be translated. Arguments not listed become placeholders.optional: 1-based indices of[…]arguments that should be translated. Arguments not listed become placeholders.At least one of
mandatoryoroptionalmust be provided.
Prerequisite for custom commands: this setting only takes effect when pylatexenc can parse the command’s arguments into
node.nodeargs. For commands pylatexenc does not know, register the argument structure withset_latex_custom_command_specfirst.
# \textcolor{color}{text} — translate only the text argument
project.set_latex_custom_command_spec("textcolor", mandatory=2)
project.set_latex_command_translatable_args("textcolor", mandatory=[2])
# \mybox[label]{title}{body} — translate only the body
project.set_latex_custom_command_spec("mybox", mandatory=2, optional=1)
project.set_latex_command_translatable_args("mybox", mandatory=[2], optional=[])
# \section[short title]{full title} — translate both
project.set_latex_command_translatable_args("section", mandatory=[1], optional=[1])Raises: SetLatexConfigError — if the name is empty, any index is < 1, or neither mandatory nor optional is provided. For remove: if the command has no config.
set_latex_custom_command_spec / remove_latex_custom_command_spec¶
project.set_latex_custom_command_spec(
cmd_name: str,
mandatory: int,
optional: int = 0,
) -> None
project.remove_latex_custom_command_spec(cmd_name: str) -> NoneRegister the argument structure of a custom command with pylatexenc. This is required before set_latex_command_translatable_args or add_latex_placeholder_command can work correctly on commands not built into pylatexenc.
mandatory: total number of mandatory{…}arguments.optional: total number of optional[…]arguments (default0). Optional arguments are assumed to come before mandatory ones — the most common LaTeX pattern.
# \myfig{label}{caption}
project.set_latex_custom_command_spec("myfig", mandatory=2)
# \mybox[label]{title}{body}
project.set_latex_custom_command_spec("mybox", mandatory=2, optional=1)
# Remove a spec
project.remove_latex_custom_command_spec("myfig")Raises: SetLatexConfigError — if the name is empty, either count is negative, or both counts are zero.
get_latex_settings¶
project.get_latex_settings() -> dictReturns the current LaTeX configuration as a plain dict:
{
"extra_placeholder_envs": ["myverbatim", "algorithm"],
"extra_math_envs": ["myequation"],
"extra_placeholder_commands": ["myref"],
"command_translatable_args": {
"myfig": {"mandatory": [2]},
"mybox": {"mandatory": [2], "optional": []},
},
"custom_command_specs": {
"myfig": {"mandatory": 2, "optional": 0},
"mybox": {"mandatory": 2, "optional": 1},
},
}Standalone usage¶
All of the above is just project.* sugar over module-level, process-wide
settings — configure_latex_settings / reset_latex_settings in
lesia.xml_manipulator_mod.latex — that you can call directly without a
Project, e.g. when calling parse_latex from your own code. See
LaTeX parsing configuration
in the Standalone API Reference for the full signature and an example.
Error reference¶
All exceptions inherit from DirectoryTranslationError.
Import path: from lesia import errors.
DirectoryTranslationError
├── ProjectConfigError
│ ├── LoadConfigError
│ └── WriteConfigError
└── ProjectError
├── InitProjectError
│ ├── InvalidPathError
│ └── ProjectAlreadyInitializedError
├── LoadProjectError
│ └── NoConfigFoundError
├── SetLLMServiceError
├── SetTypstConfigError
├── SetLatexConfigError
├── SetSourceDirError
│ ├── DirectoryDoesNotExistError
│ ├── NotADirectoryError
│ └── AnalyzeDirError
├── LangAlreadyInProjectError
├── AddLanguageError
│ └── LangDirExistsError
├── AddCustomLanguageError
├── RemoveCustomLanguageError
├── NoSourceLanguageError
├── RemoveLanguageError
│ └── TargetLanguageNotInProjectError
├── SyncFilesError
│ └── NoTargetLanguagesError
├── CopyFileDirError
├── AddTranslatableFileError
│ └── FileDoesNotExistError
├── GetTranslatableFilesError
├── TranslateFileError
│ ├── UntranslatableFileError
│ ├── TranslationProcessError
│ └── ChunkTranslationFailed
├── CorrectTranslationError
│ ├── CorrectingTranslationError
│ ├── ChecksumNotFoundError
│ └── NoSourceFileError
├── TranslationCacheSyncError
└── TranslationCacheClearErrorChunkTranslationFailed carries the untranslated chunk text in its .chunk attribute and the original exception in .original_exception. Most error classes that wrap a cause expose it as .original_exception as well.