Skip to content

Commit 39c828d

Browse files
authored
Merge pull request #33 from l4l/locales
2 parents 62c4aff + 03896ae commit 39c828d

File tree

5 files changed

+154
-5
lines changed

5 files changed

+154
-5
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## Features
44

5+
- Support localization (#33)
56
- Search by keywords in apps mode (#20)
67
- Magic separators support: `!!` for args, `#` for envs and `~` for workdir (#19)
78
- Display full path for ambiguous binapps (0b47575)

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ itertools = "0.9.0"
3434
euclid = "0.22.1"
3535
nom = { version = "6.0.1", default-features = false, features = ["std", "regexp"] }
3636
regex = "1.4.2"
37+
libc = "0.2.81"
3738

3839
[profile.release]
3940
lto = true

src/desktop.rs

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ use xdg::BaseDirectories;
66

77
use crate::icon::Icon;
88

9+
mod locale;
10+
911
pub static XDG_DIRS: OnceCell<BaseDirectories> = OnceCell::new();
1012

1113
pub struct Entry {
@@ -83,8 +85,19 @@ fn traverse_dir_entry(mut entries: &mut Vec<Entry>, dir_entry: DirEntry) {
8385
return;
8486
}
8587
};
88+
8689
let main_section = entry.section("Desktop Entry");
87-
match (main_section.attr("Name"), main_section.attr("Exec")) {
90+
let locale = locale::Locale::current();
91+
92+
let localized_entry = |attr_name: &str| {
93+
locale
94+
.keys()
95+
.filter_map(|key| main_section.attr_with_param(attr_name, key))
96+
.next()
97+
.or_else(|| main_section.attr(attr_name))
98+
};
99+
100+
match (localized_entry("Name"), main_section.attr("Exec")) {
88101
(Some(n), Some(e)) => {
89102
entries.push(Entry {
90103
name: n.to_owned(),
@@ -96,14 +109,12 @@ fn traverse_dir_entry(mut entries: &mut Vec<Entry>, dir_entry: DirEntry) {
96109
.to_owned(),
97110
path: dir_entry_path,
98111
exec: e.to_owned(),
99-
// TODO: use `attr_with_param` with locale first
100-
name_with_keywords: n.to_owned()
101-
+ main_section.attr("Keywords").unwrap_or_default(),
112+
name_with_keywords: n.to_owned() + localized_entry("Keywords").unwrap_or_default(),
102113
is_terminal: main_section
103114
.attr("Terminal")
104115
.map(|s| s == "true")
105116
.unwrap_or(false),
106-
icon: main_section.attr("Icon").and_then(|name| {
117+
icon: localized_entry("Icon").and_then(|name| {
107118
let icon_path = Path::new(name);
108119

109120
if icon_path.is_absolute() {

src/desktop/locale.rs

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
use std::ffi::CStr;
2+
3+
use once_cell::sync::OnceCell;
4+
use regex::Regex;
5+
6+
#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
7+
pub struct Locale<'a> {
8+
lang: Option<&'a str>,
9+
country: Option<&'a str>,
10+
modifier: Option<&'a str>,
11+
}
12+
13+
const LOCALE_REGEX: &str = r#"(?x)
14+
^
15+
([[:alpha:]]+) # lang
16+
(?:_([[:alpha:]]+))? # country
17+
(?:\.[^@]*)? # encoding
18+
(?:@(.*))? # modifier
19+
$"#;
20+
21+
impl<'a> Locale<'a> {
22+
fn from_caputres(s: &'a str, captures: regex::Captures<'_>) -> Self {
23+
Self {
24+
lang: captures.get(1).map(|m| &s[m.range()]),
25+
country: captures.get(2).map(|m| &s[m.range()]),
26+
modifier: captures.get(3).map(|m| &s[m.range()]),
27+
}
28+
}
29+
}
30+
31+
impl Locale<'static> {
32+
pub fn current<'a>() -> &'a Self {
33+
static LOCALE: OnceCell<Option<Locale<'static>>> = OnceCell::new();
34+
LOCALE
35+
.get_or_init(|| {
36+
let s = unsafe {
37+
let ptr = libc::setlocale(libc::LC_MESSAGES, b"\0".as_ptr().cast());
38+
if ptr.is_null() {
39+
return None;
40+
}
41+
CStr::from_ptr(ptr)
42+
}
43+
.to_str()
44+
.ok()?;
45+
46+
let re = Regex::new(LOCALE_REGEX).unwrap();
47+
48+
let c = re.captures(s)?;
49+
50+
Some(Self::from_caputres(s, c))
51+
})
52+
.as_ref()
53+
.unwrap_or(&Self {
54+
lang: None,
55+
country: None,
56+
modifier: None,
57+
})
58+
}
59+
60+
pub fn keys(&self) -> impl Iterator<Item = impl AsRef<str>> + '_ {
61+
static LOCALE_ITERS: OnceCell<Vec<String>> = OnceCell::new();
62+
LOCALE_ITERS
63+
.get_or_init(|| {
64+
let mut v = vec![];
65+
if let Some(((l, c), m)) = self.lang.zip(self.country).zip(self.modifier) {
66+
v.push(format!("{}_{}@{}", l, c, m));
67+
}
68+
if let Some((l, c)) = self.lang.zip(self.country) {
69+
v.push(format!("{}_{}", l, c));
70+
}
71+
if let Some((l, m)) = self.lang.zip(self.modifier) {
72+
v.push(format!("{}@{}", l, m));
73+
}
74+
if let Some(l) = self.lang {
75+
v.push(l.to_string());
76+
}
77+
78+
v
79+
})
80+
.clone()
81+
.into_iter()
82+
}
83+
}
84+
85+
#[cfg(test)]
86+
mod tests {
87+
use super::*;
88+
89+
use test_case::test_case;
90+
91+
#[test]
92+
fn regex_compiles() {
93+
let _ = Regex::new(LOCALE_REGEX).unwrap();
94+
}
95+
96+
#[test]
97+
fn regex_doesnt_match_empty() {
98+
let re = Regex::new(LOCALE_REGEX).unwrap();
99+
assert!(re.captures("").is_none());
100+
}
101+
102+
impl Locale<'static> {
103+
fn new(
104+
lang: impl Into<Option<&'static str>>,
105+
country: impl Into<Option<&'static str>>,
106+
modifier: impl Into<Option<&'static str>>,
107+
) -> Self {
108+
Self {
109+
lang: lang.into(),
110+
country: country.into(),
111+
modifier: modifier.into(),
112+
}
113+
}
114+
}
115+
116+
#[test_case("qw", Locale::new("qw", None, None); "lang")]
117+
#[test_case("qw_ER", Locale::new("qw", "ER", None); "lang, country")]
118+
#[test_case("qw_ER.ty", Locale::new("qw", "ER", None); "lang, country, encoding")]
119+
#[test_case(
120+
"qw_ER.ty@ui",
121+
Locale::new("qw", "ER", "ui");
122+
"lang, country, encoding, modifier"
123+
)]
124+
#[test_case("qw@ui", Locale::new("qw", None, "ui"); "lang, modifier")]
125+
fn regex_compiles(s: &str, x: Locale<'static>) {
126+
let re = Regex::new(LOCALE_REGEX).unwrap();
127+
let c = re.captures(s).unwrap();
128+
129+
let m = c.get(0).unwrap();
130+
assert_eq!(m.start(), 0);
131+
assert_eq!(m.end(), s.len());
132+
133+
assert_eq!(Locale::from_caputres(s, c), x);
134+
}
135+
}

0 commit comments

Comments
 (0)