From b90b3e3ecfe2eda2f28b5b48cc5966818fe560da Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Sun, 2 Aug 2026 18:53:38 +0300 Subject: [PATCH] feat(locale): add Fluent translations Default to the platform language with persisted overrides across native and web builds. Refs #630 --- Cargo.lock | 403 ++++++++++++++++++++++++++- Cargo.toml | 5 + i18n.toml | 5 + locales/de-DE/opencadstudio.ftl | 113 ++++++++ locales/en-US/opencadstudio.ftl | 113 ++++++++ locales/fr-FR/opencadstudio.ftl | 113 ++++++++ locales/hi-IN/opencadstudio.ftl | 101 +++++++ locales/nl-NL/opencadstudio.ftl | 113 ++++++++ locales/ru-RU/opencadstudio.ftl | 116 ++++++++ locales/tr-TR/opencadstudio.ftl | 101 +++++++ locales/zh-CN/opencadstudio.ftl | 101 +++++++ src/app/mod.rs | 11 +- src/app/settings.rs | 4 + src/app/update/file.rs | 4 + src/app/update/mod.rs | 16 ++ src/app/view/mod.rs | 58 ++-- src/app/view/modal.rs | 63 ++--- src/i18n.rs | 153 ++++++++++ src/lib.rs | 1 + src/main.rs | 1 + src/modules/draw/modify/copy.rs | 24 +- src/modules/draw/modify/translate.rs | 13 +- src/ui/command_line.rs | 11 +- src/ui/modal.rs | 2 +- src/ui/ribbon/mod.rs | 2 +- src/ui/window/options.rs | 46 +-- 26 files changed, 1581 insertions(+), 112 deletions(-) create mode 100644 i18n.toml create mode 100644 locales/de-DE/opencadstudio.ftl create mode 100644 locales/en-US/opencadstudio.ftl create mode 100644 locales/fr-FR/opencadstudio.ftl create mode 100644 locales/hi-IN/opencadstudio.ftl create mode 100644 locales/nl-NL/opencadstudio.ftl create mode 100644 locales/ru-RU/opencadstudio.ftl create mode 100644 locales/tr-TR/opencadstudio.ftl create mode 100644 locales/zh-CN/opencadstudio.ftl create mode 100644 src/i18n.rs diff --git a/Cargo.lock b/Cargo.lock index 7b6f41ba..434e3165 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,6 +30,8 @@ dependencies = [ "fontdb", "getrandom 0.3.4", "glam", + "i18n-embed", + "i18n-embed-fl", "iced", "iced_aw", "iced_core", @@ -45,6 +47,7 @@ dependencies = [ "printpdf", "rayon", "rfd", + "rust-embed", "rustc-hash 2.1.3", "semver", "serde", @@ -116,7 +119,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -371,6 +374,15 @@ dependencies = [ "x11rb", ] +[[package]] +name = "arc-swap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" +dependencies = [ + "rustversion", +] + [[package]] name = "arg_enum_proc_macro" version = "0.3.4" @@ -661,6 +673,15 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "basic-toml" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" +dependencies = [ + "serde", +] + [[package]] name = "bincode" version = "1.3.3" @@ -760,6 +781,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + [[package]] name = "block-padding" version = "0.3.3" @@ -1002,7 +1032,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", "inout", ] @@ -1116,6 +1146,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "convert_case" version = "0.4.0" @@ -1238,6 +1274,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -1300,6 +1345,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + [[package]] name = "ctor" version = "0.10.1" @@ -1391,8 +1445,19 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "crypto-common", + "block-buffer 0.10.4", + "crypto-common 0.1.7", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid", + "crypto-common 0.2.2", ] [[package]] @@ -1413,6 +1478,17 @@ dependencies = [ "objc2 0.6.4", ] +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + [[package]] name = "dlib" version = "0.5.3" @@ -1664,6 +1740,15 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "find-crate" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59a98bbaacea1c0eb6a0876280051b892eb73594fd90cf3b20e9c817029c57d2" +dependencies = [ + "toml 0.5.11", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -1698,6 +1783,51 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8" +[[package]] +name = "fluent" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8137a6d5a2c50d6b0ebfcb9aaa91a28154e0a70605f112d30cb0cd4a78670477" +dependencies = [ + "fluent-bundle", + "unic-langid", +] + +[[package]] +name = "fluent-bundle" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01203cb8918f5711e73891b347816d932046f95f54207710bda99beaeb423bf4" +dependencies = [ + "fluent-langneg", + "fluent-syntax", + "intl-memoizer", + "intl_pluralrules", + "rustc-hash 2.1.3", + "self_cell", + "smallvec", + "unic-langid", +] + +[[package]] +name = "fluent-langneg" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eebbe59450baee8282d71676f3bfed5689aeab00b27545e83e5f14b1195e8b0" +dependencies = [ + "unic-langid", +] + +[[package]] +name = "fluent-syntax" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54f0d287c53ffd184d04d8677f590f4ac5379785529e5e08b1c8083acdd5c198" +dependencies = [ + "memchr 2.8.3", + "thiserror 2.0.18", +] + [[package]] name = "foldhash" version = "0.1.5" @@ -2172,6 +2302,83 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "hybrid-array" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" +dependencies = [ + "typenum", +] + +[[package]] +name = "i18n-config" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e06b90c8a0d252e203c94344b21e35a30f3a3a85dc7db5af8f8df9f3e0c63ef" +dependencies = [ + "basic-toml", + "log", + "serde", + "serde_derive", + "thiserror 1.0.69", + "unic-langid", +] + +[[package]] +name = "i18n-embed" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a217bbb075dcaefb292efa78897fc0678245ca67f265d12c351e42268fcb0305" +dependencies = [ + "arc-swap", + "fluent", + "fluent-langneg", + "fluent-syntax", + "i18n-embed-impl", + "intl-memoizer", + "log", + "parking_lot", + "rust-embed", + "sys-locale", + "thiserror 1.0.69", + "unic-langid", + "walkdir", + "web-sys", +] + +[[package]] +name = "i18n-embed-fl" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "602e6bd30c3db2749e13e38b363a3d98d9d41de1d8de7a79c31bb69e45b47cda" +dependencies = [ + "find-crate", + "fluent", + "fluent-syntax", + "i18n-config", + "i18n-embed", + "proc-macro-error3", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.119", + "unic-langid", +] + +[[package]] +name = "i18n-embed-impl" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f2cc0e0523d1fe6fc2c6f66e5038624ea8091b3e7748b5e8e0c84b1698db6c2" +dependencies = [ + "find-crate", + "i18n-config", + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "iana-time-zone" version = "0.1.65" @@ -2494,6 +2701,25 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "intl-memoizer" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "310da2e345f5eb861e7a07ee182262e94975051db9e4223e909ba90f392f163f" +dependencies = [ + "type-map", + "unic-langid", +] + +[[package]] +name = "intl_pluralrules" +version = "7.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078ea7b7c29a2b4df841a7f6ac8775ff6074020c6776d48491ce2268e068f972" +dependencies = [ + "unic-langid", +] + [[package]] name = "inventory" version = "0.3.24" @@ -2872,7 +3098,7 @@ dependencies = [ "nom_locate", "rand 0.9.5", "rangemap", - "sha2", + "sha2 0.10.9", "stringprep", "thiserror 2.0.18", "time", @@ -2993,7 +3219,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ "cfg-if", - "digest", + "digest 0.10.7", ] [[package]] @@ -3029,6 +3255,22 @@ dependencies = [ "autocfg", ] +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -4127,6 +4369,28 @@ dependencies = [ "version_check", ] +[[package]] +name = "proc-macro-error-attr3" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0084e6206a967a2dad822180626b2f6b07a3b379325e8f1ec0438e33a469ba7" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error3" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cf066225f2373bc711684792b69bdeac0356019b007e721090c24d92d5d5a50" +dependencies = [ + "proc-macro-error-attr3", + "proc-macro2", + "quote", + "syn 3.0.3", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -4687,6 +4951,41 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" +[[package]] +name = "rust-embed" +version = "8.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9e7760e252aaba7b09f4be00e36476cf585bdb68a53552ac954cdf504ab4bc9" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bcfc4d6f53af43755f7a723e4b6b8794fcce052a178dd8c6c1dadc5f5343097" +dependencies = [ + "mime_guess", + "proc-macro2", + "quote", + "rust-embed-utils", + "syn 2.0.119", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ffa149f6aa81b58a5b3011d01a857c4ed12c7a732d2c51947a4c7c692185f0" +dependencies = [ + "sha2 0.11.0", + "walkdir", +] + [[package]] name = "rustc-hash" version = "1.1.0" @@ -5013,8 +5312,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", ] [[package]] @@ -5354,6 +5664,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "sys-locale" version = "0.3.2" @@ -5514,6 +5835,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "serde_core", + "zerovec", +] + [[package]] name = "tinyvec" version = "1.12.0" @@ -5529,6 +5861,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + [[package]] name = "toml" version = "1.1.3+spec-1.1.0" @@ -5800,6 +6141,15 @@ dependencies = [ "core_maths", ] +[[package]] +name = "type-map" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" +dependencies = [ + "rustc-hash 2.1.3", +] + [[package]] name = "typenum" version = "1.20.1" @@ -5823,6 +6173,25 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "unic-langid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ba52c9b05311f4f6e62d5d9d46f094bd6e84cb8df7b3ef952748d752a7d05" +dependencies = [ + "unic-langid-impl", +] + +[[package]] +name = "unic-langid-impl" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce1bf08044d4b7a94028c93786f8566047edc11110595914de93362559bc658" +dependencies = [ + "serde", + "tinystr", +] + [[package]] name = "unicase" version = "2.9.0" @@ -6885,7 +7254,7 @@ version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0986a8b1d586b7d3e4fe3d9ea39fb451ae22869dcea4aa109d287a374d866087" dependencies = [ - "toml", + "toml 1.1.3+spec-1.1.0", "version_check", ] @@ -7099,12 +7468,28 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" + [[package]] name = "zeroize" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "serde", + "zerofrom", +] + [[package]] name = "zmij" version = "1.0.23" diff --git a/Cargo.toml b/Cargo.toml index b17bb868..4e67408d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,9 @@ acadrust = { version = "0.4", features = ["serde"] } dwg-thumbnailer = { path = "crates/dwg-thumbnailer" } flate2 = "1" image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] } +i18n-embed = { version = "0.16", features = ["fluent-system"] } +i18n-embed-fl = "0.10" +rust-embed = "8" inventory = "0.3" truck-shapeops = { version = "0.4", optional = true } rustc-hash = "2" @@ -64,6 +67,7 @@ iced_widget = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0 [target.'cfg(not(target_arch = "wasm32"))'.dependencies] ocs_plugin_api = { path = "crates/ocs_plugin_api", features = ["host"] } +i18n-embed = { version = "0.16", features = ["desktop-requester"] } rayon = "1" open = "5" printpdf = { version = "0.9.1", default-features = false } @@ -71,6 +75,7 @@ ureq = { version = "3", default-features = false, features = ["rustls", "platfor [target.'cfg(target_arch = "wasm32")'.dependencies] ocs_plugin_api = { path = "crates/ocs_plugin_api" } +i18n-embed = { version = "0.16", features = ["web-sys-requester"] } console_error_panic_hook = "0.1" log = "0.4" getrandom = { version = "0.3", features = ["wasm_js"] } diff --git a/i18n.toml b/i18n.toml new file mode 100644 index 00000000..f50d471b --- /dev/null +++ b/i18n.toml @@ -0,0 +1,5 @@ +fallback_language = "en-US" + +[fluent] +assets_dir = "locales" +domain = "opencadstudio" diff --git a/locales/de-DE/opencadstudio.ftl b/locales/de-DE/opencadstudio.ftl new file mode 100644 index 00000000..fd5b0fb7 --- /dev/null +++ b/locales/de-DE/opencadstudio.ftl @@ -0,0 +1,113 @@ +language-system = Systemsprache +language-english = English +language-turkish = Türkçe +language-dutch = Nederlands +language-french = Français +language-german = Deutsch +language-hindi = हिन्दी +language-russian = Русский +language-chinese-simplified = 简体中文 + +action-close = Schließen +action-options = Optionen +action-plugins = Plug-ins + +ribbon-tab-draw = Zeichnen +ribbon-tab-annotate = Beschriften +ribbon-tab-insert = Einfügen +ribbon-tab-model = Modell +ribbon-tab-layout = Layout +ribbon-tab-manage = Verwalten +ribbon-tab-view = Ansicht + +options-language-section = Sprache +options-language-label = Oberflächensprache: +options-open-save-section = Öffnen und Speichern +options-default-save-format-label = Standardspeicherformat: +options-default-save-format-help = Wird beim ersten Speichern einer neuen Zeichnung verwendet. Vorhandene Zeichnungen behalten Dateityp und Version bei. +options-theme-section = Design +options-theme-label = Anwendungsdesign: +options-theme-help = Das Ändern einer Grundfarbe aktiviert das benutzerdefinierte Design. Alle Komponentenfarben werden aus diesen sechs Farben erzeugt. +options-color-background = Hintergrund +options-color-text = Text +options-color-primary = Primär +options-color-success = Erfolg +options-color-warning = Warnung +options-color-danger = Gefahr + +command-line-ready = Open CAD Studio ist bereit. +command-line-hint = Geben Sie einen Befehl ein oder verwenden Sie das Menüband. Öffnen Sie OBJ über die Registerkarte Einfügen. +command-line-label = Befehl: +command-line-literal-spaces = Wörtliche Leerzeichen: Das Leerzeichen bleibt in der Zeile, statt den Befehl auszuführen. Bleibt bis zum Ausschalten aktiv. + +start-new-drawing = Neue Zeichnung +start-open-file = Datei öffnen… +start-donate = Spenden +start-send-feedback = Feedback senden +start-sponsors = Sponsoren +start-tutorials = Lernprogramme +start-loading-videos = Videos werden geladen… +start-videos-online = Videos werden aus dem Internet geladen. +start-open-playlist = Wiedergabeliste auf YouTube öffnen +start-discussions = Diskussionen +start-pinned = Angeheftet +start-loading-discussions = Diskussionen werden geladen… +start-discussions-online = Diskussionen werden von GitHub geladen. +start-open-discussions = Diskussionen auf GitHub öffnen +start-supporters = Unterstützer +start-support-on-patreon = Auf Patreon unterstützen +start-recent-files = Zuletzt verwendete Dateien +start-videos = Videos +start-welcome = Willkommen +start-recent-documents = Zuletzt verwendete Dokumente +start-no-recent-files = Geöffnete Dateien werden hier angezeigt. +start-browser-storage = Browserspeicher +start-keep-recent-files = Zuletzt verwendete Dateien behalten + +modal-about = Info +modal-keyboard-shortcuts = Tastenkombinationen +modal-command-aliases = Befehlsaliase +modal-find-replace = Suchen und Ersetzen +modal-plugin-manager = Plug-in-Manager +modal-update-available = Aktualisierung verfügbar +modal-layer-manager = Layer-Manager +modal-layer-state-manager = Layerstatus-Manager +modal-edit-layer-state = Layerstatus bearbeiten +modal-plot = Plotten +modal-layout-manager = Layout-Manager +modal-scale-manager = Maßstabs-Manager +modal-annotation-object-scale = Maßstab des Beschriftungsobjekts +modal-plot-style-editor = Plotstileditor +modal-text-style-manager = Textstil-Manager +modal-multiline-style-manager = Mehrfachlinienstil-Manager +modal-table-style-manager = Tabellenstil-Manager +modal-multileader-style-manager = Multi-Führungslinienstil-Manager +modal-dimension-style-manager = Bemaßungsstil-Manager +modal-default-application = Standardanwendung +modal-save-warning = Speicherwarnung +modal-unable-save = Zeichnung kann nicht gespeichert werden +modal-drawing-changed = Zeichnung auf dem Datenträger geändert +modal-delete-layer = Layer löschen +modal-unsaved-changes = Nicht gespeicherte Änderungen +modal-point-style = Punktstil +modal-attribute-editor = Attribut-Editor +modal-save-drawing-as = Zeichnung speichern unter + +command-move-base = + MOVE Basispunkt angeben [{ $count -> + [one] { $count } Objekt + *[other] { $count } Objekte + }]: +command-move-target = MOVE Zielpunkt angeben [Basis { $x },{ $y }]: +command-copy-array-count = COPY Anzahl der Elemente im Array eingeben: +command-copy-base = + COPY Basispunkt angeben [{ $count -> + [one] { $count } Objekt + *[other] { $count } Objekte + }]: +command-copy-array-target = COPY Zweiten Punkt für das Array mit { $count } Elementen angeben [Basis { $x },{ $y }]: +command-copy-target = + COPY Zielpunkt angeben [{ $count -> + [one] bisher { $count } Kopie + *[other] bisher { $count } Kopien + } | Array | Eingabe=fertig | Basis { $x },{ $y }]: diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl new file mode 100644 index 00000000..93a374f8 --- /dev/null +++ b/locales/en-US/opencadstudio.ftl @@ -0,0 +1,113 @@ +language-system = System language +language-english = English +language-turkish = Türkçe +language-dutch = Nederlands +language-french = Français +language-german = Deutsch +language-hindi = हिन्दी +language-russian = Русский +language-chinese-simplified = Simplified Chinese + +action-close = Close +action-options = Options +action-plugins = Plugins + +ribbon-tab-draw = Draw +ribbon-tab-annotate = Annotate +ribbon-tab-insert = Insert +ribbon-tab-model = Model +ribbon-tab-layout = Layout +ribbon-tab-manage = Manage +ribbon-tab-view = View + +options-language-section = Language +options-language-label = Interface language: +options-open-save-section = Open and Save +options-default-save-format-label = Default save format: +options-default-save-format-help = Used for the first save of a new drawing. Existing drawings keep their file type and version. +options-theme-section = Theme +options-theme-label = Application theme: +options-theme-help = Changing a base colour switches to Custom. The theme generates every component shade from these six colours. +options-color-background = Background +options-color-text = Text +options-color-primary = Primary +options-color-success = Success +options-color-warning = Warning +options-color-danger = Danger + +command-line-ready = Open CAD Studio ready. +command-line-hint = Type a command or use the ribbon. Open OBJ from the Insert tab. +command-line-label = Command: +command-line-literal-spaces = Literal spaces: Space stays in the line instead of running the command. Stays on until toggled off. + +start-new-drawing = New Drawing +start-open-file = Open File… +start-donate = Donate +start-send-feedback = Send Feedback +start-sponsors = Sponsors +start-tutorials = Tutorials +start-loading-videos = Loading videos… +start-videos-online = Videos load from the internet. +start-open-playlist = Open playlist on YouTube +start-discussions = Discussions +start-pinned = Pinned +start-loading-discussions = Loading discussions… +start-discussions-online = Discussions load from GitHub. +start-open-discussions = Open Discussions on GitHub +start-supporters = Supporters +start-support-on-patreon = Support on Patreon +start-recent-files = Recent Files +start-videos = Videos +start-welcome = Welcome +start-recent-documents = Recent Documents +start-no-recent-files = Files you open will show up here. +start-browser-storage = Browser storage +start-keep-recent-files = Keep recent files + +modal-about = About +modal-keyboard-shortcuts = Keyboard Shortcuts +modal-command-aliases = Command Aliases +modal-find-replace = Find and Replace +modal-plugin-manager = Plugin Manager +modal-update-available = Update Available +modal-layer-manager = Layer Manager +modal-layer-state-manager = Layer State Manager +modal-edit-layer-state = Edit Layer State +modal-plot = Plot +modal-layout-manager = Layout Manager +modal-scale-manager = Scale Manager +modal-annotation-object-scale = Annotation Object Scale +modal-plot-style-editor = Plot Style Editor +modal-text-style-manager = Text Style Manager +modal-multiline-style-manager = Multiline Style Manager +modal-table-style-manager = Table Style Manager +modal-multileader-style-manager = Multileader Style Manager +modal-dimension-style-manager = Dimension Style Manager +modal-default-application = Default Application +modal-save-warning = Save Warning +modal-unable-save = Unable to Save Drawing +modal-drawing-changed = Drawing Changed on Disk +modal-delete-layer = Delete Layer +modal-unsaved-changes = Unsaved Changes +modal-point-style = Point Style +modal-attribute-editor = Attribute Editor +modal-save-drawing-as = Save Drawing As + +command-move-base = + MOVE Specify base point [{ $count -> + [one] { $count } object + *[other] { $count } objects + }]: +command-move-target = MOVE Specify destination [base { $x },{ $y }]: +command-copy-array-count = COPY Enter number of items to array: +command-copy-base = + COPY Specify base point [{ $count -> + [one] { $count } object + *[other] { $count } objects + }]: +command-copy-array-target = COPY Specify second point for { $count }-item array [base { $x },{ $y }]: +command-copy-target = + COPY Specify destination [{ $count -> + [one] { $count } copy so far + *[other] { $count } copies so far + } | Array | Enter=done | base { $x },{ $y }]: diff --git a/locales/fr-FR/opencadstudio.ftl b/locales/fr-FR/opencadstudio.ftl new file mode 100644 index 00000000..fcea2630 --- /dev/null +++ b/locales/fr-FR/opencadstudio.ftl @@ -0,0 +1,113 @@ +language-system = Langue du système +language-english = English +language-turkish = Türkçe +language-dutch = Nederlands +language-french = Français +language-german = Deutsch +language-hindi = हिन्दी +language-russian = Русский +language-chinese-simplified = 简体中文 + +action-close = Fermer +action-options = Options +action-plugins = Extensions + +ribbon-tab-draw = Dessin +ribbon-tab-annotate = Annoter +ribbon-tab-insert = Insérer +ribbon-tab-model = Modèle +ribbon-tab-layout = Présentation +ribbon-tab-manage = Gérer +ribbon-tab-view = Affichage + +options-language-section = Langue +options-language-label = Langue de l'interface : +options-open-save-section = Ouvrir et enregistrer +options-default-save-format-label = Format d'enregistrement par défaut : +options-default-save-format-help = Utilisé lors du premier enregistrement d'un nouveau dessin. Les dessins existants conservent leur type de fichier et leur version. +options-theme-section = Thème +options-theme-label = Thème de l'application : +options-theme-help = La modification d'une couleur de base active le thème personnalisé. Le thème génère toutes les nuances des composants à partir de ces six couleurs. +options-color-background = Arrière-plan +options-color-text = Texte +options-color-primary = Principale +options-color-success = Succès +options-color-warning = Avertissement +options-color-danger = Danger + +command-line-ready = Open CAD Studio est prêt. +command-line-hint = Saisissez une commande ou utilisez le ruban. Ouvrez un fichier OBJ depuis l'onglet Insérer. +command-line-label = Commande : +command-line-literal-spaces = Espaces littéraux : l'espace reste dans la ligne au lieu d'exécuter la commande. Ce mode reste actif jusqu'à sa désactivation. + +start-new-drawing = Nouveau dessin +start-open-file = Ouvrir un fichier… +start-donate = Faire un don +start-send-feedback = Envoyer des commentaires +start-sponsors = Sponsors +start-tutorials = Tutoriels +start-loading-videos = Chargement des vidéos… +start-videos-online = Les vidéos sont chargées depuis Internet. +start-open-playlist = Ouvrir la playlist sur YouTube +start-discussions = Discussions +start-pinned = Épinglé +start-loading-discussions = Chargement des discussions… +start-discussions-online = Les discussions sont chargées depuis GitHub. +start-open-discussions = Ouvrir les discussions sur GitHub +start-supporters = Contributeurs +start-support-on-patreon = Soutenir sur Patreon +start-recent-files = Fichiers récents +start-videos = Vidéos +start-welcome = Bienvenue +start-recent-documents = Documents récents +start-no-recent-files = Les fichiers que vous ouvrez apparaîtront ici. +start-browser-storage = Stockage du navigateur +start-keep-recent-files = Conserver les fichiers récents + +modal-about = À propos +modal-keyboard-shortcuts = Raccourcis clavier +modal-command-aliases = Alias de commandes +modal-find-replace = Rechercher et remplacer +modal-plugin-manager = Gestionnaire d'extensions +modal-update-available = Mise à jour disponible +modal-layer-manager = Gestionnaire de calques +modal-layer-state-manager = Gestionnaire d'états de calque +modal-edit-layer-state = Modifier l'état du calque +modal-plot = Tracer +modal-layout-manager = Gestionnaire de présentations +modal-scale-manager = Gestionnaire d'échelles +modal-annotation-object-scale = Échelle de l'objet annotatif +modal-plot-style-editor = Éditeur de styles de tracé +modal-text-style-manager = Gestionnaire de styles de texte +modal-multiline-style-manager = Gestionnaire de styles de multilignes +modal-table-style-manager = Gestionnaire de styles de tableau +modal-multileader-style-manager = Gestionnaire de styles de lignes de repère multiples +modal-dimension-style-manager = Gestionnaire de styles de cote +modal-default-application = Application par défaut +modal-save-warning = Avertissement d'enregistrement +modal-unable-save = Impossible d'enregistrer le dessin +modal-drawing-changed = Dessin modifié sur le disque +modal-delete-layer = Supprimer le calque +modal-unsaved-changes = Modifications non enregistrées +modal-point-style = Style de point +modal-attribute-editor = Éditeur d'attributs +modal-save-drawing-as = Enregistrer le dessin sous + +command-move-base = + MOVE Spécifiez le point de base [{ $count -> + [one] { $count } objet + *[other] { $count } objets + }] : +command-move-target = MOVE Spécifiez la destination [base { $x },{ $y }] : +command-copy-array-count = COPY Saisissez le nombre d'éléments du réseau : +command-copy-base = + COPY Spécifiez le point de base [{ $count -> + [one] { $count } objet + *[other] { $count } objets + }] : +command-copy-array-target = COPY Spécifiez le second point du réseau de { $count } éléments [base { $x },{ $y }] : +command-copy-target = + COPY Spécifiez la destination [{ $count -> + [one] { $count } copie jusqu'ici + *[other] { $count } copies jusqu'ici + } | Réseau | Entrée=terminer | base { $x },{ $y }] : diff --git a/locales/hi-IN/opencadstudio.ftl b/locales/hi-IN/opencadstudio.ftl new file mode 100644 index 00000000..56abc4ed --- /dev/null +++ b/locales/hi-IN/opencadstudio.ftl @@ -0,0 +1,101 @@ +language-system = सिस्टम भाषा +language-english = English +language-turkish = Türkçe +language-dutch = Nederlands +language-french = Français +language-german = Deutsch +language-hindi = हिन्दी +language-russian = Русский +language-chinese-simplified = 简体中文 + +action-close = बंद करें +action-options = विकल्प +action-plugins = प्लग-इन + +ribbon-tab-draw = ड्रॉ +ribbon-tab-annotate = एनोटेट +ribbon-tab-insert = इन्सर्ट +ribbon-tab-model = मॉडल +ribbon-tab-layout = लेआउट +ribbon-tab-manage = प्रबंधित करें +ribbon-tab-view = दृश्य + +options-language-section = भाषा +options-language-label = इंटरफ़ेस भाषा: +options-open-save-section = खोलना और सहेजना +options-default-save-format-label = डिफ़ॉल्ट सहेजने का प्रारूप: +options-default-save-format-help = नई ड्रॉइंग को पहली बार सहेजते समय उपयोग होता है। मौजूदा ड्रॉइंग अपना फ़ाइल प्रकार और संस्करण बनाए रखती हैं। +options-theme-section = थीम +options-theme-label = अनुप्रयोग थीम: +options-theme-help = किसी मूल रंग को बदलने पर कस्टम थीम सक्रिय हो जाती है। थीम इन छह रंगों से सभी घटक शेड बनाती है। +options-color-background = पृष्ठभूमि +options-color-text = पाठ +options-color-primary = प्राथमिक +options-color-success = सफलता +options-color-warning = चेतावनी +options-color-danger = खतरा + +command-line-ready = Open CAD Studio तैयार है। +command-line-hint = कोई कमांड लिखें या रिबन का उपयोग करें। OBJ को इन्सर्ट टैब से खोलें। +command-line-label = कमांड: +command-line-literal-spaces = वास्तविक स्पेस: स्पेस कमांड चलाने के बजाय पंक्ति में रहता है। दोबारा बंद करने तक यह चालू रहता है। + +start-new-drawing = नई ड्रॉइंग +start-open-file = फ़ाइल खोलें… +start-donate = दान करें +start-send-feedback = प्रतिक्रिया भेजें +start-sponsors = प्रायोजक +start-tutorials = ट्यूटोरियल +start-loading-videos = वीडियो लोड हो रहे हैं… +start-videos-online = वीडियो इंटरनेट से लोड होते हैं। +start-open-playlist = YouTube पर प्लेलिस्ट खोलें +start-discussions = चर्चाएँ +start-pinned = पिन किया गया +start-loading-discussions = चर्चाएँ लोड हो रही हैं… +start-discussions-online = चर्चाएँ GitHub से लोड होती हैं। +start-open-discussions = GitHub पर चर्चाएँ खोलें +start-supporters = समर्थक +start-support-on-patreon = Patreon पर समर्थन करें +start-recent-files = हाल की फ़ाइलें +start-videos = वीडियो +start-welcome = स्वागत है +start-recent-documents = हाल के दस्तावेज़ +start-no-recent-files = आपके द्वारा खोली गई फ़ाइलें यहाँ दिखाई देंगी। +start-browser-storage = ब्राउज़र संग्रहण +start-keep-recent-files = हाल की फ़ाइलें रखें + +modal-about = परिचय +modal-keyboard-shortcuts = कीबोर्ड शॉर्टकट +modal-command-aliases = कमांड उपनाम +modal-find-replace = खोजें और बदलें +modal-plugin-manager = प्लग-इन प्रबंधक +modal-update-available = अपडेट उपलब्ध है +modal-layer-manager = लेयर प्रबंधक +modal-layer-state-manager = लेयर स्थिति प्रबंधक +modal-edit-layer-state = लेयर स्थिति संपादित करें +modal-plot = प्लॉट +modal-layout-manager = लेआउट प्रबंधक +modal-scale-manager = स्केल प्रबंधक +modal-annotation-object-scale = एनोटेशन ऑब्जेक्ट स्केल +modal-plot-style-editor = प्लॉट शैली संपादक +modal-text-style-manager = टेक्स्ट शैली प्रबंधक +modal-multiline-style-manager = मल्टीलाइन शैली प्रबंधक +modal-table-style-manager = तालिका शैली प्रबंधक +modal-multileader-style-manager = मल्टीलीडर शैली प्रबंधक +modal-dimension-style-manager = डाइमेंशन शैली प्रबंधक +modal-default-application = डिफ़ॉल्ट अनुप्रयोग +modal-save-warning = सहेजने की चेतावनी +modal-unable-save = ड्रॉइंग सहेजी नहीं जा सकती +modal-drawing-changed = डिस्क पर ड्रॉइंग बदल गई +modal-delete-layer = लेयर हटाएँ +modal-unsaved-changes = न सहेजे गए बदलाव +modal-point-style = बिंदु शैली +modal-attribute-editor = विशेषता संपादक +modal-save-drawing-as = ड्रॉइंग इस रूप में सहेजें + +command-move-base = MOVE आधार बिंदु निर्दिष्ट करें [{ $count } ऑब्जेक्ट]: +command-move-target = MOVE गंतव्य निर्दिष्ट करें [आधार { $x },{ $y }]: +command-copy-array-count = COPY ऐरे में आइटमों की संख्या दर्ज करें: +command-copy-base = COPY आधार बिंदु निर्दिष्ट करें [{ $count } ऑब्जेक्ट]: +command-copy-array-target = COPY { $count }-आइटम ऐरे के लिए दूसरा बिंदु निर्दिष्ट करें [आधार { $x },{ $y }]: +command-copy-target = COPY गंतव्य निर्दिष्ट करें [अब तक { $count } प्रतियाँ | ऐरे | Enter=पूर्ण | आधार { $x },{ $y }]: diff --git a/locales/nl-NL/opencadstudio.ftl b/locales/nl-NL/opencadstudio.ftl new file mode 100644 index 00000000..5d5086a0 --- /dev/null +++ b/locales/nl-NL/opencadstudio.ftl @@ -0,0 +1,113 @@ +language-system = Systeemtaal +language-english = English +language-turkish = Türkçe +language-dutch = Nederlands +language-french = Français +language-german = Deutsch +language-hindi = हिन्दी +language-russian = Русский +language-chinese-simplified = 简体中文 + +action-close = Sluiten +action-options = Opties +action-plugins = Plug-ins + +ribbon-tab-draw = Tekenen +ribbon-tab-annotate = Annoteren +ribbon-tab-insert = Invoegen +ribbon-tab-model = Model +ribbon-tab-layout = Lay-out +ribbon-tab-manage = Beheren +ribbon-tab-view = Beeld + +options-language-section = Taal +options-language-label = Interfacetaal: +options-open-save-section = Openen en opslaan +options-default-save-format-label = Standaard opslagformaat: +options-default-save-format-help = Wordt gebruikt wanneer een nieuwe tekening voor het eerst wordt opgeslagen. Bestaande tekeningen behouden hun bestandstype en versie. +options-theme-section = Thema +options-theme-label = Toepassingsthema: +options-theme-help = Als u een basiskleur wijzigt, wordt het aangepaste thema geactiveerd. Het thema maakt alle componenttinten op basis van deze zes kleuren. +options-color-background = Achtergrond +options-color-text = Tekst +options-color-primary = Primair +options-color-success = Geslaagd +options-color-warning = Waarschuwing +options-color-danger = Gevaar + +command-line-ready = Open CAD Studio is gereed. +command-line-hint = Typ een opdracht of gebruik het lint. Open OBJ via het tabblad Invoegen. +command-line-label = Opdracht: +command-line-literal-spaces = Letterlijke spaties: een spatie blijft in de regel staan in plaats van de opdracht uit te voeren. Blijft actief tot u dit uitschakelt. + +start-new-drawing = Nieuwe tekening +start-open-file = Bestand openen… +start-donate = Doneren +start-send-feedback = Feedback verzenden +start-sponsors = Sponsors +start-tutorials = Zelfstudies +start-loading-videos = Video's laden… +start-videos-online = Video's worden via internet geladen. +start-open-playlist = Afspeellijst openen op YouTube +start-discussions = Discussies +start-pinned = Vastgezet +start-loading-discussions = Discussies laden… +start-discussions-online = Discussies worden geladen vanaf GitHub. +start-open-discussions = Discussies openen op GitHub +start-supporters = Ondersteuners +start-support-on-patreon = Ondersteunen op Patreon +start-recent-files = Recente bestanden +start-videos = Video's +start-welcome = Welkom +start-recent-documents = Recente documenten +start-no-recent-files = Bestanden die u opent, verschijnen hier. +start-browser-storage = Browseropslag +start-keep-recent-files = Recente bestanden bewaren + +modal-about = Info +modal-keyboard-shortcuts = Sneltoetsen +modal-command-aliases = Opdrachtaliassen +modal-find-replace = Zoeken en vervangen +modal-plugin-manager = Plug-inbeheer +modal-update-available = Update beschikbaar +modal-layer-manager = Lagenbeheer +modal-layer-state-manager = Laagstatusbeheer +modal-edit-layer-state = Laagstatus bewerken +modal-plot = Afdrukken +modal-layout-manager = Lay-outbeheer +modal-scale-manager = Schaalbeheer +modal-annotation-object-scale = Schaal van annotatieobject +modal-plot-style-editor = Afdrukstijleditor +modal-text-style-manager = Tekststijlbeheer +modal-multiline-style-manager = Meervoudige-lijnstijlbeheer +modal-table-style-manager = Tabelstijlbeheer +modal-multileader-style-manager = Multileaderstijlbeheer +modal-dimension-style-manager = Maatstijlbeheer +modal-default-application = Standaardtoepassing +modal-save-warning = Opslagwaarschuwing +modal-unable-save = Tekening kan niet worden opgeslagen +modal-drawing-changed = Tekening gewijzigd op schijf +modal-delete-layer = Laag verwijderen +modal-unsaved-changes = Niet-opgeslagen wijzigingen +modal-point-style = Puntstijl +modal-attribute-editor = Kenmerkeditor +modal-save-drawing-as = Tekening opslaan als + +command-move-base = + MOVE Geef het basispunt op [{ $count -> + [one] { $count } object + *[other] { $count } objecten + }]: +command-move-target = MOVE Geef de bestemming op [basis { $x },{ $y }]: +command-copy-array-count = COPY Voer het aantal items in de matrix in: +command-copy-base = + COPY Geef het basispunt op [{ $count -> + [one] { $count } object + *[other] { $count } objecten + }]: +command-copy-array-target = COPY Geef het tweede punt voor de matrix met { $count } items op [basis { $x },{ $y }]: +command-copy-target = + COPY Geef de bestemming op [{ $count -> + [one] { $count } kopie tot nu toe + *[other] { $count } kopieën tot nu toe + } | Matrix | Enter=gereed | basis { $x },{ $y }]: diff --git a/locales/ru-RU/opencadstudio.ftl b/locales/ru-RU/opencadstudio.ftl new file mode 100644 index 00000000..84496e59 --- /dev/null +++ b/locales/ru-RU/opencadstudio.ftl @@ -0,0 +1,116 @@ +language-system = Язык системы +language-english = English +language-turkish = Türkçe +language-dutch = Nederlands +language-french = Français +language-german = Deutsch +language-hindi = हिन्दी +language-russian = Русский +language-chinese-simplified = 简体中文 + +action-close = Закрыть +action-options = Параметры +action-plugins = Плагины + +ribbon-tab-draw = Рисование +ribbon-tab-annotate = Аннотации +ribbon-tab-insert = Вставка +ribbon-tab-model = Модель +ribbon-tab-layout = Лист +ribbon-tab-manage = Управление +ribbon-tab-view = Вид + +options-language-section = Язык +options-language-label = Язык интерфейса: +options-open-save-section = Открытие и сохранение +options-default-save-format-label = Формат сохранения по умолчанию: +options-default-save-format-help = Используется при первом сохранении нового чертежа. Существующие чертежи сохраняют тип и версию файла. +options-theme-section = Тема +options-theme-label = Тема приложения: +options-theme-help = Изменение базового цвета включает пользовательскую тему. Все оттенки компонентов создаются из этих шести цветов. +options-color-background = Фон +options-color-text = Текст +options-color-primary = Основной +options-color-success = Успех +options-color-warning = Предупреждение +options-color-danger = Опасность + +command-line-ready = Open CAD Studio готов. +command-line-hint = Введите команду или используйте ленту. Откройте OBJ на вкладке «Вставка». +command-line-label = Команда: +command-line-literal-spaces = Буквальные пробелы: пробел остаётся в строке, а не запускает команду. Режим действует до отключения. + +start-new-drawing = Новый чертёж +start-open-file = Открыть файл… +start-donate = Поддержать +start-send-feedback = Отправить отзыв +start-sponsors = Спонсоры +start-tutorials = Учебные материалы +start-loading-videos = Загрузка видео… +start-videos-online = Видео загружаются из Интернета. +start-open-playlist = Открыть плейлист на YouTube +start-discussions = Обсуждения +start-pinned = Закреплено +start-loading-discussions = Загрузка обсуждений… +start-discussions-online = Обсуждения загружаются с GitHub. +start-open-discussions = Открыть обсуждения на GitHub +start-supporters = Сторонники +start-support-on-patreon = Поддержать на Patreon +start-recent-files = Недавние файлы +start-videos = Видео +start-welcome = Добро пожаловать +start-recent-documents = Недавние документы +start-no-recent-files = Открытые вами файлы появятся здесь. +start-browser-storage = Хранилище браузера +start-keep-recent-files = Хранить недавние файлы + +modal-about = О программе +modal-keyboard-shortcuts = Сочетания клавиш +modal-command-aliases = Псевдонимы команд +modal-find-replace = Найти и заменить +modal-plugin-manager = Диспетчер плагинов +modal-update-available = Доступно обновление +modal-layer-manager = Диспетчер слоёв +modal-layer-state-manager = Диспетчер состояний слоёв +modal-edit-layer-state = Изменить состояние слоя +modal-plot = Печать +modal-layout-manager = Диспетчер листов +modal-scale-manager = Диспетчер масштабов +modal-annotation-object-scale = Масштаб аннотативного объекта +modal-plot-style-editor = Редактор стилей печати +modal-text-style-manager = Диспетчер стилей текста +modal-multiline-style-manager = Диспетчер стилей мультилиний +modal-table-style-manager = Диспетчер стилей таблиц +modal-multileader-style-manager = Диспетчер стилей мультивыносок +modal-dimension-style-manager = Диспетчер стилей размеров +modal-default-application = Приложение по умолчанию +modal-save-warning = Предупреждение о сохранении +modal-unable-save = Не удалось сохранить чертёж +modal-drawing-changed = Чертёж изменён на диске +modal-delete-layer = Удалить слой +modal-unsaved-changes = Несохранённые изменения +modal-point-style = Стиль точек +modal-attribute-editor = Редактор атрибутов +modal-save-drawing-as = Сохранить чертёж как + +command-move-base = + MOVE Укажите базовую точку [{ $count -> + [one] { $count } объект + [few] { $count } объекта + *[many] { $count } объектов + }]: +command-move-target = MOVE Укажите точку назначения [база { $x },{ $y }]: +command-copy-array-count = COPY Введите число элементов массива: +command-copy-base = + COPY Укажите базовую точку [{ $count -> + [one] { $count } объект + [few] { $count } объекта + *[many] { $count } объектов + }]: +command-copy-array-target = COPY Укажите вторую точку массива из { $count } элементов [база { $x },{ $y }]: +command-copy-target = + COPY Укажите точку назначения [{ $count -> + [one] создана { $count } копия + [few] создано { $count } копии + *[many] создано { $count } копий + } | Массив | Enter=готово | база { $x },{ $y }]: diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl new file mode 100644 index 00000000..03a8d16c --- /dev/null +++ b/locales/tr-TR/opencadstudio.ftl @@ -0,0 +1,101 @@ +language-system = Sistem dili +language-english = English +language-turkish = Türkçe +language-dutch = Nederlands +language-french = Français +language-german = Deutsch +language-hindi = हिन्दी +language-russian = Русский +language-chinese-simplified = 简体中文 + +action-close = Kapat +action-options = Seçenekler +action-plugins = Eklentiler + +ribbon-tab-draw = Çizim +ribbon-tab-annotate = Açıklama +ribbon-tab-insert = Ekle +ribbon-tab-model = Model +ribbon-tab-layout = Yerleşim +ribbon-tab-manage = Yönet +ribbon-tab-view = Görünüm + +options-language-section = Dil +options-language-label = Arayüz dili: +options-open-save-section = Açma ve Kaydetme +options-default-save-format-label = Varsayılan kayıt biçimi: +options-default-save-format-help = Yeni bir çizimin ilk kaydında kullanılır. Mevcut çizimler dosya türünü ve sürümünü korur. +options-theme-section = Tema +options-theme-label = Uygulama teması: +options-theme-help = Temel renklerden birini değiştirmek Özel temaya geçer. Tema, tüm bileşen tonlarını bu altı renkten üretir. +options-color-background = Arka plan +options-color-text = Metin +options-color-primary = Ana renk +options-color-success = Başarı +options-color-warning = Uyarı +options-color-danger = Tehlike + +command-line-ready = Open CAD Studio hazır. +command-line-hint = Bir komut yazın veya şeridi kullanın. OBJ dosyasını Ekle sekmesinden açın. +command-line-label = Komut: +command-line-literal-spaces = Değişmez boşluklar: Boşluk, komutu çalıştırmak yerine satırda kalır. Yeniden kapatılana kadar etkin kalır. + +start-new-drawing = Yeni Çizim +start-open-file = Dosya Aç… +start-donate = Bağış Yap +start-send-feedback = Geri Bildirim Gönder +start-sponsors = Sponsorlar +start-tutorials = Eğitimler +start-loading-videos = Videolar yükleniyor… +start-videos-online = Videolar internetten yüklenir. +start-open-playlist = Oynatma listesini YouTube'da aç +start-discussions = Tartışmalar +start-pinned = Sabitlenmiş +start-loading-discussions = Tartışmalar yükleniyor… +start-discussions-online = Tartışmalar GitHub'dan yüklenir. +start-open-discussions = Tartışmaları GitHub'da Aç +start-supporters = Destekçiler +start-support-on-patreon = Patreon'da Destekle +start-recent-files = Son Dosyalar +start-videos = Videolar +start-welcome = Hoş Geldiniz +start-recent-documents = Son Belgeler +start-no-recent-files = Açtığınız dosyalar burada görünür. +start-browser-storage = Tarayıcı depolama alanı +start-keep-recent-files = Son dosyaları sakla + +modal-about = Hakkında +modal-keyboard-shortcuts = Klavye Kısayolları +modal-command-aliases = Komut Kısaltmaları +modal-find-replace = Bul ve Değiştir +modal-plugin-manager = Eklenti Yöneticisi +modal-update-available = Güncelleme Var +modal-layer-manager = Katman Yöneticisi +modal-layer-state-manager = Katman Durumu Yöneticisi +modal-edit-layer-state = Katman Durumunu Düzenle +modal-plot = Yazdır +modal-layout-manager = Yerleşim Yöneticisi +modal-scale-manager = Ölçek Yöneticisi +modal-annotation-object-scale = Açıklama Nesnesi Ölçeği +modal-plot-style-editor = Yazdırma Stili Düzenleyicisi +modal-text-style-manager = Metin Stili Yöneticisi +modal-multiline-style-manager = Çoklu Çizgi Stili Yöneticisi +modal-table-style-manager = Tablo Stili Yöneticisi +modal-multileader-style-manager = Çoklu Kılavuz Stili Yöneticisi +modal-dimension-style-manager = Ölçülendirme Stili Yöneticisi +modal-default-application = Varsayılan Uygulama +modal-save-warning = Kayıt Uyarısı +modal-unable-save = Çizim Kaydedilemiyor +modal-drawing-changed = Çizim Diskte Değişti +modal-delete-layer = Katmanı Sil +modal-unsaved-changes = Kaydedilmemiş Değişiklikler +modal-point-style = Nokta Stili +modal-attribute-editor = Öznitelik Düzenleyicisi +modal-save-drawing-as = Çizimi Farklı Kaydet + +command-move-base = MOVE Temel noktayı belirtin [{ $count } nesne]: +command-move-target = MOVE Hedef noktayı belirtin [temel { $x },{ $y }]: +command-copy-array-count = COPY Dizideki öğe sayısını girin: +command-copy-base = COPY Temel noktayı belirtin [{ $count } nesne]: +command-copy-array-target = COPY { $count } öğeli dizi için ikinci noktayı belirtin [temel { $x },{ $y }]: +command-copy-target = COPY Hedef noktayı belirtin [şimdiye kadar { $count } kopya | Dizi | Enter=tamam | temel { $x },{ $y }]: diff --git a/locales/zh-CN/opencadstudio.ftl b/locales/zh-CN/opencadstudio.ftl new file mode 100644 index 00000000..b643289a --- /dev/null +++ b/locales/zh-CN/opencadstudio.ftl @@ -0,0 +1,101 @@ +language-system = 跟随系统 +language-english = English +language-turkish = Türkçe +language-dutch = Nederlands +language-french = Français +language-german = Deutsch +language-hindi = हिन्दी +language-russian = Русский +language-chinese-simplified = 简体中文 + +action-close = 关闭 +action-options = 选项 +action-plugins = 插件 + +ribbon-tab-draw = 绘图 +ribbon-tab-annotate = 注释 +ribbon-tab-insert = 插入 +ribbon-tab-model = 模型 +ribbon-tab-layout = 布局 +ribbon-tab-manage = 管理 +ribbon-tab-view = 视图 + +options-language-section = 语言 +options-language-label = 界面语言: +options-open-save-section = 打开和保存 +options-default-save-format-label = 默认保存格式: +options-default-save-format-help = 用于新图形的首次保存。现有图形保留其文件类型和版本。 +options-theme-section = 主题 +options-theme-label = 应用程序主题: +options-theme-help = 更改基础颜色会切换到自定义主题。主题将从这六种颜色生成所有组件色调。 +options-color-background = 背景 +options-color-text = 文字 +options-color-primary = 主要 +options-color-success = 成功 +options-color-warning = 警告 +options-color-danger = 危险 + +command-line-ready = Open CAD Studio 已就绪。 +command-line-hint = 输入命令或使用功能区。可从“插入”选项卡打开 OBJ。 +command-line-label = 命令: +command-line-literal-spaces = 保留空格:空格会留在输入行中而不执行命令,直到再次切换此模式。 + +start-new-drawing = 新建图形 +start-open-file = 打开文件… +start-donate = 捐赠 +start-send-feedback = 发送反馈 +start-sponsors = 赞助商 +start-tutorials = 教程 +start-loading-videos = 正在加载视频… +start-videos-online = 视频通过互联网加载。 +start-open-playlist = 在 YouTube 上打开播放列表 +start-discussions = 讨论 +start-pinned = 已置顶 +start-loading-discussions = 正在加载讨论… +start-discussions-online = 讨论从 GitHub 加载。 +start-open-discussions = 在 GitHub 上打开讨论 +start-supporters = 支持者 +start-support-on-patreon = 在 Patreon 上支持 +start-recent-files = 最近文件 +start-videos = 视频 +start-welcome = 欢迎 +start-recent-documents = 最近使用的文档 +start-no-recent-files = 您打开的文件将显示在此处。 +start-browser-storage = 浏览器存储 +start-keep-recent-files = 保留最近文件 + +modal-about = 关于 +modal-keyboard-shortcuts = 键盘快捷键 +modal-command-aliases = 命令别名 +modal-find-replace = 查找和替换 +modal-plugin-manager = 插件管理器 +modal-update-available = 有可用更新 +modal-layer-manager = 图层管理器 +modal-layer-state-manager = 图层状态管理器 +modal-edit-layer-state = 编辑图层状态 +modal-plot = 打印 +modal-layout-manager = 布局管理器 +modal-scale-manager = 比例管理器 +modal-annotation-object-scale = 注释对象比例 +modal-plot-style-editor = 打印样式编辑器 +modal-text-style-manager = 文字样式管理器 +modal-multiline-style-manager = 多线样式管理器 +modal-table-style-manager = 表格样式管理器 +modal-multileader-style-manager = 多重引线样式管理器 +modal-dimension-style-manager = 标注样式管理器 +modal-default-application = 默认应用程序 +modal-save-warning = 保存警告 +modal-unable-save = 无法保存图形 +modal-drawing-changed = 磁盘上的图形已更改 +modal-delete-layer = 删除图层 +modal-unsaved-changes = 未保存的更改 +modal-point-style = 点样式 +modal-attribute-editor = 属性编辑器 +modal-save-drawing-as = 图形另存为 + +command-move-base = MOVE 指定基点 [{ $count } 个对象]: +command-move-target = MOVE 指定目标点 [基点 { $x },{ $y }]: +command-copy-array-count = COPY 输入阵列项目数: +command-copy-base = COPY 指定基点 [{ $count } 个对象]: +command-copy-array-target = COPY 为 { $count } 项阵列指定第二点 [基点 { $x },{ $y }]: +command-copy-target = COPY 指定目标点 [已复制 { $count } 个 | 阵列 | 回车=完成 | 基点 { $x },{ $y }]: diff --git a/src/app/mod.rs b/src/app/mod.rs index 18a02b2a..8427171e 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -855,6 +855,8 @@ pub(super) struct OpenCADStudio { save_dialog_for_unsaved: bool, /// User preference for the first save of a new/unsaved drawing. default_save_format: String, + /// Persisted interface language selection. + language: crate::i18n::Language, // ── DimStyle Dialog ─────────────────────────────────────────────────── /// Name of the style currently shown in the dialog. @@ -1602,6 +1604,8 @@ pub enum Message { OptionsThemeChanged(String), /// Edit one of Custom theme's six base colours as #RRGGBB. OptionsThemeColorChanged(usize, String), + /// Switch the interface language and redraw localized views. + LanguageChanged(crate::i18n::Language), ClearScene, SetWireframe(bool), /// Set the active tab's render mode (one of acadrust's seven visual @@ -2648,6 +2652,10 @@ impl OpenCADStudio { } fn new() -> Self { + let config = config::AppConfig::load(); + if let Err(error) = crate::i18n::set_language(config.settings.language) { + eprintln!("Unable to apply saved UI language: {error}"); + } // Boot with only the Welcome/Start tab. The user creates drawings // explicitly (File → New); we never auto-spawn Drawing1. let start_tab = DocumentTab::new_start(); @@ -2827,6 +2835,7 @@ impl OpenCADStudio { save_dialog_filename: "drawing.dwg".to_string(), save_dialog_for_unsaved: false, default_save_format: crate::io::DEFAULT_SAVE_FORMAT.to_string(), + language: crate::i18n::Language::default(), // Plot style active_plot_style: crate::io::plot_style::PlotStyleTable::load_named( crate::io::plot_style::DEFAULT_PLOT_STYLE, @@ -2997,7 +3006,7 @@ impl OpenCADStudio { // so preferences, recents, status-bar layout, ribbon density and print // options survive across sessions (issue #68). `last_saved_config` is // seeded below so the first change — not the boot — triggers a write. - app.apply_config(config::AppConfig::load()); + app.apply_config(config); // Load command aliases from ocad.pgp (writes the shipped default file // on first launch). The hide-set keeps aliases out of autocomplete while // their target command still shows. diff --git a/src/app/settings.rs b/src/app/settings.rs index f941d33c..c7ed4f09 100644 --- a/src/app/settings.rs +++ b/src/app/settings.rs @@ -151,6 +151,9 @@ pub struct UserSettings { /// (#188). pub bg_color: Option<[u8; 3]>, pub paper_bg_color: Option<[u8; 3]>, + /// Interface language preference. `System` negotiates against the + /// platform locale on every launch. + pub language: crate::i18n::Language, } impl Default for UserSettings { @@ -177,6 +180,7 @@ impl Default for UserSettings { pick_drag_rect: false, bg_color: None, paper_bg_color: None, + language: crate::i18n::Language::default(), } } } diff --git a/src/app/update/file.rs b/src/app/update/file.rs index 065cffbe..8fa0aa5d 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -288,6 +288,7 @@ impl OpenCADStudio { pick_drag_rect: self.pick_drag_rect, bg_color: self.default_bg_color.map(f4_to_u3), paper_bg_color: self.default_paper_bg_color.map(f4_to_u3), + language: self.language, } } @@ -317,6 +318,9 @@ impl OpenCADStudio { self.pick_drag_rect = s.pick_drag_rect; self.default_bg_color = s.bg_color.map(u3_to_f4); self.default_paper_bg_color = s.paper_bg_color.map(u3_to_f4); + if crate::i18n::set_language(s.language).is_ok() { + self.language = s.language; + } // Push the restored background onto every drawing tab that exists now // (the start tab and any initial drawing). Tabs created later pick it // up via `apply_bg_default` at their construction site. diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 22b26039..7ab4a373 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -4323,6 +4323,22 @@ impl OpenCADStudio { Task::none() } + Message::LanguageChanged(language) => { + if self.language == language { + return Task::none(); + } + match crate::i18n::set_language(language) { + Ok(()) => { + self.language = language; + self.persist_settings_if_changed(); + } + Err(error) => self + .command_line + .push_error(&format!("Unable to change UI language: {error}")), + } + Task::none() + } + Message::AboutOpen => { self.active_modal = Some(super::ModalKind::About); Task::none() diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index 845ef34b..cec34342 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -2318,7 +2318,7 @@ fn start_page_content<'a>( let headline = text("Open CAD Studio").size(40).style(start_primary_style); // Plain outlined button (Open / New / Help / Contribute). - let outline_btn = |label: &'static str, msg: Message| { + let outline_btn = |label: String, msg: Message| { button(text(label).size(14)) .on_press(msg) .padding([10, 22]) @@ -2346,7 +2346,7 @@ fn start_page_content<'a>( button( row![ crate::ui::icons::themed_danger_text(crate::ui::icons::HEART, 14.0), - text("Donate").size(14), + text(crate::tr!("start-donate")).size(14), ] .spacing(5) .align_y(iced::Center), @@ -2360,8 +2360,8 @@ fn start_page_content<'a>( }; let primary_row = WrapFlow::new(vec![ - outline_btn("New Drawing", Message::TabNew).into(), - outline_btn("Open File…", Message::OpenFile).into(), + outline_btn(crate::tr!("start-new-drawing"), Message::TabNew).into(), + outline_btn(crate::tr!("start-open-file"), Message::OpenFile).into(), donate_btn.into(), ]) .spacing_x(12.0) @@ -2371,16 +2371,16 @@ fn start_page_content<'a>( #[cfg_attr(target_arch = "wasm32", allow(unused_mut))] let mut secondary_items: Vec> = vec![ outline_btn( - "Send Feedback", + crate::tr!("start-send-feedback"), Message::RibbonToolClick { tool_id: "REPORT".to_string(), event: crate::modules::ModuleEvent::Command("REPORT".to_string()), }, ) .into(), - outline_btn("Options", Message::OptionsOpen).into(), + outline_btn(crate::tr!("action-options"), Message::OptionsOpen).into(), ]; - secondary_items.push(outline_btn("Plugins", Message::PluginManagerOpen).into()); + secondary_items.push(outline_btn(crate::tr!("action-plugins"), Message::PluginManagerOpen).into()); // The web build is already in the browser, so only the desktop offers a // link to the web version. #[cfg(not(target_arch = "wasm32"))] @@ -2403,7 +2403,7 @@ fn start_page_content<'a>( .report_natural_width(action_width_out.clone()); let sponsors = column![ - text("Sponsors").size(15), + text(crate::tr!("start-sponsors")).size(15), mouse_area( container( iced::widget::svg(iced::widget::svg::Handle::from_memory(include_bytes!( @@ -2505,7 +2505,7 @@ fn start_page_content<'a>( // whole thumbnail remains visible when that width changes. let thumb_h = (panel_w - VIDEO_PANEL_PADDING * 2.0 - VIDEO_SCROLL_GUTTER) * 9.0 / 16.0; - let mut list = column![text("Tutorials").size(15)] + let mut list = column![text(crate::tr!("start-tutorials")).size(15)] .spacing(10) .width(Fill) // Keep the scrollbar off the thumbnails. @@ -2545,14 +2545,14 @@ fn start_page_content<'a>( } if videos.is_empty() { let note = if videos_loading { - "Loading videos…" + crate::tr!("start-loading-videos") } else { - "Videos load from the internet." + crate::tr!("start-videos-online") }; list = list.push(text(note).size(12).style(start_muted_style)); } let playlist_btn = mouse_area( - container(text("Open playlist on YouTube").size(12)) + container(text(crate::tr!("start-open-playlist")).size(12)) .padding([6, 10]) .width(Fill) .center_x(Fill) @@ -2605,7 +2605,7 @@ fn start_page_content<'a>( // web builds read the CI-generated snapshot. Both sources mark pinned // discussions and sort them before the rest of the list. let discussions_panel: Element<'a, Message> = { - let mut list = column![text("Discussions").size(15)] + let mut list = column![text(crate::tr!("start-discussions")).size(15)] .spacing(8) .width(Fill); for discussion in discussions { @@ -2618,7 +2618,7 @@ fn start_page_content<'a>( .align_y(iced::Center); if discussion.pinned { meta = meta.push( - text("Pinned") + text(crate::tr!("start-pinned")) .size(10) .style(start_primary_style), ); @@ -2661,14 +2661,14 @@ fn start_page_content<'a>( } if discussions.is_empty() { let note = if discussions_loading { - "Loading discussions…" + crate::tr!("start-loading-discussions") } else { - "Discussions load from GitHub." + crate::tr!("start-discussions-online") }; list = list.push(text(note).size(12).style(start_muted_style)); } let open_btn = mouse_area( - container(text("Open Discussions on GitHub").size(12)) + container(text(crate::tr!("start-open-discussions")).size(12)) .padding([6, 10]) .width(Fill) .center_x(Fill) @@ -2725,7 +2725,7 @@ fn start_page_content<'a>( // shows, so the rail always invites support. let supporters: Element<'a, Message> = { let mut list = column![ - text("Supporters").size(15), + text(crate::tr!("start-supporters")).size(15), Space::new().height(iced::Length::Fixed(12.0)), ] .spacing(6) @@ -2746,7 +2746,7 @@ fn start_page_content<'a>( container( iced::widget::row![ crate::ui::icons::themed_danger_text(crate::ui::icons::HEART, 13.0), - text("Support on Patreon").size(12), + text(crate::tr!("start-support-on-patreon")).size(12), ] .spacing(6) .align_y(iced::Center), @@ -2831,7 +2831,7 @@ fn start_page_content<'a>( .height(Fill) .into(), StartLayout::Compact => { - let tab_btn = |label: &'static str, section: super::StartSection| { + let tab_btn = |label: String, section: super::StartSection| { let is_active = active == section; button(text(label).size(14)) .on_press(Message::StartSectionSelect(section)) @@ -2864,11 +2864,11 @@ fn start_page_content<'a>( }) }; let tab_bar = Row::with_children(vec![ - tab_btn("Recent Files", super::StartSection::Recent).into(), - tab_btn("Videos", super::StartSection::Videos).into(), - tab_btn("Welcome", super::StartSection::Welcome).into(), - tab_btn("Discussions", super::StartSection::Discussions).into(), - tab_btn("Supporters", super::StartSection::Supporters).into(), + tab_btn(crate::tr!("start-recent-files"), super::StartSection::Recent).into(), + tab_btn(crate::tr!("start-videos"), super::StartSection::Videos).into(), + tab_btn(crate::tr!("start-welcome"), super::StartSection::Welcome).into(), + tab_btn(crate::tr!("start-discussions"), super::StartSection::Discussions).into(), + tab_btn(crate::tr!("start-supporters"), super::StartSection::Supporters).into(), ]) .spacing(6.0) .align_y(iced::Center) @@ -2944,11 +2944,11 @@ pub(super) fn recent_files_panel<'a>( ) -> Element<'a, Message> { // Title mirrors the Supporters rail: size 15 in the bright text colour, // followed by a 12px gap before the content. - let title = text("Recent Documents").size(15); + let title = text(crate::tr!("start-recent-documents")).size(15); let body: Element<'a, Message> = if recents.is_empty() { container( - text("Files you open will show up here.") + text(crate::tr!("start-no-recent-files")) .size(12) .style(start_muted_style) ) @@ -2976,7 +2976,7 @@ pub(super) fn recent_files_panel<'a>( // directory line. #[cfg(target_arch = "wasm32")] let dir = if dir.is_empty() { - "Browser storage".to_string() + crate::tr!("start-browser-storage") } else { dir }; @@ -3087,7 +3087,7 @@ pub(super) fn recent_files_panel<'a>( .padding([2, 6]) .width(iced::Length::Fixed(46.0)); let limit_row = row![ - text("Keep recent files").size(11).style(start_muted_style).width(Fill), + text(crate::tr!("start-keep-recent-files")).size(11).style(start_muted_style).width(Fill), button(crate::ui::icons::themed(crate::ui::icons::MINUS, 11.0)) .on_press(Message::SetRecentLimit(shown.saturating_sub(STEP))) .padding([3, 6]) diff --git a/src/app/view/modal.rs b/src/app/view/modal.rs index d01c5193..02b8fe49 100644 --- a/src/app/view/modal.rs +++ b/src/app/view/modal.rs @@ -5,41 +5,41 @@ use iced::{Background, Element, Fill, Fit, Theme}; impl OpenCADStudio { /// Title shown in the active modal's title bar. Keep in sync with the /// [`Self::modal_content`] dispatch. - pub(super) fn modal_title(&self) -> &'static str { + pub(super) fn modal_title(&self) -> String { use super::super::ModalKind as K; match self.active_modal { - Some(K::About) => "About", - Some(K::Shortcuts) => "Keyboard Shortcuts", - Some(K::Aliases) => "Command Aliases", - Some(K::Options) => "Options", - Some(K::FindReplace) => "Find and Replace", - Some(K::PluginManager) => "Plugin Manager", - Some(K::UpdateNotice) => "Update Available", - Some(K::Layers) => "Layer Manager", - Some(K::LayerStateManager) => "Layer State Manager", - Some(K::LayerStateEditor) => "Edit Layer State", - Some(K::Plot) => "Plot", - Some(K::LayoutManager) => "Layout Manager", - Some(K::ScaleManager) => "Scale Manager", - Some(K::AnnoObjectScale) => "Annotation Object Scale", - Some(K::Plotstyle) => "Plot Style Editor", - Some(K::TextStyle) => "Text Style Manager", - Some(K::MlStyle) => "Multiline Style Manager", - Some(K::TableStyle) => "Table Style Manager", - Some(K::MLeaderStyle) => "Multileader Style Manager", - Some(K::DimStyle) => "Dimension Style Manager", - Some(K::AssocPrompt) => "Default Application", - Some(K::AecDropWarning) => "Save Warning", + Some(K::About) => crate::tr!("modal-about"), + Some(K::Shortcuts) => crate::tr!("modal-keyboard-shortcuts"), + Some(K::Aliases) => crate::tr!("modal-command-aliases"), + Some(K::Options) => crate::tr!("action-options"), + Some(K::FindReplace) => crate::tr!("modal-find-replace"), + Some(K::PluginManager) => crate::tr!("modal-plugin-manager"), + Some(K::UpdateNotice) => crate::tr!("modal-update-available"), + Some(K::Layers) => crate::tr!("modal-layer-manager"), + Some(K::LayerStateManager) => crate::tr!("modal-layer-state-manager"), + Some(K::LayerStateEditor) => crate::tr!("modal-edit-layer-state"), + Some(K::Plot) => crate::tr!("modal-plot"), + Some(K::LayoutManager) => crate::tr!("modal-layout-manager"), + Some(K::ScaleManager) => crate::tr!("modal-scale-manager"), + Some(K::AnnoObjectScale) => crate::tr!("modal-annotation-object-scale"), + Some(K::Plotstyle) => crate::tr!("modal-plot-style-editor"), + Some(K::TextStyle) => crate::tr!("modal-text-style-manager"), + Some(K::MlStyle) => crate::tr!("modal-multiline-style-manager"), + Some(K::TableStyle) => crate::tr!("modal-table-style-manager"), + Some(K::MLeaderStyle) => crate::tr!("modal-multileader-style-manager"), + Some(K::DimStyle) => crate::tr!("modal-dimension-style-manager"), + Some(K::AssocPrompt) => crate::tr!("modal-default-application"), + Some(K::AecDropWarning) => crate::tr!("modal-save-warning"), #[cfg(not(target_arch = "wasm32"))] - Some(K::FileInUse) => "Unable to Save Drawing", + Some(K::FileInUse) => crate::tr!("modal-unable-save"), #[cfg(not(target_arch = "wasm32"))] - Some(K::ExternalChange) => "Drawing Changed on Disk", - Some(K::LayerDeleteWarning) => "Delete Layer", - Some(K::Unsaved) => "Unsaved Changes", - Some(K::PointStyle) => "Point Style", - Some(K::AttributeEditor) => "Attribute Editor", - Some(K::SaveDialog) => "Save Drawing As", - None => "", + Some(K::ExternalChange) => crate::tr!("modal-drawing-changed"), + Some(K::LayerDeleteWarning) => crate::tr!("modal-delete-layer"), + Some(K::Unsaved) => crate::tr!("modal-unsaved-changes"), + Some(K::PointStyle) => crate::tr!("modal-point-style"), + Some(K::AttributeEditor) => crate::tr!("modal-attribute-editor"), + Some(K::SaveDialog) => crate::tr!("modal-save-drawing-as"), + None => String::new(), } } @@ -90,6 +90,7 @@ impl OpenCADStudio { &self.default_save_format, &self.ui_theme, &self.theme_color_inputs, + self.language, flow, ) }, diff --git a/src/i18n.rs b/src/i18n.rs new file mode 100644 index 00000000..d0ea8713 --- /dev/null +++ b/src/i18n.rs @@ -0,0 +1,153 @@ +//! 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; +#[cfg(target_arch = "wasm32")] +use i18n_embed::WebLanguageRequester; +use rust_embed::RustEmbed; +use serde::{Deserialize, Serialize}; +use std::sync::OnceLock; + +#[derive(RustEmbed)] +#[folder = "locales/"] +struct Localizations; + +/// User-selectable UI language. `System` keeps following the platform's +/// preferred locale while explicit choices remain stable across restarts. +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] +pub enum Language { + #[default] + #[serde(rename = "system")] + System, + #[serde(rename = "en-US")] + EnUs, + #[serde(rename = "tr-TR")] + TrTr, + #[serde(rename = "nl-NL")] + NlNl, + #[serde(rename = "fr-FR")] + FrFr, + #[serde(rename = "de-DE")] + DeDe, + #[serde(rename = "hi-IN")] + HiIn, + #[serde(rename = "ru-RU")] + RuRu, + #[serde(rename = "zh-CN")] + ZhCn, +} + +impl Language { + pub const ALL: [Language; 9] = [ + Language::System, + Language::EnUs, + Language::TrTr, + Language::NlNl, + Language::FrFr, + Language::DeDe, + Language::HiIn, + Language::RuRu, + Language::ZhCn, + ]; + + fn requested(self) -> Vec { + match self { + Language::System => system_languages(), + Language::EnUs => vec!["en-US".parse().expect("valid locale")], + Language::TrTr => vec!["tr-TR".parse().expect("valid locale")], + Language::NlNl => vec!["nl-NL".parse().expect("valid locale")], + Language::FrFr => vec!["fr-FR".parse().expect("valid locale")], + Language::DeDe => vec!["de-DE".parse().expect("valid locale")], + Language::HiIn => vec!["hi-IN".parse().expect("valid locale")], + Language::RuRu => vec!["ru-RU".parse().expect("valid locale")], + Language::ZhCn => vec!["zh-CN".parse().expect("valid locale")], + } + } +} + +impl std::fmt::Display for Language { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let label = match self { + Language::System => crate::tr!("language-system"), + Language::EnUs => crate::tr!("language-english"), + Language::TrTr => crate::tr!("language-turkish"), + Language::NlNl => crate::tr!("language-dutch"), + Language::FrFr => crate::tr!("language-french"), + Language::DeDe => crate::tr!("language-german"), + Language::HiIn => crate::tr!("language-hindi"), + Language::RuRu => crate::tr!("language-russian"), + Language::ZhCn => crate::tr!("language-chinese-simplified"), + }; + f.write_str(&label) + } +} + +fn system_languages() -> Vec { + #[cfg(target_arch = "wasm32")] + let requested = WebLanguageRequester::requested_languages(); + #[cfg(not(target_arch = "wasm32"))] + let requested = DesktopLanguageRequester::requested_languages(); + requested +} + +fn load_language( + loader: &FluentLanguageLoader, + language: Language, +) -> Result<(), i18n_embed::I18nEmbedError> { + let mut requested = language.requested(); + if requested.is_empty() { + requested.push(loader.fallback_language().clone()); + } + i18n_embed::select(loader, &Localizations, &requested).map(|_| ()) +} + +pub fn loader() -> &'static FluentLanguageLoader { + static LOADER: OnceLock = OnceLock::new(); + LOADER.get_or_init(|| { + let loader = fluent_language_loader!(); + if let Err(error) = load_language(&loader, Language::System) { + eprintln!("Unable to load system UI language: {error}"); + loader + .load_languages(&Localizations, &[loader.fallback_language().clone()]) + .expect("fallback UI language must be embedded"); + } + loader + }) +} + +/// Apply a user preference process-wide. The loader swaps resources atomically, +/// so the next Iced view pass immediately receives the new language. +pub fn set_language(language: Language) -> Result<(), i18n_embed::I18nEmbedError> { + load_language(loader(), language) +} + +/// Built-in ribbon modules have stable ids; plug-ins keep their supplied title +/// until they provide their own localization bundle. +pub fn ribbon_module_title(id: &str, fallback: &str) -> String { + match id { + "draw" => crate::tr!("ribbon-tab-draw"), + "annotate" => crate::tr!("ribbon-tab-annotate"), + "insert" => crate::tr!("ribbon-tab-insert"), + "model" => crate::tr!("ribbon-tab-model"), + "layout" => crate::tr!("ribbon-tab-layout"), + "manage" => crate::tr!("ribbon-tab-manage"), + "view" => crate::tr!("ribbon-tab-view"), + _ => fallback.to_string(), + } +} + +#[macro_export] +macro_rules! tr { + ($message_id:literal $(,)?) => { + i18n_embed_fl::fl!($crate::i18n::loader(), $message_id) + }; + ($message_id:literal, $($name:ident = $value:expr),+ $(,)?) => { + i18n_embed_fl::fl!( + $crate::i18n::loader(), + $message_id, + $($name = $value),+ + ) + }; +} diff --git a/src/lib.rs b/src/lib.rs index eb9e2d5d..20875412 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,7 @@ pub mod config; pub mod cli; pub mod command; pub mod entities; +pub mod i18n; pub mod io; #[cfg(not(target_arch = "wasm32"))] pub(crate) mod network; diff --git a/src/main.rs b/src/main.rs index 158e28e4..6c893d83 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,6 +10,7 @@ mod config; mod cli; mod command; mod entities; +mod i18n; mod io; #[cfg(not(target_arch = "wasm32"))] mod network; diff --git a/src/modules/draw/modify/copy.rs b/src/modules/draw/modify/copy.rs index b41f0713..bb055407 100644 --- a/src/modules/draw/modify/copy.rs +++ b/src/modules/draw/modify/copy.rs @@ -61,23 +61,27 @@ impl CadCommand for CopyCommand { fn prompt(&self) -> String { if self.awaiting_count { - return "COPY Enter number of items to array:".to_string(); + return crate::tr!("command-copy-array-count"); } match &self.step { - Step::Base => format!( - "COPY Specify base point [{} objects]:", - self.handles.len() + Step::Base => crate::tr!( + "command-copy-base", + count = (self.handles.len() as i64), ), Step::Placing(base) => { if let Some(n) = self.array_count { - format!( - "COPY Specify second point for {n}-item array [base {:.3},{:.3}]:", - base.x, base.y + crate::tr!( + "command-copy-array-target", + count = (n as i64), + x = format!("{:.3}", base.x), + y = format!("{:.3}", base.y), ) } else { - format!( - "COPY Specify destination [{} copies so far | Array | Enter=done | base {:.3},{:.3}]:", - self.count, base.x, base.y + crate::tr!( + "command-copy-target", + count = (self.count as i64), + x = format!("{:.3}", base.x), + y = format!("{:.3}", base.y), ) } } diff --git a/src/modules/draw/modify/translate.rs b/src/modules/draw/modify/translate.rs index c9959fa4..d47cc1da 100644 --- a/src/modules/draw/modify/translate.rs +++ b/src/modules/draw/modify/translate.rs @@ -53,13 +53,14 @@ impl CadCommand for MoveCommand { fn prompt(&self) -> String { match &self.step { - Step::Base => format!( - "MOVE Specify base point [{} objects]:", - self.handles.len() + Step::Base => crate::tr!( + "command-move-base", + count = (self.handles.len() as i64), ), - Step::Target(base) => format!( - "MOVE Specify destination [base {:.3},{:.3}]:", - base.x, base.y + Step::Target(base) => crate::tr!( + "command-move-target", + x = format!("{:.3}", base.x), + y = format!("{:.3}", base.y), ), } } diff --git a/src/ui/command_line.rs b/src/ui/command_line.rs index dc769c4f..5d770f82 100644 --- a/src/ui/command_line.rs +++ b/src/ui/command_line.rs @@ -111,8 +111,8 @@ pub enum EntryKind { impl CommandLine { pub fn new() -> Self { let mut cl = Self::default(); - cl.push_info("Open CAD Studio ready."); - cl.push_info("Type a command or use the ribbon. Open OBJ: INSERT tab."); + cl.push_info(&crate::tr!("command-line-ready")); + cl.push_info(&crate::tr!("command-line-hint")); cl } @@ -430,7 +430,7 @@ impl CommandLine { } }); let prompt = container( - text("Command:").size(11).style(|theme: &Theme| iced::widget::text::Style { + text(crate::tr!("command-line-label")).size(11).style(|theme: &Theme| iced::widget::text::Style { color: Some(theme.palette().success.base.color), }), ) @@ -467,10 +467,7 @@ impl CommandLine { ..Default::default() } }); - let literal_tip = container( - text("Literal spaces: Space stays in the line instead of running the command (same as typing a leading '>'). Stays on until toggled off.") - .size(11), - ) + let literal_tip = container(text(crate::tr!("command-line-literal-spaces")).size(11)) .padding([3, 6]) .style(container::bordered_box); let literal_btn = tooltip(literal_btn, literal_tip, tooltip::Position::Top).gap(4); diff --git a/src/ui/modal.rs b/src/ui/modal.rs index 71fc3fce..90139a5d 100644 --- a/src/ui/modal.rs +++ b/src/ui/modal.rs @@ -253,7 +253,7 @@ pub fn backdrop<'a>( /// title bar; pass `Vector::ZERO` to keep it centred. pub fn modal<'a>( base: impl Into>, - title: &'a str, + title: impl iced::widget::text::IntoFragment<'a>, content: impl Into>, on_close: Message, offset: Vector, diff --git a/src/ui/ribbon/mod.rs b/src/ui/ribbon/mod.rs index 2cae9b81..8461e887 100644 --- a/src/ui/ribbon/mod.rs +++ b/src/ui/ribbon/mod.rs @@ -422,7 +422,7 @@ impl Ribbon { let is_active = i == self.active; let is_contextual = module.id() == "layout"; let btn = container( - button(text(module.title()).size(12)) + button(text(crate::i18n::ribbon_module_title(module.id(), module.title())).size(12)) .on_press(Message::RibbonSelectTab(i)) .style(move |theme: &Theme, status| { let palette = theme.palette(); diff --git a/src/ui/window/options.rs b/src/ui/window/options.rs index 865a54a5..1e20f6eb 100644 --- a/src/ui/window/options.rs +++ b/src/ui/window/options.rs @@ -9,6 +9,7 @@ pub fn view_window<'a>( default_save_format: &'a str, ui_theme: &'a UiThemeConfig, theme_color_inputs: &'a [String; 6], + language: crate::i18n::Language, sizing: crate::ui::modal::ModalSizing, ) -> Element<'a, Message> { let selected_format = crate::io::SAVE_FORMAT_OPTIONS @@ -25,12 +26,12 @@ pub fn view_window<'a>( let palette = ui_theme.palette.to_iced(); let colors = [ - ("Background", palette.background), - ("Text", palette.text), - ("Primary", palette.primary), - ("Success", palette.success), - ("Warning", palette.warning), - ("Danger", palette.danger), + (crate::tr!("options-color-background"), palette.background), + (crate::tr!("options-color-text"), palette.text), + (crate::tr!("options-color-primary"), palette.primary), + (crate::tr!("options-color-success"), palette.success), + (crate::tr!("options-color-warning"), palette.warning), + (crate::tr!("options-color-danger"), palette.danger), ]; let mut color_controls = column![].spacing(8); @@ -60,16 +61,31 @@ pub fn view_window<'a>( ); } - let close = button(text("Close").size(12)) + let close = button(text(crate::tr!("action-close")).size(12)) .on_press(Message::CloseModal) .padding([6, 18]) .style(button::secondary); let content = column![ - text("Open and Save").size(15), + text(crate::tr!("options-language-section")).size(15), Space::new().height(10), row![ - text("Default save format:").size(12).width(150), + text(crate::tr!("options-language-label")).size(12).width(150), + iced::widget::pick_list( + Some(language), + crate::i18n::Language::ALL, + |value| value.to_string(), + ) + .on_select(Message::LanguageChanged) + .width(sizing.width), + ] + .spacing(12) + .align_y(iced::Center), + Space::new().height(22), + text(crate::tr!("options-open-save-section")).size(15), + Space::new().height(10), + row![ + text(crate::tr!("options-default-save-format-label")).size(12).width(150), iced::widget::pick_list( selected_format, crate::io::SAVE_FORMAT_OPTIONS, @@ -81,16 +97,14 @@ pub fn view_window<'a>( .spacing(12) .align_y(iced::Center), Space::new().height(8), - text( - "Used for the first save of a new drawing. Existing drawings keep their file type and version." - ) + text(crate::tr!("options-default-save-format-help")) .size(11) .width(sizing.width), Space::new().height(22), - text("Theme").size(15), + text(crate::tr!("options-theme-section")).size(15), Space::new().height(10), row![ - text("Iced theme:").size(12).width(150), + text(crate::tr!("options-theme-label")).size(12).width(150), iced::widget::pick_list( selected_theme, theme_options, @@ -102,9 +116,7 @@ pub fn view_window<'a>( .spacing(12) .align_y(iced::Center), Space::new().height(8), - text( - "Changing a base colour switches to Custom. Iced generates every component shade from these six colours." - ) + text(crate::tr!("options-theme-help")) .size(11) .width(sizing.width), Space::new().height(12),