Initial fluent support

This commit is contained in:
ochibani 2024-06-02 23:02:48 +00:00
parent 1db63c854b
commit adc55301b0
No known key found for this signature in database
GPG Key ID: 2C6B61CE0C704ED4
7 changed files with 1069 additions and 561 deletions

1334
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -6,22 +6,22 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
async-std = {version = "1.12.0", features = ["attributes"]}
chrono = "0.4.19" chrono = "0.4.19"
dark-light = "1.0.0"
dirs = "4.0.0" dirs = "4.0.0"
fluent-bundle = "0.15.3"
filename = "0.1.1"
gdk = { git = "https://github.com/gtk-rs/gtk4-rs.git", package = "gdk4"} gdk = { git = "https://github.com/gtk-rs/gtk4-rs.git", package = "gdk4"}
gdk-pixbuf = "0.9.0" gdk-pixbuf = "0.9.0"
gettext-rs = "0.7.0"
gtk = { version = "0.4.8", package = "gtk4", features = ["v4_8"] }
gio = { version = "0.15.0" } gio = { version = "0.15.0" }
gtk-sys = "0.15"
glib = "0.10.3" glib = "0.10.3"
rust-ini = "0.16" gstreamer = "0.20.5"
gtk = { version = "0.4.8", package = "gtk4", features = ["v4_8"] }
gtk-sys = "0.15"
regex = "1.4.3" regex = "1.4.3"
rust-ini = "0.16"
secfmt = "0.1.1" secfmt = "0.1.1"
subprocess = "0.2.6" subprocess = "0.2.6"
dark-light = "1.0.0"
async-std = {version = "1.12.0", features = ["attributes"]}
gstreamer = "0.20.5"
zbus = "3.12.0"
tempfile = "3.10.1" tempfile = "3.10.1"
filename = "0.1.1" zbus = "3.12.0"

View File

@ -625,7 +625,7 @@
</child> </child>
<child type="end"> <child type="end">
<object class="GtkButton" id="aboutbutton"> <object class="GtkButton" id="aboutbutton">
<property name="label">About</property> <property name="label" translatable="yes">button</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="can-focus">True</property> <property name="can-focus">True</property>
<property name="receives-default">True</property> <property name="receives-default">True</property>

75
locales/ar.ftl Normal file
View File

@ -0,0 +1,75 @@
blue-recorder = المسجّل الأزرق
area-chooser = محدد المنطقة
wayland-msg = غير مدعوم في وايلاند
default-command = تشغيل أمر عند الانتهاء:
file-name = اسم الملفّ الافتراضي:
mp4-format = MP4 (MPEG-4 Part 14)
mkv-format = MKV (Matroska multimedia container format)
webm-format = WEBM (Open Web Media File)
gif-format = GIF (Graphics Interchange Format)
avi-format = AVI (Audio Video Interleaved)
wmv-format = WMV (Windows Media Video)
nut-format = NUT (NUT Recording Format)
audio-input = مصدر صوت PulseAudio الافتراضي
record-video = تسجيل الصوت
record-audio = تسجيل الصوت
show-mouse = إظهار الفأرة
follow-mouse = متابعة الفأرة
auto-hide = إخفاء تلقائي
run-command = تشغيل أمر عند الانتهاء:
frames = الإطارات:
delay = التأخير:
audio-source = مدخل الصوت:
about = عن البرنامج
dialog-comment = مسجّل شاشة بسيط لسطح مكتب لينكس، يدعم إكس ووايلاند.
translator-credits = translator-credits
already-exist = File already exist. Do you want to overwrite it?
blue-recorder-preferences = Blue Recorder Preferences
file-name-overwritten = File Name (Will be overwritten)..
enter-command = Enter your command here..
select-window = Select a Window
select-area = Select an Area
default-frames = Default frames:
default-delay = Default delay:
default-folder = Default folder:
default-pipeline = Default pipeline:
record-mouse = Record Mouse
position-msg = Your area position has been saved!
stop-recording = stop recording

75
locales/en-US.ftl Normal file
View File

@ -0,0 +1,75 @@
blue-recorder = Blue Recorder
area-chooser = Area Chooser
wayland-msg = Not supported in Wayland
default-command = Default command:
file-name = Default filename:
mp4-format = MP4 (MPEG-4 Part 14)
mkv-format = MKV (Matroska multimedia container format)
webm-format = WEBM (Open Web Media File)
gif-format = GIF (Graphics Interchange Format)
avi-format = AVI (Audio Video Interleaved)
wmv-format = WMV (Windows Media Video)
nut-format = NUT (NUT Recording Format)
audio-input = Default PulseAudio Input Source
record-video = Record Video
record-audio = Record Audio
show-mouse = Show Mouse
follow-mouse = Follow Mouse
auto-hide = Auto Hide
run-command = Run Command After Recording:
frames = Frames:
delay = Delay:
audio-source = Audio Input Source:
about = about
dialog-comment = A simple screen recorder for Linux desktop. Supports Wayland & Xorg.
translator-credits = translator-credits
already-exist = File already exist. Do you want to overwrite it?
blue-recorder-preferences = Blue Recorder Preferences
file-name-overwritten = File Name (Will be overwritten)..
enter-command = Enter your command here..
select-window = Select a Window
select-area = Select an Area
default-frames = Default frames:
default-delay = Default delay:
default-folder = Default folder:
default-pipeline = Default pipeline:
record-mouse = Record Mouse
position-msg = Your area position has been saved!
stop-recording = stop recording

View File

@ -2,7 +2,6 @@ extern crate subprocess;
use crate::utils::{is_snap, is_wayland}; use crate::utils::{is_snap, is_wayland};
use crate::wayland_record::{CursorModeTypes, RecordTypes, WaylandRecorder}; use crate::wayland_record::{CursorModeTypes, RecordTypes, WaylandRecorder};
use chrono::prelude::*; use chrono::prelude::*;
use gettextrs::gettext;
use gtk::{prelude::*, ResponseType}; use gtk::{prelude::*, ResponseType};
use gtk::{ButtonsType, DialogFlags, MessageDialog, MessageType}; use gtk::{ButtonsType, DialogFlags, MessageDialog, MessageType};
use gtk::{CheckButton, ComboBoxText, Entry, FileChooserNative, SpinButton, Window}; use gtk::{CheckButton, ComboBoxText, Entry, FileChooserNative, SpinButton, Window};
@ -36,6 +35,7 @@ pub struct Ffmpeg {
pub record_window: Rc<RefCell<bool>>, pub record_window: Rc<RefCell<bool>>,
pub main_context: gtk::glib::MainContext, pub main_context: gtk::glib::MainContext,
pub temp_video_filename: String, pub temp_video_filename: String,
pub bundle: String,
} }
impl Ffmpeg { impl Ffmpeg {
@ -70,7 +70,7 @@ impl Ffmpeg {
DialogFlags::all(), DialogFlags::all(),
MessageType::Warning, MessageType::Warning,
ButtonsType::YesNo, ButtonsType::YesNo,
&gettext("File already exist. Do you want to overwrite it?"), &self.bundle,
); );
let answer = self.main_context.block_on(message_dialog.run_future()); let answer = self.main_context.block_on(message_dialog.run_future());

View File

@ -1,5 +1,4 @@
extern crate gdk; extern crate gdk;
extern crate gettextrs;
extern crate gio; extern crate gio;
extern crate gtk; extern crate gtk;
mod area_capture; mod area_capture;
@ -10,7 +9,8 @@ mod wayland_record;
mod utils; mod utils;
use ffmpeg_interface::Ffmpeg; use ffmpeg_interface::Ffmpeg;
use gettextrs::{bindtextdomain, gettext, setlocale, textdomain, LocaleCategory}; use fluent_bundle::bundle::FluentBundle;
use fluent_bundle::FluentResource;
use gtk::glib; use gtk::glib;
use gtk::prelude::*; use gtk::prelude::*;
use gtk::{ use gtk::{
@ -43,23 +43,32 @@ pub fn build_ui(application: &Application) {
let builder: Builder = Builder::from_string(ui_src.as_str()); let builder: Builder = Builder::from_string(ui_src.as_str());
// Translate // Translate
let mut po_path_abs = { let mut ftl_path = {
let mut current_exec_dir = std::env::current_exe().unwrap(); let mut current_exec_dir = std::env::current_exe().unwrap();
current_exec_dir.pop(); current_exec_dir.pop();
current_exec_dir current_exec_dir
}.join(Path::new("locales"));
if !ftl_path.exists() {
ftl_path = std::fs::canonicalize(Path::new(
&std::env::var("LOCPATH").unwrap_or_else(|_| String::from("locales")),
)).unwrap();
} }
.join(Path::new("po")); let supported_lang: Vec<String> = std::fs::read_dir(&ftl_path)
.unwrap()
if !po_path_abs.exists() { .map(|entry| {
po_path_abs = std::fs::canonicalize(Path::new( let path = entry.unwrap().path();
&std::env::var("PO_DIR").unwrap_or_else(|_| String::from("po")), path.file_stem().unwrap().to_string_lossy().to_string()
)) }).collect();
.unwrap(); let mut locale = std::env::var("LANG").unwrap_or("en-US".to_string());
if !supported_lang.contains(&locale) {
locale = String::from("en-US");
} }
let ftl_file = std::fs::read_to_string(
setlocale(LocaleCategory::LcAll, ""); format!("{}/{}.ftl", ftl_path.to_str().unwrap(),locale.split('.').next().unwrap())
bindtextdomain("blue-recorder", po_path_abs.to_str().unwrap()).unwrap(); ).unwrap();
textdomain("blue-recorder").unwrap(); let res = FluentResource::try_new(ftl_file).unwrap();
let mut bundle = FluentBundle::default();
bundle.add_resource(res).expect("Failed to add localization resources to the bundle.");
// Config initialize // Config initialize
config_management::initialize(); config_management::initialize();
@ -104,9 +113,11 @@ pub fn build_ui(application: &Application) {
// --- default properties // --- default properties
// Windows // Windows
main_window.set_title(Some(&gettext("Blue Recorder"))); main_window.set_title(Some(&bundle.format_pattern(bundle.get_message("blue-recorder").unwrap()
.value().unwrap(), None, &mut vec![]).to_string()));
main_window.set_application(Some(application)); main_window.set_application(Some(application));
area_chooser_window.set_title(Some(&gettext("Area Chooser"))); // Title is hidden area_chooser_window.set_title(Some(&bundle.format_pattern(bundle.get_message("area-chooser").unwrap()
.value().unwrap(), None, &mut vec![]).to_string())); // Title is hidden
// disable interaction with main window // disable interaction with main window
// main_window.set_can_focus(false); // main_window.set_can_focus(false);
@ -121,26 +132,36 @@ pub fn build_ui(application: &Application) {
area_grab_button.set_can_focus(false); area_grab_button.set_can_focus(false);
area_grab_button.set_can_target(false); area_grab_button.set_can_target(false);
area_grab_button.add_css_class("disabled"); area_grab_button.add_css_class("disabled");
area_grab_button.set_tooltip_text(Some(&gettext("Not supported in Wayland"))); area_grab_button.set_tooltip_text(Some(&bundle.format_pattern(bundle.get_message("wayland-msg").unwrap()
.value().unwrap(), None, &mut vec![]).to_string()));
} }
// Entries // Entries
filename_entry.set_placeholder_text(Some(&gettext("Default filename:"))); filename_entry.set_placeholder_text(Some(&bundle.format_pattern(bundle.get_message("file-name").unwrap()
command_entry.set_placeholder_text(Some(&gettext("Default command:"))); .value().unwrap(), None, &mut vec![]).to_string()));
command_entry.set_placeholder_text(Some(&bundle.format_pattern(bundle.get_message("default-command").unwrap()
.value().unwrap(), None, &mut vec![]).to_string()));
filename_entry.set_text(&config_management::get("default", "filename")); filename_entry.set_text(&config_management::get("default", "filename"));
command_entry.set_text(&config_management::get("default", "command")); command_entry.set_text(&config_management::get("default", "command"));
// CheckBox // CheckBox
format_chooser_combobox.append(Some("mp4"), &gettext("MP4 (MPEG-4 Part 14)")); format_chooser_combobox.append(Some("mp4"), &bundle.format_pattern(bundle.get_message("mp4-format").unwrap()
.value().unwrap(), None, &mut vec![]).to_string());
format_chooser_combobox.append( format_chooser_combobox.append(
Some("mkv"), Some("mkv"),
&gettext("MKV (Matroska multimedia container format)"), &bundle.format_pattern(bundle.get_message("mkv-format").unwrap()
.value().unwrap(), None, &mut vec![]).to_string(),
); );
format_chooser_combobox.append(Some("webm"), &gettext("WEBM (Open Web Media File)")); format_chooser_combobox.append(Some("webm"), &bundle.format_pattern(bundle.get_message("webm-format").unwrap()
format_chooser_combobox.append(Some("gif"), &gettext("GIF (Graphics Interchange Format)")); .value().unwrap(), None, &mut vec![]).to_string());
format_chooser_combobox.append(Some("avi"), &gettext("AVI (Audio Video Interleaved)")); format_chooser_combobox.append(Some("gif"), &bundle.format_pattern(bundle.get_message("gif-format").unwrap()
format_chooser_combobox.append(Some("wmv"), &gettext("WMV (Windows Media Video)")); .value().unwrap(), None, &mut vec![]).to_string());
format_chooser_combobox.append(Some("nut"), &gettext("NUT (NUT Recording Format)")); format_chooser_combobox.append(Some("avi"), &bundle.format_pattern(bundle.get_message("avi-format").unwrap()
.value().unwrap(), None, &mut vec![]).to_string());
format_chooser_combobox.append(Some("wmv"), &bundle.format_pattern(bundle.get_message("wmv-format").unwrap()
.value().unwrap(), None, &mut vec![]).to_string());
format_chooser_combobox.append(Some("nut"), &bundle.format_pattern(bundle.get_message("nut-format").unwrap()
.value().unwrap(), None, &mut vec![]).to_string());
format_chooser_combobox.set_active(Some(0)); format_chooser_combobox.set_active(Some(0));
// Get audio sources // Get audio sources
@ -171,18 +192,24 @@ pub fn build_ui(application: &Application) {
.collect() .collect()
}; };
audio_source_combobox.append(Some("default"), &gettext("Default PulseAudio Input Source")); audio_source_combobox.append(Some("default"), &bundle.format_pattern(bundle.get_message("audio-input").unwrap()
.value().unwrap(), None, &mut vec![]).to_string());
for (id, audio_source) in sources_descriptions.iter().enumerate() { for (id, audio_source) in sources_descriptions.iter().enumerate() {
audio_source_combobox.append(Some(id.to_string().as_str()), audio_source); audio_source_combobox.append(Some(id.to_string().as_str()), audio_source);
} }
audio_source_combobox.set_active(Some(0)); audio_source_combobox.set_active(Some(0));
// Switchs // Switchs
video_switch.set_label(Some(&gettext("Record Video"))); video_switch.set_label(Some(&bundle.format_pattern(bundle.get_message("record-video").unwrap()
audio_switch.set_label(Some(&gettext("Record Audio"))); .value().unwrap(), None, &mut vec![]).to_string()));
mouse_switch.set_label(Some(&gettext("Show Mouse"))); audio_switch.set_label(Some(&bundle.format_pattern(bundle.get_message("record-audio").unwrap()
follow_mouse_switch.set_label(Some(&gettext("Follow Mouse"))); .value().unwrap(), None, &mut vec![]).to_string()));
hide_switch.set_label(Some(&gettext("Auto Hide"))); mouse_switch.set_label(Some(&bundle.format_pattern(bundle.get_message("show-mouse").unwrap()
.value().unwrap(), None, &mut vec![]).to_string()));
follow_mouse_switch.set_label(Some(&bundle.format_pattern(bundle.get_message("follow-mouse").unwrap()
.value().unwrap(), None, &mut vec![]).to_string()));
hide_switch.set_label(Some(&bundle.format_pattern(bundle.get_message("auto-hide").unwrap()
.value().unwrap(), None, &mut vec![]).to_string()));
video_switch.set_active(config_management::get_bool("default", "videocheck")); video_switch.set_active(config_management::get_bool("default", "videocheck"));
audio_switch.set_active(config_management::get_bool("default", "audiocheck")); audio_switch.set_active(config_management::get_bool("default", "audiocheck"));
mouse_switch.set_active(config_management::get_bool("default", "mousecheck")); mouse_switch.set_active(config_management::get_bool("default", "mousecheck"));
@ -343,10 +370,14 @@ pub fn build_ui(application: &Application) {
} }
} }
// Labels // Labels
command_label.set_label(&gettext("Run Command After Recording:")); command_label.set_label(&bundle.format_pattern(bundle.get_message("run-command").unwrap()
frames_label.set_label(&gettext("Frames:")); .value().unwrap(), None, &mut vec![]).to_string());
delay_label.set_label(&gettext("Delay:")); frames_label.set_label(&bundle.format_pattern(bundle.get_message("frames").unwrap()
audio_source_label.set_label(&gettext("Audio Input Source:")); .value().unwrap(), None, &mut vec![]).to_string());
delay_label.set_label(&bundle.format_pattern(bundle.get_message("delay").unwrap()
.value().unwrap(), None, &mut vec![]).to_string());
audio_source_label.set_label(&bundle.format_pattern(bundle.get_message("audio-source").unwrap()
.value().unwrap(), None, &mut vec![]).to_string());
// Spin // Spin
frames_spin.set_value( frames_spin.set_value(
@ -416,6 +447,8 @@ pub fn build_ui(application: &Application) {
// --- connections // --- connections
// Show dialog window when about button clicked then hide it after close // Show dialog window when about button clicked then hide it after close
let _about_dialog: AboutDialog = about_dialog.to_owned(); let _about_dialog: AboutDialog = about_dialog.to_owned();
about_button.set_label(&bundle.format_pattern(bundle.get_message("about").unwrap()
.value().unwrap(), None, &mut vec![]).to_string());
about_button.connect_clicked(move |_| { about_button.connect_clicked(move |_| {
_about_dialog.show(); _about_dialog.show();
_about_dialog.set_hide_on_close(true); _about_dialog.set_hide_on_close(true);
@ -434,9 +467,6 @@ pub fn build_ui(application: &Application) {
let _area_chooser_window = area_chooser_window.clone(); let _area_chooser_window = area_chooser_window.clone();
let mut _area_capture = area_capture.clone(); let mut _area_capture = area_capture.clone();
area_set_button.connect_clicked(move |_| { area_set_button.connect_clicked(move |_| {
_area_capture
.borrow_mut()
.get_window_by_name(&gettext("Area Chooser"));
_area_chooser_window.hide(); _area_chooser_window.hide();
}); });
@ -467,6 +497,8 @@ pub fn build_ui(application: &Application) {
let main_context = glib::MainContext::default(); let main_context = glib::MainContext::default();
let wayland_record = main_context.block_on(WaylandRecorder::new()); let wayland_record = main_context.block_on(WaylandRecorder::new());
let bundle_msg = bundle.format_pattern(bundle.get_message("already-exist").unwrap()
.value().unwrap(), None, &mut vec![]).to_string();
// Init record struct // Init record struct
let ffmpeg_record_interface: Rc<RefCell<Ffmpeg>> = Rc::new(RefCell::new(Ffmpeg { let ffmpeg_record_interface: Rc<RefCell<Ffmpeg>> = Rc::new(RefCell::new(Ffmpeg {
filename: ( filename: (
@ -491,6 +523,7 @@ pub fn build_ui(application: &Application) {
record_window, record_window,
main_context, main_context,
temp_video_filename: String::new(), temp_video_filename: String::new(),
bundle: bundle_msg,
})); }));
// Record Button // Record Button
@ -581,14 +614,14 @@ pub fn build_ui(application: &Application) {
let logo = Image::from_file(&about_icon_path.to_str().unwrap()); let logo = Image::from_file(&about_icon_path.to_str().unwrap());
about_dialog.set_transient_for(Some(&main_window)); about_dialog.set_transient_for(Some(&main_window));
about_dialog.set_program_name(Some(&gettext("Blue Recorder"))); about_dialog.set_program_name(Some(&bundle.format_pattern(bundle.get_message("blue-recorder").unwrap()
.value().unwrap(), None, &mut vec![]).to_string()));
about_dialog.set_version(Some("0.2.0")); about_dialog.set_version(Some("0.2.0"));
about_dialog.set_copyright(Some("© 2021 Salem Yaslem")); about_dialog.set_copyright(Some("© 2021 Salem Yaslem"));
about_dialog.set_wrap_license(true); about_dialog.set_wrap_license(true);
about_dialog.set_license(Some("Blue Recorder is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.\n\nBlue Recorder is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n\nSee the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Blue Recorder. If not, see <http://www.gnu.org/licenses/>.")); about_dialog.set_license(Some("Blue Recorder is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.\n\nBlue Recorder is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n\nSee the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Blue Recorder. If not, see <http://www.gnu.org/licenses/>."));
about_dialog.set_comments(Some(&gettext( about_dialog.set_comments(Some(&bundle.format_pattern(bundle.get_message("dialog-comment").unwrap()
"A simple screen recorder for Linux desktop. Supports Wayland & Xorg.", .value().unwrap(), None, &mut vec![]).to_string()));
)));
about_dialog.set_authors(&[ about_dialog.set_authors(&[
"Salem Yaslem <s@sy.sa>", "Salem Yaslem <s@sy.sa>",
"M.Hanny Sabbagh <mhsabbagh@outlook.com>", "M.Hanny Sabbagh <mhsabbagh@outlook.com>",
@ -602,7 +635,8 @@ pub fn build_ui(application: &Application) {
"Abdullah Al-Baroty <albaroty@gmail.com>", "Abdullah Al-Baroty <albaroty@gmail.com>",
]); ]);
// Translators: Replace "translator-credits" with your names, one name per line // Translators: Replace "translator-credits" with your names, one name per line
about_dialog.set_translator_credits(Some(&gettext("translator-credits"))); about_dialog.set_translator_credits(Some(&bundle.format_pattern(bundle.get_message("translator-credits").unwrap()
.value().unwrap(), None, &mut vec![]).to_string()));
about_dialog.set_website(Some("https://github.com/xlmnxp/blue-recorder/")); about_dialog.set_website(Some("https://github.com/xlmnxp/blue-recorder/"));
about_dialog.set_logo_icon_name(Some("blue-recorder")); about_dialog.set_logo_icon_name(Some("blue-recorder"));
about_dialog.set_logo(logo.paintable().as_ref()); about_dialog.set_logo(logo.paintable().as_ref());