Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ separately in [CHANGELOG-DEPS.md](CHANGELOG-DEPS.md).

## Unreleased

- **File format labels and relative modification dates follow the interface
language.** File detection keeps its original identifiers.

- **The interface is available in Italian.** Select Italiano under Language
in Appearance settings.

- **Going up to a parent folder selects the folder you came from and scrolls
it into view, whichever way you go up.** Backspace and Back already did
this; clicking a breadcrumb segment, a folder in the sidebar tree, or typing
Expand Down
7 changes: 4 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,8 +248,9 @@ the same change:
`msgid!` form described in
[Localization](docs/features/LOCALIZATION.md#for-developers-writing-strings).
2. Regenerate `locales/en.json` with the extraction test.
3. Add or update the matching entries in both bundled packs,
`locales/de.json` and `locales/fr.json`. Preserve every placeholder and
3. Add or update the matching entries in all bundled packs:
`locales/de.json`, `locales/fr.json`, `locales/it.json`, and `locales/pl.json`.
Preserve every placeholder and
plural shape; do not ship newly added text as an English fallback.
4. Run the i18n extraction and bundled-pack tests before finishing.

Expand Down Expand Up @@ -279,7 +280,7 @@ Before finishing code changes:
- If the change touches icons, update
[docs/features/ICONS.md](docs/features/ICONS.md) (see [Icons](#icons)).
- If the change adds or rewrites user-visible text, regenerate the English
catalog, translate the new entries in both bundled language packs, and run
catalog, translate the new entries in all bundled language packs, and run
the i18n tests (see [Localization](#localization)).
- If the change displays a count of anything, group it (see
[Counts](#counts)).
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ The ❌ rows are real gaps, not modesty.
| **Built-in text and image editing** | ✅ editor + redact/annotate | ⚠️ Quick Look markup | ❌ | ⚠️ opens another app |
| **Capture-safe screenshot mode** | ✅ Private Mode | ❌ | ❌ | ❌ |
| **Global / indexed search** | ⚠️ Spotlight-backed, single query box | ✅ rich | ✅ indexed | ✅ Tracker / Baloo |
| **Localization** | ⚠️ four bundled languages, any other importable | ✅ dozens | ✅ dozens | ✅ dozens |
| **Localization** | ⚠️ five bundled languages, any other importable | ✅ dozens | ✅ dozens | ✅ dozens |
| **Accessibility / maturity** | ❌ young, single-dev | ✅ decades | ✅ decades | ✅ mature |

✅ first-class · ⚠️ partial / varies · ❌ absent
Expand Down
1 change: 1 addition & 0 deletions crates/ferail-core/src/i18n/pack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ pub const EN_JSON: &str = include_str!("../../../../locales/en.json");
/// (in the config dir) takes precedence, so anyone can fix a string
/// locally before contributing it back.
pub const BUNDLED: &[(&str, &str)] = &[
("it", include_str!("../../../../locales/it.json")),
("fr", include_str!("../../../../locales/fr.json")),
("de", include_str!("../../../../locales/de.json")),
// Contributed by Bohun (github.com/bohunamiga).
Expand Down
45 changes: 25 additions & 20 deletions crates/ferail-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -565,8 +565,8 @@ pub fn now_unix() -> i64 {
/// Why this is allowed on the paint path when sizes/dates are otherwise
/// pre-formatted: a *relative* duration is timezone-independent (`now - mtime`
/// is identical in every zone), so unlike absolute hour-of-day formatting it
/// needs no local-timezone machinery: it is pure integer arithmetic plus one
/// small allocation, bounded to the handful of on-screen rows.
/// needs no local-timezone machinery. Arithmetic and translation lookups
/// stay bounded to the handful of on-screen rows.
pub fn humanize_mtime(mtime_unix: i64, now_unix: i64) -> String {
const MIN: i64 = 60;
const HOUR: i64 = 3_600;
Expand All @@ -581,51 +581,43 @@ pub fn humanize_mtime(mtime_unix: i64, now_unix: i64) -> String {
// date rather than a nonsensical negative "ago".
if diff < 0 {
return if diff > -MIN {
"just now".to_string()
crate::tr!("just now").to_string()
} else {
format_date(mtime_unix)
};
}
if diff == 0 {
return "just now".to_string();
return crate::tr!("just now").to_string();
}
if diff < MIN {
return format!("{diff} second{} ago", plural(diff));
return crate::trn!("{n} second ago", "{n} seconds ago", diff).to_string();
}
if diff < HOUR {
let (m, s) = (diff / MIN, diff % MIN);
return if s == 0 {
format!("{m} min ago")
crate::tr!("{m} min ago", m = m).to_string()
} else {
format!("{m} min {s} sec ago")
crate::tr!("{m} min {s} sec ago", m = m, s = s).to_string()
};
}
if diff < DAY {
let (h, m) = (diff / HOUR, (diff % HOUR) / MIN);
return if m == 0 {
format!("{h} hr ago")
crate::tr!("{h} hr ago", h = h).to_string()
} else {
format!("{h} hr {m} min ago")
crate::tr!("{h} hr {m} min ago", h = h, m = m).to_string()
};
}
if diff < WEEK {
let d = diff / DAY;
return format!("{d} day{} ago", plural(d));
return crate::trn!("{n} day ago", "{n} days ago", d).to_string();
}
if diff < YEAR {
return format_month_day(mtime_unix);
}
format_date(mtime_unix)
}

fn plural(n: i64) -> &'static str {
if n == 1 {
""
} else {
"s"
}
}

/// Days-from-unix-epoch → (Y, M, D) via Howard Hinnant's `civil_from_days`.
fn ymd(unix: i64) -> (i32, u32, u32) {
let days = unix.div_euclid(86_400);
Expand All @@ -645,9 +637,22 @@ fn ymd(unix: i64) -> (i32, u32, u32) {
fn format_month_day(unix: i64) -> String {
let (_, m, d) = ymd(unix);
const NAMES: [&str; 12] = [
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
crate::msgid!("Jan"),
crate::msgid!("Feb"),
crate::msgid!("Mar"),
crate::msgid!("Apr"),
crate::msgid!("May"),
crate::msgid!("Jun"),
crate::msgid!("Jul"),
crate::msgid!("Aug"),
crate::msgid!("Sep"),
crate::msgid!("Oct"),
crate::msgid!("Nov"),
crate::msgid!("Dec"),
];
format!("{} {}", NAMES[(m as usize - 1).min(11)], d)
let month = NAMES[(m as usize - 1).min(11)];
let month = crate::i18n::tr_dynamic(month).unwrap_or_else(|| month.into());
format!("{month} {d}")
}

fn format_date(unix: i64) -> String {
Expand Down
178 changes: 90 additions & 88 deletions crates/ferail-fs-native/src/magic/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,126 +167,128 @@ impl MagicType {
pub fn display_name(&self) -> &'static str {
match self {
// Documents
MagicType::Pdf => "PDF document",
MagicType::Pdf => ferail_core::msgid!("PDF document"),

// Office: extension is more familiar to users than
// "Document (Word)" style. Keep concise.
MagicType::DocWord | MagicType::DocWordMacro | MagicType::DocWordOle => "Word document",
MagicType::DocWord | MagicType::DocWordMacro | MagicType::DocWordOle => {
ferail_core::msgid!("Word document")
}
MagicType::DocExcel | MagicType::DocExcelMacro | MagicType::DocExcelOle => {
"Excel spreadsheet"
ferail_core::msgid!("Excel spreadsheet")
}
MagicType::DocPowerPoint
| MagicType::DocPowerPointMacro
| MagicType::DocPowerPointOle => "PowerPoint presentation",
MagicType::OleCompound => "Compound document",
| MagicType::DocPowerPointOle => ferail_core::msgid!("PowerPoint presentation"),
MagicType::OleCompound => ferail_core::msgid!("Compound document"),

// Archives: match the historical flat-table labels exactly
// so existing format_label tests keep passing.
MagicType::Zip => "ZIP archive",
MagicType::ZipEncrypted => "ZIP archive",
MagicType::Rar => "RAR archive",
MagicType::SevenZip => "7z archive",
MagicType::Lha => "LHA archive",
MagicType::Lzx => "LZX archive",
MagicType::ExeAmiga => "Amiga executable",
MagicType::ObjAmiga => "Amiga object file",
MagicType::LibAmiga => "Amiga link library",
MagicType::AmigaIcon => "Amiga icon",
MagicType::Ilbm => "IFF ILBM image",
MagicType::Svx8 => "IFF 8SVX audio",
MagicType::IffAnim => "IFF ANIM animation",
MagicType::IffSmus => "IFF SMUS score",
MagicType::TrackerModule => "Tracker module",
MagicType::AdfDisk => "Amiga disk image",
MagicType::DmsDisk => "DMS disk image",
MagicType::AmigaGuide => "AmigaGuide document",
MagicType::Tar => "TAR archive",
MagicType::Gzip => "Gzip archive",
MagicType::Xz => "XZ archive",
MagicType::Bzip2 => "Bzip2 archive",
MagicType::Zstd => "Zstandard archive",

MagicType::AppJar => "Java JAR",
MagicType::AppApk => "Android APK",
MagicType::Zip => ferail_core::msgid!("ZIP archive"),
MagicType::ZipEncrypted => ferail_core::msgid!("ZIP archive"),
MagicType::Rar => ferail_core::msgid!("RAR archive"),
MagicType::SevenZip => ferail_core::msgid!("7z archive"),
MagicType::Lha => ferail_core::msgid!("LHA archive"),
MagicType::Lzx => ferail_core::msgid!("LZX archive"),
MagicType::ExeAmiga => ferail_core::msgid!("Amiga executable"),
MagicType::ObjAmiga => ferail_core::msgid!("Amiga object file"),
MagicType::LibAmiga => ferail_core::msgid!("Amiga link library"),
MagicType::AmigaIcon => ferail_core::msgid!("Amiga icon"),
MagicType::Ilbm => ferail_core::msgid!("IFF ILBM image"),
MagicType::Svx8 => ferail_core::msgid!("IFF 8SVX audio"),
MagicType::IffAnim => ferail_core::msgid!("IFF ANIM animation"),
MagicType::IffSmus => ferail_core::msgid!("IFF SMUS score"),
MagicType::TrackerModule => ferail_core::msgid!("Tracker module"),
MagicType::AdfDisk => ferail_core::msgid!("Amiga disk image"),
MagicType::DmsDisk => ferail_core::msgid!("DMS disk image"),
MagicType::AmigaGuide => ferail_core::msgid!("AmigaGuide document"),
MagicType::Tar => ferail_core::msgid!("TAR archive"),
MagicType::Gzip => ferail_core::msgid!("Gzip archive"),
MagicType::Xz => ferail_core::msgid!("XZ archive"),
MagicType::Bzip2 => ferail_core::msgid!("Bzip2 archive"),
MagicType::Zstd => ferail_core::msgid!("Zstandard archive"),

MagicType::AppJar => ferail_core::msgid!("Java JAR"),
MagicType::AppApk => ferail_core::msgid!("Android APK"),

// Images
MagicType::Jpeg => "JPEG image",
MagicType::Png => "PNG image",
MagicType::Gif => "GIF image",
MagicType::Bmp => "BMP image",
MagicType::Webp => "WebP image",
MagicType::Ico => "Icon file",
MagicType::Tiff => "TIFF image",
MagicType::Heic => "HEIC image",
MagicType::Svg => "XML / SVG",
MagicType::Jpeg => ferail_core::msgid!("JPEG image"),
MagicType::Png => ferail_core::msgid!("PNG image"),
MagicType::Gif => ferail_core::msgid!("GIF image"),
MagicType::Bmp => ferail_core::msgid!("BMP image"),
MagicType::Webp => ferail_core::msgid!("WebP image"),
MagicType::Ico => ferail_core::msgid!("Icon file"),
MagicType::Tiff => ferail_core::msgid!("TIFF image"),
MagicType::Heic => ferail_core::msgid!("HEIC image"),
MagicType::Svg => ferail_core::msgid!("XML / SVG"),

// Video
MagicType::Mp4 => "MP4 video",
MagicType::Mov => "QuickTime movie",
MagicType::Avi => "AVI video",
MagicType::Mkv => "Matroska / WebM",
MagicType::Webm => "Matroska / WebM",
MagicType::Mp4 => ferail_core::msgid!("MP4 video"),
MagicType::Mov => ferail_core::msgid!("QuickTime movie"),
MagicType::Avi => ferail_core::msgid!("AVI video"),
MagicType::Mkv => ferail_core::msgid!("Matroska / WebM"),
MagicType::Webm => ferail_core::msgid!("Matroska / WebM"),

// Audio
MagicType::Mp3 => "MP3 audio",
MagicType::Wav => "WAV audio",
MagicType::Flac => "FLAC audio",
MagicType::Ogg => "Ogg audio",
MagicType::Aiff => "AIFF audio",
MagicType::M4a => "M4A audio",
MagicType::Asf => "Windows Media",
MagicType::Mp3 => ferail_core::msgid!("MP3 audio"),
MagicType::Wav => ferail_core::msgid!("WAV audio"),
MagicType::Flac => ferail_core::msgid!("FLAC audio"),
MagicType::Ogg => ferail_core::msgid!("Ogg audio"),
MagicType::Aiff => ferail_core::msgid!("AIFF audio"),
MagicType::M4a => ferail_core::msgid!("M4A audio"),
MagicType::Asf => ferail_core::msgid!("Windows Media"),

// Data
MagicType::Json => "JSON",
MagicType::Xml => "XML",
MagicType::Html => "HTML document",
MagicType::NfoScene => "Scene NFO",
MagicType::NfoKodi => "Kodi NFO",
MagicType::NfoMsInfo => "System Information NFO",
MagicType::ChecksumSfv => "SFV checksum list",
MagicType::ChecksumList => "Checksum list",
MagicType::Sqlite => "SQLite database",
MagicType::Json => ferail_core::msgid!("JSON"),
MagicType::Xml => ferail_core::msgid!("XML"),
MagicType::Html => ferail_core::msgid!("HTML document"),
MagicType::NfoScene => ferail_core::msgid!("Scene NFO"),
MagicType::NfoKodi => ferail_core::msgid!("Kodi NFO"),
MagicType::NfoMsInfo => ferail_core::msgid!("System Information NFO"),
MagicType::ChecksumSfv => ferail_core::msgid!("SFV checksum list"),
MagicType::ChecksumList => ferail_core::msgid!("Checksum list"),
MagicType::Sqlite => ferail_core::msgid!("SQLite database"),

// Executables: keep "executable" in the string so
// icons::classify_file's substring matcher still resolves
// to FileTypeTint::Executable.
MagicType::ExeWindows => "PE / DOS executable",
MagicType::DllWindows => "PE / DOS executable",
MagicType::ExeWindowsNet => "PE / DOS executable",
MagicType::ExeLinux => "ELF executable",
MagicType::SoLinux => "ELF executable",
MagicType::ExeMac => "Mach-O executable",
MagicType::DylibMac => "Mach-O dylib",
MagicType::ObjMac => "Mach-O object",
MagicType::MachOFat => "Mach-O fat / Java class",
MagicType::ExeWindows => ferail_core::msgid!("PE / DOS executable"),
MagicType::DllWindows => ferail_core::msgid!("PE / DOS executable"),
MagicType::ExeWindowsNet => ferail_core::msgid!("PE / DOS executable"),
MagicType::ExeLinux => ferail_core::msgid!("ELF executable"),
MagicType::SoLinux => ferail_core::msgid!("ELF executable"),
MagicType::ExeMac => ferail_core::msgid!("Mach-O executable"),
MagicType::DylibMac => ferail_core::msgid!("Mach-O dylib"),
MagicType::ObjMac => ferail_core::msgid!("Mach-O object"),
MagicType::MachOFat => ferail_core::msgid!("Mach-O fat / Java class"),

// Scripts: keep "script" in the string for the
// classifier substring matcher.
MagicType::ScriptBash => "Shell script",
MagicType::ScriptPython => "Python script",
MagicType::ScriptPerl => "Perl script",
MagicType::ScriptRuby => "Ruby script",
MagicType::ScriptNode => "Node.js script",
MagicType::ScriptOther => "Script",
MagicType::ScriptBash => ferail_core::msgid!("Shell script"),
MagicType::ScriptPython => ferail_core::msgid!("Python script"),
MagicType::ScriptPerl => ferail_core::msgid!("Perl script"),
MagicType::ScriptRuby => ferail_core::msgid!("Ruby script"),
MagicType::ScriptNode => ferail_core::msgid!("Node.js script"),
MagicType::ScriptOther => ferail_core::msgid!("Script"),

// Text
MagicType::TextIni => "INI",
MagicType::TextReg => "Windows Registry",
MagicType::TextPlain => "Plain text",
MagicType::Utf8Bom => "UTF-8 text",
MagicType::Utf16Le => "UTF-16 LE text",
MagicType::Utf16Be => "UTF-16 BE text",
MagicType::TextIni => ferail_core::msgid!("INI"),
MagicType::TextReg => ferail_core::msgid!("Windows Registry"),
MagicType::TextPlain => ferail_core::msgid!("Plain text"),
MagicType::Utf8Bom => ferail_core::msgid!("UTF-8 text"),
MagicType::Utf16Le => ferail_core::msgid!("UTF-16 LE text"),
MagicType::Utf16Be => ferail_core::msgid!("UTF-16 BE text"),

// Windows
MagicType::Lnk => "Windows shortcut",
MagicType::Url => "Internet shortcut",
MagicType::Lnk => ferail_core::msgid!("Windows shortcut"),
MagicType::Url => ferail_core::msgid!("Internet shortcut"),

// Fonts
MagicType::OpenType => "OpenType font",
MagicType::TrueType => "TrueType font",
MagicType::OpenType => ferail_core::msgid!("OpenType font"),
MagicType::TrueType => ferail_core::msgid!("TrueType font"),

MagicType::Folder => "Folder",
MagicType::Binary => "Binary",
MagicType::Folder => ferail_core::msgid!("Folder"),
MagicType::Binary => ferail_core::msgid!("Binary"),
MagicType::Unknown => "",
}
}
Expand Down
8 changes: 5 additions & 3 deletions crates/ferail-gpui/src/entry_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -249,8 +249,10 @@ pub fn gather(path: &Path, known_size: Option<u64>) -> EntryInfo {
let fmt_date = fsn::stat_info::format_local_datetime;

// ---- General ----
let mut general =
InfoSection::new(tr!("General").to_string()).text_if(tr!("Kind").to_string(), kind.clone());
let mut general = InfoSection::new(tr!("General").to_string()).text_if(
tr!("Kind").to_string(),
crate::i18n::tr_dyn(&kind).to_string(),
);
if let Some(uti) = sh.uti.clone() {
general = general.text_if(tr!("Type").to_string(), uti);
}
Expand Down Expand Up @@ -1183,7 +1185,7 @@ impl Render for EntryInfoView {
div()
.text_scale_xs()
.text_color(muted)
.child(self.kind.clone()),
.child(crate::i18n::tr_dyn(&self.kind)),
);

let content = v_flex()
Expand Down
Loading