#!/usr/bin/env python3
"""Build the static website using the application's supported locales."""
import hashlib
import json
import re
import shutil
import sys
from html import escape
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
BASE = "https://www.opencadstudio.com"
REPO = "https://github.com/HakanSeven12/OpenCADStudio"
def locale_path(locale):
return f"/{locale}/"
def load_catalogs():
catalogs = {p.stem: json.loads(p.read_text()) for p in sorted((ROOT / "site/locales").glob("*.json"))}
supported = {p.parent.name for p in (ROOT / "locales").glob("*/opencadstudio.ftl")}
if catalogs.keys() != supported:
raise ValueError(f"Website locale mismatch: {catalogs.keys() ^ supported}")
source = catalogs["en-US"]
for locale, messages in catalogs.items():
if messages.keys() != source.keys():
raise ValueError(f"{locale}: translation key mismatch: {messages.keys() ^ source.keys()}")
for key, value in messages.items():
if not isinstance(value, str) or not value.strip():
raise ValueError(f"{locale}: empty translation for {key}")
if set(re.findall(r"\{\w+\}", value)) != set(re.findall(r"\{\w+\}", source[key])):
raise ValueError(f"{locale}: placeholder mismatch for {key}")
return catalogs
def build(output):
catalogs = load_catalogs()
template = (ROOT / "index.html").read_text()
(output / "assets").mkdir(parents=True, exist_ok=True)
def asset(source):
fingerprint = hashlib.sha256(source.read_bytes()).hexdigest()[:12]
path = f"/assets/{source.stem}-{fingerprint}{source.suffix}"
shutil.copyfile(source, output / path.lstrip("/"))
return path
logo = asset(ROOT / "assets/logo.svg")
workspace = asset(ROOT / "site/workspace.png")
modeling = asset(ROOT / "site/modeling.png")
shutil.copyfile(ROOT / "assets/logo.svg", output / "assets/logo.svg")
for filename in ("favicon.ico", "favicon.svg", "favicon.png"):
shutil.copyfile(ROOT / "site" / filename, output / filename)
icons = ('\n'
'\n'
'\n'
'')
manifest = json.loads((ROOT / "site/site.webmanifest").read_text())
manifest["icons"] = [{"src": "/favicon.svg", "sizes": "any", "type": "image/svg+xml"}]
for size in (192, 512):
manifest["icons"].append({"src": asset(ROOT / f"site/icon-{size}.png"), "sizes": f"{size}x{size}", "type": "image/png"})
shutil.copyfile(ROOT / "site/apple-touch-icon.png", output / "apple-touch-icon.png")
css_version = hashlib.sha256((ROOT / "site/site.css").read_bytes()).hexdigest()[:12]
script = "const SITE_LOCALES = " + json.dumps(list(catalogs)) + ";\n" + (ROOT / "site/site.js").read_text()
(output / "site.js").write_text(script, encoding="utf-8")
js_version = hashlib.sha256(script.encode()).hexdigest()[:12]
alternates = '\n'.join(f'' for lang in catalogs)
alternates += f'\n'
for locale, messages in catalogs.items():
path = locale_path(locale)
destination = output / path.lstrip("/")
destination.mkdir(parents=True, exist_ok=True)
language_links = '\n'.join(
f'{escape(labels["name"])}' for lang, labels in catalogs.items()
)
schema = {
"@context": "https://schema.org", "@type": "SoftwareApplication",
"name": "Open CAD Studio", "url": BASE + path, "inLanguage": locale,
"description": messages["description"], "applicationCategory": "DesignApplication",
"operatingSystem": "Windows, Linux, macOS, Web", "isAccessibleForFree": True,
"license": "https://www.gnu.org/licenses/gpl-3.0.html", "codeRepository": REPO,
"downloadUrl": REPO + "/releases/latest", "installUrl": BASE + "/app/",
"offers": {"@type": "Offer", "price": "0", "priceCurrency": "USD"},
}
values = {key: escape(value) for key, value in messages.items()}
values.update(locale=locale, direction="rtl" if locale == "ar-SA" else "ltr",
path=path, url=BASE + path, og_locale=locale.replace("-", "_"),
logo=logo, workspace=workspace, modeling=modeling, icons=icons,
css_version=css_version, js_version=js_version, alternates=alternates,
languages=escape(messages["languages"].format(count=len(catalogs))),
language_links=language_links,
schema=json.dumps(schema, ensure_ascii=False).replace("<", "\\u003c"))
rendered = re.sub(r"\{\{(\w+)\}\}", lambda match: values[match[1]], template)
(destination / "index.html").write_text(rendered, encoding="utf-8")
localized_manifest = {**manifest, "lang": locale, "dir": values["direction"],
"description": messages["description"], "start_url": path,
"shortcuts": [{"name": messages["launch"], "url": "/app/"}]}
(destination / "site.webmanifest").write_text(json.dumps(localized_manifest, ensure_ascii=False, indent=2) + '\n', encoding="utf-8")
for filename in ("index.html", "site.webmanifest"):
shutil.copyfile(output / "en-US" / filename, output / filename)
urls = f'{BASE}/\n' + ''.join(f'{BASE}{locale_path(locale)}\n' for locale in catalogs)
(output / "sitemap.xml").write_text('\n\n' + urls + '\n')
app = output / "app/index.html"
if app.exists():
html = re.sub(r']*\brel=["\'](?:(?:shortcut )?icon|apple-touch-icon)["\'])[^>]*>', '', app.read_text())
app.write_text(html.replace('', icons + '\n'), encoding="utf-8")
print(f"Built {len(catalogs)} website languages and current application icons")
if __name__ == "__main__":
build(Path(sys.argv[1] if len(sys.argv) > 1 else "dist"))