mirror of
https://github.com/ankitects/anki.git
synced 2025-11-06 04:37:22 -05:00
feat: add i18n to launcher (#4361)
* add anki_i18n and locale_config crates to launcher * add launcher.ftl * add tr to state * replace most hardcoded strings with translations * add support for `launcher` rustcfg to trim translations * use marker structs to denote type of translations * move underscores into generated code * Update cargo-license, which may fix the license order issue (dae)
This commit is contained in:
parent
b0665a8ef1
commit
e0b0d0d19b
11 changed files with 252 additions and 110 deletions
35
Cargo.lock
generated
35
Cargo.lock
generated
|
|
@ -3555,6 +3555,7 @@ dependencies = [
|
||||||
name = "launcher"
|
name = "launcher"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"anki_i18n",
|
||||||
"anki_io",
|
"anki_io",
|
||||||
"anki_process",
|
"anki_process",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
|
@ -3563,6 +3564,7 @@ dependencies = [
|
||||||
"embed-resource",
|
"embed-resource",
|
||||||
"libc",
|
"libc",
|
||||||
"libc-stdhandle",
|
"libc-stdhandle",
|
||||||
|
"locale_config",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"widestring",
|
"widestring",
|
||||||
"windows 0.61.3",
|
"windows 0.61.3",
|
||||||
|
|
@ -3702,6 +3704,19 @@ version = "0.4.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed"
|
checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "locale_config"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "08d2c35b16f4483f6c26f0e4e9550717a2f6575bcd6f12a53ff0c490a94a6934"
|
||||||
|
dependencies = [
|
||||||
|
"lazy_static",
|
||||||
|
"objc",
|
||||||
|
"objc-foundation",
|
||||||
|
"regex",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
version = "0.4.13"
|
version = "0.4.13"
|
||||||
|
|
@ -4380,6 +4395,26 @@ dependencies = [
|
||||||
"malloc_buf",
|
"malloc_buf",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc-foundation"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9"
|
||||||
|
dependencies = [
|
||||||
|
"block",
|
||||||
|
"objc",
|
||||||
|
"objc_id",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc_id"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b"
|
||||||
|
dependencies = [
|
||||||
|
"objc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "object"
|
name = "object"
|
||||||
version = "0.36.7"
|
version = "0.36.7"
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,7 @@ itertools = "0.14.0"
|
||||||
junction = "1.2.0"
|
junction = "1.2.0"
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
libc-stdhandle = "0.1"
|
libc-stdhandle = "0.1"
|
||||||
|
locale_config = "0.3.0"
|
||||||
maplit = "1.0.2"
|
maplit = "1.0.2"
|
||||||
nom = "8.0.0"
|
nom = "8.0.0"
|
||||||
num-format = "0.4.4"
|
num-format = "0.4.4"
|
||||||
|
|
|
||||||
33
ftl/core/launcher.ftl
Normal file
33
ftl/core/launcher.ftl
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
launcher-title = Anki Launcher
|
||||||
|
launcher-press-enter-to-start = Press enter to start Anki.
|
||||||
|
launcher-anki-will-start-shortly = Anki will start shortly.
|
||||||
|
launcher-you-can-close-this-window = You can close this window.
|
||||||
|
launcher-updating-anki = Updating Anki...
|
||||||
|
launcher-latest-anki = Latest Anki (just press Enter)
|
||||||
|
launcher-choose-a-version = Choose a version
|
||||||
|
launcher-sync-project-changes = Sync project changes
|
||||||
|
launcher-keep-existing-version = Keep existing version ({ $current })
|
||||||
|
launcher-revert-to-previous = Revert to previous version ({ $prev })
|
||||||
|
launcher-allow-betas = Allow betas: { $state }
|
||||||
|
launcher-on = on
|
||||||
|
launcher-off = off
|
||||||
|
launcher-cache-downloads = Cache downloads: { $state }
|
||||||
|
launcher-download-mirror = Download mirror: { $state }
|
||||||
|
launcher-uninstall = Uninstall
|
||||||
|
launcher-invalid-input = Invalid input. Please try again.
|
||||||
|
launcher-latest-releases = Latest releases: { $releases }
|
||||||
|
launcher-enter-the-version-you-want = Enter the version you want to install:
|
||||||
|
launcher-versions-before-cant-be-installed = Versions before 2.1.50 can't be installed.
|
||||||
|
launcher-invalid-version = Invalid version.
|
||||||
|
launcher-unable-to-check-for-versions = Unable to check for Anki versions. Please check your internet connection.
|
||||||
|
launcher-checking-for-updates = Checking for updates...
|
||||||
|
launcher-uninstall-confirm = Uninstall Anki's program files? (y/n)
|
||||||
|
launcher-uninstall-cancelled = Uninstall cancelled.
|
||||||
|
launcher-program-files-removed = Program files removed.
|
||||||
|
launcher-remove-all-profiles-confirm = Remove all profiles/cards? (y/n)
|
||||||
|
launcher-user-data-removed = User data removed.
|
||||||
|
launcher-download-mirror-options = Download mirror options:
|
||||||
|
launcher-mirror-no-mirror = No mirror
|
||||||
|
launcher-mirror-china = China
|
||||||
|
launcher-mirror-disabled = Mirror disabled.
|
||||||
|
launcher-mirror-china-enabled = China mirror enabled.
|
||||||
|
|
@ -8,11 +8,13 @@ publish = false
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
anki_i18n.workspace = true
|
||||||
anki_io.workspace = true
|
anki_io.workspace = true
|
||||||
anki_process.workspace = true
|
anki_process.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
camino.workspace = true
|
camino.workspace = true
|
||||||
dirs.workspace = true
|
dirs.workspace = true
|
||||||
|
locale_config.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
|
||||||
[target.'cfg(all(unix, not(target_os = "macos")))'.dependencies]
|
[target.'cfg(all(unix, not(target_os = "macos")))'.dependencies]
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ use std::process::Command;
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
use std::time::UNIX_EPOCH;
|
use std::time::UNIX_EPOCH;
|
||||||
|
|
||||||
|
use anki_i18n::I18n;
|
||||||
use anki_io::copy_file;
|
use anki_io::copy_file;
|
||||||
use anki_io::create_dir_all;
|
use anki_io::create_dir_all;
|
||||||
use anki_io::modified_time;
|
use anki_io::modified_time;
|
||||||
|
|
@ -31,6 +32,7 @@ use crate::platform::respawn_launcher;
|
||||||
mod platform;
|
mod platform;
|
||||||
|
|
||||||
struct State {
|
struct State {
|
||||||
|
tr: I18n<anki_i18n::Launcher>,
|
||||||
current_version: Option<String>,
|
current_version: Option<String>,
|
||||||
prerelease_marker: std::path::PathBuf,
|
prerelease_marker: std::path::PathBuf,
|
||||||
uv_install_root: std::path::PathBuf,
|
uv_install_root: std::path::PathBuf,
|
||||||
|
|
@ -100,7 +102,14 @@ fn run() -> Result<()> {
|
||||||
|
|
||||||
let (exe_dir, resources_dir) = get_exe_and_resources_dirs()?;
|
let (exe_dir, resources_dir) = get_exe_and_resources_dirs()?;
|
||||||
|
|
||||||
|
let locale = locale_config::Locale::user_default().to_string();
|
||||||
|
|
||||||
let mut state = State {
|
let mut state = State {
|
||||||
|
tr: I18n::new(&[if !locale.is_empty() {
|
||||||
|
locale
|
||||||
|
} else {
|
||||||
|
"en".to_owned()
|
||||||
|
}]),
|
||||||
current_version: None,
|
current_version: None,
|
||||||
prerelease_marker: uv_install_root.join("prerelease"),
|
prerelease_marker: uv_install_root.join("prerelease"),
|
||||||
uv_install_root: uv_install_root.clone(),
|
uv_install_root: uv_install_root.clone(),
|
||||||
|
|
@ -160,7 +169,7 @@ fn run() -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
print!("\x1B[2J\x1B[H"); // Clear screen and move cursor to top
|
print!("\x1B[2J\x1B[H"); // Clear screen and move cursor to top
|
||||||
println!("\x1B[1mAnki Launcher\x1B[0m\n");
|
println!("\x1B[1m{}\x1B[0m\n", state.tr.launcher_title());
|
||||||
|
|
||||||
ensure_os_supported()?;
|
ensure_os_supported()?;
|
||||||
|
|
||||||
|
|
@ -178,15 +187,18 @@ fn run() -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg!(unix) && !cfg!(target_os = "macos") {
|
if cfg!(unix) && !cfg!(target_os = "macos") {
|
||||||
println!("\nPress enter to start Anki.");
|
println!("\n{}", state.tr.launcher_press_enter_to_start());
|
||||||
let mut input = String::new();
|
let mut input = String::new();
|
||||||
let _ = stdin().read_line(&mut input);
|
let _ = stdin().read_line(&mut input);
|
||||||
} else {
|
} else {
|
||||||
// on Windows/macOS, the user needs to close the terminal/console
|
// on Windows/macOS, the user needs to close the terminal/console
|
||||||
// currently, but ideas on how we can avoid this would be good!
|
// currently, but ideas on how we can avoid this would be good!
|
||||||
println!();
|
println!();
|
||||||
println!("Anki will start shortly.");
|
println!("{}", state.tr.launcher_anki_will_start_shortly());
|
||||||
println!("\x1B[1mYou can close this window.\x1B[0m\n");
|
println!(
|
||||||
|
"\x1B[1m{}\x1B[0m\n",
|
||||||
|
state.tr.launcher_you_can_close_this_window()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// respawn the launcher as a disconnected subprocess for normal startup
|
// respawn the launcher as a disconnected subprocess for normal startup
|
||||||
|
|
@ -258,7 +270,7 @@ fn handle_version_install_or_update(state: &State, choice: MainMenuChoice) -> Re
|
||||||
// Remove sync marker before attempting sync
|
// Remove sync marker before attempting sync
|
||||||
let _ = remove_file(&state.sync_complete_marker);
|
let _ = remove_file(&state.sync_complete_marker);
|
||||||
|
|
||||||
println!("Updating Anki...\n");
|
println!("{}\n", state.tr.launcher_updating_anki());
|
||||||
|
|
||||||
let python_version_trimmed = if state.user_python_version_path.exists() {
|
let python_version_trimmed = if state.user_python_version_path.exists() {
|
||||||
let python_version = read_file(&state.user_python_version_path)?;
|
let python_version = read_file(&state.user_python_version_path)?;
|
||||||
|
|
@ -440,44 +452,62 @@ fn file_timestamp_secs(path: &std::path::Path) -> i64 {
|
||||||
|
|
||||||
fn get_main_menu_choice(state: &State) -> Result<MainMenuChoice> {
|
fn get_main_menu_choice(state: &State) -> Result<MainMenuChoice> {
|
||||||
loop {
|
loop {
|
||||||
println!("1) Latest Anki (press Enter)");
|
println!("1) {}", state.tr.launcher_latest_anki());
|
||||||
println!("2) Choose a version");
|
println!("2) {}", state.tr.launcher_choose_a_version());
|
||||||
|
|
||||||
if let Some(current_version) = &state.current_version {
|
if let Some(current_version) = &state.current_version {
|
||||||
let normalized_current = normalize_version(current_version);
|
let normalized_current = normalize_version(current_version);
|
||||||
|
|
||||||
if state.pyproject_modified_by_user {
|
if state.pyproject_modified_by_user {
|
||||||
println!("3) Sync project changes");
|
println!("3) {}", state.tr.launcher_sync_project_changes());
|
||||||
} else {
|
} else {
|
||||||
println!("3) Keep existing version ({normalized_current})");
|
println!(
|
||||||
|
"3) {}",
|
||||||
|
state.tr.launcher_keep_existing_version(normalized_current)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(prev_version) = &state.previous_version {
|
if let Some(prev_version) = &state.previous_version {
|
||||||
if state.current_version.as_ref() != Some(prev_version) {
|
if state.current_version.as_ref() != Some(prev_version) {
|
||||||
let normalized_prev = normalize_version(prev_version);
|
let normalized_prev = normalize_version(prev_version);
|
||||||
println!("4) Revert to previous version ({normalized_prev})");
|
println!(
|
||||||
|
"4) {}",
|
||||||
|
state.tr.launcher_revert_to_previous(normalized_prev)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
let betas_enabled = state.prerelease_marker.exists();
|
let betas_enabled = state.prerelease_marker.exists();
|
||||||
println!(
|
println!(
|
||||||
"5) Allow betas: {}",
|
"5) {}",
|
||||||
if betas_enabled { "on" } else { "off" }
|
state.tr.launcher_allow_betas(if betas_enabled {
|
||||||
|
state.tr.launcher_on()
|
||||||
|
} else {
|
||||||
|
state.tr.launcher_off()
|
||||||
|
})
|
||||||
);
|
);
|
||||||
let cache_enabled = !state.no_cache_marker.exists();
|
let cache_enabled = !state.no_cache_marker.exists();
|
||||||
println!(
|
println!(
|
||||||
"6) Cache downloads: {}",
|
"6) {}",
|
||||||
if cache_enabled { "on" } else { "off" }
|
state.tr.launcher_cache_downloads(if cache_enabled {
|
||||||
|
state.tr.launcher_on()
|
||||||
|
} else {
|
||||||
|
state.tr.launcher_off()
|
||||||
|
})
|
||||||
);
|
);
|
||||||
let mirror_enabled = is_mirror_enabled(state);
|
let mirror_enabled = is_mirror_enabled(state);
|
||||||
println!(
|
println!(
|
||||||
"7) Download mirror: {}",
|
"7) {}",
|
||||||
if mirror_enabled { "on" } else { "off" }
|
state.tr.launcher_download_mirror(if mirror_enabled {
|
||||||
|
state.tr.launcher_on()
|
||||||
|
} else {
|
||||||
|
state.tr.launcher_off()
|
||||||
|
})
|
||||||
);
|
);
|
||||||
println!();
|
println!();
|
||||||
println!("8) Uninstall");
|
println!("8) {}", state.tr.launcher_uninstall());
|
||||||
print!("> ");
|
print!("> ");
|
||||||
let _ = stdout().flush();
|
let _ = stdout().flush();
|
||||||
|
|
||||||
|
|
@ -499,7 +529,7 @@ fn get_main_menu_choice(state: &State) -> Result<MainMenuChoice> {
|
||||||
if state.current_version.is_some() {
|
if state.current_version.is_some() {
|
||||||
MainMenuChoice::KeepExisting
|
MainMenuChoice::KeepExisting
|
||||||
} else {
|
} else {
|
||||||
println!("Invalid input. Please try again.\n");
|
println!("{}\n", state.tr.launcher_invalid_input());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -511,7 +541,7 @@ fn get_main_menu_choice(state: &State) -> Result<MainMenuChoice> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
println!("Invalid input. Please try again.\n");
|
println!("{}\n", state.tr.launcher_invalid_input());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
"5" => MainMenuChoice::ToggleBetas,
|
"5" => MainMenuChoice::ToggleBetas,
|
||||||
|
|
@ -519,7 +549,7 @@ fn get_main_menu_choice(state: &State) -> Result<MainMenuChoice> {
|
||||||
"7" => MainMenuChoice::DownloadMirror,
|
"7" => MainMenuChoice::DownloadMirror,
|
||||||
"8" => MainMenuChoice::Uninstall,
|
"8" => MainMenuChoice::Uninstall,
|
||||||
_ => {
|
_ => {
|
||||||
println!("Invalid input. Please try again.");
|
println!("{}\n", state.tr.launcher_invalid_input());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -534,9 +564,9 @@ fn get_version_kind(state: &State) -> Result<Option<VersionKind>> {
|
||||||
.map(|v| v.as_str())
|
.map(|v| v.as_str())
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join(", ");
|
.join(", ");
|
||||||
println!("Latest releases: {releases_str}");
|
println!("{}", state.tr.launcher_latest_releases(releases_str));
|
||||||
|
|
||||||
println!("Enter the version you want to install:");
|
println!("{}", state.tr.launcher_enter_the_version_you_want());
|
||||||
print!("> ");
|
print!("> ");
|
||||||
let _ = stdout().flush();
|
let _ = stdout().flush();
|
||||||
|
|
||||||
|
|
@ -560,11 +590,11 @@ fn get_version_kind(state: &State) -> Result<Option<VersionKind>> {
|
||||||
Ok(Some(version_kind))
|
Ok(Some(version_kind))
|
||||||
}
|
}
|
||||||
(None, true) => {
|
(None, true) => {
|
||||||
println!("Versions before 2.1.50 can't be installed.");
|
println!("{}", state.tr.launcher_versions_before_cant_be_installed());
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
println!("Invalid version.\n");
|
println!("{}\n", state.tr.launcher_invalid_version());
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -700,7 +730,7 @@ fn fetch_versions(state: &State) -> Result<Vec<String>> {
|
||||||
let output = match cmd.utf8_output() {
|
let output = match cmd.utf8_output() {
|
||||||
Ok(output) => output,
|
Ok(output) => output,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
print!("Unable to check for Anki versions. Please check your internet connection.\n\n");
|
print!("{}\n\n", state.tr.launcher_unable_to_check_for_versions());
|
||||||
return Err(e.into());
|
return Err(e.into());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -709,7 +739,7 @@ fn fetch_versions(state: &State) -> Result<Vec<String>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_releases(state: &State) -> Result<Releases> {
|
fn get_releases(state: &State) -> Result<Releases> {
|
||||||
println!("Checking for updates...");
|
println!("{}", state.tr.launcher_checking_for_updates());
|
||||||
let include_prereleases = state.prerelease_marker.exists();
|
let include_prereleases = state.prerelease_marker.exists();
|
||||||
let all_versions = fetch_versions(state)?;
|
let all_versions = fetch_versions(state)?;
|
||||||
let all_versions = filter_and_normalize_versions(all_versions, include_prereleases);
|
let all_versions = filter_and_normalize_versions(all_versions, include_prereleases);
|
||||||
|
|
@ -911,7 +941,7 @@ fn get_anki_addons21_path() -> Result<std::path::PathBuf> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_uninstall(state: &State) -> Result<bool> {
|
fn handle_uninstall(state: &State) -> Result<bool> {
|
||||||
println!("Uninstall Anki's program files? (y/n)");
|
println!("{}", state.tr.launcher_uninstall_confirm());
|
||||||
print!("> ");
|
print!("> ");
|
||||||
let _ = stdout().flush();
|
let _ = stdout().flush();
|
||||||
|
|
||||||
|
|
@ -920,7 +950,7 @@ fn handle_uninstall(state: &State) -> Result<bool> {
|
||||||
let input = input.trim().to_lowercase();
|
let input = input.trim().to_lowercase();
|
||||||
|
|
||||||
if input != "y" {
|
if input != "y" {
|
||||||
println!("Uninstall cancelled.");
|
println!("{}", state.tr.launcher_uninstall_cancelled());
|
||||||
println!();
|
println!();
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|
@ -928,11 +958,11 @@ fn handle_uninstall(state: &State) -> Result<bool> {
|
||||||
// Remove program files
|
// Remove program files
|
||||||
if state.uv_install_root.exists() {
|
if state.uv_install_root.exists() {
|
||||||
anki_io::remove_dir_all(&state.uv_install_root)?;
|
anki_io::remove_dir_all(&state.uv_install_root)?;
|
||||||
println!("Program files removed.");
|
println!("{}", state.tr.launcher_program_files_removed());
|
||||||
}
|
}
|
||||||
|
|
||||||
println!();
|
println!();
|
||||||
println!("Remove all profiles/cards? (y/n)");
|
println!("{}", state.tr.launcher_remove_all_profiles_confirm());
|
||||||
print!("> ");
|
print!("> ");
|
||||||
let _ = stdout().flush();
|
let _ = stdout().flush();
|
||||||
|
|
||||||
|
|
@ -942,7 +972,7 @@ fn handle_uninstall(state: &State) -> Result<bool> {
|
||||||
|
|
||||||
if input == "y" && state.anki_base_folder.exists() {
|
if input == "y" && state.anki_base_folder.exists() {
|
||||||
anki_io::remove_dir_all(&state.anki_base_folder)?;
|
anki_io::remove_dir_all(&state.anki_base_folder)?;
|
||||||
println!("User data removed.");
|
println!("{}", state.tr.launcher_user_data_removed());
|
||||||
}
|
}
|
||||||
|
|
||||||
println!();
|
println!();
|
||||||
|
|
@ -1036,9 +1066,9 @@ fn get_mirror_urls(state: &State) -> Result<Option<(String, String)>> {
|
||||||
|
|
||||||
fn show_mirror_submenu(state: &State) -> Result<()> {
|
fn show_mirror_submenu(state: &State) -> Result<()> {
|
||||||
loop {
|
loop {
|
||||||
println!("Download mirror options:");
|
println!("{}", state.tr.launcher_download_mirror_options());
|
||||||
println!("1) No mirror");
|
println!("1) {}", state.tr.launcher_mirror_no_mirror());
|
||||||
println!("2) China");
|
println!("2) {}", state.tr.launcher_mirror_china());
|
||||||
print!("> ");
|
print!("> ");
|
||||||
let _ = stdout().flush();
|
let _ = stdout().flush();
|
||||||
|
|
||||||
|
|
@ -1052,14 +1082,14 @@ fn show_mirror_submenu(state: &State) -> Result<()> {
|
||||||
if state.mirror_path.exists() {
|
if state.mirror_path.exists() {
|
||||||
let _ = remove_file(&state.mirror_path);
|
let _ = remove_file(&state.mirror_path);
|
||||||
}
|
}
|
||||||
println!("Mirror disabled.");
|
println!("{}", state.tr.launcher_mirror_disabled());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
"2" => {
|
"2" => {
|
||||||
// Write China mirror URLs
|
// Write China mirror URLs
|
||||||
let china_mirrors = "https://registry.npmmirror.com/-/binary/python-build-standalone/\nhttps://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple/";
|
let china_mirrors = "https://registry.npmmirror.com/-/binary/python-build-standalone/\nhttps://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple/";
|
||||||
write_file(&state.mirror_path, china_mirrors)?;
|
write_file(&state.mirror_path, china_mirrors)?;
|
||||||
println!("China mirror enabled.");
|
println!("{}", state.tr.launcher_mirror_china_enabled());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
"" => {
|
"" => {
|
||||||
|
|
@ -1067,7 +1097,7 @@ fn show_mirror_submenu(state: &State) -> Result<()> {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
println!("Invalid input. Please try again.");
|
println!("{}", state.tr.launcher_invalid_input());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,10 +23,10 @@ use write_strings::write_strings;
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
// generate our own requirements
|
// generate our own requirements
|
||||||
let map = get_ftl_data();
|
let mut map = get_ftl_data();
|
||||||
check(&map);
|
check(&map);
|
||||||
let modules = get_modules(&map);
|
let mut modules = get_modules(&map);
|
||||||
write_strings(&map, &modules);
|
write_strings(&map, &modules, "strings.rs", "All");
|
||||||
|
|
||||||
typescript::write_ts_interface(&modules)?;
|
typescript::write_ts_interface(&modules)?;
|
||||||
python::write_py_interface(&modules)?;
|
python::write_py_interface(&modules)?;
|
||||||
|
|
@ -41,5 +41,12 @@ fn main() -> Result<()> {
|
||||||
write_file_if_changed(path, meta_json)?;
|
write_file_if_changed(path, meta_json)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// generate strings for the launcher
|
||||||
|
map.iter_mut()
|
||||||
|
.for_each(|(_, modules)| modules.retain(|module, _| module == "launcher"));
|
||||||
|
modules.retain(|module| module.name == "launcher");
|
||||||
|
write_strings(&map, &modules, "strings_launcher.rs", "Launcher");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,15 @@
|
||||||
// Copyright: Ankitects Pty Ltd and contributors
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
// Include auto-generated content
|
|
||||||
|
|
||||||
#![allow(clippy::all)]
|
#![allow(clippy::all)]
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct All;
|
||||||
|
|
||||||
|
// Include auto-generated content
|
||||||
include!(concat!(env!("OUT_DIR"), "/strings.rs"));
|
include!(concat!(env!("OUT_DIR"), "/strings.rs"));
|
||||||
|
|
||||||
|
impl Translations for All {
|
||||||
|
const STRINGS: &phf::Map<&str, &phf::Map<&str, &str>> = &_STRINGS;
|
||||||
|
const KEYS_BY_MODULE: &[&[&str]] = &_KEYS_BY_MODULE;
|
||||||
|
}
|
||||||
|
|
|
||||||
15
rslib/i18n/src/generated_launcher.rs
Normal file
15
rslib/i18n/src/generated_launcher.rs
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
#![allow(clippy::all)]
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Launcher;
|
||||||
|
|
||||||
|
// Include auto-generated content
|
||||||
|
include!(concat!(env!("OUT_DIR"), "/strings_launcher.rs"));
|
||||||
|
|
||||||
|
impl Translations for Launcher {
|
||||||
|
const STRINGS: &phf::Map<&str, &phf::Map<&str, &str>> = &_STRINGS;
|
||||||
|
const KEYS_BY_MODULE: &[&[&str]] = &_KEYS_BY_MODULE;
|
||||||
|
}
|
||||||
|
|
@ -2,8 +2,10 @@
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
mod generated;
|
mod generated;
|
||||||
|
mod generated_launcher;
|
||||||
|
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
use std::marker::PhantomData;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
|
@ -12,8 +14,6 @@ use fluent::FluentArgs;
|
||||||
use fluent::FluentResource;
|
use fluent::FluentResource;
|
||||||
use fluent::FluentValue;
|
use fluent::FluentValue;
|
||||||
use fluent_bundle::bundle::FluentBundle as FluentBundleOrig;
|
use fluent_bundle::bundle::FluentBundle as FluentBundleOrig;
|
||||||
use generated::KEYS_BY_MODULE;
|
|
||||||
use generated::STRINGS;
|
|
||||||
use num_format::Locale;
|
use num_format::Locale;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use unic_langid::LanguageIdentifier;
|
use unic_langid::LanguageIdentifier;
|
||||||
|
|
@ -22,6 +22,9 @@ type FluentBundle<T> = FluentBundleOrig<T, intl_memoizer::concurrent::IntlLangMe
|
||||||
|
|
||||||
pub use fluent::fluent_args as tr_args;
|
pub use fluent::fluent_args as tr_args;
|
||||||
|
|
||||||
|
pub use crate::generated::All;
|
||||||
|
pub use crate::generated_launcher::Launcher;
|
||||||
|
|
||||||
pub trait Number: Into<FluentNumber> {
|
pub trait Number: Into<FluentNumber> {
|
||||||
fn round(self) -> Self;
|
fn round(self) -> Self;
|
||||||
}
|
}
|
||||||
|
|
@ -187,20 +190,67 @@ fn get_bundle_with_extra(
|
||||||
get_bundle(text, extra_text, &locales)
|
get_bundle(text, extra_text, &locales)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
pub trait Translations {
|
||||||
pub struct I18n {
|
const STRINGS: &phf::Map<&str, &phf::Map<&str, &str>>;
|
||||||
inner: Arc<Mutex<I18nInner>>,
|
const KEYS_BY_MODULE: &[&[&str]];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct I18n<P: Translations = All> {
|
||||||
|
inner: Arc<Mutex<I18nInner>>,
|
||||||
|
_translations_type: std::marker::PhantomData<P>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P: Translations> I18n<P> {
|
||||||
fn get_key(module_idx: usize, translation_idx: usize) -> &'static str {
|
fn get_key(module_idx: usize, translation_idx: usize) -> &'static str {
|
||||||
KEYS_BY_MODULE
|
P::KEYS_BY_MODULE
|
||||||
.get(module_idx)
|
.get(module_idx)
|
||||||
.and_then(|translations| translations.get(translation_idx))
|
.and_then(|translations| translations.get(translation_idx))
|
||||||
.cloned()
|
.cloned()
|
||||||
.unwrap_or("invalid-module-or-translation-index")
|
.unwrap_or("invalid-module-or-translation-index")
|
||||||
}
|
}
|
||||||
|
|
||||||
impl I18n {
|
fn get_modules(langs: &[LanguageIdentifier], desired_modules: &[String]) -> Vec<String> {
|
||||||
|
langs
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.map(|lang| {
|
||||||
|
let mut buf = String::new();
|
||||||
|
let lang_name = remapped_lang_name(&lang);
|
||||||
|
if let Some(strings) = P::STRINGS.get(lang_name) {
|
||||||
|
if desired_modules.is_empty() {
|
||||||
|
// empty list, provide all modules
|
||||||
|
for value in strings.values() {
|
||||||
|
buf.push_str(value)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for module_name in desired_modules {
|
||||||
|
if let Some(text) = strings.get(module_name.as_str()) {
|
||||||
|
buf.push_str(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buf
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This temporarily behaves like the older code; in the future we could
|
||||||
|
/// either access each &str separately, or load them on demand.
|
||||||
|
fn ftl_localized_text(lang: &LanguageIdentifier) -> Option<String> {
|
||||||
|
let lang = remapped_lang_name(lang);
|
||||||
|
if let Some(module) = P::STRINGS.get(lang) {
|
||||||
|
let mut text = String::new();
|
||||||
|
for module_text in module.values() {
|
||||||
|
text.push_str(module_text)
|
||||||
|
}
|
||||||
|
Some(text)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn template_only() -> Self {
|
pub fn template_only() -> Self {
|
||||||
Self::new::<&str>(&[])
|
Self::new::<&str>(&[])
|
||||||
}
|
}
|
||||||
|
|
@ -225,7 +275,7 @@ impl I18n {
|
||||||
let mut output_langs = vec![];
|
let mut output_langs = vec![];
|
||||||
for lang in input_langs {
|
for lang in input_langs {
|
||||||
// if the language is bundled in the binary
|
// if the language is bundled in the binary
|
||||||
if let Some(text) = ftl_localized_text(&lang).or_else(|| {
|
if let Some(text) = Self::ftl_localized_text(&lang).or_else(|| {
|
||||||
// when testing, allow missing translations
|
// when testing, allow missing translations
|
||||||
if cfg!(test) {
|
if cfg!(test) {
|
||||||
Some(String::new())
|
Some(String::new())
|
||||||
|
|
@ -244,7 +294,7 @@ impl I18n {
|
||||||
|
|
||||||
// add English templates
|
// add English templates
|
||||||
let template_lang = "en-US".parse().unwrap();
|
let template_lang = "en-US".parse().unwrap();
|
||||||
let template_text = ftl_localized_text(&template_lang).unwrap();
|
let template_text = Self::ftl_localized_text(&template_lang).unwrap();
|
||||||
let template_bundle = get_bundle_with_extra(&template_text, None).unwrap();
|
let template_bundle = get_bundle_with_extra(&template_text, None).unwrap();
|
||||||
bundles.push(template_bundle);
|
bundles.push(template_bundle);
|
||||||
output_langs.push(template_lang);
|
output_langs.push(template_lang);
|
||||||
|
|
@ -261,6 +311,7 @@ impl I18n {
|
||||||
bundles,
|
bundles,
|
||||||
langs: output_langs,
|
langs: output_langs,
|
||||||
})),
|
})),
|
||||||
|
_translations_type: PhantomData,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -270,7 +321,7 @@ impl I18n {
|
||||||
message_index: usize,
|
message_index: usize,
|
||||||
args: FluentArgs,
|
args: FluentArgs,
|
||||||
) -> String {
|
) -> String {
|
||||||
let key = get_key(module_index, message_index);
|
let key = Self::get_key(module_index, message_index);
|
||||||
self.translate(key, Some(args)).into()
|
self.translate(key, Some(args)).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -305,7 +356,7 @@ impl I18n {
|
||||||
/// implementation.
|
/// implementation.
|
||||||
pub fn resources_for_js(&self, desired_modules: &[String]) -> ResourcesForJavascript {
|
pub fn resources_for_js(&self, desired_modules: &[String]) -> ResourcesForJavascript {
|
||||||
let inner = self.inner.lock().unwrap();
|
let inner = self.inner.lock().unwrap();
|
||||||
let resources = get_modules(&inner.langs, desired_modules);
|
let resources = Self::get_modules(&inner.langs, desired_modules);
|
||||||
ResourcesForJavascript {
|
ResourcesForJavascript {
|
||||||
langs: inner.langs.iter().map(ToString::to_string).collect(),
|
langs: inner.langs.iter().map(ToString::to_string).collect(),
|
||||||
resources,
|
resources,
|
||||||
|
|
@ -313,47 +364,6 @@ impl I18n {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_modules(langs: &[LanguageIdentifier], desired_modules: &[String]) -> Vec<String> {
|
|
||||||
langs
|
|
||||||
.iter()
|
|
||||||
.cloned()
|
|
||||||
.map(|lang| {
|
|
||||||
let mut buf = String::new();
|
|
||||||
let lang_name = remapped_lang_name(&lang);
|
|
||||||
if let Some(strings) = STRINGS.get(lang_name) {
|
|
||||||
if desired_modules.is_empty() {
|
|
||||||
// empty list, provide all modules
|
|
||||||
for value in strings.values() {
|
|
||||||
buf.push_str(value)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for module_name in desired_modules {
|
|
||||||
if let Some(text) = strings.get(module_name.as_str()) {
|
|
||||||
buf.push_str(text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
buf
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This temporarily behaves like the older code; in the future we could either
|
|
||||||
/// access each &str separately, or load them on demand.
|
|
||||||
fn ftl_localized_text(lang: &LanguageIdentifier) -> Option<String> {
|
|
||||||
let lang = remapped_lang_name(lang);
|
|
||||||
if let Some(module) = STRINGS.get(lang) {
|
|
||||||
let mut text = String::new();
|
|
||||||
for module_text in module.values() {
|
|
||||||
text.push_str(module_text)
|
|
||||||
}
|
|
||||||
Some(text)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct I18nInner {
|
struct I18nInner {
|
||||||
// bundles in preferred language order, with template English as the
|
// bundles in preferred language order, with template English as the
|
||||||
// last element
|
// last element
|
||||||
|
|
@ -490,7 +500,7 @@ mod test {
|
||||||
#[test]
|
#[test]
|
||||||
fn i18n() {
|
fn i18n() {
|
||||||
// English template
|
// English template
|
||||||
let tr = I18n::new(&["zz"]);
|
let tr = I18n::<All>::new(&["zz"]);
|
||||||
assert_eq!(tr.translate("valid-key", None), "a valid key");
|
assert_eq!(tr.translate("valid-key", None), "a valid key");
|
||||||
assert_eq!(tr.translate("invalid-key", None), "invalid-key");
|
assert_eq!(tr.translate("invalid-key", None), "invalid-key");
|
||||||
|
|
||||||
|
|
@ -513,7 +523,7 @@ mod test {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Another language
|
// Another language
|
||||||
let tr = I18n::new(&["ja_JP"]);
|
let tr = I18n::<All>::new(&["ja_JP"]);
|
||||||
assert_eq!(tr.translate("valid-key", None), "キー");
|
assert_eq!(tr.translate("valid-key", None), "キー");
|
||||||
assert_eq!(tr.translate("only-in-english", None), "not translated");
|
assert_eq!(tr.translate("only-in-english", None), "not translated");
|
||||||
assert_eq!(tr.translate("invalid-key", None), "invalid-key");
|
assert_eq!(tr.translate("invalid-key", None), "invalid-key");
|
||||||
|
|
@ -524,7 +534,7 @@ mod test {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Decimal separator
|
// Decimal separator
|
||||||
let tr = I18n::new(&["pl-PL"]);
|
let tr = I18n::<All>::new(&["pl-PL"]);
|
||||||
// Polish will use a comma if the string is translated
|
// Polish will use a comma if the string is translated
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
tr.translate("one-arg-key", Some(tr_args!["one"=>2.07])),
|
tr.translate("one-arg-key", Some(tr_args!["one"=>2.07])),
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ use crate::extract::VariableKind;
|
||||||
use crate::gather::TranslationsByFile;
|
use crate::gather::TranslationsByFile;
|
||||||
use crate::gather::TranslationsByLang;
|
use crate::gather::TranslationsByLang;
|
||||||
|
|
||||||
pub fn write_strings(map: &TranslationsByLang, modules: &[Module]) {
|
pub fn write_strings(map: &TranslationsByLang, modules: &[Module], out_fn: &str, tag: &str) {
|
||||||
let mut buf = String::new();
|
let mut buf = String::new();
|
||||||
|
|
||||||
// lang->module map
|
// lang->module map
|
||||||
|
|
@ -25,23 +25,25 @@ pub fn write_strings(map: &TranslationsByLang, modules: &[Module]) {
|
||||||
// ordered list of translations by module
|
// ordered list of translations by module
|
||||||
write_translation_key_index(modules, &mut buf);
|
write_translation_key_index(modules, &mut buf);
|
||||||
// methods to generate messages
|
// methods to generate messages
|
||||||
write_methods(modules, &mut buf);
|
write_methods(modules, &mut buf, tag);
|
||||||
|
|
||||||
let dir = PathBuf::from(std::env::var("OUT_DIR").unwrap());
|
let dir = PathBuf::from(std::env::var("OUT_DIR").unwrap());
|
||||||
let path = dir.join("strings.rs");
|
let path = dir.join(out_fn);
|
||||||
fs::write(path, buf).unwrap();
|
fs::write(path, buf).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_methods(modules: &[Module], buf: &mut String) {
|
fn write_methods(modules: &[Module], buf: &mut String, tag: &str) {
|
||||||
buf.push_str(
|
buf.push_str(
|
||||||
r#"
|
r#"
|
||||||
use crate::{I18n,Number};
|
#[allow(unused_imports)]
|
||||||
|
use crate::{I18n,Number,Translations};
|
||||||
|
#[allow(unused_imports)]
|
||||||
use fluent::{FluentValue, FluentArgs};
|
use fluent::{FluentValue, FluentArgs};
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
impl I18n {
|
|
||||||
"#,
|
"#,
|
||||||
);
|
);
|
||||||
|
writeln!(buf, "impl I18n<{tag}> {{").unwrap();
|
||||||
for module in modules {
|
for module in modules {
|
||||||
for translation in &module.translations {
|
for translation in &module.translations {
|
||||||
let func = translation.key.to_snake_case();
|
let func = translation.key.to_snake_case();
|
||||||
|
|
@ -142,7 +144,7 @@ fn write_translation_key_index(modules: &[Module], buf: &mut String) {
|
||||||
|
|
||||||
writeln!(
|
writeln!(
|
||||||
buf,
|
buf,
|
||||||
"pub(crate) const KEYS_BY_MODULE: [&[&str]; {count}] = [",
|
"pub(crate) const _KEYS_BY_MODULE: [&[&str]; {count}] = [",
|
||||||
count = modules.len(),
|
count = modules.len(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
@ -162,7 +164,7 @@ fn write_translation_key_index(modules: &[Module], buf: &mut String) {
|
||||||
fn write_lang_map(map: &TranslationsByLang, buf: &mut String) {
|
fn write_lang_map(map: &TranslationsByLang, buf: &mut String) {
|
||||||
buf.push_str(
|
buf.push_str(
|
||||||
"
|
"
|
||||||
pub(crate) const STRINGS: phf::Map<&str, &phf::Map<&str, &str>> = phf::phf_map! {
|
pub(crate) const _STRINGS: phf::Map<&str, &phf::Map<&str, &str>> = phf::phf_map! {
|
||||||
",
|
",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -256,7 +256,7 @@ fn check_for_unstaged_changes() {
|
||||||
|
|
||||||
fn generate_licences() -> Result<String> {
|
fn generate_licences() -> Result<String> {
|
||||||
if which::which("cargo-license").is_err() {
|
if which::which("cargo-license").is_err() {
|
||||||
Command::run("cargo install cargo-license@0.5.1")?;
|
Command::run("cargo install cargo-license@0.7.0")?;
|
||||||
}
|
}
|
||||||
let output = Command::run_with_output([
|
let output = Command::run_with_output([
|
||||||
"cargo-license",
|
"cargo-license",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue