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.
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.
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:
- ReportLab PDF generation library: ReportLab Documentation
- BeautifulSoup HTML parsing library: BeautifulSoup4 Documentation
- GNU gettext PO file format for localization: GNU gettext — PO File Format
- W3C guide on strings and bidirectional text: W3C — Strings and Bidi
Freelancer Skill Development Series 2026
Python for Freelancers — 16 Articles
Series Python for Freelancers — 16 Articles | Zy Yazan


