SVG Translation Tool
This tool extracts multilingual text pairs from SVG files and applies translations to other SVG files by inserting missing <text systemLanguage="XX"> blocks.
Installation
This tool requires Python 3.10+. Install the lightweight core dependencies with:
pip install CopySVGTranslation
Quick Start
The recommended API uses the class-based SVGTranslationExtractor and SVGTranslationInjector classes.
Extract translations from an SVG
from pathlib import Path
from CopySVGTranslation import SVGTranslationExtractor
extractor = SVGTranslationExtractor(
svg_file_path=Path("examples/source_multilingual.svg"),
case_insensitive=True,
)
result = extractor.extract()
if not result.error:
print(result.to_json())
# {
# "new": {"hello": {"ar": "مرحبا", "fr": "Bonjour"}, ...},
# "title": {...},
# "tspans_by_id": {...},
# "title_new": {...},
# }
Inject translations into an SVG
from pathlib import Path
from CopySVGTranslation import SVGTranslationInjector
injector = SVGTranslationInjector(
case_insensitive=True,
overwrite=False,
pretty_print=True,
)
translations = {
"new": {
"Hello": {"ar": "مرحبًا", "fr": "Bonjour"},
}
}
result = injector.inject(
inject_file=Path("examples/target_missing_translations.svg"),
all_mappings=translations,
target_path=Path("translated/target.svg"),
save_result=True,
)
if not result.new_stats.error:
print(f"Inserted: {result.new_stats.inserted_translations}")
print(f"Updated: {result.new_stats.updated_translations}")
print(f"Skipped: {result.new_stats.skipped_translations}")
print(f"Languages: {result.new_stats.all_languages}")
Extracting and injecting in a single step
For convenience, the high-level workflow functions combine both phases:
from pathlib import Path
from CopySVGTranslation import svg_extract_and_inject
tree = svg_extract_and_inject(
extract_file=Path("examples/source_multilingual.svg"),
inject_file=Path("examples/target_missing_translations.svg"),
data_output_file=Path("examples/data.json"),
save_result=True,
)
if tree is not None:
print("Injection completed!")
The helper stores the extracted phrases under Path("examples/data.json") and,
when save_result=True, writes the translated SVG to
output_dir=Path("./translated").
API Reference
SVGTranslationExtractor
The primary class for extracting translation data from SVG files.
from CopySVGTranslation import SVGTranslationExtractor
extractor = SVGTranslationExtractor(
svg_file_path: str | Path,
case_insensitive: bool = True,
)
result: ExtractorData = extractor.extract()
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
svg_file_path |
str | Path |
— | Path to the SVG file to process. |
case_insensitive |
bool |
True |
If True, default text keys are lowercased for case-insensitive matching. |
Returns: ExtractorData — a dataclass with the following fields:
| Field | Type | Description |
|---|---|---|
new |
dict[str, dict[str, str]] |
Mapping of normalized source text → language code → translation. |
tspans_by_id |
dict[str, str] |
Mapping of <tspan> ID → text content. |
title |
dict[str, Any] |
Title-like entries (ending with a 4-digit year) with the year removed. |
title_new |
dict[str, Any] |
New-format title translations. |
error |
str |
Error message if extraction failed, empty string on success. |
Use result.to_json() to get a plain dictionary suitable for JSON serialization.
SVGTranslationInjector
The primary class for injecting translations into SVG files.
from CopySVGTranslation import SVGTranslationInjector
injector = SVGTranslationInjector(
case_insensitive: bool = True,
overwrite: bool = False,
pretty_print: bool = True,
)
result: InjectorData = injector.inject(
inject_file: Path | str,
all_mappings: Mapping | None = None,
target_path: Path | None = None,
save_result: bool = False,
)
Constructor Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
case_insensitive |
bool |
True |
If True, translation lookups are case-insensitive. |
overwrite |
bool |
False |
If True, existing language nodes are updated in place instead of being skipped. |
pretty_print |
bool |
True |
If True, the output SVG is formatted with indentation. |
inject() Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
inject_file |
Path | str |
— | Path to the SVG file to inject translations into. |
all_mappings |
Mapping | None |
None |
Translation mapping dictionary (see Data Model). |
target_path |
Path | None |
None |
Output file path when save_result=True. |
save_result |
bool |
False |
If True, writes the modified SVG to target_path. |
Returns: InjectorData — a dataclass with the following fields:
| Field | Type | Description |
|---|---|---|
tree |
etree._ElementTree | None |
The parsed (and possibly modified) SVG tree. |
new_stats |
InjectorStats |
Statistics about the injection run. |
InjectorStats fields:
| Field | Type | Description |
|---|---|---|
all_languages |
int |
Total number of languages in the SVG after injection. |
new_languages |
int |
Number of new languages added. |
processed_switches |
int |
Number of <switch> elements processed. |
inserted_translations |
int |
Number of new <text> nodes inserted. |
skipped_translations |
int |
Number of existing translations skipped (not overwritten). |
updated_translations |
int |
Number of existing translations updated in place. |
languages_before |
list[str] |
Sorted list of language codes before injection. |
languages_after |
list[str] |
Sorted list of newly added language codes. |
error |
str |
Error message if injection failed, empty string on success. |
Use result.new_stats.to_json() to get a plain dictionary of the stats.
ExtractorData
Dataclass returned by SVGTranslationExtractor.extract(). See the extractor documentation above for field details.
InjectorData
Dataclass returned by SVGTranslationInjector.inject(). See the injector documentation above for field details.
High-Level Workflows
svg_extract_and_inject()
Extract translations from one SVG and inject them into another in a single call.
from CopySVGTranslation import svg_extract_and_inject
tree = svg_extract_and_inject(
extract_file: Path | str,
inject_file: Path | str,
output_file: Path | None = None,
data_output_file: Path | None = None,
overwrite: bool | None = None,
save_result: bool = False,
)
svg_extract_and_injects()
Inject a pre-built translation mapping into an SVG file.
from CopySVGTranslation import svg_extract_and_injects
tree = svg_extract_and_injects(
translations: Mapping,
inject_file: Path | str,
output_dir: Path | None = None,
save_result: bool = False,
)
Legacy (Deprecated) Functions
⚠️ Deprecation Notice: The function-based
extract()andinject()APIs are deprecated and will be removed in a future release. Migrate toSVGTranslationExtractorandSVGTranslationInjectorrespectively.
extract() (deprecated)
from CopySVGTranslation import extract
# Deprecated — use SVGTranslationExtractor instead
translations = extract(
svg_file_path=Path("arabic.svg"),
case_insensitive=True,
)
Migration:
# Before (deprecated)
from CopySVGTranslation import extract
translations = extract(svg_file_path=Path("arabic.svg"), case_insensitive=True)
# After (recommended)
from CopySVGTranslation import SVGTranslationExtractor
extractor = SVGTranslationExtractor(svg_file_path=Path("arabic.svg"), case_insensitive=True)
result = extractor.extract()
translations = result.to_json() if not result.error else None
inject() (deprecated)
from CopySVGTranslation import inject
# Deprecated — use SVGTranslationInjector instead
tree, stats = inject(
inject_file=Path("target.svg"),
all_mappings=translations,
output_dir=Path("./translated"),
save_result=True,
return_stats=True,
)
Migration:
# Before (deprecated)
from CopySVGTranslation import inject
tree, stats = inject(
inject_file=Path("target.svg"),
all_mappings=translations,
output_file=Path("translated/target.svg"),
save_result=True,
return_stats=True,
)
# After (recommended)
from CopySVGTranslation import SVGTranslationInjector
injector = SVGTranslationInjector(case_insensitive=True, overwrite=False)
result = injector.inject(
inject_file=Path("target.svg"),
all_mappings=translations,
target_path=Path("translated/target.svg"),
save_result=True,
)
tree = result.tree
stats = result.new_stats.to_json()
Data Model
The extractor produces a JSON document with these top-level keys:
{
"new": {
"normalized english text": {
"ar": "Arabic translation",
"fr": "French translation"
}
},
"title": {
"text without year": {
"ar": "...",
"fr": "..."
}
},
"tspans_by_id": {
"tspan-id": "Text content"
},
"title_new": {
"text without year": {
"ar": "...",
"fr": "..."
}
}
}
| Key | Description |
|---|---|
new |
Primary mapping of normalized source text → language code → translation. |
title |
Title-like entries (text ending with a 4-digit year) with the year stripped. |
title_new |
New-format title translations preserving additional metadata. |
tspans_by_id |
Mapping of <tspan> element IDs to their text content. |
Older exports may omit the wrapper and look like {"english": {"ar": "…"}}. The injector transparently accepts both structures, but the recommended format is the nested layout shown above.
Extract Example
Input SVG (arabic.svg)
<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg">
<switch>
<text id="t0-ar" systemLanguage="ar">
<tspan id="t0-ar">الموسيقى في عام 2020</tspan>
</text>
<text id="t0-fr" systemLanguage="fr">
<tspan id="t0-fr">La musique en 2020</tspan>
</text>
<text id="t0">
<tspan id="t0">Music in 2020</tspan>
</text>
</switch>
<switch>
<text id="t1-ar" systemLanguage="ar">
<tspan id="t1-ar">مرحبا</tspan>
</text>
<text id="t1-fr" systemLanguage="fr">
<tspan id="t1-fr">Bonjour</tspan>
</text>
<text id="t1">
<tspan id="t1">Hello</tspan>
</text>
</switch>
</svg>
Python code
from pathlib import Path
from CopySVGTranslation import SVGTranslationExtractor
extractor = SVGTranslationExtractor(
svg_file_path=Path("arabic.svg"),
case_insensitive=True,
)
result = extractor.extract()
print(result.to_json())
Extracted JSON
{
"new": {
"music in 2020": {
"ar": "الموسيقى في عام 2020",
"fr": "La musique en 2020"
},
"hello": {
"ar": "مرحبا",
"fr": "Bonjour"
}
},
"title": {
"music in": {
"ar": "الموسيقى في عام",
"fr": "La musique en"
}
},
"tspans_by_id": {
"t0": "Music in 2020",
"t1": "Hello"
},
"title_new": {},
"error": ""
}
Injection Example
Input SVG (target.svg)
<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg">
<switch>
<text id="t0">
<tspan id="t0">Hello</tspan>
</text>
</switch>
<switch>
<text id="t1">
<tspan id="t1">Music in 2020</tspan>
</text>
</switch>
</svg>
Python code
from pathlib import Path
from CopySVGTranslation import SVGTranslationInjector
injector = SVGTranslationInjector(
case_insensitive=True,
overwrite=False,
pretty_print=True,
)
translations = {
"new": {
"hello": {"ar": "مرحبا", "fr": "Bonjour"},
"music in 2020": {"ar": "الموسيقى في عام 2020", "fr": "La musique en 2020"},
}
}
result = injector.inject(
inject_file=Path("target.svg"),
all_mappings=translations,
target_path=Path("translated/target.svg"),
save_result=True,
)
print(f"Inserted: {result.new_stats.inserted_translations}")
print(f"Languages: {result.new_stats.all_languages}")
Output SVG (translated/target.svg)
<?xml version='1.0' encoding='utf-8'?>
<svg xmlns="http://www.w3.org/2000/svg">
<switch>
<text id="t0">
<tspan id="t0">Hello</tspan>
</text>
<text id="t0-ar" systemLanguage="ar">
<tspan id="t0-ar">مرحبا</tspan>
</text>
<text id="t0-fr" systemLanguage="fr">
<tspan id="t0-fr">Bonjour</tspan>
</text>
</switch>
<switch>
<text id="t1">
<tspan id="t1">Music in 2020</tspan>
</text>
<text id="t1-ar" systemLanguage="ar">
<tspan id="t1-ar">الموسيقى في عام 2020</tspan>
</text>
<text id="t1-fr" systemLanguage="fr">
<tspan id="t1-fr">La musique en 2020</tspan>
</text>
</switch>
</svg>
Testing
Run the unit tests:
python -m pytest tests -v
Implementation Details
Text Normalization
The tool normalizes text by:
- Trimming leading and trailing whitespace
- Replacing multiple internal whitespace characters with a single space
- Optionally converting to lowercase for case-insensitive matching
ID Generation
When adding new translation nodes, the tool generates unique IDs by:
- Taking the existing ID and appending the language code (e.g.,
text2213becomestext2213-ar) - If the generated ID already exists, appending a numeric suffix until unique (e.g.,
text2213-ar-1)
Error Handling
The tool includes comprehensive error handling for:
- Missing input files
- Invalid XML structure
- Missing required attributes
- File permission issues
- Nested
<tspan>structures (raisesSvgNestedTspanExceptionError) - Invalid SVG structures like
<tref>elements (raisesSvgStructureExceptionError)
Metadata
Release files for CopySVGTranslation 0.2.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 | |
|---|---|---|---|---|
| copysvgtranslation-0.2.0.tar.gz | 126.8 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| copysvgtranslation-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 158.0 kB
Release files / copysvgtranslation-0.2.0.tar.gz
| Download URL | copysvgtranslation-0.2.0.tar.gz |
|---|---|
| Size | 126.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
714bc5e0caf4f7778ea4c838b3b1976dd857041df70bd3bab679cd8e17e18be8
|
|
BLAKE2b-256 checksum How to use checksums |
3bfbd444b629e580c2a2a79209f782edcc4a9e661436745ddd38728283fa6ff8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 Aug 2, 2026.
Transparency logRelease files / copysvgtranslation-0.2.0-py3-none-any.whl
| Download URL | copysvgtranslation-0.2.0-py3-none-any.whl |
|---|---|
| Size | 31.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
977f777b6225d895cde0b583e843aedadbda98d7bc9335bbd1a5347e1d0f1168
|
|
BLAKE2b-256 checksum How to use checksums |
3287757fd4d0d80101f075b8b37d7f6ee3dc0078f4ecce2737866a37975ef3fd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 Aug 2, 2026.
Transparency logSecurity for CopySVGTranslation 0.2.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 copysvgtranslation-0.2.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 Aug 2, 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.