Skip to main content

SVG Translation Tool

PyPi Version

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() and inject() APIs are deprecated and will be removed in a future release. Migrate to SVGTranslationExtractor and SVGTranslationInjector respectively.

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., text2213 becomes text2213-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 (raises SvgNestedTspanExceptionError)
  • Invalid SVG structures like <tref> elements (raises SvgStructureExceptionError)

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)

Source distribution for CopySVGTranslation 0.2.0
File Size Uploaded Provenance
copysvgtranslation-0.2.0.tar.gz 126.8 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for CopySVGTranslation 0.2.0
File Interpreter ABI Platform
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 log

Release 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 log

Security 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.

Select from 2 files

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 log

A 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.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page