#!/usr/bin/env python3 """Export static web and desktop labels from the application's Fluent catalog.""" import argparse import html import json import re from pathlib import Path import xml.etree.ElementTree as ET ROOT = Path(__file__).resolve().parents[1] XML_LANG = '{http://www.w3.org/XML/1998/namespace}lang' KEYS = { 'title': 'common.open-cad-studio-web-app', 'loading': 'common.loading', 'comment': 'common.a-cad-application-for-2d-3d-drawing-and-design', 'summary': 'common.value-2d-3d-cad-application-for-dwg-and-dxf-drawings', 'description': 'common.create-and-edit-technical-drawings-in-dwg-and-dxf-formats', 'drawing': 'common.drawing', 'design': 'common.design', 'engineering': 'common.engineering', 'Launch': 'common.launch-open-cad-studio', 'InstallerDescription': 'common.open-cad-studio-installer', 'Installer': 'common.installer', 'DwgDrawing': 'common.dwg-drawing', 'DxfDrawing': 'common.dxf-drawing', 'DesktopShortcut': 'common.desktop-shortcut', 'DesktopShortcutDescription': 'common.add-a-shortcut-to-open-cad-studio-on-the-desktop', 'DowngradeError': 'common.newer-version-already-installed', 'Open': 'common.open', 'Desktop': 'common.desktop', } def labels(path): # Only static attributes are exported; the app resolves dynamic Fluent messages. catalog = {} group = key = None for line in path.read_text().splitlines(): if match := re.match(r'^([A-Za-z][\w-]*)\s*=', line): group, key = match[1], None elif match := re.match(r'^ \.([A-Za-z][\w-]*)\s*=\s*(.*)', line): key = f'{group}.{match[1]}' catalog[key] = match[2] elif key and line.startswith(' '): catalog[key] += '\n' + line.strip() def resolve(key, seen=()): if key in seen: raise ValueError(f'{path}: cyclic reference {key}') text = re.sub(r'\{\s*([A-Za-z][\w-]*\.[A-Za-z][\w-]*)\s*\}', lambda match: resolve(match[1], (*seen, key)), catalog[key]) if re.search(r'[{}]|__ocs_', text): raise ValueError(f'{path}: {key} is not a static label') return text return {name: resolve(key) for name, key in KEYS.items()} def outputs(): localized = {path.parent.name: labels(path) for path in sorted((ROOT / 'locales').glob('*/opencadstudio.ftl'))} english = localized['en-US'] yield ROOT / 'web/locale-labels.json', json.dumps( {locale: {key: text[key] for key in ('title', 'loading')} for locale, text in localized.items()}, ensure_ascii=False, indent=2) + '\n' page = ROOT / 'web-app.html' content = re.sub(r'[^<]*', f'{html.escape(english["title"])}', page.read_text()) content = re.sub(r'(
]*>)[^<]*(
)', lambda match: match[1] + html.escape(english['loading']) + match[2], content) yield page, content desktop = ROOT / 'packaging/OpenCADStudio.desktop' lines = [line for line in desktop.read_text().splitlines() if not re.match(r'^(Comment|Keywords)(\[|=)', line)] for locale, text in localized.items(): # Generic language fallbacks cover regional variants; Chinese has two scripts. suffix = '' if locale == 'en-US' else '[' + ( locale.replace('-', '_') if locale.startswith('zh-') else locale.split('-')[0]) + ']' lines.append(f'Comment{suffix}={text["comment"]}') lines.append(f'Keywords{suffix}=CAD;DWG;DXF;{text["drawing"]};{text["design"]};{text["engineering"]};2D;3D;') yield desktop, '\n'.join(lines) + '\n' metainfo = ROOT / 'packaging/io.github.HakanSeven12.OpenCadStudio.metainfo.xml' component = ET.fromstring(metainfo.read_text()) for child in list(component): if child.tag in ('summary', 'description', 'keywords'): component.remove(child) index = list(component).index(component.find('name')) + 1 description = ET.Element('description') keywords = ET.Element('keywords') for word in ('CAD', 'DWG', 'DXF'): ET.SubElement(keywords, 'keyword').text = word for locale, text in localized.items(): attributes = {} if locale == 'en-US' else {XML_LANG: locale} summary = ET.Element('summary', attributes) summary.text = text['summary'] component.insert(index, summary) index += 1 ET.SubElement(description, 'p', attributes).text = text['description'] for key in ('drawing', 'design', 'engineering'): ET.SubElement(keywords, 'keyword', attributes).text = text[key] component.insert(index, description) component.insert(index + 1, keywords) ET.indent(component, space=' ') yield metainfo, '\n' + ET.tostring(component, encoding='unicode') + '\n' # The release MSI uses the existing en-US culture. installer = ET.Element('WixLocalization', { 'xmlns': 'http://schemas.microsoft.com/wix/2006/localization', 'Culture': 'en-US', 'Codepage': '1252', }) for key, text in english.items(): if key[0].isupper(): ET.SubElement(installer, 'String', {'Id': key}).text = text ET.SubElement(installer, 'String', {'Id': 'ApplicationDescription'}).text = english['summary'] ET.indent(installer, space=' ') yield ROOT / 'packaging/windows/strings.en-US.wxl', '\n' + ET.tostring(installer, encoding='unicode') + '\n' def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--check', action='store_true', help='Fail if generated labels need updating.') args = parser.parse_args() stale = [] for path, content in outputs(): if path.exists() and path.read_text() == content: continue if args.check: stale.append(str(path.relative_to(ROOT))) else: path.write_text(content) if stale: parser.exit(1, 'Run python3 scripts/export-locales.py: ' + ', '.join(stale) + '\n') if __name__ == '__main__': main()