Browse Source

feat(bundler/NSIS): allow specifying custom lang files (#6867)

* feat(bundler/NSIS): allow specifying custom lang files

* add dunc as dep on all platforms

* clippy

* Update tooling/bundler/src/bundle/windows/nsis.rs

Co-authored-by: Fabian-Lars <fabianlars@fabianlars.de>

* Update core/tauri-utils/src/config.rs

Co-authored-by: Fabian-Lars <fabianlars@fabianlars.de>

* schema files

---------

Co-authored-by: Fabian-Lars <fabianlars@fabianlars.de>
Co-authored-by: Lucas Nogueira <lucas@tauri.studio>
Amr Bashir 2 years ago
parent
commit
2948820579

+ 7 - 0
.changes/nsis-custom-language-files.md

@@ -0,0 +1,7 @@
+---
+'tauri-bundler': 'minor'
+'tauri-utils': 'minor'
+'cli.rs': 'minor'
+---
+
+Allow specifying custom language files of Tauri's custom messages for the NSIS installer

+ 10 - 0
core/tauri-config-schema/schema.json

@@ -1715,6 +1715,16 @@
             "type": "string"
           }
         },
+        "customLanguageFiles": {
+          "description": "A key-value pair where the key is the language and the value is the path to a custom `.nsh` file that holds the translated text for tauri's custom messages.\n\nSee <https://github.com/tauri-apps/tauri/blob/dev/tooling/bundler/src/bundle/windows/templates/nsis-languages/English.nsh> for an example `.nsh` file.\n\n**Note**: the key must be a valid NSIS language and it must be added to [`NsisConfig`] languages array,",
+          "type": [
+            "object",
+            "null"
+          ],
+          "additionalProperties": {
+            "type": "string"
+          }
+        },
         "displayLanguageSelector": {
           "description": "Whether to display a language selector dialog before the installer and uninstaller windows are rendered or not. By default the OS language is selected, with a fallback to the first language in the `languages` array.",
           "default": false,

+ 7 - 0
core/tauri-utils/src/config.rs

@@ -461,6 +461,13 @@ pub struct NsisConfig {
   ///
   /// See <https://github.com/kichik/nsis/tree/9465c08046f00ccb6eda985abbdbf52c275c6c4d/Contrib/Language%20files> for the complete list of languages.
   pub languages: Option<Vec<String>>,
+  /// A key-value pair where the key is the language and the
+  /// value is the path to a custom `.nsh` file that holds the translated text for tauri's custom messages.
+  ///
+  /// See <https://github.com/tauri-apps/tauri/blob/dev/tooling/bundler/src/bundle/windows/templates/nsis-languages/English.nsh> for an example `.nsh` file.
+  ///
+  /// **Note**: the key must be a valid NSIS language and it must be added to [`NsisConfig`] languages array,
+  pub custom_language_files: Option<HashMap<String, PathBuf>>,
   /// Whether to display a language selector dialog before the installer and uninstaller windows are rendered or not.
   /// By default the OS language is selected, with a fallback to the first language in the `languages` array.
   #[serde(default, alias = "display-language-selector")]

+ 1 - 0
tooling/bundler/Cargo.toml

@@ -39,6 +39,7 @@ semver = "1"
 sha1 = "0.10"
 sha2 = "0.10"
 zip = "0.6"
+dunce = "1"
 
 [target."cfg(target_os = \"windows\")".dependencies]
 uuid = { version = "1", features = [ "v4", "v5" ] }

+ 7 - 0
tooling/bundler/src/bundle/settings.rs

@@ -274,6 +274,13 @@ pub struct NsisSettings {
   ///
   /// See <https://github.com/kichik/nsis/tree/9465c08046f00ccb6eda985abbdbf52c275c6c4d/Contrib/Language%20files> for the complete list of languages.
   pub languages: Option<Vec<String>>,
+  /// An key-value pair where the key is the language and the
+  /// value is the path to a custom `.nsi` file that holds the translated text for tauri's custom messages.
+  ///
+  /// See <https://github.com/tauri-apps/tauri/blob/dev/tooling/bundler/src/bundle/windows/templates/nsis-languages/English.nsh> for an example `.nsi` file.
+  ///
+  /// **Note**: the key must be a valid NSIS language and it must be added to [`NsisConfig`]languages array,
+  pub custom_language_files: Option<HashMap<String, PathBuf>>,
   /// Whether to display a language selector dialog before the installer and uninstaller windows are rendered or not.
   /// By default the OS language is selected, with a fallback to the first language in the `languages` array.
   pub display_language_selector: bool,

+ 103 - 43
tooling/bundler/src/bundle/windows/nsis.rs

@@ -8,9 +8,9 @@ use crate::{
   bundle::{
     common::CommandExt,
     windows::util::{
-      download, download_and_verify, extract_zip, remove_unc_lossy, HashAlgorithm,
-      NSIS_OUTPUT_FOLDER_NAME, NSIS_UPDATER_OUTPUT_FOLDER_NAME, WEBVIEW2_BOOTSTRAPPER_URL,
-      WEBVIEW2_X64_INSTALLER_GUID, WEBVIEW2_X86_INSTALLER_GUID,
+      download, download_and_verify, extract_zip, HashAlgorithm, NSIS_OUTPUT_FOLDER_NAME,
+      NSIS_UPDATER_OUTPUT_FOLDER_NAME, WEBVIEW2_BOOTSTRAPPER_URL, WEBVIEW2_X64_INSTALLER_GUID,
+      WEBVIEW2_X86_INSTALLER_GUID,
     },
   },
   Settings,
@@ -26,7 +26,7 @@ use tauri_utils::{
 };
 
 use std::{
-  collections::BTreeMap,
+  collections::{BTreeMap, HashMap},
   fs::{copy, create_dir_all, remove_dir_all, rename, write},
   path::{Path, PathBuf},
   process::Command,
@@ -189,35 +189,31 @@ fn build_nsis_app_installer(
   let mut install_mode = NSISInstallerMode::CurrentUser;
   let mut languages = vec!["English".into()];
   let mut custom_template_path = None;
+  let mut custom_language_files = None;
   if let Some(nsis) = &settings.windows().nsis {
     custom_template_path = nsis.template.clone();
+    custom_language_files = nsis.custom_language_files.clone();
     install_mode = nsis.install_mode;
     if let Some(langs) = &nsis.languages {
       languages.clear();
       languages.extend_from_slice(langs);
     }
     if let Some(license) = &nsis.license {
-      data.insert(
-        "license",
-        to_json(remove_unc_lossy(license.canonicalize()?)),
-      );
+      data.insert("license", to_json(dunce::canonicalize(license)?));
     }
     if let Some(installer_icon) = &nsis.installer_icon {
       data.insert(
         "installer_icon",
-        to_json(remove_unc_lossy(installer_icon.canonicalize()?)),
+        to_json(dunce::canonicalize(installer_icon)?),
       );
     }
     if let Some(header_image) = &nsis.header_image {
-      data.insert(
-        "header_image",
-        to_json(remove_unc_lossy(header_image.canonicalize()?)),
-      );
+      data.insert("header_image", to_json(dunce::canonicalize(header_image)?));
     }
     if let Some(sidebar_image) = &nsis.sidebar_image {
       data.insert(
         "sidebar_image",
-        to_json(remove_unc_lossy(sidebar_image.canonicalize()?)),
+        to_json(dunce::canonicalize(sidebar_image)?),
       );
     }
 
@@ -234,7 +230,28 @@ fn build_nsis_app_installer(
       NSISInstallerMode::Both => "both",
     }),
   );
+
+  let languages_data =  languages
+  .iter()
+  .filter_map(|lang| {
+    if let Some(data) = get_lang_data(lang, custom_language_files.as_ref()) {
+      Some(data)
+    } else {
+      log::warn!("Custom tauri messages for {lang} are not translated.\nIf it is a valid language listed on <https://github.com/kichik/nsis/tree/9465c08046f00ccb6eda985abbdbf52c275c6c4d/Contrib/Language%20files>, please open a Tauri feature request\n or you can provide a custom language file for it in `tauri.conf.json > tauri > bundle > windows > nsis > custom_language_files`");
+      None
+    }
+  }).collect::<Vec<_>>();
+
   data.insert("languages", to_json(languages.clone()));
+  data.insert(
+    "language_files",
+    to_json(
+      languages_data
+        .iter()
+        .map(|d| d.0.clone())
+        .collect::<Vec<_>>(),
+    ),
+  );
 
   let main_binary = settings
     .binaries()
@@ -380,17 +397,12 @@ fn build_nsis_app_installer(
       .0,
   )?;
 
-  for lang in languages {
-    if let Some((data, encoding)) = get_lang_data(&lang) {
+  for (lang, data) in languages_data.iter() {
+    if let Some((content, encoding)) = data {
       write(
         output_path.join(lang).with_extension("nsh"),
-        encoding.encode(data).0,
+        encoding.encode(content).0,
       )?;
-    } else {
-      return Err(
-        anyhow::anyhow!("Language {lang} not implemented. If it is a valid language listed on <https://github.com/kichik/nsis/tree/9465c08046f00ccb6eda985abbdbf52c275c6c4d/Contrib/Language%20files>, please open a Tauri feature request")
-          .into()
-      );
     }
   }
 
@@ -451,7 +463,7 @@ fn generate_resource_data(settings: &Settings) -> crate::Result<ResourcesMap> {
 
   for src in settings.resource_files() {
     let src = src?;
-    let resource_path = remove_unc_lossy(cwd.join(&src).canonicalize()?);
+    let resource_path = dunce::canonicalize(cwd.join(&src))?;
 
     // In some glob resource paths like `assets/**/*` a file might appear twice
     // because the `tauri_utils::resources::ResourcePaths` iterator also reads a directory
@@ -485,7 +497,7 @@ fn generate_binaries_data(settings: &Settings) -> crate::Result<BinariesMap> {
 
   for src in settings.external_binaries() {
     let src = src?;
-    let binary_path = remove_unc_lossy(cwd.join(&src).canonicalize()?);
+    let binary_path = dunce::canonicalize(cwd.join(&src))?;
     let dest_filename = src
       .file_name()
       .expect("failed to extract external binary filename")
@@ -511,42 +523,90 @@ fn generate_binaries_data(settings: &Settings) -> crate::Result<BinariesMap> {
   Ok(binaries)
 }
 
-fn get_lang_data(lang: &str) -> Option<(&'static str, &'static encoding_rs::Encoding)> {
+fn get_lang_data(
+  lang: &str,
+  custom_lang_files: Option<&HashMap<String, PathBuf>>,
+) -> Option<(
+  String,
+  Option<(&'static str, &'static encoding_rs::Encoding)>,
+)> {
   use encoding_rs::*;
+
+  if let Some(path) = custom_lang_files.and_then(|h| h.get(lang)) {
+    return Some((
+      dunce::canonicalize(path)
+        .unwrap()
+        .to_string_lossy()
+        .to_string(),
+      None,
+    ));
+  }
+
+  let lang_file = format!("{lang}.nsh");
   match lang.to_lowercase().as_str() {
     "arabic" => Some((
-      include_str!("./templates/nsis-languages/Arabic.nsh"),
-      UTF_16LE,
+      lang_file,
+      Some((
+        include_str!("./templates/nsis-languages/Arabic.nsh"),
+        UTF_16LE,
+      )),
+    )),
+    "dutch" => Some((
+      lang_file,
+      Some((include_str!("./templates/nsis-languages/Dutch.nsh"), UTF_8)),
     )),
-    "dutch" => Some((include_str!("./templates/nsis-languages/Dutch.nsh"), UTF_8)),
     "english" => Some((
-      include_str!("./templates/nsis-languages/English.nsh"),
-      UTF_8,
+      lang_file,
+      Some((
+        include_str!("./templates/nsis-languages/English.nsh"),
+        UTF_8,
+      )),
     )),
     "japanese" => Some((
-      include_str!("./templates/nsis-languages/Japanese.nsh"),
-      UTF_8,
+      lang_file,
+      Some((
+        include_str!("./templates/nsis-languages/Japanese.nsh"),
+        UTF_8,
+      )),
     )),
     "portuguesebr" => Some((
-      include_str!("./templates/nsis-languages/PortugueseBR.nsh"),
-      UTF_8,
+      lang_file,
+      Some((
+        include_str!("./templates/nsis-languages/PortugueseBR.nsh"),
+        UTF_8,
+      )),
     )),
     "tradchinese" => Some((
-      include_str!("./templates/nsis-languages/TradChinese.nsh"),
-      UTF_8,
+      lang_file,
+      Some((
+        include_str!("./templates/nsis-languages/TradChinese.nsh"),
+        UTF_8,
+      )),
     )),
     "simpchinese" => Some((
-      include_str!("./templates/nsis-languages/SimpChinese.nsh"),
-      UTF_8,
+      lang_file,
+      Some((
+        include_str!("./templates/nsis-languages/SimpChinese.nsh"),
+        UTF_8,
+      )),
+    )),
+    "french" => Some((
+      lang_file,
+      Some((include_str!("./templates/nsis-languages/French.nsh"), UTF_8)),
     )),
-    "french" => Some((include_str!("./templates/nsis-languages/French.nsh"), UTF_8)),
     "spanish" => Some((
-      include_str!("./templates/nsis-languages/Spanish.nsh"),
-      UTF_8,
+      lang_file,
+      Some((
+        include_str!("./templates/nsis-languages/Spanish.nsh"),
+        UTF_8,
+      )),
     )),
     "spanishinternational" => Some((
-      include_str!("./templates/nsis-languages/SpanishInternational.nsh"),
-      UTF_8,
+      lang_file,
+      Some((
+        include_str!("./templates/nsis-languages/SpanishInternational.nsh"),
+        UTF_8,
+      )),
     )),
     _ => None,
   }

+ 2 - 2
tooling/bundler/src/bundle/windows/templates/installer.nsi

@@ -272,8 +272,8 @@ FunctionEnd
 !insertmacro MUI_LANGUAGE "{{this}}"
 {{/each}}
 !insertmacro MUI_RESERVEFILE_LANGDLL
-{{#each languages}}
-  !include "{{this}}.nsh"
+{{#each language_files}}
+  !include "{{this}}"
 {{/each}}
 
 Function .onInit

+ 2 - 6
tooling/bundler/src/bundle/windows/util.rs

@@ -5,7 +5,7 @@
 use std::{
   fs::{create_dir_all, File},
   io::{Cursor, Read, Write},
-  path::{Path, PathBuf},
+  path::Path,
 };
 
 use log::info;
@@ -74,7 +74,7 @@ fn verify(data: &Vec<u8>, hash: &str, mut hasher: impl Digest) -> crate::Result<
 }
 
 #[cfg(target_os = "windows")]
-pub fn try_sign(file_path: &PathBuf, settings: &Settings) -> crate::Result<()> {
+pub fn try_sign(file_path: &std::path::PathBuf, settings: &Settings) -> crate::Result<()> {
   use tauri_utils::display_path;
 
   if let Some(certificate_thumbprint) = settings.windows().certificate_thumbprint.as_ref() {
@@ -134,7 +134,3 @@ pub fn extract_zip(data: &[u8], path: &Path) -> crate::Result<()> {
 
   Ok(())
 }
-
-pub fn remove_unc_lossy<P: AsRef<Path>>(p: P) -> PathBuf {
-  PathBuf::from(p.as_ref().to_string_lossy().replacen(r"\\?\", "", 1))
-}

+ 7 - 0
tooling/cli/Cargo.lock

@@ -882,6 +882,12 @@ dependencies = [
  "dtoa",
 ]
 
+[[package]]
+name = "dunce"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b"
+
 [[package]]
 name = "dyn-clone"
 version = "1.0.11"
@@ -3349,6 +3355,7 @@ dependencies = [
  "attohttpc",
  "bitness",
  "dirs-next",
+ "dunce",
  "encoding_rs",
  "glob",
  "handlebars",

+ 10 - 0
tooling/cli/schema.json

@@ -1715,6 +1715,16 @@
             "type": "string"
           }
         },
+        "customLanguageFiles": {
+          "description": "A key-value pair where the key is the language and the value is the path to a custom `.nsh` file that holds the translated text for tauri's custom messages.\n\nSee <https://github.com/tauri-apps/tauri/blob/dev/tooling/bundler/src/bundle/windows/templates/nsis-languages/English.nsh> for an example `.nsh` file.\n\n**Note**: the key must be a valid NSIS language and it must be added to [`NsisConfig`] languages array,",
+          "type": [
+            "object",
+            "null"
+          ],
+          "additionalProperties": {
+            "type": "string"
+          }
+        },
         "displayLanguageSelector": {
           "description": "Whether to display a language selector dialog before the installer and uninstaller windows are rendered or not. By default the OS language is selected, with a fallback to the first language in the `languages` array.",
           "default": false,

+ 1 - 0
tooling/cli/src/helpers/config.rs

@@ -106,6 +106,7 @@ pub fn nsis_settings(config: NsisConfig) -> tauri_bundler::NsisSettings {
     installer_icon: config.installer_icon,
     install_mode: config.install_mode,
     languages: config.languages,
+    custom_language_files: config.custom_language_files,
     display_language_selector: config.display_language_selector,
   }
 }