Complete application localization across all supported languages

This commit is contained in:
Hakan Seven committed 2026-09-07 01:05:02 +03:00
1 parent 96c794c072
commit d66e89ca19
78 files changed
+26043 -877

No files matched your search

+1 -1
View File
@@ -270,7 +270,7 @@ jobs:
if ($LASTEXITCODE -ne 0) { throw "candle failed" }
$mainObj = Join-Path $objDir "main.wixobj"
$uiObj = Join-Path $objDir "ui.wixobj"
& $light $mainObj $uiObj -ext WixUIExtension -out $msiPath
& $light $mainObj $uiObj -ext WixUIExtension -cultures:en-us -loc packaging/windows/strings.en-US.wxl -out $msiPath
if ($LASTEXITCODE -ne 0) { throw "light failed" }
- name: Sign MSI installer (Azure Trusted Signing)
Generated
+1
View File
@@ -18,6 +18,7 @@ dependencies = [
"dwg-thumbnailer",
"env_logger",
"flate2",
"fluent-syntax",
"fontdb",
"getrandom 0.3.4",
"glam",
+2 -1
View File
@@ -28,7 +28,7 @@ iced_aw = { git = "https://github.com/HakanSeven12/iced_aw.git", branch = "agent
bytemuck = { version = "1.25", features = ["derive"] }
glam = { version = "0.33", features = ["bytemuck"] }
rfd = "0.17"
clap = { version = "4", features = ["derive"] }
clap = { version = "4", features = ["derive", "string"] }
env_logger = "0.11"
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "5b56571a190e7a17c8f12d36390d2938b0fb72f7", features = ["serde"] }
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "6a5e6fc2822f6af6b2611cd605b44437b35158f0", features = ["acis", "offset"] }
@@ -51,6 +51,7 @@ cosmic-text = "0.15"
lyon_tessellation = "1.0.20"
[dev-dependencies]
fluent-syntax = "0.12"
naga = { version = "27", features = ["wgsl-in"] }
[target.'cfg(target_os = "windows")'.dependencies]
+5
View File
@@ -209,6 +209,11 @@ Bug reports, focused pull requests, translations, documentation improvements, an
- Use [Discussions](https://github.com/HakanSeven12/OpenCADStudio/discussions) for questions and ideas.
- Report vulnerabilities privately by following the [security policy](SECURITY.md).
Application translations live in `locales/*/opencadstudio.ftl`; source labels map through
`src/locale_catalog.rs`. After editing translations, run `python3 scripts/export-locales.py`
to refresh web and desktop packaging labels. Validate with `python3 scripts/test_site.py`
and `cargo test --lib i18n::tests`.
## Project growth
### Stars
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+42 -2
View File
@@ -1,11 +1,51 @@
[Desktop Entry]
Name=Open CAD Studio
Comment=A CAD application for 2D/3D drawing and design
Exec=OpenCADStudio %F
Icon=io.github.HakanSeven12.OpenCadStudio
Terminal=false
Type=Application
Categories=Graphics;Engineering;
MimeType=image/vnd.dwg;image/vnd.dxf;
Keywords=CAD;DWG;DXF;drawing;design;engineering;2D;3D;
StartupWMClass=io.github.HakanSeven12.OpenCadStudio
Comment[ar]=تطبيق CAD للرسم والتصميم ثنائي وثلاثي الأبعاد
Keywords[ar]=CAD;DWG;DXF;الرسم;تصميم;هندسي;2D;3D;
Comment[bg]=CAD приложение за 2D/3D чертане и проектиране
Keywords[bg]=CAD;DWG;DXF;Чертеж;Проектиране;Инженерен;2D;3D;
Comment[cs]=Aplikace CAD pro 2D/3D kreslení a návrh
Keywords[cs]=CAD;DWG;DXF;Kreslení;Návrh;Strojírenství;2D;3D;
Comment[de]=Eine CAD-Anwendung für 2D/3D-Zeichnung und Konstruktion
Keywords[de]=CAD;DWG;DXF;Zeichnung;Konstruktion;Technisch;2D;3D;
Comment[el]=Εφαρμογή CAD για σχεδίαση και σχεδιασμό 2D/3D
Keywords[el]=CAD;DWG;DXF;Σχέδιο;Σχεδιασμός;Μηχανολογικές;2D;3D;
Comment=A CAD application for 2D/3D drawing and design
Keywords=CAD;DWG;DXF;Drawing;Design;Engineering;2D;3D;
Comment[es]=Una aplicación CAD para dibujo y diseño 2D/3D
Keywords[es]=CAD;DWG;DXF;Dibujo;Diseño;Ingeniería;2D;3D;
Comment[fi]=CAD-sovellus 2D/3D-piirtämiseen ja suunnitteluun
Keywords[fi]=CAD;DWG;DXF;Piirustus;Suunnittelu;Tekniikka;2D;3D;
Comment[fr]=Une application CAO pour le dessin et la conception 2D/3D
Keywords[fr]=CAD;DWG;DXF;Dessin;Conception;Génie mécanique;2D;3D;
Comment[hi]=2D/3D आरेखन और डिज़ाइन के लिए CAD अनुप्रयोग
Keywords[hi]=CAD;DWG;DXF;ड्रॉइंग;डिज़ाइन;इंजीनियरिंग;2D;3D;
Comment[hu]=CAD-alkalmazás 2D/3D rajzoláshoz és tervezéshez
Keywords[hu]=CAD;DWG;DXF;Rajzolás;Tervezés;Mérnöki;2D;3D;
Comment[it]=Un’applicazione CAD per disegno e progettazione 2D/3D
Keywords[it]=CAD;DWG;DXF;Disegno;Progettazione;Ingegneria;2D;3D;
Comment[ja]=2D/3Dの作図と設計のためのCADアプリ
Keywords[ja]=CAD;DWG;DXF;図面;設計;工学表記;2D;3D;
Comment[ko]=2D/3D 제도 및 설계를 위한 CAD 앱
Keywords[ko]=CAD;DWG;DXF;그리기;설계;공학;2D;3D;
Comment[nl]=Een CAD-toepassing voor 2D/3D-tekenen en ontwerpen
Keywords[nl]=CAD;DWG;DXF;Tekening;Ontwerp;Technisch;2D;3D;
Comment[pl]=Aplikacja CAD do rysowania i projektowania 2D/3D
Keywords[pl]=CAD;DWG;DXF;Rysunek;Projektowanie;Inżynieria;2D;3D;
Comment[pt]=Um aplicativo CAD para desenho e projeto 2D/3D
Keywords[pt]=CAD;DWG;DXF;Desenho;Projeto;Engenharia;2D;3D;
Comment[ru]=CAD-приложение для 2D/3D-черчения и проектирования
Keywords[ru]=CAD;DWG;DXF;Чертёж;Проектирование;Инженерный;2D;3D;
Comment[tr]=2D/3D çizim ve tasarım için bir CAD uygulaması
Keywords[tr]=CAD;DWG;DXF;Çizim;Tasarım;Mühendislik;2D;3D;
Comment[zh_CN]=用于 2D/3D 绘图和设计的 CAD 应用程序
Keywords[zh_CN]=CAD;DWG;DXF;绘图;设计;工程学;2D;3D;
Comment[zh_TW]=用於 2D/3D 製圖與設計的 CAD 應用程式
Keywords[zh_TW]=CAD;DWG;DXF;繪圖;設計;工程學;2D;3D;
@@ -1,48 +1,132 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- AppStream metadata: integrates the AppImage with Linux software centres
(name, summary, icon, categories, screenshots). Installed into
/usr/share/metainfo/ inside the AppDir by the release workflow. -->
<component type="desktop-application">
<id>io.github.HakanSeven12.OpenCadStudio</id>
<metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0-or-later</project_license>
<name>Open CAD Studio</name>
<summary xml:lang="ar-SA">تطبيق CAD ثنائي وثلاثي الأبعاد لرسومات DWG وDXF</summary>
<summary xml:lang="bg-BG">2D/3D CAD приложение за DWG и DXF чертежи</summary>
<summary xml:lang="cs-CZ">2D/3D CAD aplikace pro výkresy DWG a DXF</summary>
<summary xml:lang="de-DE">2D/3D-CAD-Anwendung für DWG- und DXF-Zeichnungen</summary>
<summary xml:lang="el-GR">Εφαρμογή CAD 2D/3D για σχέδια DWG και DXF</summary>
<summary>2D/3D CAD application for DWG and DXF drawings</summary>
<summary xml:lang="es-ES">Aplicación CAD 2D/3D para dibujos DWG y DXF</summary>
<summary xml:lang="fi-FI">2D/3D-CAD-sovellus DWG- ja DXF-piirustuksille</summary>
<summary xml:lang="fr-FR">Application CAO 2D/3D pour les dessins DWG et DXF</summary>
<summary xml:lang="hi-IN">DWG और DXF चित्रों के लिए 2D/3D CAD अनुप्रयोग</summary>
<summary xml:lang="hu-HU">2D/3D CAD-alkalmazás DWG és DXF rajzokhoz</summary>
<summary xml:lang="it-IT">Applicazione CAD 2D/3D per disegni DWG e DXF</summary>
<summary xml:lang="ja-JP">DWGとDXF図面用の2D/3D CADアプリ</summary>
<summary xml:lang="ko-KR">DWG 및 DXF 도면용 2D/3D CAD 앱</summary>
<summary xml:lang="nl-NL">2D/3D-CAD-toepassing voor DWG- en DXF-tekeningen</summary>
<summary xml:lang="pl-PL">Aplikacja CAD 2D/3D do rysunków DWG i DXF</summary>
<summary xml:lang="pt-BR">Aplicativo CAD 2D/3D para desenhos DWG e DXF</summary>
<summary xml:lang="ru-RU">Приложение 2D/3D CAD для чертежей DWG и DXF</summary>
<summary xml:lang="tr-TR">DWG ve DXF çizimleri için 2D/3D CAD uygulaması</summary>
<summary xml:lang="zh-CN">用于 DWG 和 DXF 图形的 2D/3D CAD 应用程序</summary>
<summary xml:lang="zh-TW">用於 DWG 與 DXF 圖面的 2D/3D CAD 應用程式</summary>
<description>
<p>
Open CAD Studio is a 2D/3D CAD application for creating and editing
technical drawings. It reads and writes the standard DWG and DXF file
formats so drawings stay compatible with other CAD software.
</p>
<p xml:lang="ar-SA">أنشئ الرسومات الفنية وحررها بتنسيقي DWG وDXF.</p>
<p xml:lang="bg-BG">Създавайте и редактирайте технически чертежи във формати DWG и DXF.</p>
<p xml:lang="cs-CZ">Vytvářejte a upravujte technické výkresy ve formátech DWG a DXF.</p>
<p xml:lang="de-DE">Technische Zeichnungen in DWG- und DXF-Formaten erstellen und bearbeiten.</p>
<p xml:lang="el-GR">Δημιουργήστε και επεξεργαστείτε τεχνικά σχέδια σε μορφές DWG και DXF.</p>
<p>Create and edit technical drawings in DWG and DXF formats.</p>
<p xml:lang="es-ES">Cree y edite dibujos técnicos en formatos DWG y DXF.</p>
<p xml:lang="fi-FI">Luo ja muokkaa teknisiä piirustuksia DWG- ja DXF-muodoissa.</p>
<p xml:lang="fr-FR">Créez et modifiez des dessins techniques aux formats DWG et DXF.</p>
<p xml:lang="hi-IN">DWG और DXF प्रारूपों में तकनीकी चित्र बनाएँ और संपादित करें।</p>
<p xml:lang="hu-HU">Műszaki rajzok készítése és szerkesztése DWG és DXF formátumban.</p>
<p xml:lang="it-IT">Crea e modifica disegni tecnici nei formati DWG e DXF.</p>
<p xml:lang="ja-JP">DWGおよびDXF形式で技術図面を作成・編集できます。</p>
<p xml:lang="ko-KR">DWG 및 DXF 형식으로 기술 도면을 만들고 편집하세요.</p>
<p xml:lang="nl-NL">Maak en bewerk technische tekeningen in DWG- en DXF-indeling.</p>
<p xml:lang="pl-PL">Twórz i edytuj rysunki techniczne w formatach DWG i DXF.</p>
<p xml:lang="pt-BR">Crie e edite desenhos técnicos nos formatos DWG e DXF.</p>
<p xml:lang="ru-RU">Создавайте и редактируйте технические чертежи в форматах DWG и DXF.</p>
<p xml:lang="tr-TR">DWG ve DXF biçimlerinde teknik çizimler oluşturun ve düzenleyin.</p>
<p xml:lang="zh-CN">创建和编辑 DWG 与 DXF 格式的技术图形。</p>
<p xml:lang="zh-TW">建立及編輯 DWG 與 DXF 格式的技術圖面。</p>
</description>
<launchable type="desktop-id">io.github.HakanSeven12.OpenCadStudio.desktop</launchable>
<categories>
<category>Graphics</category>
<category>Engineering</category>
</categories>
<keywords>
<keyword>CAD</keyword>
<keyword>DWG</keyword>
<keyword>DXF</keyword>
<keyword>drawing</keyword>
<keyword>design</keyword>
<keyword>engineering</keyword>
<keyword xml:lang="ar-SA">الرسم</keyword>
<keyword xml:lang="ar-SA">تصميم</keyword>
<keyword xml:lang="ar-SA">هندسي</keyword>
<keyword xml:lang="bg-BG">Чертеж</keyword>
<keyword xml:lang="bg-BG">Проектиране</keyword>
<keyword xml:lang="bg-BG">Инженерен</keyword>
<keyword xml:lang="cs-CZ">Kreslení</keyword>
<keyword xml:lang="cs-CZ">Návrh</keyword>
<keyword xml:lang="cs-CZ">Strojírenství</keyword>
<keyword xml:lang="de-DE">Zeichnung</keyword>
<keyword xml:lang="de-DE">Konstruktion</keyword>
<keyword xml:lang="de-DE">Technisch</keyword>
<keyword xml:lang="el-GR">Σχέδιο</keyword>
<keyword xml:lang="el-GR">Σχεδιασμός</keyword>
<keyword xml:lang="el-GR">Μηχανολογικές</keyword>
<keyword>Drawing</keyword>
<keyword>Design</keyword>
<keyword>Engineering</keyword>
<keyword xml:lang="es-ES">Dibujo</keyword>
<keyword xml:lang="es-ES">Diseño</keyword>
<keyword xml:lang="es-ES">Ingeniería</keyword>
<keyword xml:lang="fi-FI">Piirustus</keyword>
<keyword xml:lang="fi-FI">Suunnittelu</keyword>
<keyword xml:lang="fi-FI">Tekniikka</keyword>
<keyword xml:lang="fr-FR">Dessin</keyword>
<keyword xml:lang="fr-FR">Conception</keyword>
<keyword xml:lang="fr-FR">Génie mécanique</keyword>
<keyword xml:lang="hi-IN">ड्रॉइंग</keyword>
<keyword xml:lang="hi-IN">डिज़ाइन</keyword>
<keyword xml:lang="hi-IN">इंजीनियरिंग</keyword>
<keyword xml:lang="hu-HU">Rajzolás</keyword>
<keyword xml:lang="hu-HU">Tervezés</keyword>
<keyword xml:lang="hu-HU">Mérnöki</keyword>
<keyword xml:lang="it-IT">Disegno</keyword>
<keyword xml:lang="it-IT">Progettazione</keyword>
<keyword xml:lang="it-IT">Ingegneria</keyword>
<keyword xml:lang="ja-JP">図面</keyword>
<keyword xml:lang="ja-JP">設計</keyword>
<keyword xml:lang="ja-JP">工学表記</keyword>
<keyword xml:lang="ko-KR">그리기</keyword>
<keyword xml:lang="ko-KR">설계</keyword>
<keyword xml:lang="ko-KR">공학</keyword>
<keyword xml:lang="nl-NL">Tekening</keyword>
<keyword xml:lang="nl-NL">Ontwerp</keyword>
<keyword xml:lang="nl-NL">Technisch</keyword>
<keyword xml:lang="pl-PL">Rysunek</keyword>
<keyword xml:lang="pl-PL">Projektowanie</keyword>
<keyword xml:lang="pl-PL">Inżynieria</keyword>
<keyword xml:lang="pt-BR">Desenho</keyword>
<keyword xml:lang="pt-BR">Projeto</keyword>
<keyword xml:lang="pt-BR">Engenharia</keyword>
<keyword xml:lang="ru-RU">Чертёж</keyword>
<keyword xml:lang="ru-RU">Проектирование</keyword>
<keyword xml:lang="ru-RU">Инженерный</keyword>
<keyword xml:lang="tr-TR">Çizim</keyword>
<keyword xml:lang="tr-TR">Tasarım</keyword>
<keyword xml:lang="tr-TR">Mühendislik</keyword>
<keyword xml:lang="zh-CN">绘图</keyword>
<keyword xml:lang="zh-CN">设计</keyword>
<keyword xml:lang="zh-CN">工程学</keyword>
<keyword xml:lang="zh-TW">繪圖</keyword>
<keyword xml:lang="zh-TW">設計</keyword>
<keyword xml:lang="zh-TW">工程學</keyword>
</keywords>
<launchable type="desktop-id">io.github.HakanSeven12.OpenCadStudio.desktop</launchable>
<categories>
<category>Graphics</category>
<category>Engineering</category>
</categories>
<url type="homepage">https://github.com/HakanSeven12/OpenCADStudio</url>
<url type="bugtracker">https://github.com/HakanSeven12/OpenCADStudio/issues</url>
<developer id="io.github.hakanseven12">
<name>Hakan Seven</name>
</developer>
<content_rating type="oars-1.1" />
<provides>
<mediatype>image/vnd.dwg</mediatype>
<mediatype>image/vnd.dxf</mediatype>
+15 -90
View File
@@ -1,26 +1,4 @@
<?xml version='1.0' encoding='windows-1252'?>
<!--
WiX source for the Windows MSI installer.
Build (CI / locally with WiX Toolset 3.x in PATH):
candle.exe -arch x64 -dVersion=<x.y.z> -dSource=target\release\OpenCADStudio.exe \
-dIcon=packaging\windows\AppIcon.ico packaging\windows\main.wxs \
-out packaging\windows\main.wixobj
light.exe packaging\windows\main.wixobj -ext WixUIExtension -out OpenCADStudio.msi
Provides:
* Per-machine install under Program Files\Open CAD Studio.
* Start Menu shortcut under Programs\Open CAD Studio.
* Desktop shortcut.
* File associations for .dwg and .dxf via a single ProgID; both
route through `OpenCADStudio.exe "%1"` so the OS hands the file
path to the app as argv[1] (consumed in OpenCADStudio::boot).
* Default Programs registration (Capabilities + RegisteredApplications)
so the app shows up in Settings > Default Apps and can be set as the
default handler for .dwg / .dxf.
* Standard Add/Remove Programs entry with icon.
* MajorUpgrade — installing a newer MSI replaces the old version.
-->
<?xml version='1.0' encoding='UTF-8'?>
<Wix xmlns='http://schemas.microsoft.com/wix/2006/wi'>
<Product
Id='*'
@@ -33,8 +11,8 @@
<Package
Id='*'
Keywords='Installer'
Description='Open CAD Studio Installer'
Keywords='!(loc.Installer)'
Description='!(loc.InstallerDescription)'
Manufacturer='Hakan Seven'
InstallerVersion='450'
Languages='1033'
@@ -44,7 +22,7 @@
<MajorUpgrade
Schedule='afterInstallInitialize'
DowngradeErrorMessage='A newer version of [ProductName] is already installed. Setup will now exit.' />
DowngradeErrorMessage='!(loc.DowngradeError)' />
<Media Id='1' Cabinet='product.cab' EmbedCab='yes' />
@@ -54,12 +32,7 @@
<Property Id='ARPURLINFOABOUT' Value='https://github.com/HakanSeven12/OpenCADStudio' />
<Property Id='ARPURLUPDATEINFO' Value='https://github.com/HakanSeven12/OpenCADStudio/releases' />
<Property Id='ARPCONTACT' Value='Hakan Seven' />
<Property Id='ARPCOMMENTS' Value='2D/3D CAD application for DWG and DXF drawings.' />
<!--
Hide "Repair" from the Apps & Features entry — there is no repair flow.
"Modify" is deliberately left enabled: WixUI_FeatureTree supports it, so
users can add/remove the optional Desktop shortcut feature later.
-->
<Property Id='ARPCOMMENTS' Value='!(loc.ApplicationDescription)' />
<Property Id='ARPNOREPAIR' Value='1' />
<Directory Id='TARGETDIR' Name='SourceDir'>
@@ -77,70 +50,40 @@
Id='StartMenuShortcut'
Directory='ProgramMenuDir'
Name='Open CAD Studio'
Description='Launch Open CAD Studio'
Description='!(loc.Launch)'
WorkingDirectory='APPLICATIONROOTDIRECTORY'
Icon='AppIcon.ico'
IconIndex='0'
Advertise='yes' />
</File>
<!-- DWG/DXF file-type icons, generated from the mimetype SVGs
(CI: magick image-vnd.{dwg,dxf}.svg -> {dwg,dxf}.ico). -->
<File Id='DwgIco' Name='dwg.ico' DiskId='1' Source='packaging\windows\dwg.ico' />
<File Id='DxfIco' Name='dxf.ico' DiskId='1' Source='packaging\windows\dxf.ico' />
<!-- DWG thumbnail provider (IThumbnailProvider). Ships beside the
exe; OpenCADStudio self-registers it under HKCU on startup
(see file_association::install_thumbnailer). -->
<File
Id='DwgThumbDll'
Name='dwg_thumbnailer_win.dll'
DiskId='1'
Source='target\release\dwg_thumbnailer_win.dll' />
<!--
File associations. Two ProgIDs (one per extension) so the
`<Verb Id='open'>` rows don't collide on the same registry
primary key — both ProgIDs invoke the same exe with the
file path as `argv[1]`.
-->
<ProgId
Id='OpenCADStudio.DWG'
Description='DWG Drawing'
Description='!(loc.DwgDrawing)'
Icon='DwgIco'
IconIndex='0'>
<Extension Id='dwg' ContentType='image/vnd.dwg'>
<Verb Id='open' Command='Open' TargetFile='OpenCADStudioEXE' Argument='"%1"' />
<Verb Id='open' Command='!(loc.Open)' TargetFile='OpenCADStudioEXE' Argument='"%1"' />
</Extension>
</ProgId>
<ProgId
Id='OpenCADStudio.DXF'
Description='DXF Drawing'
Description='!(loc.DxfDrawing)'
Icon='DxfIco'
IconIndex='0'>
<Extension Id='dxf' ContentType='image/vnd.dxf'>
<Verb Id='open' Command='Open' TargetFile='OpenCADStudioEXE' Argument='"%1"' />
<Verb Id='open' Command='!(loc.Open)' TargetFile='OpenCADStudioEXE' Argument='"%1"' />
</Extension>
</ProgId>
</Component>
<!--
Default Programs registration. The ProgId/Extension rows above
register the handlers, but modern Windows (8+) guards the actual
default via a per-user UserChoice hash an installer can't forge.
Publishing Capabilities + a RegisteredApplications entry is the
supported way in: it makes Open CAD Studio appear in Settings >
Default Apps and the "Open with" list, so .dwg / .dxf can be set
to it. The keys are HKLM-wide, so the shell picks them up on its
next association query without a forced refresh.
-->
<Component Id='DefaultPrograms' Guid='6f3e9b2a-1c4d-4f8a-9b7e-2d5a8c1f0e63' Win64='yes'>
<!--
Applications\<exename>\DefaultIcon is what Windows shows in the
"Open with" context-menu list and "Choose another app" picker.
Without it the entry appears with a blank generic document icon.
-->
<RegistryValue
Root='HKLM'
Key='SOFTWARE\Classes\Applications\OpenCADStudio.exe\DefaultIcon'
@@ -181,7 +124,7 @@
Key='Software\Open CAD Studio\Capabilities'
Name='ApplicationDescription'
Type='string'
Value='2D/3D CAD application for DWG and DXF drawings.' />
Value='!(loc.ApplicationDescription)' />
<RegistryValue
Root='HKLM'
Key='Software\Open CAD Studio\Capabilities\FileAssociations'
@@ -219,18 +162,12 @@
</Component>
</Directory>
</Directory>
<!--
Desktop shortcut. A non-advertised shortcut with an explicit Target;
the HKCU registry value is the component KeyPath (a shortcut on its own
cannot be a KeyPath for a per-user resource).
-->
<Directory Id='DesktopFolder' Name='Desktop'>
<Directory Id='DesktopFolder' Name='!(loc.Desktop)'>
<Component Id='DesktopShortcutComp' Guid='c5c7f1c4-3d8a-4efd-b049-4662d9143dcd'>
<Shortcut
Id='DesktopShortcut'
Name='Open CAD Studio'
Description='Launch Open CAD Studio'
Description='!(loc.Launch)'
Target='[APPLICATIONROOTDIRECTORY]OpenCADStudio.exe'
WorkingDirectory='APPLICATIONROOTDIRECTORY'
Icon='AppIcon.ico'
@@ -250,25 +187,15 @@
<ComponentRef Id='MainExecutable' />
<ComponentRef Id='DefaultPrograms' />
<ComponentRef Id='ProgramMenuDirComp' />
<!-- Optional, user-deselectable in the feature tree (default on). -->
<Feature
Id='DesktopShortcut'
Title='Desktop shortcut'
Description='Add a shortcut to Open CAD Studio on the desktop.'
Title='!(loc.DesktopShortcut)'
Description='!(loc.DesktopShortcutDescription)'
Level='1'
Absent='allow'>
<ComponentRef Id='DesktopShortcutComp' />
</Feature>
</Feature>
<!--
Refresh the shell after install so the new .dwg / .dxf file-type icons
and associations appear immediately instead of only after a reboot or an
icon-cache rebuild. `ie4uinit -show` rebuilds the per-user icon cache.
Immediate execution keeps property access ([SystemFolder]); Return='ignore'
means it can never fail the install, and the NOT REMOVE condition skips it
on uninstall.
-->
<CustomAction
Id='RefreshShellIcons'
Directory='APPLICATIONROOTDIRECTORY'
@@ -278,8 +205,6 @@
<InstallExecuteSequence>
<Custom Action='RefreshShellIcons' After='InstallFinalize'>NOT REMOVE</Custom>
</InstallExecuteSequence>
<!-- Installer UI + finish-screen launch checkbox (see ui.wxs). -->
<UIRef Id='OpenCADStudioUI' />
</Product>
+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<WixLocalization xmlns="http://schemas.microsoft.com/wix/2006/localization" Culture="en-US" Codepage="1252">
<String Id="Launch">Launch Open CAD Studio</String>
<String Id="InstallerDescription">Open CAD Studio Installer</String>
<String Id="Installer">Installer</String>
<String Id="DwgDrawing">DWG Drawing</String>
<String Id="DxfDrawing">DXF Drawing</String>
<String Id="DesktopShortcut">Desktop shortcut</String>
<String Id="DesktopShortcutDescription">Add a shortcut to Open CAD Studio on the desktop.</String>
<String Id="DowngradeError">A newer version of [ProductName] is already installed. Setup will now exit.</String>
<String Id="Open">Open</String>
<String Id="Desktop">Desktop</String>
<String Id="ApplicationDescription">2D/3D CAD application for DWG and DXF drawings</String>
</WixLocalization>
+2 -32
View File
@@ -1,39 +1,9 @@
<?xml version='1.0' encoding='windows-1252'?>
<!--
Installer UI for the Windows MSI, kept separate from the package/feature
definition in main.wxs. Linked in via `<UIRef Id='OpenCADStudioUI' />`
there; both files are compiled together by candle and linked by light
(which needs `-ext WixUIExtension` for the WixUI_Minimal dialog set).
Provides:
* WixUI_FeatureTree wizard (Welcome → License → Features → Progress →
Finish). The Features page lets the user deselect the optional Desktop
shortcut feature (default on).
* A pre-ticked "Launch Open CAD Studio" checkbox on the finish screen
that starts the freshly installed exe when the user clicks Finish.
-->
<?xml version='1.0' encoding='UTF-8'?>
<Wix xmlns='http://schemas.microsoft.com/wix/2006/wi'>
<Fragment>
<!--
Finish-screen "Launch Open CAD Studio" checkbox.
WIXUI_EXITDIALOGOPTIONALCHECKBOX=1 pre-ticks the checkbox; the Publish
below fires the launch action when the user clicks Finish with it checked.
-->
<Property Id='WIXUI_EXITDIALOGOPTIONALCHECKBOXTEXT' Value='Launch Open CAD Studio' />
<Property Id='WIXUI_EXITDIALOGOPTIONALCHECKBOXTEXT' Value='!(loc.Launch)' />
<Property Id='WIXUI_EXITDIALOGOPTIONALCHECKBOX' Value='1' />
<!--
License shown on the WixUI_Minimal EULA page. Without this variable WiX
falls back to its built-in placeholder (Lorem ipsum) RTF. Point it at the
project's GPL-3 text (License.rtf, generated from the repo LICENSE).
-->
<WixVariable Id='WixUILicenseRtf' Value='$(var.License)' />
<!--
Start the installed exe after the wizard closes. FileKey points at the
File defined in main.wxs (light resolves the cross-fragment reference).
asyncNoWait lets the installer exit cleanly while the app starts.
-->
<CustomAction
Id='LaunchApplication'
FileKey='OpenCADStudioEXE'
+140
View File
@@ -0,0 +1,140 @@
#!/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'<title>[^<]*</title>',
f'<title>{html.escape(english["title"])}</title>', page.read_text())
content = re.sub(r'(<div class="sub"[^>]*>)[^<]*(</div>)',
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, '<?xml version="1.0" encoding="UTF-8"?>\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', '<?xml version="1.0" encoding="UTF-8"?>\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()
+38
View File
@@ -0,0 +1,38 @@
"""Check literal translation lookups. Fluent syntax and rendering are tested in i18n.rs."""
import json
from pathlib import Path
import re
ROOT = Path(__file__).resolve().parents[1]
STRING = r'"(?:[^"\\]|\\.)*"'
lookup_text = (ROOT / 'src/locale_catalog.rs').read_text()
lookup = {}
for match in re.finditer(r'(' + STRING + r')\s*=>\s*Some\(\(\s*"([\w-]+)"\s*,\s*"([\w-]+)"\s*,?\s*\)\)', lookup_text):
source = json.loads(match[1])
assert source not in lookup, f'Duplicate source lookup: {source}'
lookup[source] = (match[2], match[3])
assert len(lookup) > 3000
keys = set()
group = None
for line in (ROOT / 'locales/en-US/opencadstudio.ftl').read_text().splitlines():
if match := re.match(r'^([A-Za-z][\w-]*)\s*=', line):
group = match[1]
elif match := re.match(r'^ \.([A-Za-z][\w-]*)\s*=', line):
keys.add((group, match[1]))
assert not set(lookup.values()) - keys, f'Missing Fluent targets: {set(lookup.values()) - keys}'
missing = []
for path in sorted((ROOT / 'src').rglob('*.rs')):
text = path.read_text()
for match in re.finditer(r'\b(?:t|tf)!\(\s*(' + STRING + ')', text, re.S):
# Rust permits escaped line continuations and literal newlines.
literal = re.sub(r'\\\n\s*', '', match[1])
literal = re.sub(r'\\u\{([0-9a-fA-F_]+)\}', lambda m: json.dumps(chr(int(m[1].replace('_', ''), 16)))[1:-1], literal)
source = json.loads(literal, strict=False)
if source not in lookup and re.search(r'[A-Za-z]{2}', source):
line = text[:match.start()].count('\n') + 1
missing.append(f'{path.relative_to(ROOT)}:{line}: {source!r}')
assert not missing, '\n'.join(missing[:30])
print(f'{len(lookup)} source lookups and literal translation calls passed')
+3
View File
@@ -12,6 +12,9 @@ from urllib.parse import urlsplit
import xml.etree.ElementTree as ET
ROOT = Path(__file__).resolve().parents[1]
subprocess.run(['python3', 'scripts/test_locales.py'], cwd=ROOT, check=True)
subprocess.run(['python3', 'scripts/export-locales.py', '--check'], cwd=ROOT, check=True)
subprocess.run(['node', 'scripts/test_web_locales.cjs'], cwd=ROOT, check=True)
class Page(HTMLParser):
+47
View File
@@ -0,0 +1,47 @@
// Run with node scripts/test_web_locales.cjs.
const assert = require('node:assert/strict');
const fs = require('node:fs');
const vm = require('node:vm');
const path = require('node:path');
const root = path.resolve(__dirname, '..');
const page = fs.readFileSync(path.join(root, 'web-app.html'), 'utf8');
const script = [...page.matchAll(/<script>([\s\S]*?)<\/script>/g)].map(match => match[1]).join('\n');
const labels = JSON.parse(fs.readFileSync(path.join(root, 'web/locale-labels.json'), 'utf8'));
async function visit(languages, saved, blocked = false) {
const caption = { textContent: '' };
const splash = { querySelector: () => caption, classList: { add() {} }, remove() {} };
const document = { documentElement: {}, getElementById: () => splash, querySelector: () => ({}) };
vm.runInNewContext(script, {
document, navigator: { languages, language: languages[0] }, Intl,
localStorage: { getItem(key) {
assert.equal(key, 'opencadstudio.settings');
if (blocked) throw new Error('Storage disabled');
return saved;
} },
fetch: async url => { assert.equal(url, 'locale-labels.json'); return { json: async () => labels }; },
setTimeout() {},
});
await new Promise(setImmediate);
return { document, caption };
}
(async () => {
const cases = Object.keys(labels).map(locale => [[locale], undefined, locale]);
cases.push(
[['en-US'], JSON.stringify({ settings: { language: 'tr-TR' } }), 'tr-TR'],
[['fr-CA'], JSON.stringify({ settings: { language: 'system' } }), 'fr-FR'],
[['zh-HK'], undefined, 'zh-TW'], [['zh-Hans-SG'], undefined, 'zh-CN'],
[['sv-SE', 'de-AT'], '{invalid', 'de-DE'], [['sv-SE'], undefined, 'en-US'],
);
for (const [languages, saved, expected] of cases) {
const { document, caption } = await visit(languages, saved);
assert.equal(document.documentElement.lang, expected);
assert.equal(document.documentElement.dir, expected === 'ar-SA' ? 'rtl' : 'ltr');
assert.equal(document.title, labels[expected].title);
assert.equal(caption.textContent, labels[expected].loading);
}
const blocked = await visit(['ja-JP'], undefined, true);
assert.equal(blocked.document.documentElement.lang, 'ja-JP');
console.log('Web loading labels, saved language, regional fallbacks and blocked storage passed');
})().catch(error => { console.error(error); process.exitCode = 1; });
+8 -8
View File
@@ -555,11 +555,11 @@ impl OpenCADStudio {
_ => return None,
};
if add.is_empty() {
self.command_line.push_info(match kw.as_str() {
self.command_line.push_info(crate::t!(match kw.as_str() {
"P" | "PREVIOUS" => "No previous selection set.",
"ALL" => "Nothing to select.",
_ => "No last object.",
});
}).as_ref());
return Some(Task::none());
}
let count = add.len();
@@ -1103,7 +1103,7 @@ impl OpenCADStudio {
});
if !valid {
self.command_line.push_error(
"MVIEW Object: select a closed paper-space circle, ellipse, or polyline.",
crate::t!("MVIEW Object: select a closed paper-space circle, ellipse, or polyline.").as_ref(),
);
if let Some(prompt) =
self.tabs[i].active_cmd.as_ref().map(|command| command.prompt())
@@ -2140,11 +2140,11 @@ impl OpenCADStudio {
if self.tabs[i].scene.reassociate_center_mark(target, source, point) {
self.tabs[i].dirty = true;
self.command_line.push_output(
"CENTERREASSOCIATE: center mark associated.",
crate::t!("CENTERREASSOCIATE: center mark associated.").as_ref(),
);
} else {
self.command_line.push_error(
"CENTERREASSOCIATE: the selected source is not circular.",
crate::t!("CENTERREASSOCIATE: the selected source is not circular.").as_ref(),
);
}
self.tabs[i].active_cmd = None;
@@ -2414,11 +2414,11 @@ impl OpenCADStudio {
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.restore_pre_cmd_tangent();
self.command_line.push_info(if space_changed {
self.command_line.push_info(crate::t!(if space_changed {
"Command cancelled because the active drawing space changed."
} else {
"Command cancelled."
});
}).as_ref());
}
CmdResult::SelectByPath {
path,
@@ -3200,7 +3200,7 @@ impl OpenCADStudio {
self.tabs[i].scene.clear_preview_wire();
self.restore_pre_cmd_tangent();
self.command_line.push_error(
"JOIN: objects don't form a single connected chain, or contain an unsupported type / tilted arc.",
crate::t!("JOIN: objects don't form a single connected chain, or contain an unsupported type / tilted arc.").as_ref(),
);
}
}
+1 -1
View File
@@ -529,7 +529,7 @@ impl OpenCADStudio {
let sel: Vec<_> = self.tabs[i].scene.selected.iter().copied().collect();
if sel.is_empty() {
self.command_line.push_error(
"WBLOCK Select entities first, or: WBLOCK <block name> or WBLOCK *",
crate::t!("WBLOCK Select entities first, or: WBLOCK <block name> or WBLOCK *").as_ref(),
);
} else {
return Some(Task::done(Message::WblockSave("*".to_string())));
+3 -3
View File
@@ -130,7 +130,7 @@ impl OpenCADStudio {
}
if opt.is_empty() {
self.command_line.push_info(
"Usage: JUSTIFYTEXT <Left|Center|Right|Middle|TL|TC|TR|ML|MC|MR|BL|BC|BR>",
crate::t!("Usage: JUSTIFYTEXT <Left|Center|Right|Middle|TL|TC|TR|ML|MC|MR|BL|BC|BR>").as_ref(),
);
return Some(Task::none());
}
@@ -401,7 +401,7 @@ impl OpenCADStudio {
Some(v) if v > 0.0 => v,
_ => {
self.command_line.push_info(
"Usage: TEXTFIT <target width> (fits selected text to that width)",
crate::t!("Usage: TEXTFIT <target width> (fits selected text to that width)").as_ref(),
);
return Some(Task::none());
}
@@ -1311,7 +1311,7 @@ impl OpenCADStudio {
let text = cmd.strip_prefix("ARCTEXT").unwrap_or("").trim().to_string();
if text.is_empty() {
self.command_line.push_info(
"Usage: ARCTEXT <text> (select an arc first; the text follows it)",
crate::t!("Usage: ARCTEXT <text> (select an arc first; the text follows it)").as_ref(),
);
return None;
}
+16 -16
View File
@@ -429,11 +429,11 @@ impl OpenCADStudio {
self.tabs[i].scene.set_drawing_limit_check(enabled);
self.tabs[i].dirty = true;
}
self.command_line.push_output(if enabled {
self.command_line.push_output(crate::t!(if enabled {
"Limits checking ON."
} else {
"Limits checking OFF."
});
}).as_ref());
}
cmd if cmd.starts_with("LIMITS SET ") => {
let tokens: Vec<&str> = cmd["LIMITS SET ".len()..].split_whitespace().collect();
@@ -492,14 +492,14 @@ impl OpenCADStudio {
"REDRAW" => {
use crate::scene::ViewportRefreshScope;
self.tabs[i].scene.request_refresh(ViewportRefreshScope::Active);
self.command_line.push_output("REDRAW: viewport refreshed.");
self.command_line.push_output(crate::t!("REDRAW: viewport refreshed.").as_ref());
return Some(Task::none());
}
// REDRAWALL — force re-rasterize of every generated viewport.
"REDRAWALL" => {
use crate::scene::ViewportRefreshScope;
self.tabs[i].scene.request_refresh(ViewportRefreshScope::All);
self.command_line.push_output("REDRAWALL: viewports refreshed.");
self.command_line.push_output(crate::t!("REDRAWALL: viewports refreshed.").as_ref());
return Some(Task::none());
}
// REGEN — full model regeneration (bump_geometry: geometry_epoch AND
@@ -509,7 +509,7 @@ impl OpenCADStudio {
// REGENALL is functionally identical (C5).
"REGEN" | "REGENALL" => {
self.tabs[i].scene.bump_geometry();
self.command_line.push_output("REGEN: regenerated model.");
self.command_line.push_output(crate::t!("REGEN: regenerated model.").as_ref());
return Some(Task::none());
}
@@ -877,8 +877,8 @@ impl OpenCADStudio {
let active = self
.active_plot_style
.as_ref()
.map(|t| format!("Active: {}", t.name))
.unwrap_or_else(|| "No plot style loaded.".into());
.map(|t| crate::tf!("Active: {}", t.name).into_owned())
.unwrap_or_else(|| crate::t!("No plot style loaded.").into_owned());
self.command_line.push_info(&active);
return Some(Task::done(Message::PlotStyleLoad));
}
@@ -887,13 +887,13 @@ impl OpenCADStudio {
.active_plot_style
.as_ref()
.map(|t| {
format!(
crate::tf!(
"Plot style: {} ({} color overrides)",
t.name,
t.aci_entries.iter().filter(|e| e.color.is_some()).count()
)
).into_owned()
})
.unwrap_or_else(|| "No plot style table loaded.".into());
.unwrap_or_else(|| crate::t!("No plot style table loaded.").into_owned());
self.command_line.push_output(&msg);
}
_ => {
@@ -1215,7 +1215,7 @@ impl OpenCADStudio {
.push_output(crate::tf!("ADJUST: {action} = {v} on {changed} image(s).").as_ref());
} else {
self.command_line.push_error(
"ADJUST: no raster images selected, or unknown property (use BRIGHTNESS|CONTRAST|FADE).",
crate::t!("ADJUST: no raster images selected, or unknown property (use BRIGHTNESS|CONTRAST|FADE).").as_ref(),
);
}
} else {
@@ -1274,7 +1274,7 @@ impl OpenCADStudio {
match value.parse::<i8>() {
Ok(mode @ -4..=4) => self.annotation_auto_scale = mode,
_ => self.command_line.push_error(
"ANNOAUTOSCALE: enter an integer from -4 through 4.",
crate::t!("ANNOAUTOSCALE: enter an integer from -4 through 4.").as_ref(),
),
}
}
@@ -1461,7 +1461,7 @@ impl OpenCADStudio {
let path = cmd.trim_start_matches("DATALINK").trim();
if path.is_empty() {
self.command_line.push_info(
"Usage: DATALINK <path-to-.csv>",
crate::t!("Usage: DATALINK <path-to-.csv>").as_ref(),
);
return Some(Task::none());
}
@@ -1574,7 +1574,7 @@ impl OpenCADStudio {
}
if jobs.is_empty() {
self.command_line
.push_error("DATALINKUPDATE: no linked tables found.");
.push_error(crate::t!("DATALINKUPDATE: no linked tables found.").as_ref());
return Some(Task::none());
}
if write_back {
@@ -1606,7 +1606,7 @@ impl OpenCADStudio {
.collect();
if updates.is_empty() {
self.command_line
.push_error("DATALINKUPDATE: linked sources could not be read.");
.push_error(crate::t!("DATALINKUPDATE: linked sources could not be read.").as_ref());
return Some(Task::none());
}
self.push_undo_snapshot(i, "DATALINKUPDATE");
@@ -1675,7 +1675,7 @@ impl OpenCADStudio {
let path = cmd.trim_start_matches("LANDXMLIMPORT").trim();
if path.is_empty() {
self.command_line.push_info(
"Usage: LANDXMLIMPORT <path-to-.xml> (imports CgPoint survey points)",
crate::t!("Usage: LANDXMLIMPORT <path-to-.xml> (imports CgPoint survey points)").as_ref(),
);
return Some(Task::none());
}
+8 -8
View File
@@ -526,7 +526,7 @@ impl OpenCADStudio {
self.tabs[i].dirty = true;
}
self.command_line
.push_output(&format!("CENTERRESET: {count} center object(s) updated."));
.push_output(&crate::tf!("CENTERRESET: {count} center object(s) updated."));
}
"CENTERREASSOCIATE" => {
@@ -549,7 +549,7 @@ impl OpenCADStudio {
self.tabs[i].dirty = true;
}
self.command_line
.push_output(&format!("CENTERREASSOCIATE: {count} center object(s) associated."));
.push_output(&crate::tf!("CENTERREASSOCIATE: {count} center object(s) associated."));
}
"CENTERDISASSOCIATE" => {
@@ -561,7 +561,7 @@ impl OpenCADStudio {
self.tabs[i].dirty = true;
}
self.command_line
.push_output(&format!("CENTERDISASSOCIATE: {count} center object(s) detached."));
.push_output(&crate::tf!("CENTERDISASSOCIATE: {count} center object(s) detached."));
}
"DIMCENTER" => {
@@ -1159,7 +1159,7 @@ impl OpenCADStudio {
match parts.get(val_idx).and_then(|s| s.parse::<f64>().ok()) {
Some(v) => return Some(self.solid_section(axis, v)),
None => self.command_line.push_info(
"Usage: SECTION [X|Y|Z] <value> (cross-sections the selected solid)",
crate::t!("Usage: SECTION [X|Y|Z] <value> (cross-sections the selected solid)").as_ref(),
),
}
}
@@ -1185,7 +1185,7 @@ impl OpenCADStudio {
return Some(self.solid_align3d(src, dst));
}
self.command_line.push_info(
"Usage: 3DALIGN <sx1 sy1 sz1 … sx3 sy3 sz3 dx1 dy1 dz1 … dx3 dy3 dz3> (18 numbers: 3 source then 3 destination points)",
crate::t!("Usage: 3DALIGN <sx1 sy1 sz1 … sx3 sy3 sz3 dx1 dy1 dz1 … dx3 dy3 dz3> (18 numbers: 3 source then 3 destination points)").as_ref(),
);
}
@@ -1214,7 +1214,7 @@ impl OpenCADStudio {
Some("Z") => 2,
_ => {
self.command_line.push_info(
"Usage: 3DMIRROR [X|Y|Z] (mirrors the selected solid across that plane)",
crate::t!("Usage: 3DMIRROR [X|Y|Z] (mirrors the selected solid across that plane)").as_ref(),
);
return None;
}
@@ -1254,7 +1254,7 @@ impl OpenCADStudio {
match angle {
Some(a) => return Some(self.solid_rotate3d(axis, a)),
None => self.command_line.push_info(
"Usage: 3DROTATE [X|Y|Z] <angle> (rotates the selected solid)",
crate::t!("Usage: 3DROTATE [X|Y|Z] <angle> (rotates the selected solid)").as_ref(),
),
}
}
@@ -1294,7 +1294,7 @@ impl OpenCADStudio {
match value {
Some(v) => return Some(self.solid_slice(axis, v, keep_low)),
None => self.command_line.push_info(
"Usage: SLICE [X|Y|Z] <value> [TOP|BOTTOM] (cuts the selected solid)",
crate::t!("Usage: SLICE [X|Y|Z] <value> [TOP|BOTTOM] (cuts the selected solid)").as_ref(),
),
}
}
+7 -7
View File
@@ -54,7 +54,7 @@ impl OpenCADStudio {
self.command_line.push_output(crate::tf!(
"SAVEALL: saved {saved} drawing(s){}.",
if skipped > 0 {
format!("; {skipped} need SAVEAS (no file path yet)")
crate::tf!("; {skipped} need SAVEAS (no file path yet)").into_owned()
} else {
String::new()
}
@@ -130,10 +130,10 @@ impl OpenCADStudio {
let style = cmd.split_whitespace().nth(1).unwrap_or("");
let Some(mode) = visual_style::mode_for_keyword(style) else {
self.command_line
.push_error(visual_style::keyword_prompt());
.push_error(crate::t!(visual_style::keyword_prompt()).as_ref());
return Some(Task::none());
};
let label = visual_style::label_for(mode);
let label = crate::t!(visual_style::label_for(mode));
self.command_line
.push_output(crate::tf!("Visual style: {label}.").as_ref());
return Some(Task::done(Message::SetRenderMode(mode)));
@@ -220,11 +220,11 @@ impl OpenCADStudio {
"PERF" => {
self.perf_hud = !self.perf_hud;
crate::perf::set_ui_enabled(self.perf_hud);
self.command_line.push_info(if self.perf_hud {
self.command_line.push_info(crate::t!(if self.perf_hud {
"PERF panel on — tracing render and interaction costs"
} else {
"PERF panel off"
});
}).as_ref());
return Some(Task::none());
}
@@ -299,7 +299,7 @@ impl OpenCADStudio {
rgb[2]
).as_ref());
} else {
self.command_line.push_info("Usage: BACKGROUND DESK <r> <g> <b> | DEFAULT");
self.command_line.push_info(crate::t!("Usage: BACKGROUND DESK <r> <g> <b> | DEFAULT").as_ref());
}
} else if let Some(rgba) = parse_background_color(&args) {
let [r, g, b, _] = rgba;
@@ -322,7 +322,7 @@ impl OpenCADStudio {
).as_ref());
} else {
self.command_line.push_info(
"Usage: BACKGROUND <r> <g> <b> (0–255) | THEME|CLASSIC|DEFAULT|DESK|BLACK|DARKGRAY|GRAY|LIGHTGRAY|WHITE",
crate::t!("Usage: BACKGROUND <r> <g> <b> (0–255) | THEME|CLASSIC|DEFAULT|DESK|BLACK|DARKGRAY|GRAY|LIGHTGRAY|WHITE").as_ref(),
);
}
}
+37 -37
View File
@@ -119,9 +119,9 @@ impl OpenCADStudio {
} else {
use std::collections::BTreeMap;
let space = if scene.current_layout == "Model" {
"Model space".to_string()
crate::t!("Model space").into_owned()
} else {
format!("Paper space '{}'", scene.current_layout)
crate::tf!("Paper space '{}'", scene.current_layout).into_owned()
};
let mut handles: Vec<u64> = Vec::with_capacity(selected.len());
let mut type_counts: BTreeMap<&'static str, usize> = BTreeMap::new();
@@ -139,18 +139,18 @@ impl OpenCADStudio {
let types: Vec<String> =
type_counts.iter().map(|(t, n)| format!("{t}×{n}")).collect();
let list: Vec<String> = handles.iter().map(|h| format!("{:X}", h)).collect();
let mut msg = format!(
let mut msg = crate::tf!(
"SELHANDLES: {} selected in {}\n Types: {}",
handles.len(),
space,
types.join(", ")
);
).into_owned();
if !block_counts.is_empty() {
let blocks: Vec<String> =
block_counts.iter().map(|(b, n)| format!("{b}×{n}")).collect();
msg.push_str(&format!("\n Blocks: {}", blocks.join(", ")));
msg.push_str(&crate::tf!("\n Blocks: {}", blocks.join(", ")).into_owned());
}
msg.push_str(&format!("\n Handles: {}", list.join(",")));
msg.push_str(&crate::tf!("\n Handles: {}", list.join(",")).into_owned());
self.command_line.push_output(&msg);
}
}
@@ -180,10 +180,10 @@ impl OpenCADStudio {
};
let details = entity_list_details(entity);
format!(
"{type_name} Handle:{:X} Layer:{} Color:{} LT:{}{}",
common.handle.value(),
common.layer,
color_str,
"{type_name} {}:{:X} {}:{} {}:{} LT:{}{}",
crate::t!("Handle"), common.handle.value(),
crate::t!("Layer"), common.layer,
crate::t!("Color"), color_str,
linetype,
if details.is_empty() {
String::new()
@@ -1360,7 +1360,7 @@ impl OpenCADStudio {
// Use the first selected object as the template.
let Some(handle) = self.tabs[i].scene.selected.iter().next().copied() else {
self.command_line.push_info(
"ADDSELECTED: select an object first, then run ADDSELECTED to draw a new one like it.",
crate::t!("ADDSELECTED: select an object first, then run ADDSELECTED to draw a new one like it.").as_ref(),
);
return Task::none();
};
@@ -1558,7 +1558,7 @@ fn add_selected_verb(entity: &acadrust::EntityType) -> Option<&'static str> {
fn entity_list_details(entity: &acadrust::EntityType) -> String {
use std::f64::consts::PI;
match entity {
acadrust::EntityType::Line(l) => format!(
acadrust::EntityType::Line(l) => crate::tf!(
"from ({:.4},{:.4},{:.4}) to ({:.4},{:.4},{:.4}) len={:.4}",
l.start.x,
l.start.y,
@@ -1570,16 +1570,16 @@ fn entity_list_details(entity: &acadrust::EntityType) -> String {
+ (l.end.y - l.start.y).powi(2)
+ (l.end.z - l.start.z).powi(2))
.sqrt()
),
acadrust::EntityType::Circle(c) => format!(
).into_owned(),
acadrust::EntityType::Circle(c) => crate::tf!(
"center ({:.4},{:.4},{:.4}) r={:.4} area={:.4}",
c.center.x,
c.center.y,
c.center.z,
c.radius,
PI * c.radius * c.radius
),
acadrust::EntityType::Arc(a) => format!(
).into_owned(),
acadrust::EntityType::Arc(a) => crate::tf!(
"center ({:.4},{:.4},{:.4}) r={:.4} start={:.2}° end={:.2}°",
a.center.x,
a.center.y,
@@ -1587,25 +1587,25 @@ fn entity_list_details(entity: &acadrust::EntityType) -> String {
a.radius,
a.start_angle.to_degrees(),
a.end_angle.to_degrees()
),
acadrust::EntityType::LwPolyline(p) => format!(
).into_owned(),
acadrust::EntityType::LwPolyline(p) => crate::tf!(
"{} vertices closed={} elevation={:.4}",
p.vertices.len(),
p.is_closed,
p.elevation
),
acadrust::EntityType::Text(t) => format!(
).into_owned(),
acadrust::EntityType::Text(t) => crate::tf!(
"\"{}\" h={:.4} at ({:.4},{:.4})",
t.value, t.height, t.insertion_point.x, t.insertion_point.y
),
acadrust::EntityType::MText(t) => format!(
).into_owned(),
acadrust::EntityType::MText(t) => crate::tf!(
"\"{}\" h={:.4} at ({:.4},{:.4})",
t.value.chars().take(40).collect::<String>(),
t.height,
t.insertion_point.x,
t.insertion_point.y
),
acadrust::EntityType::Insert(ins) => format!(
).into_owned(),
acadrust::EntityType::Insert(ins) => crate::tf!(
"block=\"{}\" at ({:.4},{:.4},{:.4}) scale=({:.4},{:.4},{:.4}) rot={:.2}°",
ins.block_name,
ins.insert_point.x,
@@ -1615,20 +1615,20 @@ fn entity_list_details(entity: &acadrust::EntityType) -> String {
ins.y_scale(),
ins.z_scale(),
ins.rotation.to_degrees()
),
acadrust::EntityType::Spline(s) => format!(
).into_owned(),
acadrust::EntityType::Spline(s) => crate::tf!(
"{} ctrl pts degree={} closed={}",
s.control_points.len(),
s.degree,
s.flags.closed
),
acadrust::EntityType::Ellipse(e) => format!(
).into_owned(),
acadrust::EntityType::Ellipse(e) => crate::tf!(
"center ({:.4},{:.4}) major_len={:.4} ratio={:.4}",
e.center.x,
e.center.y,
e.major_axis_length(),
e.minor_axis_ratio
),
).into_owned(),
_ => String::new(),
}
}
@@ -1967,13 +1967,13 @@ fn entity_extra_info(entity: &acadrust::EntityType) -> String {
"BLK={} @({:.3},{:.3})",
e.block_name, e.insert_point.x, e.insert_point.y
),
EntityType::LwPolyline(e) => format!("{} vertices", e.vertices.len()),
EntityType::Polyline(e) => format!("{} vertices", e.vertices.len()),
EntityType::Polyline2D(e) => format!("{} vertices", e.vertices.len()),
EntityType::Polyline3D(e) => format!("{} vertices", e.vertices.len()),
EntityType::LwPolyline(e) => crate::tf!("{} vertices", e.vertices.len()).into_owned(),
EntityType::Polyline(e) => crate::tf!("{} vertices", e.vertices.len()).into_owned(),
EntityType::Polyline2D(e) => crate::tf!("{} vertices", e.vertices.len()).into_owned(),
EntityType::Polyline3D(e) => crate::tf!("{} vertices", e.vertices.len()).into_owned(),
EntityType::Hatch(e) => format!("PAT={}", e.pattern.name),
EntityType::Dimension(e) => format!("{:.3}", e.base().actual_measurement),
EntityType::Spline(e) => format!("{} ctrl pts", e.control_points.len()),
EntityType::Spline(e) => crate::tf!("{} ctrl pts", e.control_points.len()).into_owned(),
_ => String::new(),
}
}
@@ -1997,7 +1997,7 @@ fn arith_eval(expr: &str) -> Result<f64, String> {
};
let v = p.expr()?;
if p.pos != p.chars.len() {
return Err(format!("unexpected '{}'", p.chars[p.pos]));
return Err(crate::tf!("unexpected '{}'", p.chars[p.pos]).into_owned());
}
Ok(v)
}
@@ -2101,7 +2101,7 @@ impl ArithParser {
Ok(v)
}
Some(c) if c.is_ascii_digit() || c == '.' => self.number(),
Some(c) => Err(format!("unexpected '{c}'")),
Some(c) => Err(crate::tf!("unexpected '{c}'").into_owned()),
None => Err("unexpected end of expression".into()),
}
}
@@ -2116,7 +2116,7 @@ impl ArithParser {
}
}
let s: String = self.chars[start..self.pos].iter().collect();
s.parse::<f64>().map_err(|_| format!("bad number '{s}'"))
s.parse::<f64>().map_err(|_| crate::tf!("bad number '{s}'").into_owned())
}
}
+8 -8
View File
@@ -706,10 +706,10 @@ impl OpenCADStudio {
}
})
.map(|s| {
format!(
crate::tf!(
"{} (h_margin:{:.2} v_margin:{:.2})",
s.name, s.horizontal_margin, s.vertical_margin
)
).into_owned()
})
.collect();
if styles.is_empty() {
@@ -909,7 +909,7 @@ impl OpenCADStudio {
.document
.dim_styles
.iter()
.map(|s| format!("{}(txt:{:.2} asz:{:.2})", s.name, s.dimtxt, s.dimasz))
.map(|s| crate::tf!("{}(txt:{:.2} asz:{:.2})", s.name, s.dimtxt, s.dimasz).into_owned())
.collect();
if styles.is_empty() {
self.command_line.push_output(crate::t!("No dim styles defined.").as_ref());
@@ -1019,7 +1019,7 @@ impl OpenCADStudio {
}
_ => {
self.command_line.push_info(
"Usage: DIMSTYLE LIST | NEW <name> | SET <name> <prop> <val>",
crate::t!("Usage: DIMSTYLE LIST | NEW <name> | SET <name> <prop> <val>").as_ref(),
);
}
}
@@ -1043,10 +1043,10 @@ impl OpenCADStudio {
.values()
.filter_map(|o| {
if let ObjectType::MultiLeaderStyle(s) = o {
Some(format!(
Some(crate::tf!(
"{}(txt:{:.2} asz:{:.2})",
s.name, s.text_height, s.arrowhead_size
))
).into_owned())
} else {
None
}
@@ -1206,13 +1206,13 @@ impl OpenCADStudio {
.text_styles
.iter()
.map(|s| {
format!(
crate::tf!(
"{} (font: {}, w: {:.2}, oblique: {:.1}°)",
s.name,
s.font_file,
s.width_factor,
s.oblique_angle.to_degrees()
)
).into_owned()
})
.collect();
if styles.is_empty() {
+5 -16
View File
@@ -91,21 +91,10 @@ impl OpenCADStudio {
.filter(|&(code, _)| code != 0)
.map(|(code, label)| (label, units::short(code), None))
.collect();
// Built once: the table it comes from never changes, and
// `KeywordCommand` wants a `&'static str`.
static PROMPT: std::sync::OnceLock<String> = std::sync::OnceLock::new();
let prompt = PROMPT.get_or_init(|| {
let listed = units::all()
.filter(|&(code, _)| code != 0)
.map(|(code, label)| format!("{label}({})", units::short(code)))
.collect::<Vec<_>>()
.join(" / ");
format!("DWGUNITS convert to [{listed}]:")
});
let current = units::label(self.tabs[i].scene.document.header.insertion_units);
let current = crate::t!(units::label(self.tabs[i].scene.document.header.insertion_units));
self.command_line
.push_info(crate::tf!("DWGUNITS current unit: {current}").as_ref());
let c = KeywordCommand::new("DWGUNITS", prompt.as_str(), choices);
let c = KeywordCommand::new("DWGUNITS", "DWGUNITS Convert drawing to:", choices);
self.command_line.push_info(&c.prompt());
self.tabs[i].active_cmd = Some(Box::new(c));
}
@@ -157,7 +146,7 @@ impl OpenCADStudio {
}
if layer.eq_ignore_ascii_case(&self.tabs[i].active_layer) {
self.command_line.push_error(
"LAYDEL: cannot delete the current layer. Make another layer current first.",
crate::t!("LAYDEL: cannot delete the current layer. Make another layer current first.").as_ref(),
);
return Some(Task::none());
}
@@ -234,7 +223,7 @@ impl OpenCADStudio {
}
if src.eq_ignore_ascii_case(&self.tabs[i].active_layer) {
self.command_line.push_error(
"LAYMRG: cannot merge the current layer. Make another layer current first.",
crate::t!("LAYMRG: cannot merge the current layer. Make another layer current first.").as_ref(),
);
return Some(Task::none());
}
@@ -280,7 +269,7 @@ impl OpenCADStudio {
let states = self.tabs[i].scene.document.layer_states();
if states.is_empty() {
self.command_line.push_info(
"LAYERSTATE: no saved states. Use LAYERSTATE SAVE <name>.",
crate::t!("LAYERSTATE: no saved states. Use LAYERSTATE SAVE <name>.").as_ref(),
);
} else {
let mut names: Vec<&str> =
+23 -23
View File
@@ -29,7 +29,7 @@ impl OpenCADStudio {
let describe = |c: &Color| match c {
Color::ByLayer => "ByLayer".to_string(),
Color::ByBlock => "ByBlock".to_string(),
Color::Index(n) => format!("index {n}"),
Color::Index(n) => crate::tf!("index {n}").into_owned(),
_ => "(custom)".to_string(),
};
let arg = if cmd == "BYLAYER" {
@@ -65,7 +65,7 @@ impl OpenCADStudio {
}
None => {
self.command_line.push_error(
"Usage: COLOR <ByLayer|ByBlock|1-255|red|yellow|green|cyan|blue|magenta|white>",
crate::t!("Usage: COLOR <ByLayer|ByBlock|1-255|red|yellow|green|cyan|blue|magenta|white>").as_ref(),
);
}
}
@@ -486,19 +486,19 @@ impl OpenCADStudio {
// Per-type breakdown so the user sees exactly what went.
let mut parts: Vec<String> = Vec::new();
if n_layers > 0 {
parts.push(format!("{n_layers} layer(s)"));
parts.push(crate::tf!("{n_layers} layer(s)").into_owned());
}
if n_styles > 0 {
parts.push(format!("{n_styles} text style(s)"));
parts.push(crate::tf!("{n_styles} text style(s)").into_owned());
}
if n_lts > 0 {
parts.push(format!("{n_lts} linetype(s)"));
parts.push(crate::tf!("{n_lts} linetype(s)").into_owned());
}
if n_blocks > 0 {
parts.push(format!("{n_blocks} block(s)"));
parts.push(crate::tf!("{n_blocks} block(s)").into_owned());
}
if n_sortents > 0 {
parts.push(format!("{n_sortents} stale draw-order table(s)"));
parts.push(crate::tf!("{n_sortents} stale draw-order table(s)").into_owned());
}
self.command_line.push_output(crate::tf!(
"PURGE: {} item(s) removed — {}.",
@@ -539,7 +539,7 @@ impl OpenCADStudio {
if prop.is_empty() {
self.command_line.push_info(
"Usage: CHPROP <prop> <val> (props: LAYER COLOR LINETYPE LTSCALE)",
crate::t!("Usage: CHPROP <prop> <val> (props: LAYER COLOR LINETYPE LTSCALE)").as_ref(),
);
} else {
let handles: Vec<_> = self.tabs[i]
@@ -940,7 +940,7 @@ impl OpenCADStudio {
let value = it.next().map(|s| s.trim().to_string());
if name.is_empty() || name == "?" {
self.command_line.push_info(
"SETVAR: LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER | CLAYER CELTYPE TEXTSTYLE (read-only)",
crate::t!("SETVAR: LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER | CLAYER CELTYPE TEXTSTYLE (read-only)").as_ref(),
);
} else {
if matches!(name.as_str(), "SHOWHIST" | "SOLIDHIST") {
@@ -964,12 +964,12 @@ impl OpenCADStudio {
self.tabs[i].dirty = true;
self.refresh_properties();
}
self.command_line.push_output(&format!("{name} = {mode}"));
self.command_line.push_output(&crate::tf!("{name} = {mode}"));
}
None => self.command_line.push_error(&format!("{name}: expected an integer from 0 to {maximum}.")),
None => self.command_line.push_error(&crate::tf!("{name}: expected an integer from 0 to {maximum}.")),
}
} else {
self.command_line.push_output(&format!("Enter new value for {name} <{current}>:"));
self.command_line.push_output(&crate::tf!("Enter new value for {name} <{current}>:"));
self.pending_setvar = Some(name.clone());
}
return Some(self.finish_dispatch(cmd));
@@ -1033,7 +1033,7 @@ impl OpenCADStudio {
self.tabs[i].dirty = true;
}
self.command_line
.push_output(&format!("{name} = {mode}"));
.push_output(&crate::tf!("{name} = {mode}"));
}
_ => self.command_line.push_error(
crate::tf!("SETVAR: {name} requires 0, 1, or 2.").as_ref(),
@@ -1095,10 +1095,10 @@ impl OpenCADStudio {
self.dimension_continue_mode = mode;
self.persist_settings_if_changed();
self.command_line
.push_output(&format!("DIMCONTINUEMODE = {mode}"));
.push_output(&crate::tf!("DIMCONTINUEMODE = {mode}"));
}
_ => self.command_line.push_error(
"SETVAR: DIMCONTINUEMODE requires 0 or 1.",
crate::t!("SETVAR: DIMCONTINUEMODE requires 0 or 1.").as_ref(),
),
}
} else {
@@ -1136,13 +1136,13 @@ impl OpenCADStudio {
crate::modules::draw::defaults::set_donut_outer_diameter(number);
}
self.command_line
.push_output(&format!("{name} = {number}"));
.push_output(&crate::tf!("{name} = {number}"));
} else {
self.command_line.push_error(if name == "DONUTID" {
self.command_line.push_error(crate::t!(if name == "DONUTID" {
"SETVAR: DONUTID requires a finite value greater than or equal to zero."
} else {
"SETVAR: DONUTOD requires a finite value greater than zero."
});
}).as_ref());
}
} else {
self.command_line.push_output(crate::tf!(
@@ -2097,7 +2097,7 @@ impl OpenCADStudio {
false,
)),
},
_ => Err(format!("SETVAR: unknown variable \"{name}\".")),
_ => Err(crate::tf!("SETVAR: unknown variable \"{name}\".").into_owned()),
}
};
match outcome {
@@ -2501,7 +2501,7 @@ impl OpenCADStudio {
self.command_line.push_output(crate::tf!("PDMODE set to {v}").as_ref());
} else {
self.command_line.push_error(
"Usage: PDMODE [value] (0=dot 1=none 2=+ 3=x 4=tick; +32 circle, +64 square)",
crate::t!("Usage: PDMODE [value] (0=dot 1=none 2=+ 3=x 4=tick; +32 circle, +64 square)").as_ref(),
);
}
}
@@ -2569,7 +2569,7 @@ impl OpenCADStudio {
self.persist_settings_if_changed();
match crate::io::file_association::register_as_handler() {
Ok(()) => self.command_line.push_output(
"FILEASSOC set to 1 — registered as a .dwg/.dxf/.bak handler",
crate::t!("FILEASSOC set to 1 — registered as a .dwg/.dxf/.bak handler").as_ref(),
),
Err(e) => self
.command_line
@@ -2614,7 +2614,7 @@ impl OpenCADStudio {
let msg = if v == 0 {
"SAVETIME set to 0 (autosave off)".to_string()
} else {
format!("SAVETIME set to {v} minute(s)")
crate::tf!("SAVETIME set to {v} minute(s)").into_owned()
};
self.command_line.push_output(&msg);
}
@@ -2646,7 +2646,7 @@ impl OpenCADStudio {
.push_output(crate::tf!("PDSIZE set to {v:.4}").as_ref());
} else {
self.command_line.push_error(
"Usage: PDSIZE [value] (>0 absolute size, <0 percent of viewport, 0 default)",
crate::t!("Usage: PDSIZE [value] (>0 absolute size, <0 percent of viewport, 0 default)").as_ref(),
);
}
}
+14 -14
View File
@@ -257,7 +257,7 @@ impl OpenCADStudio {
let path = cmd.trim_start_matches("CUIEXPORT").trim();
if path.is_empty() {
self.command_line.push_info(
"Usage: CUIEXPORT <path> — save the keyboard shortcuts to a file.",
crate::t!("Usage: CUIEXPORT <path> — save the keyboard shortcuts to a file.").as_ref(),
);
return Some(Task::none());
}
@@ -290,7 +290,7 @@ impl OpenCADStudio {
.trim();
if path.is_empty() {
self.command_line.push_info(
"Usage: CUIIMPORT <path> — load keyboard shortcuts from a file.",
crate::t!("Usage: CUIIMPORT <path> — load keyboard shortcuts from a file.").as_ref(),
);
return Some(Task::none());
}
@@ -381,12 +381,12 @@ impl OpenCADStudio {
"COLORSCHEME",
"COLORSCHEME Enter theme name (or ? to list):",
);
self.command_line.push_output(
"Available themes: DARK LIGHT DRACULA NORD SOLARIZED_LIGHT SOLARIZED_DARK \
self.command_line.push_output(&crate::tf!("Available themes: {}",
"DARK LIGHT DRACULA NORD SOLARIZED_LIGHT SOLARIZED_DARK \
GRUVBOX_LIGHT GRUVBOX_DARK TOKYONIGHT TOKYONIGHTSTORM TOKYONIGHTLIGHT \
KANAGAWAWAVE KANAGAWADRAGON KANAGAWALOTUS MOONFLY NIGHTFLY OXOCARBON FERRA",
);
self.command_line.push_info(&format!(
));
self.command_line.push_info(&crate::tf!(
"Current color scheme: <{}>",
self.ui_theme.name
));
@@ -398,23 +398,23 @@ impl OpenCADStudio {
let sub = cmd.split_once(' ').map(|(_, r)| r.trim()).unwrap_or("");
let upper_sub = sub.to_ascii_uppercase();
if upper_sub.is_empty() {
self.command_line.push_output(&format!(
self.command_line.push_output(&crate::tf!(
"Current color scheme: <{}>",
self.ui_theme.name
));
return Some(Task::none());
}
if upper_sub == "LIST" || upper_sub == "?" {
self.command_line.push_output(
"Available themes: DARK LIGHT DRACULA NORD SOLARIZED_LIGHT SOLARIZED_DARK \
self.command_line.push_output(&crate::tf!("Available themes: {}",
"DARK LIGHT DRACULA NORD SOLARIZED_LIGHT SOLARIZED_DARK \
GRUVBOX_LIGHT GRUVBOX_DARK TOKYONIGHT TOKYONIGHTSTORM TOKYONIGHTLIGHT \
KANAGAWAWAVE KANAGAWADRAGON KANAGAWALOTUS MOONFLY NIGHTFLY OXOCARBON FERRA",
);
));
return Some(Task::none());
}
if sub == "0" || sub == "1" {
self.command_line.push_error(
"COLORSCHEME: 0 and 1 are COLORTHEME options. Enter a theme name (e.g. DARK, LIGHT, DRACULA) or use COLORTHEME.",
crate::t!("COLORSCHEME: 0 and 1 are COLORTHEME options. Enter a theme name (e.g. DARK, LIGHT, DRACULA) or use COLORTHEME.").as_ref(),
);
return Some(Task::none());
}
@@ -630,7 +630,7 @@ impl OpenCADStudio {
}
_ => {
self.command_line.push_error(
"VPORTS: unknown option. Use VPORTS 2H | 2V | 4 | SINGLE",
crate::t!("VPORTS: unknown option. Use VPORTS 2H | 2V | 4 | SINGLE").as_ref(),
);
vec![]
}
@@ -1161,7 +1161,7 @@ impl OpenCADStudio {
.collect();
if vps.len() < 2 {
self.command_line.push_error(
"SYNCPVIEWPORTS: select two or more viewports (the first is the master).",
crate::t!("SYNCPVIEWPORTS: select two or more viewports (the first is the master).").as_ref(),
);
return Some(Task::none());
}
@@ -1215,7 +1215,7 @@ impl OpenCADStudio {
// that work.
None => self
.command_line
.push_info(visual_style::keyword_prompt()),
.push_info(crate::t!(visual_style::keyword_prompt()).as_ref()),
}
}
+1 -1
View File
@@ -836,7 +836,7 @@ impl OpenCADStudio {
if let Some(doc) = target {
let Some(i) = self.tabs.iter().position(|t| t.id == doc) else {
self.command_line
.push_error("Automation target document closed");
.push_error(crate::t!("Automation target document closed").as_ref());
return Task::none();
};
self.active_tab = i;
+13 -13
View File
@@ -60,12 +60,12 @@ impl OpenCADStudio {
}
self.find_replace.current_match = Some(target);
self.find_replace.status = format!(
self.find_replace.status = crate::tf!(
"{} of {} — {}",
index + 1,
matches.len(),
match_label(target)
);
).into_owned();
self.tabs[i].scene.deselect_all();
self.tabs[i]
.scene
@@ -99,7 +99,7 @@ impl OpenCADStudio {
let search = self.find_replace.search.clone();
let replacement = self.find_replace.replacement.clone();
if match_is_locked(&self.tabs[i].scene, target) {
self.find_replace.status = "The matching object is on a locked layer.".to_string();
self.find_replace.status = crate::t!("The matching object is on a locked layer.").into_owned();
return;
}
self.push_undo_snapshot(i, "FIND/REPLACE");
@@ -131,10 +131,10 @@ impl OpenCADStudio {
self.tabs[i].dirty = true;
self.find_replace.current_match = None;
let remaining = self.find_text_matches(i).len();
self.find_replace.status = format!(
self.find_replace.status = crate::tf!(
"Replaced 1 occurrence in {}; {remaining} matching object(s) remain.",
match_label(target)
);
).into_owned();
self.command_line.push_output(crate::tf!(
"FIND/REPLACE: replaced 1 occurrence of \"{search}\"."
).as_ref());
@@ -192,10 +192,10 @@ impl OpenCADStudio {
}
self.tabs[i].dirty = true;
self.find_replace.current_match = None;
self.find_replace.status = format!(
self.find_replace.status = crate::tf!(
"Replaced {replaced} occurrence(s) in {} object(s).",
changed.len()
);
).into_owned();
self.command_line.push_output(crate::tf!(
"FIND/REPLACE: replaced {replaced} occurrence(s) of \"{search}\"."
).as_ref());
@@ -286,9 +286,9 @@ impl OpenCADStudio {
fn no_match_status(search: &str) -> String {
if search.trim().is_empty() {
"Enter text to find.".to_string()
crate::t!("Enter text to find.").into_owned()
} else {
format!("\"{search}\" was not found.")
crate::tf!("\"{search}\" was not found.").into_owned()
}
}
@@ -372,16 +372,16 @@ fn match_is_locked(scene: &crate::scene::Scene, target: FindMatchKey) -> bool {
fn match_label(target: FindMatchKey) -> String {
match target {
FindMatchKey::Entity(handle) => format!("handle {:X}", handle.value()),
FindMatchKey::Entity(handle) => crate::tf!("handle {:X}", handle.value()).into_owned(),
FindMatchKey::BlockEntityInInsert { entity, insert } => {
format!(
crate::tf!(
"block {:X}, text {:X}",
insert.value(),
entity.value()
)
).into_owned()
}
FindMatchKey::InsertAttribute { insert, index } => {
format!("block {:X}, attribute {}", insert.value(), index + 1)
crate::tf!("block {:X}, attribute {}", insert.value(), index + 1).into_owned()
}
}
}
+1 -1
View File
@@ -160,7 +160,7 @@ impl std::fmt::Display for QSelectOp {
QSelectOp::Gt => "> Greater than",
QSelectOp::Lt => "< Less than",
};
f.write_str(s)
f.write_str(crate::t!(s).as_ref())
}
}
+6 -6
View File
@@ -161,7 +161,7 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
crate::tf!("Unknown visual style \"{name}\".").as_ref(),
);
self.command_line
.push_info(visual_style::keyword_prompt());
.push_info(crate::t!(visual_style::keyword_prompt()).as_ref());
}
}
}
@@ -181,7 +181,7 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
let path = crate::sys::file_dialog()
.set_title(title)
.add_filter(filter_name, &exts)
.add_filter("All Files", &["*"])
.add_filter(crate::t!("All Files").as_ref(), &["*"])
.pick_file()
.await
.map(|h| crate::sys::handle_path(&h));
@@ -520,8 +520,8 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
BlockPaletteMsg::PickFile => iced::Task::perform(
async {
let handle = rfd::AsyncFileDialog::new()
.set_title("Select Drawing to Insert as Block")
.add_filter("DWG/DXF Files", &["dwg", "dxf", "DWG", "DXF"])
.set_title(crate::t!("Select Drawing to Insert as Block").as_ref())
.add_filter(crate::t!("DWG/DXF Files").as_ref(), &["dwg", "dxf", "DWG", "DXF"])
.pick_file()
.await;
match handle {
@@ -535,12 +535,12 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
match self.import_file_as_block(path) {
Ok(name) => {
self.command_line
.push_output(&format!("Inserting \"{name}\" from file."));
.push_output(&crate::tf!("Inserting \"{name}\" from file."));
self.refresh_block_palette();
self.start_block_placement(&name);
}
Err(e) if e != "Cancelled" => {
self.command_line.push_error(&format!("INSERT FILE: {e}"));
self.command_line.push_error(&crate::tf!("INSERT FILE: {e}"));
}
Err(_) => {}
}
+23 -23
View File
@@ -2576,7 +2576,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
let pick = Task::perform(
async move {
let mut dlg = crate::sys::file_dialog()
.set_title("Save Drawing As")
.set_title(crate::t!("Save Drawing As").as_ref())
.set_file_name(default_name)
.add_filter(filter_label, &[filter_ext]);
if let Some(dir) = seed_dir {
@@ -2934,8 +2934,8 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
plot_style.as_ref(),
render_options,
)
.map(|_| format!("Exported: {}", worker_path.display()))
.map_err(|e| format!("Export failed: {e}"))
.map(|_| crate::tf!("Exported: {}", worker_path.display()).into_owned())
.map_err(|e| crate::tf!("Export failed: {e}").into_owned())
};
self.run_plot_work(self.plot_dialog.background, false, work)
}
@@ -2978,12 +2978,12 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
plot_style.as_ref(),
render_options,
)
.map(|_| {format!(
.map(|_| {crate::tf!(
"Plotted window to {}",
worker_path
.file_name().unwrap_or_default().to_string_lossy()
)
}).map_err(|e| format!("Plot failed: {e}"))
).into_owned()
}).map_err(|e| crate::tf!("Plot failed: {e}").into_owned())
};
self.run_plot_work(self.plot_dialog.background, false, work)
}
@@ -3071,7 +3071,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
let page_setup = self.tabs[i]
.scene
.plot_settings_for(&name)
.ok_or_else(|| format!("Layout '{name}' has no page setup."))?;
.ok_or_else(|| crate::tf!("Layout '{name}' has no page setup.").into_owned())?;
{
let scene = &mut self.tabs[i].scene;
scene.current_layout = name.clone();
@@ -3092,10 +3092,10 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
self.tabs[i].scene.restore_saved_camera();
}
if dialog.style_missing && dialog.apply_plot_styles {
return Err(format!(
return Err(crate::tf!(
"Layout '{name}' plot style table '{}' is not loaded.",
dialog.style_name
));
).into_owned());
}
let plot_style = self.dialog_plot_style(&dialog);
let params = match dialog.area.as_str() {
@@ -3135,7 +3135,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
rotation_deg,
scale,
clip,
) = params.ok_or_else(|| format!("Layout '{name}' plot area is empty."))?;
) = params.ok_or_else(|| crate::tf!("Layout '{name}' plot area is empty.").into_owned())?;
(
std::sync::Arc::new(wires),
hatches,
@@ -3219,8 +3219,8 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
&worker_path,
None,
)
.map(|_| format!("Exported {} layouts to {}", pages.len(), worker_path.display()))
.map_err(|error| format!("Export failed: {error}"))
.map(|_| crate::tf!("Exported {} layouts to {}", pages.len(), worker_path.display()).into_owned())
.map_err(|error| crate::tf!("Export failed: {error}").into_owned())
};
self.run_print_all_work(dialog.background, work)
}
@@ -3269,8 +3269,8 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
.and_then(|_| {
crate::io::print_to_printer::print_existing_pdf(&temp_path, &options)
})
.map(|printer| format!("Sent {} layouts to printer: {printer}", pages.len()))
.map_err(|error| format!("Print failed: {error}"))
.map(|printer| crate::tf!("Sent {} layouts to printer: {printer}", pages.len()).into_owned())
.map_err(|error| crate::tf!("Print failed: {error}").into_owned())
};
self.run_print_all_work(true, work)
}
@@ -4480,7 +4480,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
plot_style.as_ref(),
render_options,
).and_then(|_| crate::io::print_to_printer::open_in_viewer(&tmp))
.map(|_| "Opened plot preview.".to_string()).map_err(|e| format!("Preview failed: {e}"))
.map(|_| crate::t!("Opened plot preview.").into_owned()).map_err(|e| crate::tf!("Preview failed: {e}").into_owned())
};
return self.run_plot_work(d.background, true, work);
}
@@ -4497,8 +4497,8 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
plot_style.as_ref(),
render_options,
).and_then(|_| crate::io::print_to_printer::print_existing_pdf(&tmp, &opts))
.map(|printer| format!("Sent to printer: {printer}"))
.map_err(|e| format!("Print failed: {e}"))
.map(|printer| crate::tf!("Sent to printer: {printer}").into_owned())
.map_err(|e| crate::tf!("Print failed: {e}").into_owned())
};
return self.run_plot_work(true, false, work);
}
@@ -4525,7 +4525,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
plot_style.as_ref(),
render_options,
).and_then(|_| crate::io::print_to_printer::open_in_viewer(&tmp))
.map(|_| "Opened plot preview.".to_string()).map_err(|e| format!("Preview failed: {e}"))
.map(|_| crate::t!("Opened plot preview.").into_owned()).map_err(|e| crate::tf!("Preview failed: {e}").into_owned())
};
return self.run_plot_work(d.background, true, work);
}
@@ -4543,8 +4543,8 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
wires, hatches, wipeouts, page_w, page_h, ox, oy, rotation, scale, clip,
plot_style, opts,
))
.map(|printer| format!("Sent to printer: {printer}"))
.map_err(|error| format!("Print failed: {error}"))
.map(|printer| crate::tf!("Sent to printer: {printer}").into_owned())
.map_err(|error| crate::tf!("Print failed: {error}").into_owned())
};
self.run_plot_work(true, false, work)
}
@@ -4860,10 +4860,10 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
Task::perform(
async move {
let dialog = crate::sys::file_dialog()
.set_title("Save Plot Style Table")
.set_title(crate::t!("Save Plot Style Table").as_ref())
.set_file_name(&default_name)
.add_filter("Plot Style Files", &["ctb", "CTB"])
.add_filter("All Files", &["*"]);
.add_filter(crate::t!("Plot Style Files").as_ref(), &["ctb", "CTB"])
.add_filter(crate::t!("All Files").as_ref(), &["*"]);
#[cfg(not(target_arch = "wasm32"))]
let dialog = match crate::io::plot_style::ensure_plot_styles_dir() {
Ok(dir) => dialog.set_directory(dir),
+38 -38
View File
@@ -907,8 +907,8 @@ impl OpenCADStudio {
Message::PdfAttachPick => Task::perform(
async {
let handle = crate::sys::file_dialog()
.set_title("Select PDF Underlay")
.add_filter("PDF Files", &["pdf", "PDF"])
.set_title(crate::t!("Select PDF Underlay").as_ref())
.add_filter(crate::t!("PDF Files").as_ref(), &["pdf", "PDF"])
.pick_file()
.await;
@@ -961,11 +961,11 @@ impl OpenCADStudio {
Message::XAttachPick => Task::perform(
async {
let handle = crate::sys::file_dialog()
.set_title("Select External Reference File")
.add_filter("CAD Files", &["dwg", "dxf", "bak", "DWG", "DXF", "BAK"])
.add_filter("DWG Files", &["dwg", "DWG"])
.add_filter("DXF Files", &["dxf", "DXF"])
.add_filter("Backup Files", &["bak", "BAK"])
.set_title(crate::t!("Select External Reference File").as_ref())
.add_filter(crate::t!("CAD Files").as_ref(), &["dwg", "dxf", "bak", "DWG", "DXF", "BAK"])
.add_filter(crate::t!("DWG Files").as_ref(), &["dwg", "DWG"])
.add_filter(crate::t!("DXF Files").as_ref(), &["dxf", "DXF"])
.add_filter(crate::t!("Backup Files").as_ref(), &["bak", "BAK"])
.pick_file()
.await;
match handle {
@@ -999,9 +999,9 @@ impl OpenCADStudio {
Task::perform(
async move {
let path = crate::sys::file_dialog()
.set_title("Save Block As")
.set_title(crate::t!("Save Block As").as_ref())
.set_file_name("block.dwg")
.add_filter("DWG Files", &["dwg"])
.add_filter(crate::t!("DWG Files").as_ref(), &["dwg"])
.save_file()
.await
.map(|h| crate::sys::handle_path(&h));
@@ -1035,10 +1035,10 @@ impl OpenCADStudio {
Task::perform(
async move {
let path = crate::sys::file_dialog()
.set_title("Save Data Extraction")
.set_title(crate::t!("Save Data Extraction").as_ref())
.set_file_name("extraction.csv")
.add_filter("CSV", &["csv"])
.add_filter("All Files", &["*"])
.add_filter(crate::t!("CSV").as_ref(), &["csv"])
.add_filter(crate::t!("All Files").as_ref(), &["*"])
.save_file()
.await
.map(|h| crate::sys::handle_path(&h));
@@ -1078,10 +1078,10 @@ impl OpenCADStudio {
Task::perform(
async {
crate::sys::file_dialog()
.set_title("Export STL")
.set_title(crate::t!("Export STL").as_ref())
.set_file_name("export.stl")
.add_filter("STL Files", &["stl"])
.add_filter("All Files", &["*"])
.add_filter(crate::t!("STL Files").as_ref(), &["stl"])
.add_filter(crate::t!("All Files").as_ref(), &["*"])
.save_file()
.await
.map(|h| crate::sys::handle_path(&h))
@@ -1115,10 +1115,10 @@ impl OpenCADStudio {
Task::perform(
async {
crate::sys::file_dialog()
.set_title("Export STEP AP203")
.set_title(crate::t!("Export STEP AP203").as_ref())
.set_file_name("export.step")
.add_filter("STEP Files", &["step", "stp"])
.add_filter("All Files", &["*"])
.add_filter(crate::t!("STEP Files").as_ref(), &["step", "stp"])
.add_filter(crate::t!("All Files").as_ref(), &["*"])
.save_file()
.await
.map(|h| crate::sys::handle_path(&h))
@@ -1145,9 +1145,9 @@ impl OpenCADStudio {
Message::ObjImport => Task::perform(
async {
crate::sys::file_dialog()
.set_title("Import OBJ Mesh")
.add_filter("Wavefront OBJ", &["obj", "OBJ"])
.add_filter("All Files", &["*"])
.set_title(crate::t!("Import OBJ Mesh").as_ref())
.add_filter(crate::t!("Wavefront OBJ").as_ref(), &["obj", "OBJ"])
.add_filter(crate::t!("All Files").as_ref(), &["*"])
.pick_file()
.await
.map(|h| crate::sys::handle_path(&h))
@@ -1286,11 +1286,11 @@ impl OpenCADStudio {
.scene
.set_projection_preserving_frame(proj);
self.ribbon.set_ortho(ortho);
self.command_line.push_output(if ortho {
self.command_line.push_output(crate::t!(if ortho {
"Projection: Orthographic"
} else {
"Projection: Perspective"
});
}).as_ref());
Task::none()
}
@@ -6613,13 +6613,13 @@ impl OpenCADStudio {
crate::plugin::external::normalize_repository(&self.plugin_repo_input)
else {
self.marketplace_status =
"Enter a GitHub URL or repository in owner/repo format.".to_string();
crate::t!("Enter a GitHub URL or repository in owner/repo format.").into_owned();
return Task::none();
};
if self.plugin_repos.contains(&repo)
|| self.plugin_registry.iter().any(|entry| entry.repo == repo)
{
self.marketplace_status = format!("{repo} is already in the catalog.");
self.marketplace_status = crate::tf!("{repo} is already in the catalog.").into_owned();
self.selected_plugin_repo = Some(repo.clone());
if !self.plugin_readmes.contains_key(&repo)
&& self.plugin_readme_loading.insert(repo.clone())
@@ -6633,7 +6633,7 @@ impl OpenCADStudio {
.iter()
.any(|plugin| plugin.repository.as_deref() == Some(repo.as_str()))
{
self.marketplace_status = format!("{repo} is already installed.");
self.marketplace_status = crate::tf!("{repo} is already installed.").into_owned();
self.selected_plugin_repo = Some(repo.clone());
if !self.plugin_readmes.contains_key(&repo)
&& self.plugin_readme_loading.insert(repo.clone())
@@ -6645,7 +6645,7 @@ impl OpenCADStudio {
self.plugin_repos.push(repo.clone());
self.plugin_repo_input.clear();
self.persist_settings_if_changed();
self.marketplace_status = format!("Fetching releases for {repo}…");
self.marketplace_status = crate::tf!("Fetching releases for {repo}…").into_owned();
self.selected_plugin_repo = Some(repo.clone());
self.plugin_readmes.remove(&repo);
self.plugin_readme_loading.insert(repo.clone());
@@ -6791,12 +6791,12 @@ impl OpenCADStudio {
.entry(repo.clone())
.or_insert_with(|| first.tag.clone());
}
if self.marketplace_status == format!("Fetching releases for {repo}…") {
if self.marketplace_status == crate::tf!("Fetching releases for {repo}…").into_owned() {
self.marketplace_status =
format!(
crate::tf!(
"Repository added. {} installable release(s) found.",
releases.len()
);
).into_owned();
}
self.repo_release_tags.insert(repo, releases);
Task::none()
@@ -6834,15 +6834,15 @@ impl OpenCADStudio {
let Some(tag) = self.repo_selected_tag.get(&repo).cloned() else {
return Task::none();
};
self.marketplace_status = format!("Installing {repo} {tag}…");
self.marketplace_status = crate::tf!("Installing {repo} {tag}…").into_owned();
self.install_task(repo, tag)
}
Message::PluginUpdate(repo, tag) => {
self.marketplace_status = format!("Updating {repo} to {tag}…");
self.marketplace_status = crate::tf!("Updating {repo} to {tag}…").into_owned();
self.install_task(repo, tag)
}
Message::PluginInstalled(Ok(id)) => {
self.marketplace_status = format!("Installed '{id}'. Restart to load it.");
self.marketplace_status = crate::tf!("Installed '{id}'. Restart to load it.").into_owned();
self.plugin_load_errors.remove(&id);
#[cfg(not(target_arch = "wasm32"))]
{
@@ -6851,7 +6851,7 @@ impl OpenCADStudio {
Task::none()
}
Message::PluginInstalled(Err(e)) => {
self.marketplace_status = format!("Install failed: {e}");
self.marketplace_status = crate::tf!("Install failed: {e}").into_owned();
Task::none()
}
Message::PluginUninstall(id) => {
@@ -6861,20 +6861,20 @@ impl OpenCADStudio {
// and allows the package directory to be deleted.
if !crate::plugin::external::remove_plugin(&id) {
self.marketplace_status =
format!("Uninstall failed: plugin '{id}' did not stop in time");
crate::tf!("Uninstall failed: plugin '{id}' did not stop in time").into_owned();
return Task::none();
}
match crate::plugin::external::uninstall(&id) {
Ok(()) => {
self.marketplace_status =
format!("Uninstalled '{id}'.");
crate::tf!("Uninstalled '{id}'.").into_owned();
self.plugin_load_errors.remove(&id);
self.loaded_plugin_ids.remove(&id);
self.rebuild_ribbon_modules();
self.external_plugins = crate::plugin::external::discover();
}
Err(e) => {
self.marketplace_status = format!("Uninstall failed: {e}");
self.marketplace_status = crate::tf!("Uninstall failed: {e}").into_owned();
}
}
}
@@ -7344,7 +7344,7 @@ impl OpenCADStudio {
Message::PlotStyleLoaded(Some(table)) => {
if table.is_stb {
self.command_line.push_error(
"Named plot style tables are not supported by the vector plotter.",
crate::t!("Named plot style tables are not supported by the vector plotter.").as_ref(),
);
return Task::none();
}
+1 -1
View File
@@ -5122,7 +5122,7 @@ impl OpenCADStudio {
}
self.tabs[i].scene.camera_generation += 1;
self.command_line
.push_output(crate::tf!("View: {}", region.label()).as_ref());
.push_output(crate::tf!("View: {}", crate::t!(region.label())).as_ref());
Task::none()
}
+5 -5
View File
@@ -1485,7 +1485,7 @@ bg={bg_ms:.1}ms n={view_count}"
);
let trace = crate::perf::snapshot_tail_text(80);
let trace = if trace.is_empty() {
"No samples yet".to_string()
t!("No samples yet").into_owned()
} else {
trace
};
@@ -2204,7 +2204,7 @@ bg={bg_ms:.1}ms n={view_count}"
crate::ui::modal::modal(
base,
"Select Color",
t!("Select Color"),
content,
Message::CloseColorPicker,
self.modal_offset,
@@ -2713,7 +2713,7 @@ fn doc_tab_context_menu(
let item = |label: &'static str, msg: Option<Message>| -> Element<'static, Message> {
let enabled = msg.is_some();
let content = container(text(label).size(12))
let content = container(text(t!(label)).size(12))
.padding([4, 12])
.width(Fill)
.style(move |theme: &Theme| {
@@ -3233,7 +3233,7 @@ fn start_page_content<'a>(
{
// Filled with the active theme's primary colour.
secondary_items.push(
button(text("OCS Web").size(14))
button(text(crate::t!("OCS Web")).size(14))
.on_press(Message::RibbonToolClick {
tool_id: "WEBVERSION".to_string(),
event: crate::modules::ModuleEvent::Command("WEBVERSION".to_string()),
@@ -3245,7 +3245,7 @@ fn start_page_content<'a>(
}
#[cfg(target_arch = "wasm32")]
secondary_items.push(
button(text("OCS Desktop").size(14))
button(text(crate::t!("OCS Desktop")).size(14))
.on_press(Message::OpenUrl(
"https://github.com/HakanSeven12/OpenCADStudio/releases/latest".to_string(),
))
+9 -9
View File
@@ -69,7 +69,7 @@ pub(super) fn text_inline_overlay(
position_canvas_overlay(anchor, panel.into())
}
// Stroke-font families the renderer ships (LibreCAD LFF; see scene/lff.rs).
// Stroke-font families bundled with the renderer; see scene/lff.rs.
const MTEXT_FONTS: [&str; 10] = [
"[Style default]",
"Standard",
@@ -465,7 +465,7 @@ fn mtext_editor_content<'a>(
.iter()
.map(|s| s.to_string())
.collect::<Vec<_>>(),
|value| value.to_string(),
|value| t!(value).into_owned(),
)
.on_select(Message::MTextFont)
.text_size(11)
@@ -626,7 +626,7 @@ fn mtext_editor_content<'a>(
.into_iter()
.map(str::to_string)
.collect::<Vec<_>>(),
|value| value.to_string(),
|value| t!(value).into_owned(),
)
.on_select(Message::MTextColumnMode)
.text_size(11)
@@ -703,7 +703,7 @@ fn mtext_editor_content<'a>(
.width(width);
let row4 = row![
lbl("Find"),
text_input("Find", &ed.find_text)
text_input(&t!("Find"), &ed.find_text)
.on_input(Message::MTextFindText)
.width(iced::Length::Fixed(140.0))
.padding(3)
@@ -713,7 +713,7 @@ fn mtext_editor_content<'a>(
.padding(3)
.style(btn_style),
lbl("Replace"),
text_input("Replace", &ed.replace_text)
text_input(&t!("Replace"), &ed.replace_text)
.on_input(Message::MTextReplaceText)
.width(iced::Length::Fixed(140.0))
.padding(3)
@@ -1409,7 +1409,7 @@ fn qselect_content<'a>(
let mut prop_options: Vec<crate::app::QSelectPropertyChoice> =
vec![crate::app::QSelectPropertyChoice {
field: String::new(),
label: QSELECT_ANY_PROP.to_string(),
label: t!(QSELECT_ANY_PROP).into_owned(),
editor: crate::app::QSelectValueEditor::Text,
}];
prop_options.extend(properties.iter().cloned());
@@ -1436,7 +1436,7 @@ fn qselect_content<'a>(
.clone()
.unwrap_or(crate::app::QSelectPropertyChoice {
field: String::new(),
label: QSELECT_ANY_PROP.to_string(),
label: t!(QSELECT_ANY_PROP).into_owned(),
editor: crate::app::QSelectValueEditor::Text,
});
@@ -1552,7 +1552,7 @@ fn qselect_content<'a>(
.spacing(8)
.width(sizing.width),
Space::new().height(4),
text(format!("{} candidate object(s)", candidate_count))
text(crate::tf!("{} candidate object(s)", candidate_count))
.size(11)
.style(|theme: &Theme| iced::widget::text::Style {
color: Some(theme.palette().background.base.text.scale_alpha(0.65)),
@@ -1567,7 +1567,7 @@ fn qselect_content<'a>(
iced::widget::pick_list(
Some(type_sel),
type_options,
|value| value.to_string(),
|value| t!(value).into_owned(),
)
.on_select(|s: String| {
if s == QSELECT_ANY_TYPE {
+14 -1
View File
@@ -21,63 +21,76 @@ use clap::Parser;
#[command(
name = "OpenCADStudio",
version,
about = "Open CAD Studio — 2D/3D CAD editor",
about = crate::t!("Open CAD Studio — 2D/3D CAD editor").into_owned(),
long_about = None,
)]
pub struct Cli {
/// CAD files to open at startup (.dwg / .dxf). Also how the OS file
/// association launches us when drawings are double-clicked — selecting
/// several hands them all to one launch, so this takes a list.
#[arg(help = crate::t!("CAD files to open at startup (.dwg / .dxf).").into_owned(), long_help = None)]
pub files: Vec<PathBuf>,
/// Start with a new empty drawing, ignoring any file argument.
#[arg(long)]
#[arg(help = crate::t!("Start with a new empty drawing.").into_owned(), long_help = None)]
pub new: bool,
/// Always start a new editor process, even when one is already running.
/// Without this, opening a drawing hands it to the running editor as a tab.
#[arg(long)]
#[arg(help = crate::t!("Start a new editor process.").into_owned(), long_help = None)]
pub new_instance: bool,
/// Open read-only: editing is allowed but saving is disabled.
#[arg(long)]
#[arg(help = crate::t!("Open read-only: saving is disabled.").into_owned(), long_help = None)]
pub read_only: bool,
/// Restrict the GPU backend (e.g. dx12, vulkan, gl, metal). Sets WGPU_BACKEND.
#[arg(long, value_name = "BACKEND")]
#[arg(help = crate::t!("GPU backend (dx12, vulkan, gl, metal). Sets WGPU_BACKEND.").into_owned(), long_help = None)]
pub backend: Option<String>,
/// Safe mode: force the GL backend, for flaky/hybrid GPU drivers.
#[arg(long, visible_alias = "no-gpu")]
#[arg(help = crate::t!("Safe mode: use the GL backend.").into_owned(), long_help = None)]
pub safe_mode: bool,
/// Force the packed renderer path that avoids shader storage buffers.
/// Normally selected automatically for adapters with insufficient limits.
#[arg(long)]
#[arg(help = crate::t!("Use the renderer for GPUs without shader storage buffers.").into_owned(), long_help = None)]
pub compat_renderer: bool,
/// Run the headless JSON automation server (stdin/stdout, or --port).
#[arg(long)]
#[arg(help = crate::t!("Run the headless JSON automation server (stdin/stdout, or --port).").into_owned(), long_help = None)]
pub serve: bool,
/// Expose the running desktop editor to AI clients over MCP stdio.
#[arg(long)]
#[arg(help = crate::t!("Connect AI clients to the running editor over MCP stdio.").into_owned(), long_help = None)]
pub mcp: bool,
/// TCP port for --serve (defaults to stdin/stdout).
#[arg(long, value_name = "PORT")]
#[arg(help = crate::t!("TCP port for --serve (defaults to stdin/stdout).").into_owned(), long_help = None)]
pub port: Option<u16>,
/// Headless convert: read IN, write OUT (format from OUT's extension), exit.
#[arg(long, num_args = 2, value_names = ["IN", "OUT"])]
#[arg(help = crate::t!("Convert IN to OUT using the output file extension, then exit.").into_owned(), long_help = None)]
pub export: Option<Vec<PathBuf>>,
/// Run a command script at startup: one command line per line of FILE.
#[arg(long, value_name = "FILE")]
#[arg(help = crate::t!("Run a command script at startup, one command per line.").into_owned(), long_help = None)]
pub script: Option<PathBuf>,
/// Log level (error|warn|info|debug|trace). Also honours RUST_LOG.
#[arg(long, value_name = "LEVEL")]
#[arg(help = crate::t!("Log level (error|warn|info|debug|trace). Also reads RUST_LOG.").into_owned(), long_help = None)]
pub log: Option<String>,
/// Internal: run as the plugin runner child process.
+105 -21
View File
@@ -1,9 +1,9 @@
//! Application language selection and embedded Fluent resources.
use i18n_embed::fluent::{fluent_language_loader, FluentLanguageLoader};
use i18n_embed::LanguageLoader;
#[cfg(not(target_arch = "wasm32"))]
use i18n_embed::DesktopLanguageRequester;
use i18n_embed::LanguageLoader;
#[cfg(target_arch = "wasm32")]
use i18n_embed::WebLanguageRequester;
use rust_embed::RustEmbed;
@@ -251,12 +251,8 @@ pub fn translate(source: impl AsRef<str>) -> Cow<'static, str> {
}
/// Translate a catalog message and replace the named values used by legacy
/// command prompts. Catalog generation protects these markers from machine
/// translation and Fluent parsing.
pub fn translate_args(
source: impl AsRef<str>,
args: &[(&str, String)],
) -> Cow<'static, str> {
/// command prompts. Markers preserve values through Fluent parsing.
pub fn translate_args(source: impl AsRef<str>, args: &[(&str, String)]) -> Cow<'static, str> {
let source = source.as_ref();
let mut translated = locale_catalog::message_attribute(source)
.map(|(message_id, attribute_id)| loader().get_attr(message_id, attribute_id))
@@ -416,17 +412,18 @@ mod tests {
{
let loader = FluentLanguageLoader::new("opencadstudio", "en-US".parse().unwrap());
load_language(&loader, language).expect("Fluent resources must parse");
let attributes: BTreeSet<String> = loader.with_message_iter(
&language.requested()[0],
|messages| {
let attributes: BTreeSet<String> =
loader.with_message_iter(&language.requested()[0], |messages| {
messages
.filter(|message| message.id.name == "donation")
.flat_map(|message| {
message.attributes.iter().map(|attr| attr.id.name.to_owned())
message
.attributes
.iter()
.map(|attr| attr.id.name.to_owned())
})
.collect()
},
);
});
assert_eq!(
attributes,
BTreeSet::from(["title", "heading", "body", "decline"].map(String::from))
@@ -438,12 +435,12 @@ mod tests {
}
#[test]
fn greek_catalog_loads_and_covers_the_source_catalog() {
fn every_catalog_covers_and_formats_the_source_catalog() {
let loader = FluentLanguageLoader::new("opencadstudio", "en-US".parse().unwrap());
load_language(&loader, Language::ElGr).expect("Greek Fluent resources must parse");
let keys = |language: &str| -> BTreeSet<(String, String)> {
loader.with_message_iter(&language.parse().unwrap(), |messages| {
messages
load_language(&loader, Language::EnUs).expect("English Fluent resources must parse");
let keys = |loader: &FluentLanguageLoader, language: Language| {
loader.with_message_iter(&language.requested()[0], |messages| {
let entries: Vec<_> = messages
.flat_map(|message| {
std::iter::once((message.id.name.to_owned(), String::new())).chain(
message.attributes.iter().map(|attribute| {
@@ -451,12 +448,99 @@ mod tests {
}),
)
})
.collect()
.collect();
let unique: BTreeSet<_> = entries.iter().cloned().collect();
assert_eq!(
entries.len(),
unique.len(),
"duplicate keys in {language:?}"
);
unique
})
};
let source = keys("en-US");
let source = keys(&loader, Language::EnUs);
assert!(!source.is_empty());
assert_eq!(keys("el-GR"), source);
let english = Localizations::get("en-US/opencadstudio.ftl").unwrap();
let english = std::str::from_utf8(&english.data).unwrap();
let variables: Vec<_> = english
.split('$')
.skip(1)
.map(|tail| {
tail.split(|ch: char| !ch.is_ascii_alphanumeric() && ch != '_' && ch != '-')
.next()
.unwrap()
})
.collect();
let args = variables.into_iter().map(|name| (name, 2)).collect();
let render = |loader: &FluentLanguageLoader, message: &str, attribute: &str| {
loader
.with_fluent_message_and_bundle(message, |fluent_message, bundle| {
let pattern = if attribute.is_empty() {
fluent_message.value()
} else {
fluent_message
.get_attribute(attribute)
.map(|attr| attr.value())
};
pattern
.map(|pattern| {
let mut errors = Vec::new();
let text = bundle.format_pattern(pattern, Some(&args), &mut errors);
assert!(errors.is_empty(), "{message}.{attribute}: {errors:?}");
text.into_owned()
})
.unwrap_or_default()
})
.unwrap()
};
let markers = |text: &str| -> BTreeSet<String> {
text.split("__ocs_")
.skip(1)
.filter_map(|tail| tail.split_once("__").map(|(marker, _)| marker.to_owned()))
.collect()
};
for language in Language::ALL.into_iter().filter(|l| *l != Language::System) {
let resource =
Localizations::get(&format!("{}/opencadstudio.ftl", language.requested()[0]))
.unwrap();
let text = std::str::from_utf8(&resource.data).unwrap();
if let Err((_, errors)) = fluent_syntax::parser::parse(text) {
panic!("Invalid Fluent syntax in {language:?}: {errors:?}");
}
let localized = FluentLanguageLoader::new("opencadstudio", "en-US".parse().unwrap());
load_language(&localized, language).expect("Fluent resources must parse");
let actual = keys(&localized, language);
let extra: Vec<_> = actual.difference(&source).collect();
assert!(extra.is_empty(), "extra keys in {language:?}: {extra:?}");
let missing: Vec<_> = source.difference(&actual).collect();
assert!(
missing.is_empty(),
"{} missing keys in {language:?}: {:?}",
missing.len(),
&missing[..missing.len().min(10)]
);
for (message, attribute) in &source {
let expected = render(&loader, message, attribute);
let translated = render(&localized, message, attribute);
assert_eq!(
markers(&translated),
markers(&expected),
"{language:?}: {message}.{attribute}"
);
assert_eq!(
translated.is_empty(),
expected.is_empty(),
"{language:?}: {message}.{attribute}"
);
}
}
}
#[test]
fn greek_language_setting_round_trips() {
let loader = FluentLanguageLoader::new("opencadstudio", "en-US".parse().unwrap());
load_language(&loader, Language::ElGr).expect("Greek Fluent resources must parse");
assert_eq!(loader.get_attr("language", "greek"), "Ελληνικά");
assert_eq!(serde_json::to_string(&Language::ElGr).unwrap(), "\"el-GR\"");
}
+25 -25
View File
@@ -163,12 +163,12 @@ impl From<&str> for OpenLoadError {
#[cfg(not(target_arch = "wasm32"))]
pub async fn pick_open_path() -> Option<(PathBuf, u64)> {
let handle = crate::sys::file_dialog()
.set_title("Open CAD file")
.add_filter("CAD Files", &["dwg", "dxf", "bak", "sv$", "DWG", "DXF", "BAK"])
.add_filter("DWG Files", &["dwg", "DWG"])
.add_filter("DXF Files", &["dxf", "DXF"])
.add_filter("Backup / Autosave", &["bak", "sv$", "BAK"])
.add_filter("All Files", &["*"])
.set_title(crate::t!("Open CAD file").as_ref())
.add_filter(crate::t!("CAD Files").as_ref(), &["dwg", "dxf", "bak", "sv$", "DWG", "DXF", "BAK"])
.add_filter(crate::t!("DWG Files").as_ref(), &["dwg", "DWG"])
.add_filter(crate::t!("DXF Files").as_ref(), &["dxf", "DXF"])
.add_filter(crate::t!("Backup / Autosave").as_ref(), &["bak", "sv$", "BAK"])
.add_filter(crate::t!("All Files").as_ref(), &["*"])
.pick_file()
.await?;
let path = crate::sys::handle_path(&handle);
@@ -181,12 +181,12 @@ pub async fn pick_open_path() -> Option<(PathBuf, u64)> {
/// rather than needing anything of their own. (#624)
pub async fn pick_layer_standard_path() -> Option<PathBuf> {
let handle = crate::sys::file_dialog()
.set_title("Load layer standard")
.set_title(crate::t!("Load layer standard").as_ref())
.add_filter(
"Drawings and standards",
crate::t!("Drawings and standards").as_ref(),
&["dwg", "dws", "dwt", "dxf", "DWG", "DWS", "DWT", "DXF"],
)
.add_filter("All Files", &["*"])
.add_filter(crate::t!("All Files").as_ref(), &["*"])
.pick_file()
.await?;
Some(crate::sys::handle_path(&handle))
@@ -195,13 +195,13 @@ pub async fn pick_layer_standard_path() -> Option<PathBuf> {
/// Pick where a set of layer mappings is written, or read back from.
pub async fn pick_layer_mapping_path(save: bool) -> Option<PathBuf> {
let dialog = crate::sys::file_dialog()
.set_title(if save {
.set_title(crate::t!(if save {
"Save layer mappings"
} else {
"Load layer mappings"
})
.add_filter("Layer mappings", &["ocslmap"])
.add_filter("All Files", &["*"]);
}).as_ref())
.add_filter(crate::t!("Layer mappings").as_ref(), &["ocslmap"])
.add_filter(crate::t!("All Files").as_ref(), &["*"]);
let handle = if save {
dialog.set_file_name("layers.ocslmap").save_file().await?
} else {
@@ -504,9 +504,9 @@ pub async fn pick_and_load_web(
progress: Arc<OpenProgressState>,
) -> WebOpenOutcome {
let Some(handle) = crate::sys::file_dialog()
.set_title("Open CAD file")
.add_filter("CAD Files", &["dwg", "dxf", "DWG", "DXF"])
.add_filter("All Files", &["*"])
.set_title(crate::t!("Open CAD file").as_ref())
.add_filter(crate::t!("CAD Files").as_ref(), &["dwg", "dxf", "DWG", "DXF"])
.add_filter(crate::t!("All Files").as_ref(), &["*"])
.pick_file()
.await
else {
@@ -1506,10 +1506,10 @@ mod save_failure_tests {
/// Show a file-open dialog and load the selected CTB or STB file.
pub async fn pick_plot_style() -> Option<plot_style::PlotStyleTable> {
let dialog = crate::sys::file_dialog()
.set_title("Load Plot Style Table")
.add_filter("Plot Style Tables", &["ctb", "CTB"])
.add_filter("CTB Files", &["ctb", "CTB"])
.add_filter("All Files", &["*"]);
.set_title(crate::t!("Load Plot Style Table").as_ref())
.add_filter(crate::t!("Plot Style Tables").as_ref(), &["ctb", "CTB"])
.add_filter(crate::t!("CTB Files").as_ref(), &["ctb", "CTB"])
.add_filter(crate::t!("All Files").as_ref(), &["*"]);
#[cfg(not(target_arch = "wasm32"))]
let dialog = match plot_style::ensure_plot_styles_dir() {
Ok(dir) => dialog.set_directory(dir),
@@ -1525,11 +1525,11 @@ pub async fn pick_plot_style() -> Option<plot_style::PlotStyleTable> {
/// Returns `(path, pixel_width, pixel_height)` or an error string.
pub async fn pick_image_file() -> Result<(PathBuf, u32, u32), String> {
let handle = crate::sys::file_dialog()
.set_title("Select Image File")
.add_filter("Images", &["png", "jpg", "jpeg", "bmp", "tiff", "tif"])
.add_filter("PNG", &["png"])
.add_filter("JPEG", &["jpg", "jpeg"])
.add_filter("All Files", &["*"])
.set_title(crate::t!("Select Image File").as_ref())
.add_filter(crate::t!("Images").as_ref(), &["png", "jpg", "jpeg", "bmp", "tiff", "tif"])
.add_filter(crate::t!("PNG").as_ref(), &["png"])
.add_filter(crate::t!("JPEG").as_ref(), &["jpg", "jpeg"])
.add_filter(crate::t!("All Files").as_ref(), &["*"])
.pick_file()
.await
.ok_or_else(|| "Cancelled".to_string())?;
+3 -3
View File
@@ -206,10 +206,10 @@ pub fn pick_pdf_path_owned(
) -> Option<std::path::PathBuf> {
let path = crate::sys::blocking_file_dialog()
.set_parent(parent)
.set_title("Export as PDF")
.set_title(crate::t!("Export as PDF").as_ref())
.set_file_name(&format!("{stem}.pdf"))
.add_filter("PDF Files", &["pdf"])
.add_filter("All Files", &["*"])
.add_filter(crate::t!("PDF Files").as_ref(), &["pdf"])
.add_filter(crate::t!("All Files").as_ref(), &["*"])
.save_file()
?;
crate::config::remember_dialog_dir(&path);
File diff suppressed because it is too large. Load diff
+7 -7
View File
@@ -280,14 +280,14 @@ impl CadCommand for ArcLengthDimensionCommand {
fn prompt(&self) -> String {
if self.awaiting_text {
return "DIMARC Enter dimension text (blank = measured value):".to_string();
return crate::t!("DIMARC Enter dimension text (blank = measured value):").into_owned();
}
if self.awaiting_angle {
return "DIMARC Specify text angle (degrees):".to_string();
return crate::t!("DIMARC Specify text angle (degrees):").into_owned();
}
match self.step {
Step::SelectObject => {
"DIMARC Select arc or polyline arc segment:".to_string()
crate::t!("DIMARC Select arc or polyline arc segment:").into_owned()
}
Step::DimLine(selection) => {
let leader_option = if selection.sweep() > std::f64::consts::FRAC_PI_2 {
@@ -295,15 +295,15 @@ impl CadCommand for ArcLengthDimensionCommand {
} else {
""
};
format!(
crate::tf!(
"DIMARC Specify arc length dimension location [Mtext/Text/Angle/Partial{leader_option}]:"
)
).into_owned()
}
Step::PartialFirst(_) => {
"DIMARC Specify first point of partial arc:".to_string()
crate::t!("DIMARC Specify first point of partial arc:").into_owned()
}
Step::PartialSecond { .. } => {
"DIMARC Specify second point of partial arc:".to_string()
crate::t!("DIMARC Specify second point of partial arc:").into_owned()
}
}
}
+3 -4
View File
@@ -385,16 +385,15 @@ impl CadCommand for DimBaselineCommand {
fn prompt(&self) -> String {
if self.base.is_none() {
"DIMBASELINE Select base dimension:".to_string()
crate::t!("DIMBASELINE Select base dimension:").into_owned()
} else if self
.base
.as_ref()
.is_some_and(|base| matches!(&base.kind, BaselineKind::Ordinate { .. }))
{
"DIMBASELINE Specify feature location [Undo/Select] <Select>:".to_string()
crate::t!("DIMBASELINE Specify feature location [Undo/Select] <Select>:").into_owned()
} else {
"DIMBASELINE Specify second extension line origin [Select/Undo] <Select>:"
.to_string()
crate::t!("DIMBASELINE Specify second extension line origin [Select/Undo] <Select>:").into_owned()
}
}
+3 -4
View File
@@ -322,16 +322,15 @@ impl CadCommand for DimContinueCommand {
fn prompt(&self) -> String {
if self.base.is_none() {
"DIMCONTINUE Select continued dimension:".to_string()
crate::t!("DIMCONTINUE Select continued dimension:").into_owned()
} else if self
.base
.as_ref()
.is_some_and(|base| matches!(&base.kind, ContinueKind::Ordinate { .. }))
{
"DIMCONTINUE Specify feature location [Undo/Select] <Select>:".to_string()
crate::t!("DIMCONTINUE Specify feature location [Undo/Select] <Select>:").into_owned()
} else {
"DIMCONTINUE Specify second extension line origin [Select/Undo] <Select>:"
.to_string()
crate::t!("DIMCONTINUE Specify second extension line origin [Select/Undo] <Select>:").into_owned()
}
}
+1 -1
View File
@@ -249,7 +249,7 @@ impl CadCommand for TextCommand {
"TEXT Current style: {}, Height: {}, Annotative: {}",
self.style_name,
self.height,
if self.annotative { "Yes" } else { "No" }
if self.annotative { t!("Yes") } else { t!("No") }
),
t!("TEXT Specify start point or [Justify/Style]:")
),
+2 -2
View File
@@ -573,7 +573,7 @@ impl CadCommand for HatchCommand {
"D",
),
CmdOption::new(
&format!("Island style: {}", self.island_style_label()),
&crate::tf!("Island style: {}", self.island_style_label()),
"Y",
),
];
@@ -603,7 +603,7 @@ impl CadCommand for HatchCommand {
"D",
),
CmdOption::new(
&format!("Island style: {}", self.island_style_label()),
&crate::tf!("Island style: {}", self.island_style_label()),
"Y",
),
];
+8 -8
View File
@@ -53,14 +53,14 @@ impl Report {
self.translated.iter().map(|(_, _, n)| n).sum()
}
/// The transaction log AutoCAD writes beside the drawing, as text.
/// Layer translation log stored beside the drawing.
pub fn to_log(&self) -> String {
let mut out = String::new();
for (from, to, moved) in &self.translated {
out.push_str(&format!("{from} -> {to} ({moved} object(s))\n"));
out.push_str(&crate::tf!("{from} -> {to} ({moved} object(s))\n").into_owned());
}
for (layer, reason) in &self.skipped {
out.push_str(&format!("{layer}: skipped — {reason}\n"));
out.push_str(&crate::tf!("{layer}: skipped — {reason}\n").into_owned());
}
out
}
@@ -85,7 +85,7 @@ pub fn load_targets(path: &Path) -> Result<Vec<TargetLayer>, String> {
.collect();
targets.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
if targets.is_empty() {
return Err("no layers found in that file".into());
return Err(crate::t!("no layers found in that file").into_owned());
}
Ok(targets)
}
@@ -143,20 +143,20 @@ pub fn translate(
if mapping.from == "0" {
report
.skipped
.push((mapping.from.clone(), "layer \"0\" cannot be translated".into()));
.push((mapping.from.clone(), crate::t!("layer \"0\" cannot be translated").into_owned()));
continue;
}
if mapping.from.eq_ignore_ascii_case(current_layer) {
report.skipped.push((
mapping.from.clone(),
"it is the current layer".into(),
crate::t!("it is the current layer").into_owned(),
));
continue;
}
if !scene.document.layers.contains(&mapping.from) {
report
.skipped
.push((mapping.from.clone(), "no such layer in this drawing".into()));
.push((mapping.from.clone(), crate::t!("no such layer in this drawing").into_owned()));
continue;
}
// Bring the target in with the standard's own properties. An existing
@@ -170,7 +170,7 @@ pub fn translate(
else {
report
.skipped
.push((mapping.from.clone(), "target layer is not in the loaded set".into()));
.push((mapping.from.clone(), crate::t!("target layer is not in the loaded set").into_owned()));
continue;
};
let mut layer = target.layer.clone();
+12 -12
View File
@@ -383,27 +383,27 @@ impl CadCommand for PolysolidCommand {
fn prompt(&self) -> String {
match self.step {
Step::Start => format!(
Step::Start => crate::tf!(
"POLYSOLID Height = {:.4}, Width = {:.4}, Justification = {}\n{}",
self.height,
self.width,
match self.justification {
PolysolidJustification::Left => "Left",
PolysolidJustification::Center => "Center",
PolysolidJustification::Right => "Right",
PolysolidJustification::Left => crate::t!("Left"),
PolysolidJustification::Center => crate::t!("Center"),
PolysolidJustification::Right => crate::t!("Right"),
},
t!("Specify start point or [Object/Height/Width/Justify] <Object>:")
),
Step::Height => format!("Specify height <{:.4}>:", self.height),
Step::Width => format!("Specify width <{:.4}>:", self.width),
Step::Justify => format!(
).into_owned(),
Step::Height => crate::tf!("Specify height <{:.4}>:", self.height).into_owned(),
Step::Width => crate::tf!("Specify width <{:.4}>:", self.width).into_owned(),
Step::Justify => crate::tf!(
"Enter justification [Left/Center/Right] <{}>:",
match self.justification {
PolysolidJustification::Left => "Left",
PolysolidJustification::Center => "Center",
PolysolidJustification::Right => "Right",
PolysolidJustification::Left => crate::t!("Left"),
PolysolidJustification::Center => crate::t!("Center"),
PolysolidJustification::Right => crate::t!("Right"),
}
),
).into_owned(),
Step::Object => t!("Select object:").into_owned(),
Step::Line if self.vertices.len() >= 3 => {
t!("Specify next point or [Arc/Close/Undo]:").into_owned()
+2 -11
View File
@@ -8,8 +8,6 @@
// command at all). Naming each style once, next to the command that applies it,
// is what keeps the three in step. (#621)
use std::sync::OnceLock;
use acadrust::entities::ViewportRenderMode as Mode;
use crate::modules::IconKind;
@@ -95,16 +93,9 @@ pub fn keyword_choices() -> Vec<(&'static str, &'static str, Option<&'static str
.collect()
}
/// The prompt those choices are announced with. Built once from the table so
/// the line the user reads cannot list something the picker does not offer.
/// Stable source used by the shared command prompt translator.
pub fn keyword_prompt() -> &'static str {
static PROMPT: OnceLock<String> = OnceLock::new();
PROMPT
.get_or_init(|| {
let listed: Vec<&str> = VISUAL_STYLES.iter().map(|style| style.label).collect();
format!("Visual style [{}]:", listed.join(" / "))
})
.as_str()
"Visual style [Wireframe 2D/Wireframe 3D/Hidden Line/Flat Shaded/Gouraud Shaded/Flat Shaded + Edges/Gouraud Shaded + Edges]:"
}
pub fn label_for(mode: Mode) -> &'static str {
+13 -9
View File
@@ -442,20 +442,24 @@ pub mod web_diag {
padding:8px 12px;max-height:40vh;overflow:auto;\
user-select:text;cursor:text;",
);
// Inline `onclick` keeps this dependency-free (no JS closures):
// Copy puts the full error text on the clipboard; Dismiss
// removes the overlay so the app stays usable underneath.
overlay.set_inner_html(
"<div><b>OpenCADStudio renderer error</b> — please copy \
this into a bug report: \
<button style=\"margin-left:8px\" onclick=\"navigator.clipboard.writeText(\
document.getElementById('ocs-err-text').innerText)\">Copy</button> \
<button onclick=\"document.getElementById('ocs-err').remove()\">\
Dismiss</button></div>\
"<div><b id=\"ocs-err-title\"></b> \
<button id=\"ocs-err-copy\" style=\"margin-left:8px\" onclick=\"navigator.clipboard.writeText(\
document.getElementById('ocs-err-text').innerText)\"></button> \
<button id=\"ocs-err-dismiss\" onclick=\"document.getElementById('ocs-err').remove()\"></button></div>\
<pre id=\"ocs-err-text\" style=\"margin:6px 0 0;\
white-space:pre-wrap;user-select:text;\"></pre>",
);
let _ = body.append_child(&overlay);
for (id, label) in [
("ocs-err-title", crate::t!("OpenCADStudio renderer error — copy this into a bug report:")),
("ocs-err-copy", crate::t!("Copy")),
("ocs-err-dismiss", crate::t!("Dismiss")),
] {
if let Some(element) = doc.get_element_by_id(id) {
element.set_text_content(Some(label.as_ref()));
}
}
match doc.get_element_by_id("ocs-err-text") {
Some(pre) => pre,
None => return,
+15 -15
View File
@@ -169,7 +169,7 @@ pub fn color_selector_varies<'a>(
on_toggle: Message,
on_more: Message,
) -> Element<'a, Message> {
let indicator = container(text("?").size(10))
let indicator = container(text(crate::t!("?")).size(10))
.style(|theme: &Theme| {
let palette = theme.palette();
container::Style {
@@ -434,7 +434,7 @@ pub fn index_color_page<'a>(
}
let recent: Element<'a, Message> = if recent_colors.is_empty() {
text("No recent colors yet")
text(crate::t!("No recent colors yet"))
.size(10)
.style(|theme: &Theme| iced::widget::text::Style {
color: Some(
@@ -464,18 +464,18 @@ pub fn index_color_page<'a>(
format!("ACI {i} RGB {r}, {g}, {b}")
}
AcadColor::Rgb { r, g, b } => {
format!("True Color RGB {r}, {g}, {b}")
crate::tf!("True Color RGB {r}, {g}, {b}").into_owned()
}
AcadColor::ByLayer => "ByLayer".to_string(),
AcadColor::ByBlock => "ByBlock".to_string(),
AcadColor::None => "None".to_string(),
AcadColor::ByLayer => t!("ByLayer").into_owned(),
AcadColor::ByBlock => t!("ByBlock").into_owned(),
AcadColor::None => t!("None").into_owned(),
};
let tabs = row![
button(text("Index Color").size(12))
button(text(crate::t!("Index Color")).size(12))
.style(button::primary)
.padding([5, 14]),
button(text("True Color").size(12))
button(text(crate::t!("True Color")).size(12))
.on_press(Message::ColorPickerTabChanged(
crate::app::ColorPickerTab::TrueColor
))
@@ -486,11 +486,11 @@ pub fn index_color_page<'a>(
let actions = container(
row![
button(text("Cancel").size(11))
button(text(crate::t!("Cancel")).size(11))
.on_press(Message::CloseColorPicker)
.style(button::secondary)
.padding([5, 14]),
button(text("OK").size(11))
button(text(crate::t!("OK")).size(11))
.on_press(Message::ColorWindowPick(current))
.style(button::primary)
.padding([5, 18]),
@@ -503,11 +503,11 @@ pub fn index_color_page<'a>(
container(
column![
tabs,
text("Standard colors").size(11),
text(crate::t!("Standard colors")).size(11),
standard,
text("CAD Color Index (ACI) 10–249").size(11),
text(crate::t!("CAD Color Index (ACI) 10–249")).size(11),
text("Color family → · Shade / intensity ↓")
text(crate::t!("Color family → · Shade / intensity ↓"))
.size(9)
.style(|theme: &Theme| iced::widget::text::Style {
color: Some(theme.palette().background.base.text.scale_alpha(0.65)),
@@ -527,11 +527,11 @@ pub fn index_color_page<'a>(
..Default::default()
}),
text("Grayscale 250–255").size(11),
text(crate::t!("Grayscale 250–255")).size(11),
grayscale,
text("Recently used").size(11),
text(crate::t!("Recently used")).size(11),
recent,
+1 -1
View File
@@ -769,7 +769,7 @@ impl CommandLine {
style
})
.padding([2, 6]);
let mcp_tip = container(text(mcp_tooltip).size(11))
let mcp_tip = container(text(t!(mcp_tooltip)).size(11))
.padding([3, 6])
.style(container::bordered_box);
let mcp_btn = container(tooltip(mcp_btn, mcp_tip, tooltip::Position::Top).gap(4))
+6 -6
View File
@@ -323,8 +323,8 @@ let mut layer_usage = vec![Vec::<String>::new(); 256];
} else {
crate::io::plot_style::LW_TABLE
.get(e.lineweight as usize)
.map(|lw| format!("{lw:.2}mm (idx {})", e.lineweight))
.unwrap_or_else(|| format!("idx {}", e.lineweight))
.map(|lw| crate::tf!("{lw:.2}mm (idx {})", e.lineweight).into_owned())
.unwrap_or_else(|| crate::tf!("idx {}", e.lineweight).into_owned())
}
})
.unwrap_or_else(|| "—".into());
@@ -491,7 +491,7 @@ let mut layer_usage = vec![Vec::<String>::new(); 256];
container(
column![
row![
text(format!("Layers using ACI {selected_aci}"))
text(crate::tf!("Layers using ACI {selected_aci}"))
.size(11),
Space::new().width(iced::Length::Fill),
@@ -500,9 +500,9 @@ let mut layer_usage = vec![Vec::<String>::new(); 256];
"{} {}",
selected_layers.len(),
if selected_layers.len() == 1 {
"layer"
t!("layer")
} else {
"layers"
t!("layers")
}
))
.size(10)
@@ -512,7 +512,7 @@ let mut layer_usage = vec![Vec::<String>::new(); 256];
if selected_layers.is_empty() {
column![
text("No layers use this ACI in the current drawing.")
text(t!("No layers use this ACI in the current drawing."))
.size(10)
.style(muted_style)
]
+3 -3
View File
@@ -254,11 +254,11 @@ impl canvas::Program<Message> for TablePreviewCanvas {
let active = &self.rows[self.active_row.min(2)];
frame.fill_text(canvas::Text {
content: format!(
"{} · {} · type {} / unit {}{}{}",
"{} · {} · {} {} / {} {}{}{}",
active.text_style,
if self.annotative { t!("Annotative") } else { t!("Drawing units") },
active.data_type,
active.unit_type,
t!("Type"), active.data_type,
t!("Unit"), active.unit_type,
if self.description.trim().is_empty() { "" } else { " · " },
self.description,
),
+1 -1
View File
@@ -118,7 +118,7 @@ pub fn color_label(c: AcadColor) -> String {
AcadColor::Index(5) => "Blue".into(),
AcadColor::Index(6) => "Magenta".into(),
AcadColor::Index(7) => "White".into(),
AcadColor::Index(n) => format!("Color {n}"),
AcadColor::Index(n) => crate::tf!("Color {n}").into_owned(),
AcadColor::Rgb { r, g, b } => format!("{r},{g},{b}"),
}
}
+5 -5
View File
@@ -210,18 +210,18 @@ pub fn view(palette: &BlockPalette, width: f32, auto_collapse: bool) -> Element<
style
})
.padding([3, 5]);
let pin = tooltip(pin, text("Auto").size(10), tooltip::Position::Bottom).gap(4);
let pin = tooltip(pin, text(crate::t!("Auto")).size(10), tooltip::Position::Bottom).gap(4);
let close = button(crate::ui::icons::themed_secondary(crate::ui::icons::CLOSE, 12.0))
.on_press(Message::Dock(DockMsg::Close(PanelId::BlockPalette)))
.style(button::subtle)
.padding([3, 5]);
let close = tooltip(close, text("Close").size(10), tooltip::Position::Bottom).gap(4);
let close = tooltip(close, text(crate::t!("Close")).size(10), tooltip::Position::Bottom).gap(4);
let title_bar = mouse_area(
container(
row![
text("Block Palette").size(12),
text(crate::t!("Block Palette")).size(12),
iced::widget::Space::new().width(Fill),
pin,
close,
@@ -239,7 +239,7 @@ pub fn view(palette: &BlockPalette, width: f32, auto_collapse: bool) -> Element<
.on_press(Message::Dock(DockMsg::DockGrab(PanelId::BlockPalette)))
.interaction(iced::mouse::Interaction::Grab);
let search_input = text_input("Search blocks…", &palette.search)
let search_input = text_input(&crate::t!("Search blocks…"), &palette.search)
.on_input(|v| Message::BlockPalette(BlockPaletteMsg::Search(v)))
.padding([4, 8])
.size(12);
@@ -264,7 +264,7 @@ pub fn view(palette: &BlockPalette, width: f32, auto_collapse: bool) -> Element<
} else {
"No matches"
};
container(text(msg).size(12).color(iced::Color { r: 0.55, g: 0.55, b: 0.55, a: 1.0 }))
container(text(crate::t!(msg)).size(12).color(iced::Color { r: 0.55, g: 0.55, b: 0.55, a: 1.0 }))
.center_x(Fill)
.center_y(Fill)
.width(Fill)
+1 -1
View File
@@ -288,7 +288,7 @@ pub fn view_window<'a>(
.into_iter()
.map(|(value, label)| Labelled {
value,
label: label.to_string(),
label: crate::t!(label).into_owned(),
})
.collect::<Vec<_>>();
+20 -2
View File
@@ -47,13 +47,31 @@ impl PlotChoice {
impl fmt::Display for PlotChoice {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.localized {
formatter.write_str(crate::i18n::translate(&self.raw).as_ref())
if let Some(name) = self.raw.strip_prefix("View: ") {
formatter.write_str(crate::tf!("View: {name}").as_ref())
} else {
formatter.write_str(crate::i18n::translate(&self.raw).as_ref())
}
} else {
formatter.write_str(&self.raw)
}
}
}
#[cfg(test)]
mod tests {
use super::PlotChoice;
#[test]
fn named_view_display_preserves_the_saved_plot_area() {
let name = "Plan: north";
let choice = PlotChoice::localized(format!("View: {name}"));
assert_eq!(choice.to_string(), crate::tf!("View: {name}").as_ref());
assert_eq!(choice.raw.strip_prefix("View: "), Some(name));
assert_eq!(PlotChoice::raw("View: PDF printer").to_string(), "View: PDF printer");
}
}
/// One of the many boolean plot options (folded into a single message so the
/// dialog needn't carry a variant per checkbox).
#[derive(Debug, Clone, Copy)]
@@ -716,7 +734,7 @@ pub fn view_window(
area_options.extend(
s.plot_views
.iter()
.map(|name| PlotChoice::raw(format!("View: {name}"))),
.map(|name| PlotChoice::localized(format!("View: {name}"))),
);
}
let mut area_row = row![
+26
View File
@@ -43,6 +43,7 @@
<link data-trunk rel="copy-file" href="web/ocs-parse-worker.js" />
<link data-trunk rel="copy-file" href="web/discussions.json" />
<link data-trunk rel="copy-file" href="web/videos.json" />
<link data-trunk rel="copy-file" href="web/locale-labels.json" />
</head>
<body>
<div id="loading">
@@ -53,6 +54,31 @@
<script>
(() => {
const splash = document.getElementById("loading");
fetch("locale-labels.json").then(response => response.json()).then(labels => {
let saved;
try {
saved = JSON.parse(localStorage.getItem("opencadstudio.settings"))?.settings?.language;
} catch {}
const preferred = navigator.languages?.length ? navigator.languages : [navigator.language];
const requested = typeof saved === "string" && saved !== "system" ? [saved, ...preferred] : preferred;
const available = Object.keys(labels);
let locale = "en-US";
for (const candidate of requested) {
try {
const language = new Intl.Locale(candidate);
const match = available.find(value => value.toLowerCase() === language.baseName.toLowerCase())
|| (language.language === "zh"
? (language.maximize().script === "Hant" ? "zh-TW" : "zh-CN")
: available.find(value => value.startsWith(language.language + "-")));
if (match) { locale = match; break; }
} catch {}
}
document.title = labels[locale].title;
document.documentElement.lang = locale;
document.documentElement.dir = locale === "ar-SA" ? "rtl" : "ltr";
const caption = splash?.querySelector(".sub");
if (caption) caption.textContent = labels[locale].loading;
}).catch(() => {});
const done = () => {
if (!splash) return;
splash.classList.add("hide");
+86
View File
@@ -0,0 +1,86 @@
{
"ar-SA": {
"title": "Open CAD Studio — الويب",
"loading": "جارٍ التحميل…"
},
"bg-BG": {
"title": "Open CAD Studio — Уеб",
"loading": "Зареждане…"
},
"cs-CZ": {
"title": "Open CAD Studio — Web",
"loading": "Načítání…"
},
"de-DE": {
"title": "Open CAD Studio — Web",
"loading": "Wird geladen…"
},
"el-GR": {
"title": "Open CAD Studio — Ιστός",
"loading": "Φόρτωση…"
},
"en-US": {
"title": "Open CAD Studio Web App",
"loading": "Loading…"
},
"es-ES": {
"title": "Open CAD Studio — Web",
"loading": "Cargando…"
},
"fi-FI": {
"title": "Open CAD Studio — Web",
"loading": "Ladataan…"
},
"fr-FR": {
"title": "Open CAD Studio — Site Web",
"loading": "Chargement…"
},
"hi-IN": {
"title": "Open CAD Studio — वेब",
"loading": "लोड हो रहा है…"
},
"hu-HU": {
"title": "Open CAD Studio — Web",
"loading": "Betöltés…"
},
"it-IT": {
"title": "Open CAD Studio — Rete",
"loading": "Caricamento…"
},
"ja-JP": {
"title": "Open CAD Studio — Web",
"loading": "読み込み中…"
},
"ko-KR": {
"title": "Open CAD Studio — 웹",
"loading": "로드 중…"
},
"nl-NL": {
"title": "Open CAD Studio — Web",
"loading": "Laden…"
},
"pl-PL": {
"title": "Open CAD Studio — Sieć",
"loading": "Ładowanie…"
},
"pt-BR": {
"title": "Open CAD Studio — Web",
"loading": "Carregando…"
},
"ru-RU": {
"title": "Open CAD Studio — Веб-сайт",
"loading": "Загрузка…"
},
"tr-TR": {
"title": "Open CAD Studio — Web",
"loading": "Yükleniyor…"
},
"zh-CN": {
"title": "Open CAD Studio — Web",
"loading": "正在加载…"
},
"zh-TW": {
"title": "Open CAD Studio — Web",
"loading": "載入中…"
}
}