python developer tools automation, translation localization software, freelance developer coding

Build Your Own Smart Python Tools: Practical Examples

|

Learn how to build custom Python tools tailored to your workflow — from an AI-powered proposal generator to Arabic UI localization QA checkers and translation string extractors.

Word count: ~2,000 · Reading time: 10 minutes

Build Your Own Smart Python Tools Tailored to Your Needs

From AI-powered proposal generators to specialized tools solving real problems for translators and localization professionals


Note: This article stands on its own — you can apply everything here without reading previous articles. That said, if you haven’t yet learned how to integrate AI models into Python, we recommend checking out: How to Use Large Language Models (AI) in Your Python Projects.

Throughout this series we’ve covered installing Python, automating daily tasks, building dynamic websites, and creating APIs ready to sell. But the question that keeps a freelancer up at night isn’t “what do I know about Python?” — it’s “what problem do I deal with every day that nobody has built a tool for yet?”

The most valuable tools aren’t the ones you find ready-made online. They’re the ones you build yourself — for yourself and your clients — because you understand the problem from the inside. In this article from Zy Yazan Platform, we’ll build three practical tools: the first automates competitive proposal generation, while the second and third are designed specifically for the needs of translators and localization professionals — a field that suffers a genuine shortage of specialized tooling.

python smart tools developer freelancer

Tool One: AI-Powered Freelance Proposal Generator

Writing proposals is one of the most time-consuming non-billable tasks in a freelancer’s life. Every project is different, but the structure is always the same: project description, task breakdown, timeline, price, payment terms. Let’s build a tool that takes a short project brief and outputs a professional PDF proposal in minutes:

pip install openai reportlab
from openai import OpenAI
from reportlab.lib.pagesizes import A4
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib import colors
from datetime import datetime, timedelta
import json, os

client_ai = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))

def generate_proposal_content(
    project_brief: str,
    freelancer_name: str,
    hourly_rate: float,
    specialty: str
) -> dict:
    """Analyzes the project brief with AI and returns structured proposal content."""
    prompt = f"""You are an expert freelancer specializing in {specialty}.
The client described their project as:
"{project_brief}"

Your hourly rate: ${hourly_rate}

Generate professional proposal content as JSON:
{{
  "project_title": "A clear, specific project title",
  "executive_summary": "3-sentence summary that proves you understand the project",
  "deliverables": [
    {{"task": "Task name", "hours": 5, "description": "Brief description"}}
  ],
  "timeline_days": 14,
  "total_hours": 20,
  "payment_terms": "50% upfront, 50% on delivery",
  "validity_days": 7
}}
Keep hours realistic. No more than 5 deliverables."""

    response = client_ai.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": prompt}],
        temperature=0.3,
        response_format={"type": "json_object"}
    )
    return json.loads(response.choices[0].message.content)


def build_proposal_pdf(
    content: dict,
    freelancer_name: str,
    hourly_rate: float,
    client_name: str,
    output_file: str = "proposal.pdf"
):
    """Builds a professionally formatted PDF proposal."""
    doc = SimpleDocTemplate(output_file, pagesize=A4,
                            rightMargin=40, leftMargin=40,
                            topMargin=40, bottomMargin=40)
    styles = getSampleStyleSheet()
    story  = []

    red   = colors.HexColor("#c0392b")
    navy  = colors.HexColor("#1a2e5e")
    light = colors.HexColor("#f9f9f9")

    title_style = ParagraphStyle('Title', parent=styles['Heading1'],
                                 fontSize=22, textColor=red, spaceAfter=6)
    sub_style   = ParagraphStyle('Sub',   parent=styles['Normal'],
                                 fontSize=11, textColor=navy, spaceAfter=14)
    normal      = styles['Normal']

    # Header
    story.append(Paragraph("FREELANCE PROJECT PROPOSAL", title_style))
    story.append(Paragraph(content["project_title"], sub_style))
    story.append(Paragraph(
        f"Prepared by: {freelancer_name}  |  For: {client_name}  |  "
        f"Date: {datetime.now().strftime('%Y-%m-%d')}  |  "
        f"Valid until: {(datetime.now() + timedelta(days=content['validity_days'])).strftime('%Y-%m-%d')}",
        normal))
    story.append(Spacer(1, 16))

    # Executive Summary
    story.append(Paragraph("Executive Summary", ParagraphStyle(
        'H2', parent=styles['Heading2'], textColor=navy, spaceAfter=6)))
    story.append(Paragraph(content["executive_summary"], normal))
    story.append(Spacer(1, 12))

    # Scope & Pricing Table
    story.append(Paragraph("Scope of Work & Pricing", ParagraphStyle(
        'H2', parent=styles['Heading2'], textColor=navy, spaceAfter=6)))

    table_data = [["Task", "Description", "Hours", "Cost (USD)"]]
    total_cost = 0
    for item in content["deliverables"]:
        cost = item["hours"] * hourly_rate
        total_cost += cost
        table_data.append([
            item["task"],
            item["description"],
            str(item["hours"]),
            f"${cost:.0f}"
        ])
    table_data.append(["", "", "TOTAL", f"${total_cost:.0f}"])

    tbl = Table(table_data, colWidths=[130, 230, 50, 80])
    tbl.setStyle(TableStyle([
        ('BACKGROUND',    (0, 0),  (-1, 0),  navy),
        ('TEXTCOLOR',     (0, 0),  (-1, 0),  colors.white),
        ('FONTNAME',      (0, 0),  (-1, 0),  'Helvetica-Bold'),
        ('BACKGROUND',    (0, 1),  (-1, -2), light),
        ('GRID',          (0, 0),  (-1, -2), 0.5, colors.HexColor("#dddddd")),
        ('FONTNAME',      (0, -1), (-1, -1), 'Helvetica-Bold'),
        ('TEXTCOLOR',     (2, -1), (-1, -1), red),
        ('LINEABOVE',     (0, -1), (-1, -1), 1.5, red),
        ('FONTSIZE',      (0, 0),  (-1, -1), 9),
        ('ALIGN',         (2, 0),  (-1, -1), 'CENTER'),
        ('BOTTOMPADDING', (0, 0),  (-1, -1), 7),
    ]))
    story.append(tbl)
    story.append(Spacer(1, 14))

    # Timeline & Terms
    story.append(Paragraph(
        f"Timeline: {content['timeline_days']} business days after project kickoff.", normal))
    story.append(Paragraph(
        f"Payment Terms: {content['payment_terms']}", normal))
    story.append(Spacer(1, 20))
    story.append(Paragraph(
        "Thank you for considering my proposal. I look forward to working with you!",
        ParagraphStyle('Footer', parent=normal, textColor=navy)))

    doc.build(story)
    print(f"✅ Proposal ready: {output_file}  (Total: ${total_cost:.0f})")


# Run the tool
proposal = generate_proposal_content(
    project_brief="Portfolio website for a graphic designer — homepage, work gallery, and contact page. Clean design, fast loading.",
    freelancer_name="Ahmad Al-Freelancer",
    hourly_rate=40,
    specialty="web development and UI design"
)
build_proposal_pdf(proposal, "Ahmad Al-Freelancer", 40, "Sara Al-Designer")

In two minutes you have a PDF ready to send. You can extend this later by adding your logo and brand colors to make it fully yours.

Tool Two: Automated UI Localization Quality Checker

Anyone working in localization knows this scenario too well: you deliver your translation, and a week later the developer comes back saying the Arabic text breaks the button layout, or that long strings overflow their containers because of RTL rendering. This is entirely preventable — with a Python script that catches these issues before the files ever leave your desk.

The problem: Translation files (JSON or PO) contain hundreds of strings. Checking them manually against UI constraints is impractical. And developers rarely share the maximum character limits for each UI element upfront.

pip install Pillow
import json
import re
from dataclasses import dataclass
from typing import Optional
from PIL import Image, ImageDraw
import os

# ---- UI element rules ----
# Each UI element type has a max character limit and a descriptive context
UI_RULES = {
    "btn_":         {"max_chars": 18,  "context": "Button label"},
    "nav_":         {"max_chars": 14,  "context": "Navigation menu item"},
    "label_":       {"max_chars": 30,  "context": "Form or field label"},
    "title_":       {"max_chars": 55,  "context": "Page or section title"},
    "tooltip_":     {"max_chars": 80,  "context": "Hover tooltip text"},
    "error_":       {"max_chars": 120, "context": "Error message"},
    "placeholder_": {"max_chars": 35,  "context": "Input field placeholder"},
}

@dataclass
class LocalizationIssue:
    key:         str
    original:    str
    translation: str
    char_count:  int
    max_allowed: int
    ui_context:  str
    severity:    str   # critical / warning

def detect_ui_rule(key: str) -> Optional[dict]:
    """Identifies which UI rule applies based on the key prefix."""
    key_lower = key.lower()
    for prefix, rule in UI_RULES.items():
        if key_lower.startswith(prefix):
            return rule
    return None

def check_rtl_issues(text: str) -> list[str]:
    """Detects potential RTL rendering problems in translated strings."""
    issues = []
    # Long digit sequences can flip text direction unexpectedly
    if re.search(r'\d{4,}', text):
        issues.append("Long number sequence may disrupt RTL text flow")
    # Mixed Arabic + Latin scripts can cause bidi rendering issues
    has_arabic = bool(re.search(r'[\u0600-\u06FF]', text))
    has_latin  = bool(re.search(r'[a-zA-Z]{3,}', text))
    if has_arabic and has_latin:
        issues.append("Bidirectional text detected — verify rendering order in UI")
    return issues

def analyze_localization_file(
    source_file: str,       # Original English JSON
    target_file: str,       # Translated Arabic JSON
    report_file: str = "localization_report.txt"
) -> list[LocalizationIssue]:
    """
    Checks both translation files and produces a QA report.
    """
    with open(source_file, encoding="utf-8") as f:
        source = json.load(f)
    with open(target_file, encoding="utf-8") as f:
        target = json.load(f)

    issues   = []
    warnings = []
    ok_count = 0

    for key, original in source.items():
        translation = target.get(key, "")

        # Missing translation
        if not translation:
            issues.append(LocalizationIssue(
                key=key, original=original, translation="[MISSING]",
                char_count=0, max_allowed=0,
                ui_context="Unknown", severity="critical"
            ))
            continue

        rule       = detect_ui_rule(key)
        rtl_issues = check_rtl_issues(translation)

        if rule:
            char_count = len(translation)
            if char_count > rule["max_chars"]:
                severity = "critical" if char_count > rule["max_chars"] * 1.3 else "warning"
                issues.append(LocalizationIssue(
                    key=key, original=original, translation=translation,
                    char_count=char_count, max_allowed=rule["max_chars"],
                    ui_context=rule["context"], severity=severity
                ))
            elif rtl_issues:
                for rtl_issue in rtl_issues:
                    warnings.append(f"[RTL] {key}: {rtl_issue}")
            else:
                ok_count += 1
        else:
            ok_count += 1

    # Write report
    with open(report_file, "w", encoding="utf-8") as f:
        f.write("=" * 60 + "\n")
        f.write("UI Localization QA Report\n")
        f.write(f"Generated: {__import__('datetime').datetime.now().strftime('%Y-%m-%d %H:%M')}\n")
        f.write("=" * 60 + "\n\n")

        criticals = [i for i in issues if i.severity == "critical"]
        warns     = [i for i in issues if i.severity == "warning"]

        f.write(f"✅ Strings passed:    {ok_count}\n")
        f.write(f"🔴 Critical issues:   {len(criticals)}\n")
        f.write(f"🟡 Warnings:          {len(warns)}\n")
        f.write(f"⚠️  RTL issues:       {len(warnings)}\n\n")

        if criticals:
            f.write("─── Critical Issues (must fix before delivery) ───\n")
            for i in criticals:
                f.write(f"\n🔑 Key:           {i.key}\n")
                f.write(f"   UI Context:    {i.ui_context}\n")
                f.write(f"   Source:        {i.original}\n")
                f.write(f"   Translation:   {i.translation}\n")
                f.write(f"   Length:        {i.char_count} / {i.max_allowed} allowed\n")
                f.write(f"   Overflow:      +{i.char_count - i.max_allowed} chars\n")

        if warnings:
            f.write("\n─── RTL Warnings ───\n")
            for w in warnings:
                f.write(f"   {w}\n")

    print(f"📋 Report saved: {report_file}")
    print(f"   🔴 Critical: {len(criticals)}  |  🟡 Warnings: {len(warns)}  |  ✅ Passed: {ok_count}")
    return issues


def generate_visual_preview(
    translation: str,
    element_type: str = "button",
    output_file: str = "preview.png"
):
    """
    Renders a simple visual mockup showing how the string fits (or overflows)
    its UI element. Useful for sharing with clients alongside the QA report.
    """
    sizes = {"button": (200, 50), "label": (300, 40), "title": (400, 60)}
    w, h  = sizes.get(element_type, (250, 50))

    img  = Image.new("RGB", (w, h), color="#f0f0f0")
    draw = ImageDraw.Draw(img)

    # Element border
    draw.rectangle([2, 2, w-3, h-3], outline="#c0392b", width=2)

    # Centered text
    draw.text((w//2, h//2), translation, fill="#1a2e5e", anchor="mm")

    # Character count indicator
    max_chars = UI_RULES.get(f"{element_type}_", {}).get("max_chars", 20)
    color = "#c0392b" if len(translation) > max_chars else "#27ae60"
    draw.text((5, 5), f"{len(translation)}/{max_chars}", fill=color)

    img.save(output_file)
    print(f"🖼️  Visual preview saved: {output_file}")

How to use the tool

Your source en.json file looks like this:

{
  "btn_submit":      "Submit",
  "btn_cancel":      "Cancel",
  "nav_dashboard":   "Dashboard",
  "title_welcome":   "Welcome to your account",
  "error_not_found": "The page you requested could not be found."
}

And the translated ar.json:

{
  "btn_submit":      "إرسال الطلب والتأكيد",
  "btn_cancel":      "إلغاء",
  "nav_dashboard":   "لوحة التحكم",
  "title_welcome":   "مرحباً بك في حسابك الشخصي على المنصة",
  "error_not_found": "الصفحة التي طلبتها غير موجودة أو ربما تم نقلها."
}
issues = analyze_localization_file("en.json", "ar.json")
# Output:
# 🔴 Critical: 1  (btn_submit — 22 chars vs. 18 allowed)
# 🟡 Warnings: 1  (title_welcome — 34 chars, within 55 limit — fine)
# ✅ Passed:   3

Instead of the developer telling you a week later that a button label breaks the layout, you catch it yourself in seconds — before the files leave your hands.

Tool Three: Contextual Translation String Extractor

The second problem translators face daily: a client sends a full codebase — HTML templates, JavaScript files, Python scripts — and asks you to translate the user-facing strings. Manually hunting through thousands of lines of code to find translatable text without accidentally breaking the code is tedious and error-prone. This tool does it in seconds.

pip install pandas beautifulsoup4 openpyxl
import re
import os
import pandas as pd
from bs4 import BeautifulSoup
from pathlib import Path
from dataclasses import dataclass, field

@dataclass
class ExtractedString:
    file_name:   str
    line_number: int
    context:     str        # Full line for translator context
    source_text: str        # The English string to translate
    string_type: str        # html_text / js_string / py_string / alt_text
    translation: str = ""   # Empty column for the translator to fill

def extract_from_html(file_path: str) -> list[ExtractedString]:
    """Extracts user-visible strings from HTML files."""
    results = []
    with open(file_path, encoding="utf-8") as f:
        content = f.read()
        lines   = content.splitlines()

    soup = BeautifulSoup(content, "html.parser")

    skip_tags = {"script", "style", "code", "pre"}
    for element in soup.find_all(string=True):
        if element.parent.name in skip_tags:
            continue
        text = element.strip()
        if len(text) < 2 or not re.search(r'[a-zA-Z]', text):
            continue
        if re.match(r'^[\d\s\W]+$', text):
            continue

        line_num = 0
        for i, line in enumerate(lines, 1):
            if text[:20] in line:
                line_num = i
                break

        results.append(ExtractedString(
            file_name=Path(file_path).name,
            line_number=line_num,
            context=lines[line_num-1].strip() if line_num else "",
            source_text=text,
            string_type="html_text"
        ))

    # Alt text — important for accessibility localization
    for img in soup.find_all("img", alt=True):
        alt = img.get("alt", "").strip()
        if len(alt) > 1 and re.search(r'[a-zA-Z]', alt):
            results.append(ExtractedString(
                file_name=Path(file_path).name,
                line_number=0,
                context=str(img)[:80],
                source_text=alt,
                string_type="alt_text"
            ))

    return results


def extract_from_javascript(file_path: str) -> list[ExtractedString]:
    """Extracts translatable string literals from JavaScript files."""
    results = []
    with open(file_path, encoding="utf-8") as f:
        lines = f.readlines()

    pattern = re.compile(r'''["'`]([A-Za-z][^"'`\n]{4,80})["'`]''')

    skip_patterns = [
        r'^https?://', r'^\./', r'\.(js|css|png|jpg|svg)$',
        r'^[A-Z_]+$',   # Uppercase constants — not UI strings
        r'^\s*//.*',    # Comments
    ]

    for line_num, line in enumerate(lines, 1):
        stripped = line.strip()
        if stripped.startswith("//") or stripped.startswith("*"):
            continue

        for match in pattern.finditer(line):
            text = match.group(1).strip()
            if any(re.search(p, text) for p in skip_patterns):
                continue
            if len(text.split()) < 1:
                continue

            results.append(ExtractedString(
                file_name=Path(file_path).name,
                line_number=line_num,
                context=line.strip()[:100],
                source_text=text,
                string_type="js_string"
            ))

    return results


def extract_from_python(file_path: str) -> list[ExtractedString]:
    """Extracts user-facing strings from Python files (labels, messages, titles)."""
    results = []
    with open(file_path, encoding="utf-8") as f:
        lines = f.readlines()

    ui_patterns = [
        re.compile(r'''(?:label|title|message|text|placeholder|description|error|success|warning|info)\s*[=:]\s*["']([A-Za-z][^"'\n]{3,100})["']''', re.I),
        re.compile(r'''_\(["']([A-Za-z][^"'\n]{3,100})["']\)'''),   # i18n gettext pattern
        re.compile(r'''print\(["']([A-Za-z][^"'\n]{3,80})["']\)'''),
    ]

    for line_num, line in enumerate(lines, 1):
        if line.strip().startswith("#"):
            continue
        for pattern in ui_patterns:
            for match in pattern.finditer(line):
                text = match.group(1).strip()
                if len(text) > 3:
                    results.append(ExtractedString(
                        file_name=Path(file_path).name,
                        line_number=line_num,
                        context=line.strip()[:100],
                        source_text=text,
                        string_type="py_string"
                    ))

    return results


def export_to_excel(
    strings: list[ExtractedString],
    output_file: str = "translation_strings.xlsx"
):
    """
    Exports extracted strings to a translator-ready Excel file.
    Each row contains: filename, line number, context, source text, translation column.
    """
    # Deduplicate
    seen   = set()
    unique = []
    for s in strings:
        if s.source_text not in seen:
            seen.add(s.source_text)
            unique.append(s)

    df = pd.DataFrame([{
        "File":                s.file_name,
        "Line":                s.line_number,
        "Element Type":        s.string_type,
        "Context (Code)":      s.context,
        "Source Text (EN)":    s.source_text,
        "Arabic Translation":  "",
        "Translator Notes":    "",
    } for s in unique])

    with pd.ExcelWriter(output_file, engine="openpyxl") as writer:
        df.to_excel(writer, index=False, sheet_name="Strings for Translation")

        ws = writer.sheets["Strings for Translation"]

        from openpyxl.styles import PatternFill, Font, Alignment
        header_fill = PatternFill("solid", fgColor="1A2E5E")
        trans_fill  = PatternFill("solid", fgColor="FFF8F8")

        for cell in ws[1]:
            cell.fill      = header_fill
            cell.font      = Font(color="FFFFFF", bold=True)
            cell.alignment = Alignment(horizontal="center")

        # Highlight the translation column in soft pink
        trans_col = 6
        for row in ws.iter_rows(min_row=2, min_col=trans_col, max_col=trans_col):
            for cell in row:
                cell.fill      = trans_fill
                cell.alignment = Alignment(horizontal="right")

        column_widths = [20, 8, 15, 50, 40, 40, 25]
        for i, width in enumerate(column_widths, 1):
            ws.column_dimensions[chr(64 + i)].width = width

    print(f"✅ Translation file ready: {output_file}")
    print(f"   {len(unique)} unique strings after deduplication (from {len(strings)} total matches)")


def process_project_folder(folder_path: str, output_file: str = "translation_strings.xlsx"):
    """
    Processes an entire project folder — handles .html, .js, and .py files in one pass.
    """
    all_strings = []
    folder = Path(folder_path)

    for file_path in folder.rglob("*"):
        if file_path.suffix == ".html":
            all_strings.extend(extract_from_html(str(file_path)))
        elif file_path.suffix == ".js":
            all_strings.extend(extract_from_javascript(str(file_path)))
        elif file_path.suffix == ".py":
            all_strings.extend(extract_from_python(str(file_path)))

    print(f"🔍 Scanned {folder_path}: found {len(all_strings)} translatable strings")
    export_to_excel(all_strings, output_file)


# Run on a project folder
process_project_folder("./my_project/", "translation_strings.xlsx")

The translator receives a clean, structured Excel file that shows the full context of every string — which file it came from, which line, and what surrounds it — then fills in the Arabic translation column and returns the file. The developer can then inject translations back into the codebase automatically.

What used to take hours of manual sifting through hundreds of lines of code now takes seconds — with zero risk of accidentally breaking the codebase or missing a string.

The Real Lesson: The Best Tool Is the One That Solves Your Problem

All three tools in this article share one thing: each was born from a real, recurring pain point. The proposal generator from the frustration of rewriting the same document structure every week. The localization checker from discovering UI overflow issues after delivery. The string extractor from hours lost manually hunting translatable text in code.

Every professional in any field knows ten repetitive manual problems that nobody has built a tool for yet — because nobody else understands those problems deeply enough. The Python skills you’ve built throughout this series let you turn that insider knowledge into a tool you can sell to a hundred other professionals who share the same pain.

Python code displayed on screen in an exciting way


Wrap-Up & What’s Next

Today we built three practical tools: an AI-powered PDF proposal generator combining OpenAI with ReportLab, a UI localization QA checker that protects translators from post-delivery surprises, and a multi-format string extractor that turns full codebases into translator-ready Excel files. Each tool is independently sellable as a service or bundled as part of a broader offering.

Recommended Next Step:

We’re approaching the end of this series. In the final article we take a step back and survey the eight most useful Python libraries every freelancer should know in 2026 — not as theory, but grounded in the practical scenarios we’ve covered throughout this series.

Continue with Article 16: The 8 Most Useful Python Libraries Every Freelancer Should Know in 2026.


References & Further Reading:

  1. ReportLab PDF generation library: ReportLab Documentation
  2. BeautifulSoup HTML parsing library: BeautifulSoup4 Documentation
  3. GNU gettext PO file format for localization: GNU gettext — PO File Format
  4. W3C guide on strings and bidirectional text: W3C — Strings and Bidi

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *