Skip to content

Commit 452fad6

Browse files
authored
improv: localization improvements
1 parent 2b480c1 commit 452fad6

File tree

8 files changed

+145
-97
lines changed

8 files changed

+145
-97
lines changed

README.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,24 @@
11
# Freedesktop Desktop Entry Specification
22

3+
[![crates.io](https://img.shields.io/crates/v/freedesktop_desktop_entry?style=flat-square&logo=rust)](https://crates.io/crates/freedesktop_desktop_entry)
4+
[![docs.rs](https://img.shields.io/badge/docs.rs-freedesktop_desktop_entry-blue?style=flat-square&logo=docs.rs)](https://docs.rs/freedesktop_desktop_entry)
5+
36
This crate provides a library for efficiently parsing [Desktop Entry](https://specifications.freedesktop.org/desktop-entry-spec/latest/index.html) files.
47

58
```rust
69
use std::fs;
710

8-
use freedesktop_desktop_entry::{default_paths, DesktopEntry, Iter, PathSource};
11+
use freedesktop_desktop_entry::{
12+
default_paths, get_languages_from_env, DesktopEntry, Iter, PathSource,
13+
};
914

1015
fn main() {
16+
let locales = get_languages_from_env();
17+
1118
for path in Iter::new(default_paths()) {
1219
let path_src = PathSource::guess_from(&path);
1320
if let Ok(bytes) = fs::read_to_string(&path) {
14-
if let Ok(entry) = DesktopEntry::decode(&path, &bytes) {
21+
if let Ok(entry) = DesktopEntry::decode_from_str(&path, &bytes, &locales) {
1522
println!("{:?}: {}\n---\n{}", path_src, path.display(), entry);
1623
}
1724
}

examples/all_files.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@ use freedesktop_desktop_entry::{
88
};
99

1010
fn main() {
11-
let locale = get_languages_from_env();
11+
let locales = get_languages_from_env();
1212

1313
for path in Iter::new(default_paths()) {
1414
let path_src = PathSource::guess_from(&path);
1515
if let Ok(bytes) = fs::read_to_string(&path) {
16-
if let Ok(entry) = DesktopEntry::decode_from_str(&path, &bytes, &locale) {
16+
if let Ok(entry) = DesktopEntry::decode_from_str(&path, &bytes, &locales) {
1717
println!("{:?}: {}\n---\n{}", path_src, path.display(), entry);
1818
}
1919
}

examples/specific_file.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use freedesktop_desktop_entry::DesktopEntry;
44

55
fn main() {
66
let path = Path::new("tests/org.mozilla.firefox.desktop");
7-
let locales = &["fr", "en"];
7+
let locales = &["fr_FR", "en", "it"];
88

99
// if let Ok(bytes) = fs::read_to_string(path) {
1010
// if let Ok(entry) = DesktopEntry::decode_from_str(path, &bytes, locales) {

src/decoder.rs

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,15 @@ impl<'a> DesktopEntry<'a> {
3333
let mut active_group = Cow::Borrowed("");
3434
let mut ubuntu_gettext_domain = None;
3535

36+
let locales = add_generic_locales(locales);
37+
3638
for line in input.lines() {
3739
process_line(
3840
line,
3941
&mut groups,
4042
&mut active_group,
4143
&mut ubuntu_gettext_domain,
42-
locales,
44+
&locales,
4345
Cow::Borrowed,
4446
)
4547
}
@@ -60,8 +62,9 @@ impl<'a> DesktopEntry<'a> {
6062
L: AsRef<str>,
6163
{
6264
let mut buf = String::new();
65+
let locales = add_generic_locales(locales);
6366

64-
paths.map(move |path| decode_from_path_with_buf(path, locales, &mut buf))
67+
paths.map(move |path| decode_from_path_with_buf(path, &locales, &mut buf))
6568
}
6669

6770
/// Return an owned [`DesktopEntry`]
@@ -73,7 +76,8 @@ impl<'a> DesktopEntry<'a> {
7376
L: AsRef<str>,
7477
{
7578
let mut buf = String::new();
76-
decode_from_path_with_buf(path, locales, &mut buf)
79+
let locales = add_generic_locales(locales);
80+
decode_from_path_with_buf(path, &locales, &mut buf)
7781
}
7882
}
7983

@@ -188,3 +192,20 @@ where
188192
ubuntu_gettext_domain,
189193
})
190194
}
195+
196+
/// Ex: if a locale equal fr_FR, add fr
197+
fn add_generic_locales<'a, L: AsRef<str>>(locales: &'a [L]) -> Vec<&'a str> {
198+
let mut v = Vec::with_capacity(locales.len() + 1);
199+
200+
for l in locales {
201+
let l = l.as_ref();
202+
203+
v.push(l);
204+
205+
if let Some(start) = memchr::memchr(b'_', l.as_bytes()) {
206+
v.push(l.split_at(start).0)
207+
}
208+
}
209+
210+
v
211+
}

src/iter.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ pub struct Iter {
99
}
1010

1111
impl Iter {
12+
/// Directories will be processed in order, starting from the end.
1213
pub fn new(directories_to_walk: Vec<PathBuf>) -> Self {
1314
Self {
1415
directories_to_walk,
@@ -25,8 +26,7 @@ impl Iterator for Iter {
2526
let mut iterator = match self.actively_walking.take() {
2627
Some(dir) => dir,
2728
None => {
28-
while !self.directories_to_walk.is_empty() {
29-
let path = self.directories_to_walk.remove(0);
29+
while let Some(path) = self.directories_to_walk.pop() {
3030
match fs::read_dir(&path) {
3131
Ok(directory) => {
3232
self.actively_walking = Some(directory);
@@ -37,6 +37,7 @@ impl Iterator for Iter {
3737
Err(_) => continue,
3838
}
3939
}
40+
4041

4142
return None;
4243
}
@@ -48,7 +49,7 @@ impl Iterator for Iter {
4849

4950
if let Ok(file_type) = entry.file_type() {
5051
if file_type.is_dir() {
51-
self.directories_to_walk.insert(0, path);
52+
self.directories_to_walk.push(path);
5253
} else if (file_type.is_file() || file_type.is_symlink())
5354
&& path.extension().map_or(false, |ext| ext == "desktop")
5455
{

src/lib.rs

Lines changed: 77 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -70,61 +70,80 @@ impl<'a> DesktopEntry<'a> {
7070
}
7171

7272
impl<'a> DesktopEntry<'a> {
73-
pub fn action_entry(&'a self, action: &str, key: &str) -> Option<&'a Cow<'a, str>> {
73+
/// An action is defined as `[Desktop Action actions-name]` where `action-name`
74+
/// is defined in the `Actions` field of `[Desktop Entry]`.
75+
/// Example: to get the `Name` field of this `new-window` action
76+
/// ```txt
77+
/// [Desktop Action new-window]
78+
/// Name=Open a New Window
79+
/// ```
80+
/// you will need to call
81+
/// ```rust
82+
/// entry.action_entry("new-window", "Name")
83+
/// ```
84+
pub fn action_entry(&'a self, action: &str, key: &str) -> Option<&'a str> {
7485
let group = self
7586
.groups
7687
.get(["Desktop Action ", action].concat().as_str());
7788

7889
Self::entry(group, key)
7990
}
8091

81-
pub fn action_entry_localized(
92+
pub fn action_entry_localized<L: AsRef<str>>(
8293
&'a self,
8394
action: &str,
8495
key: &str,
85-
locale: Option<&str>,
96+
locales: &[L],
8697
) -> Option<Cow<'a, str>> {
8798
let group = self
8899
.groups
89100
.get(["Desktop Action ", action].concat().as_str());
90101

91-
Self::localized_entry(self.ubuntu_gettext_domain.as_deref(), group, key, locale)
102+
Self::localized_entry(self.ubuntu_gettext_domain.as_deref(), group, key, locales)
92103
}
93104

94-
pub fn action_exec(&'a self, action: &str) -> Option<&'a Cow<'a, str>> {
105+
pub fn action_exec(&'a self, action: &str) -> Option<&'a str> {
95106
self.action_entry(action, "Exec")
96107
}
97108

98-
pub fn action_name(&'a self, action: &str, locale: Option<&str>) -> Option<Cow<'a, str>> {
99-
self.action_entry_localized(action, "Name", locale)
109+
pub fn action_name<L: AsRef<str>>(
110+
&'a self,
111+
action: &str,
112+
locales: &[L],
113+
) -> Option<Cow<'a, str>> {
114+
self.action_entry_localized(action, "Name", locales)
100115
}
101116

117+
/// Return actions separated by `;`
102118
pub fn actions(&'a self) -> Option<&'a str> {
103119
self.desktop_entry("Actions")
104120
}
105121

122+
/// Return categories separated by `;`
106123
pub fn categories(&'a self) -> Option<&'a str> {
107124
self.desktop_entry("Categories")
108125
}
109126

110-
pub fn comment(&'a self, locale: Option<&str>) -> Option<Cow<'a, str>> {
111-
self.desktop_entry_localized("Comment", locale)
127+
pub fn comment<L: AsRef<str>>(&'a self, locales: &[L]) -> Option<Cow<'a, str>> {
128+
self.desktop_entry_localized("Comment", locales)
112129
}
113130

131+
/// A desktop entry field is any field under the
132+
/// `[Desktop Entry]` line
114133
pub fn desktop_entry(&'a self, key: &str) -> Option<&'a str> {
115134
Self::entry(self.groups.get("Desktop Entry"), key).map(|e| e.as_ref())
116135
}
117136

118-
pub fn desktop_entry_localized(
137+
pub fn desktop_entry_localized<L: AsRef<str>>(
119138
&'a self,
120139
key: &str,
121-
locale: Option<&str>,
140+
locales: &[L],
122141
) -> Option<Cow<'a, str>> {
123142
Self::localized_entry(
124143
self.ubuntu_gettext_domain.as_deref(),
125144
self.groups.get("Desktop Entry"),
126145
key,
127-
locale,
146+
locales,
128147
)
129148
}
130149

@@ -136,8 +155,8 @@ impl<'a> DesktopEntry<'a> {
136155
self.desktop_entry("X-Flatpak")
137156
}
138157

139-
pub fn generic_name(&'a self, locale: Option<&str>) -> Option<Cow<'a, str>> {
140-
self.desktop_entry_localized("GenericName", locale)
158+
pub fn generic_name<L: AsRef<str>>(&'a self, locales: &[L]) -> Option<Cow<'a, str>> {
159+
self.desktop_entry_localized("GenericName", locales)
141160
}
142161

143162
pub fn icon(&'a self) -> Option<&'a str> {
@@ -148,16 +167,18 @@ impl<'a> DesktopEntry<'a> {
148167
self.appid.as_ref()
149168
}
150169

151-
pub fn keywords(&'a self) -> Option<Cow<'a, str>> {
152-
self.desktop_entry_localized("Keywords", None)
170+
/// Return keywords separated by `;`
171+
pub fn keywords<L: AsRef<str>>(&'a self, locales: &[L]) -> Option<Cow<'a, str>> {
172+
self.desktop_entry_localized("Keywords", locales)
153173
}
154174

175+
/// Return mime types separated by `;`
155176
pub fn mime_type(&'a self) -> Option<&'a str> {
156177
self.desktop_entry("MimeType")
157178
}
158179

159-
pub fn name(&'a self, locale: Option<&str>) -> Option<Cow<'a, str>> {
160-
self.desktop_entry_localized("Name", locale)
180+
pub fn name<L: AsRef<str>>(&'a self, locales: &[L]) -> Option<Cow<'a, str>> {
181+
self.desktop_entry_localized("Name", locales)
161182
}
162183

163184
pub fn no_display(&'a self) -> bool {
@@ -192,33 +213,42 @@ impl<'a> DesktopEntry<'a> {
192213
self.desktop_entry(key).map_or(false, |v| v == "true")
193214
}
194215

195-
fn entry(group: Option<&'a KeyMap<'a>>, key: &str) -> Option<&'a Cow<'a, str>> {
196-
group.and_then(|group| group.get(key)).map(|key| &key.0)
216+
fn entry(group: Option<&'a KeyMap<'a>>, key: &str) -> Option<&'a str> {
217+
group
218+
.and_then(|group| group.get(key))
219+
.map(|key| key.0.as_ref())
197220
}
198221

199-
pub(crate) fn localized_entry(
222+
pub(crate) fn localized_entry<L: AsRef<str>>(
200223
ubuntu_gettext_domain: Option<&'a str>,
201224
group: Option<&'a KeyMap<'a>>,
202225
key: &str,
203-
locale: Option<&str>,
226+
locales: &[L],
204227
) -> Option<Cow<'a, str>> {
205-
group.and_then(|group| group.get(key)).and_then(|key| {
206-
locale
207-
.and_then(|locale| match key.1.get(locale).cloned() {
208-
Some(value) => Some(value),
209-
None => {
210-
if let Some(pos) = locale.find('_') {
211-
key.1.get(&locale[..pos]).cloned()
212-
} else {
213-
None
228+
let Some(group) = group else {
229+
return None;
230+
};
231+
232+
let Some((default_value, locale_map)) = group.get(key) else {
233+
return None;
234+
};
235+
236+
for locale in locales {
237+
match locale_map.get(locale.as_ref()) {
238+
Some(value) => return Some(value.clone()),
239+
None => {
240+
if let Some(pos) = memchr::memchr(b'_', locale.as_ref().as_bytes()) {
241+
if let Some(value) = locale_map.get(&locale.as_ref()[..pos]) {
242+
return Some(value.clone());
214243
}
215244
}
216-
})
217-
.or_else(|| {
218-
ubuntu_gettext_domain.map(|domain| Cow::Owned(dgettext(domain, &key.0)))
219-
})
220-
.or(Some(key.0.clone()))
221-
})
245+
}
246+
}
247+
}
248+
if let Some(domain) = ubuntu_gettext_domain {
249+
return Some(Cow::Owned(dgettext(domain, &default_value)));
250+
}
251+
return Some(default_value.clone());
222252
}
223253
}
224254

@@ -292,6 +322,7 @@ impl PathSource {
292322

293323
/// Returns the default paths in which desktop entries should be searched for based on the current
294324
/// environment.
325+
/// Paths are sorted by priority, in reverse, e.i the path with the greater priority will be at the end.
295326
///
296327
/// Panics in case determining the current home directory fails.
297328
pub fn default_paths() -> Vec<PathBuf> {
@@ -300,40 +331,31 @@ pub fn default_paths() -> Vec<PathBuf> {
300331
data_dirs.push(base_dirs.get_data_home());
301332
data_dirs.append(&mut base_dirs.get_data_dirs());
302333

303-
data_dirs.iter().map(|d| d.join("applications")).collect()
334+
data_dirs
335+
.iter()
336+
.map(|d| d.join("applications"))
337+
.rev()
338+
.collect()
304339
}
305340

306-
fn dgettext(domain: &str, message: &str) -> String {
341+
pub(crate) fn dgettext(domain: &str, message: &str) -> String {
307342
use gettextrs::{setlocale, LocaleCategory};
308343
setlocale(LocaleCategory::LcAll, "");
309344
gettextrs::dgettext(domain, message)
310345
}
311346

312-
// todo: support more variable syntax like fr_FR.
313-
// This will require some work in decode and values query
314-
// for now, just remove the _* part, cause it seems more common
315-
316347
/// Get the configured user language env variables.
317348
/// See https://wiki.archlinux.org/title/Locale#LANG:_default_locale for more information
318349
pub fn get_languages_from_env() -> Vec<String> {
319350
let mut l = Vec::new();
320351

321-
if let Ok(mut lang) = std::env::var("LANG") {
322-
if let Some(start) = memchr::memchr(b'_', lang.as_bytes()) {
323-
lang.truncate(start);
324-
l.push(lang)
325-
} else {
326-
l.push(lang);
327-
}
352+
if let Ok(lang) = std::env::var("LANG") {
353+
l.push(lang);
328354
}
329355

330356
if let Ok(lang) = std::env::var("LANGUAGES") {
331357
lang.split(':').for_each(|lang| {
332-
if let Some(start) = memchr::memchr(b'_', lang.as_bytes()) {
333-
l.push(lang.split_at(start).0.to_owned())
334-
} else {
335-
l.push(lang.to_owned());
336-
}
358+
l.push(lang.to_owned());
337359
})
338360
}
339361

0 commit comments

Comments
 (0)