mirror of
https://github.com/storytold/filmcraft.git
synced 2026-11-02 18:09:21 +00:00
Merge integration track i18n
This commit is contained in:
+6
-1
@@ -9,12 +9,17 @@ iconography, images or other Adobe assets; open licences only; every asset attri
|
||||
|
||||
| Asset | Author | Source | Licence |
|
||||
|---|---|---|---|
|
||||
| `crates/ui-egui/src/i18n/sv.tsv` | FilmCraft contributors | Original work: clean-room Swedish interface translations | MIT OR Apache-2.0 |
|
||||
| `crates/ui-egui/src/i18n/it.tsv` | FilmCraft contributors | Original work: clean-room Italian interface translations | MIT OR Apache-2.0 |
|
||||
| `crates/ui-egui/src/i18n/tr.tsv` | FilmCraft contributors | Original work: clean-room Turkish interface translations | MIT OR Apache-2.0 |
|
||||
| `crates/ui-egui/src/i18n/id.tsv` | FilmCraft contributors | Original work: clean-room Indonesian interface translations | MIT OR Apache-2.0 |
|
||||
| `crates/ui-egui/src/i18n/ko.tsv` | FilmCraft contributors | Original work: clean-room Korean interface translations | MIT OR Apache-2.0 |
|
||||
| `crates/ui-egui/src/i18n/cs.tsv` | FilmCraft contributors | Original work: AI-assisted Czech interface translations; not yet proofread by a native speaker | MIT OR Apache-2.0 |
|
||||
| `crates/ui-egui/src/i18n/fr.tsv` | FilmCraft contributors | Original work: clean-room French interface translations | MIT OR Apache-2.0 |
|
||||
| `crates/ui-egui/src/i18n/pl.tsv` | FilmCraft contributors | Original work: clean-room Polish interface translations | MIT OR Apache-2.0 |
|
||||
| `crates/ui-egui/src/i18n/pt-br.tsv` | FilmCraft contributors | Original work: clean-room Brazilian Portuguese interface translations | MIT OR Apache-2.0 |
|
||||
| `crates/ui-egui/src/i18n/ru.tsv` | FilmCraft contributors | Original work: clean-room Russian interface translations | MIT OR Apache-2.0 |
|
||||
| `crates/ui-egui/src/i18n/bs.tsv` | FilmCraft contributors | Original work: AI-assisted clean-room Bosnian interface translations, not yet native-reviewed | MIT OR Apache-2.0 |
|
||||
| `crates/ui-egui/src/i18n/ru.tsv` | FilmCraft contributors | Original work: clean-room Russian interface translation (full catalog) | MIT OR Apache-2.0 |
|
||||
| `crates/ui-egui/src/i18n/uk.tsv` | FilmCraft contributors | Original work: clean-room Ukrainian interface translations | MIT OR Apache-2.0 |
|
||||
| `assets/fonts/Inter-Regular.ttf` | The Inter Project Authors | https://github.com/rsms/inter | OFL-1.1 (`assets/fonts/OFL-Inter.txt`) |
|
||||
| `assets/fonts/Inter-Medium.ttf` | The Inter Project Authors | https://github.com/rsms/inter | OFL-1.1 (`assets/fonts/OFL-Inter.txt`) |
|
||||
|
||||
+7
-3
@@ -5,7 +5,7 @@
|
||||
|
||||
**Stage: alpha** · next: beta, ~19 points of ready-for-real-work and ~400–700 h away, plus opening Premiere's `.prproj` ([why](docs/target-app-parity.md#stage); passes the [core-workflow gate](docs/roadmap.md#alpha-gate))
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-22 · **Change:** minor (ProRes alpha final-run conformance and native macOS validation) · **Target:** Adobe Premiere Pro 2026 (26.5.2)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-22 · **Change:** minor (ProRes alpha final-run conformance and native macOS validation; language table: Czech, #1156) · **Target:** Adobe Premiere Pro 2026 (26.5.2)
|
||||
|
||||
Progress toward parity with Adobe Premiere Pro. This page is the summary; the detail is in
|
||||
[docs/target-app-parity.md](docs/target-app-parity.md) (assessment, methodology, calibration),
|
||||
@@ -72,7 +72,7 @@ Evidence for every row: [docs/target-app-parity.md](docs/target-app-parity.md#by
|
||||
|
||||
## Languages
|
||||
|
||||
Premiere ships 10 interface languages. Measured from our catalogs (3,498 strings):
|
||||
Premiere ships 10 interface languages. Measured from our catalogs (3,593 strings, last measured 2026-10-19):
|
||||
|
||||
| Language | Status | Strings |
|
||||
|---|---|---|
|
||||
@@ -85,12 +85,15 @@ Premiere ships 10 interface languages. Measured from our catalogs (3,498 strings
|
||||
| Portuguese (Brazil) | partial (whole interface; native review welcome) | ~100% |
|
||||
| Indonesian | partial (whole interface; native review welcome) | ~100% |
|
||||
| Italian | partial (whole interface; native review welcome) | ~100% |
|
||||
| Bosnian | partial (whole interface; AI-assisted, native review welcome) | ~100% |
|
||||
| Korean | partial (whole interface; needs an installed Hangul font; native review welcome) | ~100% |
|
||||
| Czech | partial (whole interface; native review needed) | 100% |
|
||||
| Russian | partial (whole interface; native review welcome) | ~100% |
|
||||
| Japanese | partial (whole interface) | 99.7% |
|
||||
| German | partial (whole interface; menus native-reviewed, the rest welcome) | ~100% |
|
||||
| Vietnamese | none | 0% |
|
||||
|
||||
Also shipped: Ukrainian (menus only, 10%). Detail: [docs/localization-parity.md](docs/localization-parity.md).
|
||||
Also shipped: Swedish (whole interface; native review welcome, ~100%), Ukrainian (menus only, 10%) and Polish (whole interface; AI-drafted, native review welcome). Detail: [docs/localization-parity.md](docs/localization-parity.md).
|
||||
|
||||
## Upcoming
|
||||
|
||||
@@ -169,6 +172,7 @@ Ranked; detail and "done when" in [docs/roadmap.md](docs/roadmap.md) and [docs/g
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-22 | minor | ProRes alpha: write the final singleton run code; 4444/XQ HD clips that failed native macOS decoding now decode, with syntax regression tests (EffectCraft #293) |
|
||||
| 2026-10-19 | minor | Language table: Czech (#1156, 100%) |
|
||||
| 2026-10-13 | minor | Language table: German covers the whole interface (~100%) |
|
||||
| 2026-10-12 | trivial | Progress log: Transform shutter on adjustment layers (#1088) |
|
||||
| 2026-10-11 | minor | Language table: German ships the menus (#630, 10.6%) |
|
||||
|
||||
@@ -145,8 +145,18 @@ pub fn install(app: &FilmcraftApp, ctx: egui::Context) -> (Receiver<String>, Men
|
||||
filmcraft_ui_egui::i18n::Language::Id
|
||||
} else if checked("app.language.italian") {
|
||||
filmcraft_ui_egui::i18n::Language::It
|
||||
} else if checked("app.language.bosnian") {
|
||||
filmcraft_ui_egui::i18n::Language::Bs
|
||||
} else if checked("app.language.korean") {
|
||||
filmcraft_ui_egui::i18n::Language::Ko
|
||||
} else if checked("app.language.czech") {
|
||||
filmcraft_ui_egui::i18n::Language::Cs
|
||||
} else if checked("app.language.polish") {
|
||||
filmcraft_ui_egui::i18n::Language::Pl
|
||||
} else if checked("app.language.swedish") {
|
||||
filmcraft_ui_egui::i18n::Language::Sv
|
||||
} else if checked("app.language.turkish") {
|
||||
filmcraft_ui_egui::i18n::Language::Tr
|
||||
} else {
|
||||
filmcraft_ui_egui::i18n::Language::En
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ use super::{Deserialize, FrameRate, Serialize, Tick};
|
||||
#[serde(rename_all = "camelCase", default)]
|
||||
pub struct GeneralPrefs {
|
||||
/// Interface language (persisted independently of projects): `en`, `ja`, `es`, `pt-br`, `uk`,
|
||||
/// `zh-cn`, `de`, `ru`, `fr`, `id`, `it`, or `system` (the default: the operating system's preferred language when the
|
||||
/// `zh-cn`, `de`, `ru`, `fr`, `id`, `it`, `ko`, `cs`, `pl`, `sv`, `tr`, or `system` (the default: the operating system's preferred language when the
|
||||
/// interface has it, otherwise English).
|
||||
pub interface_language: String,
|
||||
/// "At Startup": `showHome` (FilmCraft: the demo project), `openMostRecent`, `emptyProject`.
|
||||
|
||||
@@ -213,16 +213,23 @@ pub(super) static CATEGORIES: &[Category] = &[
|
||||
Kind::Choice(&[
|
||||
("system", "System Language"),
|
||||
("en", "English"),
|
||||
("ja", "日本語"),
|
||||
("es", "Español"),
|
||||
("pt-br", "Português (Brasil)"),
|
||||
("uk", "Українська"),
|
||||
("zh-cn", "简体中文"),
|
||||
("de", "Deutsch"),
|
||||
("ru", "Русский"),
|
||||
("fr", "Français"),
|
||||
// Alphabetical by the language's own name after English; Cyrillic, then
|
||||
// CJK names follow the Latin ones. Edit ▸ Language lists them the same way.
|
||||
("id", "Bahasa Indonesia"),
|
||||
("bs", "Bosanski"),
|
||||
("cs", "Čeština"),
|
||||
("de", "Deutsch"),
|
||||
("es", "Español"),
|
||||
("fr", "Français"),
|
||||
("it", "Italiano"),
|
||||
("pl", "Polski"),
|
||||
("pt-br", "Português (Brasil)"),
|
||||
("sv", "Svenska"),
|
||||
("tr", "Türkçe"),
|
||||
("ru", "Русский"),
|
||||
("uk", "Українська"),
|
||||
("ja", "日本語"),
|
||||
("zh-cn", "简体中文"),
|
||||
("ko", "한국어"),
|
||||
]),
|
||||
true,
|
||||
|
||||
@@ -50,6 +50,31 @@ fn interface_language_persists_and_rejects_hostile_values() {
|
||||
}
|
||||
set(&mut s, "general.interfaceLanguage", json!("it"));
|
||||
assert_eq!(Preferences::load(&path).general.interface_language, "it");
|
||||
// Korean and Czech likewise store only their bare codes.
|
||||
for value in [json!("ko-KR"), json!("KO")] {
|
||||
assert!(s.execute("prefs.set", json!({"key": "general.interfaceLanguage", "value": value})).is_err());
|
||||
}
|
||||
set(&mut s, "general.interfaceLanguage", json!("ko"));
|
||||
assert_eq!(Preferences::load(&path).general.interface_language, "ko");
|
||||
for value in [json!("cs-CZ"), json!("CS")] {
|
||||
assert!(s.execute("prefs.set", json!({"key": "general.interfaceLanguage", "value": value})).is_err());
|
||||
}
|
||||
set(&mut s, "general.interfaceLanguage", json!("cs"));
|
||||
assert_eq!(Preferences::load(&path).general.interface_language, "cs");
|
||||
set(&mut s, "general.interfaceLanguage", json!("pl"));
|
||||
assert_eq!(Preferences::load(&path).general.interface_language, "pl");
|
||||
// Swedish is stored as `sv`, not a locale tag
|
||||
for value in [json!("sv-SE"), json!("sv-FI"), json!("SV")] {
|
||||
assert!(s.execute("prefs.set", json!({"key": "general.interfaceLanguage", "value": value})).is_err());
|
||||
}
|
||||
set(&mut s, "general.interfaceLanguage", json!("sv"));
|
||||
assert_eq!(Preferences::load(&path).general.interface_language, "sv");
|
||||
// Turkish is stored as `tr`, not a locale tag
|
||||
for value in [json!("tr-TR"), json!("TR")] {
|
||||
assert!(s.execute("prefs.set", json!({"key": "general.interfaceLanguage", "value": value})).is_err());
|
||||
}
|
||||
set(&mut s, "general.interfaceLanguage", json!("tr"));
|
||||
assert_eq!(Preferences::load(&path).general.interface_language, "tr");
|
||||
// back to following the operating system (#218)
|
||||
set(&mut s, "general.interfaceLanguage", json!("system"));
|
||||
assert_eq!(Preferences::load(&path).general.interface_language, "system");
|
||||
|
||||
@@ -38,8 +38,8 @@ the committed span and undo. Set `FILMCRAFT_UI_SNAPSHOT_DIR` to render screensho
|
||||
## Localisation
|
||||
|
||||
Edit > Language offers English, Japanese, Spanish, Brazilian Portuguese, Ukrainian, Simplified
|
||||
Chinese, German, Russian, French, Indonesian, Korean and Italian (all but Ukrainian and Russian cover
|
||||
the whole interface; those two cover the core menus). The language is stored in the engine's `general.interfaceLanguage` preference,
|
||||
Chinese, German, Russian, French, Indonesian, Korean, Italian, Czech, Polish, Swedish and Turkish (all but Ukrainian cover
|
||||
the whole interface; Ukrainian covers the core menus). The language is stored in the engine's `general.interfaceLanguage` preference,
|
||||
restored on startup, and also available in Settings > General.
|
||||
Its default, System Language (`system`), follows the operating system: the first of the user's
|
||||
preferred languages that the interface has (the host supplies them through
|
||||
@@ -53,10 +53,11 @@ fills translated templates, and `i18n::t` translates names from registries. Plac
|
||||
(including user filenames containing braces) are inserted literally. Catalog translations are
|
||||
original work using ordinary language, without proprietary localisation resources.
|
||||
|
||||
Spanish, Japanese, Simplified Chinese, French, Brazilian Portuguese and German cover menus, panels, dialogs, settings and registry labels. Searches accept both the
|
||||
Spanish, Japanese, Simplified Chinese, French, Brazilian Portuguese, German and Russian cover
|
||||
menus, panels, dialogs, settings and registry labels. Searches accept both the
|
||||
translated label and its English source, including Unicode capitals. Project content, command ids
|
||||
and preference values retain their original values. Engine errors, CLI and MCP messages remain
|
||||
English. Ukrainian and Russian currently cover the core menus; they fall back to English elsewhere.
|
||||
English. Ukrainian currently covers the core menus; it falls back to English elsewhere.
|
||||
|
||||
A string a catalog lacks shows in English, so a missing translation never fails a test or a
|
||||
build. `cargo xtask i18n-report` prints each language's missing strings (menus, `tl!` literals and
|
||||
|
||||
@@ -118,7 +118,7 @@ pub fn show(app: &mut FilmcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
}
|
||||
let ws_resp = btn(ui, Icon::Workspaces, "workspaces", tl!("Workspaces"), app);
|
||||
// workspace name (caps)
|
||||
let ws = crate::i18n::t(&app.ui.workspace).to_uppercase();
|
||||
let ws = crate::i18n::to_uppercase(crate::i18n::t(&app.ui.workspace));
|
||||
let wg = p.layout_no_wrap(ws.clone(), Tokens::ui(11.0), t.text_dim);
|
||||
let wr = Rect::from_min_size(pos2(rx - wg.size().x + 10.0, rect.center().y - 10.0), vec2(wg.size().x + 8.0, 20.0));
|
||||
let wresp = ui.interact(wr, egui::Id::unique("hdr-ws-name"), Sense::click());
|
||||
@@ -186,3 +186,15 @@ pub fn show(app: &mut FilmcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn workspace_name_uppercasing_is_turkish_aware() {
|
||||
crate::i18n::set_current(crate::i18n::Language::Tr);
|
||||
assert_eq!(crate::i18n::to_uppercase("Birleştirme"), "BİRLEŞTİRME");
|
||||
assert_eq!(crate::i18n::to_uppercase("Altyazılar ve Grafikler"), "ALTYAZILAR VE GRAFİKLER");
|
||||
crate::i18n::set_current(crate::i18n::Language::En);
|
||||
assert_eq!(crate::i18n::to_uppercase("Birleştirme"), "BIRLEŞTIRME");
|
||||
}
|
||||
}
|
||||
|
||||
+121
-5
@@ -47,8 +47,17 @@ pub enum Language {
|
||||
Fr,
|
||||
Id,
|
||||
It,
|
||||
/// Bosnian, Latin script (Ijekavian); `bs-Cyrl` locales use it too.
|
||||
Bs,
|
||||
/// Korean; Hangul needs an installed system font ([`korean_font_available`]).
|
||||
Ko,
|
||||
/// Czech; Latin with háčky and čárky, covered by the bundled fonts.
|
||||
Cs,
|
||||
/// Polish.
|
||||
Pl,
|
||||
/// Swedish.
|
||||
Sv,
|
||||
Tr,
|
||||
}
|
||||
|
||||
static JAPANESE: OnceLock<Catalog> = OnceLock::new();
|
||||
@@ -60,11 +69,17 @@ static GERMAN: OnceLock<Catalog> = OnceLock::new();
|
||||
static RUSSIAN: OnceLock<Catalog> = OnceLock::new();
|
||||
static FRENCH: OnceLock<Catalog> = OnceLock::new();
|
||||
static KOREAN: OnceLock<Catalog> = OnceLock::new();
|
||||
static POLISH: OnceLock<Catalog> = OnceLock::new();
|
||||
static INDONESIAN: OnceLock<Catalog> = OnceLock::new();
|
||||
static ITALIAN: OnceLock<Catalog> = OnceLock::new();
|
||||
static BOSNIAN: OnceLock<Catalog> = OnceLock::new();
|
||||
static CZECH: OnceLock<Catalog> = OnceLock::new();
|
||||
static SWEDISH: OnceLock<Catalog> = OnceLock::new();
|
||||
static TURKISH: OnceLock<Catalog> = OnceLock::new();
|
||||
|
||||
impl Language {
|
||||
pub const ALL: [Self; 12] = [Self::En, Self::Ja, Self::Es, Self::PtBr, Self::Uk, Self::ZhCn, Self::De, Self::Ru, Self::Fr, Self::Id, Self::Ko, Self::It];
|
||||
pub const ALL: [Self; 17] =
|
||||
[Self::En, Self::Ja, Self::Es, Self::PtBr, Self::Uk, Self::ZhCn, Self::De, Self::Ru, Self::Fr, Self::Id, Self::Ko, Self::It, Self::Cs, Self::Pl, Self::Sv, Self::Tr, Self::Bs];
|
||||
|
||||
/// BCP 47 tag for AccessKit and the web IME's hidden input. Keep the persisted spelling
|
||||
/// (`pt-br`, `zh-cn`) out of the protocol-facing value.
|
||||
@@ -81,7 +96,12 @@ impl Language {
|
||||
Self::Fr => "fr",
|
||||
Self::Id => "id",
|
||||
Self::It => "it",
|
||||
Self::Bs => "bs",
|
||||
Self::Ko => "ko",
|
||||
Self::Cs => "cs",
|
||||
Self::Pl => "pl",
|
||||
Self::Sv => "sv",
|
||||
Self::Tr => "tr",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +118,12 @@ impl Language {
|
||||
Self::Fr => "Français",
|
||||
Self::Id => "Bahasa Indonesia",
|
||||
Self::It => "Italiano",
|
||||
Self::Bs => "Bosanski",
|
||||
Self::Ko => "한국어",
|
||||
Self::Cs => "Čeština",
|
||||
Self::Pl => "Polski",
|
||||
Self::Sv => "Svenska",
|
||||
Self::Tr => "Türkçe",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +140,12 @@ impl Language {
|
||||
"fr" => Some(Self::Fr),
|
||||
"id" => Some(Self::Id),
|
||||
"it" => Some(Self::It),
|
||||
"bs" => Some(Self::Bs),
|
||||
"ko" => Some(Self::Ko),
|
||||
"cs" => Some(Self::Cs),
|
||||
"pl" => Some(Self::Pl),
|
||||
"sv" => Some(Self::Sv),
|
||||
"tr" => Some(Self::Tr),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -126,11 +156,11 @@ impl Language {
|
||||
/// any German (`de-AT`, `de_CH.UTF-8`…) gets the German one and any French (`fr-CA`, `fr_BE.UTF-8`…)
|
||||
/// the French one; any Indonesian (`id-ID`, `id_ID.UTF-8`, and the legacy `in-ID` that older Java
|
||||
/// and Android runtimes report) the Indonesian one; any Italian (`it-IT`, `it-CH`, `it_IT.UTF-8`…)
|
||||
/// the Italian one. Chinese gets the Simplified catalog
|
||||
/// the Italian one; any Bosnian (`bs-BA`, `bs_BA.UTF-8`, `bs-Latn-BA`…) the Bosnian one; any Swedish (`sv-SE`, `sv-FI`, `sv_SE.UTF-8`…) the Swedish one; any Turkish (`tr`, `tr-TR`, `tr_TR.UTF-8`…) the Turkish one. Chinese gets the Simplified catalog
|
||||
/// only for Simplified locales (`zh`, `zh-CN`, `zh-SG`, `zh-Hans…`); Traditional ones (`zh-TW`,
|
||||
/// `zh-HK`, `zh-MO`, `zh-Hant…`) are skipped.
|
||||
pub fn from_locales(tags: &[String]) -> Self {
|
||||
const PRIMARY: [(&str, Language); 12] = [
|
||||
const PRIMARY: [(&str, Language); 17] = [
|
||||
("en", Language::En),
|
||||
("ja", Language::Ja),
|
||||
("es", Language::Es),
|
||||
@@ -142,7 +172,12 @@ impl Language {
|
||||
("id", Language::Id),
|
||||
("in", Language::Id),
|
||||
("it", Language::It),
|
||||
("bs", Language::Bs),
|
||||
("ko", Language::Ko),
|
||||
("cs", Language::Cs),
|
||||
("pl", Language::Pl),
|
||||
("sv", Language::Sv),
|
||||
("tr", Language::Tr),
|
||||
];
|
||||
tags.iter()
|
||||
.find_map(|tag| {
|
||||
@@ -174,7 +209,12 @@ impl Language {
|
||||
Self::Fr => "fr",
|
||||
Self::Id => "id",
|
||||
Self::It => "it",
|
||||
Self::Bs => "bs",
|
||||
Self::Ko => "ko",
|
||||
Self::Cs => "cs",
|
||||
Self::Pl => "pl",
|
||||
Self::Sv => "sv",
|
||||
Self::Tr => "tr",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,7 +232,12 @@ impl Language {
|
||||
Self::Fr => Some(FRENCH.get_or_init(|| Catalog::parse(include_str!("i18n/fr.tsv")))),
|
||||
Self::Id => Some(INDONESIAN.get_or_init(|| Catalog::parse(include_str!("i18n/id.tsv")))),
|
||||
Self::It => Some(ITALIAN.get_or_init(|| Catalog::parse(include_str!("i18n/it.tsv")))),
|
||||
Self::Bs => Some(BOSNIAN.get_or_init(|| Catalog::parse(include_str!("i18n/bs.tsv")))),
|
||||
Self::Ko => Some(KOREAN.get_or_init(|| Catalog::parse(include_str!("i18n/ko.tsv")))),
|
||||
Self::Cs => Some(CZECH.get_or_init(|| Catalog::parse(include_str!("i18n/cs.tsv")))),
|
||||
Self::Pl => Some(POLISH.get_or_init(|| Catalog::parse(include_str!("i18n/pl.tsv")))),
|
||||
Self::Sv => Some(SWEDISH.get_or_init(|| Catalog::parse(include_str!("i18n/sv.tsv")))),
|
||||
Self::Tr => Some(TURKISH.get_or_init(|| Catalog::parse(include_str!("i18n/tr.tsv")))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,6 +250,43 @@ impl Language {
|
||||
pub fn has(self, text: &str) -> bool {
|
||||
self.catalog().and_then(|c| c.plain(text)).is_some()
|
||||
}
|
||||
|
||||
/// Uppercase `text`, using Turkish-aware rules (`i` → `İ`, `ı` → `I`) when the language is
|
||||
/// Turkish.
|
||||
pub fn to_uppercase(self, text: &str) -> String {
|
||||
if self == Self::Tr {
|
||||
let mapped: String = text
|
||||
.chars()
|
||||
.map(|c| match c {
|
||||
'i' => 'İ',
|
||||
'ı' => 'I',
|
||||
other => other,
|
||||
})
|
||||
.collect();
|
||||
mapped.to_uppercase()
|
||||
} else {
|
||||
text.to_uppercase()
|
||||
}
|
||||
}
|
||||
|
||||
/// Lowercase `text`, using Turkish-aware rules (`İ` → `i`, `I` → `ı`) when the language is
|
||||
/// Turkish. Decomposed `I` followed by `\u{0307}` is normalized to `i`.
|
||||
pub fn to_lowercase(self, text: &str) -> String {
|
||||
if self == Self::Tr {
|
||||
let normalized = text.replace("I\u{0307}", "i");
|
||||
let mapped: String = normalized
|
||||
.chars()
|
||||
.map(|c| match c {
|
||||
'İ' => 'i',
|
||||
'I' => 'ı',
|
||||
other => other,
|
||||
})
|
||||
.collect();
|
||||
mapped.to_lowercase()
|
||||
} else {
|
||||
text.to_lowercase()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
@@ -229,9 +311,43 @@ pub fn t(text: &str) -> &str {
|
||||
current().tr(text)
|
||||
}
|
||||
|
||||
/// Match a lowercased search query against both the source and the displayed label.
|
||||
/// Uppercase `text` in the current interface language (with Turkish-aware casing when the
|
||||
/// language is Turkish).
|
||||
pub fn to_uppercase(text: &str) -> String {
|
||||
current().to_uppercase(text)
|
||||
}
|
||||
|
||||
/// Lowercase `text` in the current interface language (with Turkish-aware casing when the
|
||||
/// language is Turkish).
|
||||
pub fn to_lowercase(text: &str) -> String {
|
||||
current().to_lowercase(text)
|
||||
}
|
||||
|
||||
fn fold_query(s: &str) -> String {
|
||||
s.replace('İ', "i").replace('\u{0307}', "").to_lowercase()
|
||||
}
|
||||
|
||||
fn fold_turkish(s: &str) -> String {
|
||||
let normalized = s.replace("I\u{0307}", "i");
|
||||
let mapped: String = normalized
|
||||
.chars()
|
||||
.map(|c| match c {
|
||||
'İ' => 'i',
|
||||
'I' => 'ı',
|
||||
other => other,
|
||||
})
|
||||
.collect();
|
||||
mapped.replace('\u{0307}', "").to_lowercase()
|
||||
}
|
||||
|
||||
/// Match a search query against both the source and the displayed label.
|
||||
pub fn matches_query(text: &str, query: &str) -> bool {
|
||||
text.to_lowercase().contains(query) || t(text).to_lowercase().contains(query)
|
||||
let q = fold_query(query);
|
||||
if fold_query(text).contains(&q) || fold_query(t(text)).contains(&q) {
|
||||
return true;
|
||||
}
|
||||
let q_tr = fold_turkish(query);
|
||||
fold_turkish(t(text)).contains(&q_tr) || fold_turkish(text).contains(&q_tr)
|
||||
}
|
||||
|
||||
/// Like [`t`], for an English string that needs a disambiguating `context` (a catalog row whose
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,8 @@
|
||||
asset: bs.tsv
|
||||
title: Bosnian interface translation catalog
|
||||
author: FilmCraft contributors
|
||||
source: original work
|
||||
license: MIT OR Apache-2.0
|
||||
license-file: LICENSE-MIT and LICENSE-APACHE
|
||||
added: 2026-10-30 by nine-codes
|
||||
notes: AI-assisted clean-room English-to-Bosnian (Latin, Ijekavian) translations, shared with storytold/craft-i18n locales/bs/; not yet reviewed by a native speaker.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,8 @@
|
||||
asset: cs.tsv
|
||||
title: Czech interface translation catalog
|
||||
author: FilmCraft contributors
|
||||
source: original work
|
||||
license: MIT OR Apache-2.0
|
||||
license-file: LICENSE-MIT, LICENSE-APACHE
|
||||
added: 2026-10-11 by Losminek
|
||||
notes: AI-assisted original Czech translations, manually rewritten as complete phrases from English meanings with a binding editing glossary; not yet proofread by a native speaker.
|
||||
@@ -3631,6 +3631,14 @@
|
||||
H.265 (HEVC) needs a hardware encoder (NVENC on NVIDIA GPUs, VideoToolbox on macOS); none was found on this machine, and FilmCraft has no software H.265 encoder yet. Choose H.264 or ProRes instead. H.265 (HEVC) nécessite un encodeur matériel (NVENC sur les GPU NVIDIA, VideoToolbox sur macOS) ; aucun n'a été trouvé sur cet ordinateur, et FilmCraft n'a pas encore d'encodeur H.265 logiciel. Choisissez plutôt H.264 ou ProRes.
|
||||
AV1 needs a hardware encoder; none was found on this machine, and FilmCraft has no software AV1 encoder yet. AV1 nécessite un encodeur matériel ; aucun n'a été trouvé sur cet ordinateur, et FilmCraft n'a pas encore d'encodeur AV1 logiciel.
|
||||
Hardware decoding: Auto uses the system's video decoder (VideoToolbox on macOS, Media Foundation on Windows, VA-API for H.264 and HEVC on Linux) for the streams it supports, and FilmCraft's own decoder for everything else or if the hardware fails. Media that is already open keeps its decoder until it is reopened. Décodage matériel : Auto utilise le décodeur vidéo du système (VideoToolbox sur macOS, Media Foundation sur Windows, VA-API pour H.264 et HEVC sous Linux) pour les flux qu'il prend en charge, et le décodeur de FilmCraft pour tout le reste ou en cas d'échec du matériel. Un média déjà ouvert garde son décodeur jusqu'à sa réouverture.
|
||||
Check for Updates… Rechercher les mises à jour…
|
||||
Checking for updates… Recherche de mises à jour…
|
||||
Could not check for updates:\n{error} Impossible de rechercher les mises à jour :\n{error}
|
||||
FilmCraft {version} is currently the newest version available. FilmCraft {version} est actuellement la version la plus récente disponible.
|
||||
Looks for a newer FilmCraft release and offers to update. Rechercher une version plus récente de FilmCraft et proposer la mise à jour.
|
||||
Set Work Area Bar In Point Définir le point d'entrée de la barre de zone de travail
|
||||
Set Work Area Bar Out Point Définir le point de sortie de la barre de zone de travail
|
||||
Transparency Grid Grille de transparence
|
||||
|
||||
# Control server: the status-bar notice when --control cannot serve.
|
||||
The control server is disabled: FILMCRAFT_CONTROL_TOKEN must contain 64 random hex characters (see docs/control-protocol.md) Le serveur de contrôle est désactivé : FILMCRAFT_CONTROL_TOKEN doit contenir 64 caractères hexadécimaux aléatoires (voir docs/control-protocol.md)
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 83.
|
@@ -17,6 +17,11 @@
|
||||
# - "Horizontal"/"Vertical" are 水平/垂直 (guides, blur, axes); vertical text is "Vertical Text".
|
||||
|
||||
About FilmCraft FilmCraft について
|
||||
Check for Updates… アップデートを確認…
|
||||
Checking for updates… アップデートを確認中…
|
||||
Could not check for updates:\n{error} アップデートを確認できませんでした:\n{error}
|
||||
FilmCraft Updates FilmCraft のアップデート
|
||||
FilmCraft {version} is currently the newest version available. FilmCraft {version} は最新バージョンです。
|
||||
Add Audio Submix Track オーディオサブミックストラックを追加
|
||||
Add Caption at Playhead 再生ヘッドの位置にキャプションを追加
|
||||
Add Chapter Marker… チャプターマーカーを追加…
|
||||
@@ -3238,6 +3243,9 @@
|
||||
QuickTime, ProRes 4444 (4:4:4) with alpha channel, 24-bit 48 kHz PCM QuickTime、ProRes 4444 (4:4:4) アルファチャンネル付き、24-bit 48 kHz PCM
|
||||
QuickTime, ProRes 4444 XQ (4:4:4), 24-bit 48 kHz PCM QuickTime、ProRes 4444 XQ (4:4:4)、24-bit 48 kHz PCM
|
||||
QuickTime, ProRes 422, 24-bit 48 kHz PCM QuickTime、ProRes 422、24-bit 48 kHz PCM
|
||||
Settings 設定
|
||||
Hide FilmCraft FilmCraft を隠す
|
||||
Hide Others ほかを隠す
|
||||
Quit FilmCraft FilmCraft を終了
|
||||
Rate Stretch Tool レートストレッチツール
|
||||
Razor Tool カミソリツール
|
||||
@@ -3621,6 +3629,7 @@
|
||||
Saves this session's log and shows it in your file manager. このセッションのログを保存し、ファイルマネージャーで表示します。
|
||||
Shows a report on this computer. このコンピューターのレポートを表示します。
|
||||
Shows the version, community links and credits in a FilmCraft window. バージョン、コミュニティへのリンク、クレジットを FilmCraft のウィンドウに表示します。
|
||||
Looks for a newer FilmCraft release and offers to update. FilmCraft の新しいリリースを確認し、アップデートを提案します。
|
||||
Couldn't open your web browser ウェブブラウザーを開けませんでした
|
||||
Open this address in your browser yourself: このアドレスをご自身でブラウザーで開いてください:
|
||||
# View > Reset Interface Zoom (#2253).
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 11 and column 59.
|
@@ -616,7 +616,7 @@
|
||||
Close All Other Projects 다른 프로젝트 모두 닫기
|
||||
Close All Projects 모든 프로젝트 닫기
|
||||
Close Bin 빈 닫기
|
||||
Close Gap 간격 닫기
|
||||
Close Gap 빈 구간 제거
|
||||
Close Other Timeline Panels 다른 타임라인 패널 닫기
|
||||
Close Panel 패널 닫기
|
||||
Close Project 프로젝트 닫기
|
||||
@@ -1098,10 +1098,10 @@
|
||||
Extend Selection Right 선택 영역 오른쪽으로 확장
|
||||
Extend Selection Up 선택 영역 위로 확장
|
||||
Extended Dynamic Range Monitoring 확장 다이내믹 레인지 모니터링
|
||||
Extract 추출
|
||||
Extract (') 추출(')
|
||||
Extract 구간 삭제 (간격 닫기)
|
||||
Extract (') 구간 삭제 (간격 닫기) (')
|
||||
Extract Audio 오디오 추출
|
||||
Extract selected text 선택한 텍스트 추출
|
||||
Extract selected text 선택한 텍스트 구간 삭제 (간격 닫기)
|
||||
Eyedropper (pick a color from the Program monitor) 스포이드(프로그램 모니터에서 색상 선택)
|
||||
Eyedropper: no picture to sample 스포이드: 샘플링할 화면 없음
|
||||
Face Detection (protect skin tones) 얼굴 감지(피부 톤 보호)
|
||||
@@ -1198,7 +1198,7 @@
|
||||
Flare Color 플레어 색상
|
||||
Flash 플래시
|
||||
Flash Cue Marker Flash 큐 마커
|
||||
Flatten 병합
|
||||
Flatten 멀티카메라 클립 펼치기
|
||||
Flicker 깜박임
|
||||
Flicker Removal 깜박임 제거
|
||||
Flickers 깜박임
|
||||
@@ -1310,7 +1310,7 @@
|
||||
Glow Width 광선 너비
|
||||
Go Back 뒤로
|
||||
Go Forward 앞으로
|
||||
Go to Gap 간격으로 이동
|
||||
Go to Gap 빈 구간으로 이동
|
||||
Go to In 시작 표시로 이동
|
||||
Go to In (Shift+I) 시작으로 이동(Shift+I)
|
||||
Go to In Point 시작 표시(In)로 이동
|
||||
@@ -1649,11 +1649,11 @@
|
||||
Level 4 레벨 4
|
||||
Levels 레벨
|
||||
Libraries 라이브러리
|
||||
Lift 들어올리기
|
||||
Lift (;) 들어내기(;)
|
||||
Lift selected text 선택한 텍스트 들어내기
|
||||
Lift 구간 삭제 (간격 유지)
|
||||
Lift (;) 구간 삭제 (간격 유지) (;)
|
||||
Lift selected text 선택한 텍스트 구간 삭제 (간격 유지)
|
||||
Lift Shadows 섀도 올리기
|
||||
Lifted blacks, softer contrast, muted colour 들뜬 검정, 부드러운 대비, 차분한 색상
|
||||
Lifted blacks, softer contrast, muted colour 검정 레벨을 높이고 대비를 부드럽게, 색은 차분하게
|
||||
Ligatures 합자
|
||||
Light 밝게
|
||||
Light 1 조명 1
|
||||
@@ -2659,7 +2659,7 @@
|
||||
Saved preset “{name}” 사전 설정 “{name}” 저장됨
|
||||
Saved template {path} 템플릿 저장됨: {path}
|
||||
Saves the view, columns, sort, thumbnail and font size. 보기, 열, 정렬, 썸네일, 글꼴 크기를 저장합니다.
|
||||
Scale 비율
|
||||
Scale 배율
|
||||
Scale (Degrees) 비율(도)
|
||||
Scale Amount 비율 양
|
||||
Scale Attribute Times 속성 시간 비율 조정
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,8 @@
|
||||
asset: pl.tsv
|
||||
title: Polish interface translation catalog
|
||||
author: FilmCraft contributors
|
||||
source: original work
|
||||
license: MIT OR Apache-2.0
|
||||
license-file: LICENSE-MIT and LICENSE-APACHE
|
||||
added: 2026-10-13 by lewandowskit
|
||||
notes: Original English-to-Polish interface translations using clean-room NLE vocabulary (AI-assisted draft).
|
||||
+3319
-84
File diff suppressed because it is too large
Load Diff
@@ -5,4 +5,9 @@ source: original work
|
||||
license: MIT OR Apache-2.0
|
||||
license-file: LICENSE-MIT and LICENSE-APACHE
|
||||
added: 2026-10-10 by grib_job
|
||||
notes: Original English-to-Russian menu translations using clean-room NLE vocabulary.
|
||||
notes: Original clean-room English-to-Russian translation of the full 3,592-string
|
||||
reference catalog, using ordinary Russian video-editing vocabulary (Premiere-style
|
||||
terms: последовательность, корзина, дорожка, визуализация). No proprietary
|
||||
localisation resource, string table or product interface was consulted. Drafted
|
||||
with the project's agents and reviewed against the tests; native-speaker review
|
||||
still welcome.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,8 @@
|
||||
asset: sv.tsv
|
||||
title: Swedish interface translation catalog
|
||||
author: FilmCraft contributors
|
||||
source: original work
|
||||
license: MIT OR Apache-2.0
|
||||
license-file: LICENSE-MIT and LICENSE-APACHE
|
||||
added: 2026-10-14 by kler
|
||||
notes: Original English-to-Swedish interface translations using clean-room NLE vocabulary.
|
||||
@@ -14,7 +14,12 @@ fn catalog_text(l: Language) -> Option<&'static str> {
|
||||
Language::Fr => Some(include_str!("fr.tsv")),
|
||||
Language::Id => Some(include_str!("id.tsv")),
|
||||
Language::It => Some(include_str!("it.tsv")),
|
||||
Language::Bs => Some(include_str!("bs.tsv")),
|
||||
Language::Ko => Some(include_str!("ko.tsv")),
|
||||
Language::Cs => Some(include_str!("cs.tsv")),
|
||||
Language::Pl => Some(include_str!("pl.tsv")),
|
||||
Language::Sv => Some(include_str!("sv.tsv")),
|
||||
Language::Tr => Some(include_str!("tr.tsv")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +54,7 @@ fn unknown_strings_pass_through_and_languages_round_trip() {
|
||||
}
|
||||
assert_eq!(Language::En.tr("File"), "File");
|
||||
assert_eq!(Language::parse("xx"), None);
|
||||
for code in ["de-DE", "fr-FR", "id-ID", "it-IT"] {
|
||||
for code in ["de-DE", "fr-FR", "id-ID", "it-IT", "cs-CZ", "sv-SE"] {
|
||||
assert_eq!(Language::parse(code), None, "the preference stores the bare code");
|
||||
}
|
||||
// language names are always shown in their own language
|
||||
@@ -62,7 +67,12 @@ fn unknown_strings_pass_through_and_languages_round_trip() {
|
||||
(Language::Fr, "Français"),
|
||||
(Language::Id, "Bahasa Indonesia"),
|
||||
(Language::It, "Italiano"),
|
||||
(Language::Bs, "Bosanski"),
|
||||
(Language::Ko, "한국어"),
|
||||
(Language::Cs, "Čeština"),
|
||||
(Language::Pl, "Polski"),
|
||||
(Language::Sv, "Svenska"),
|
||||
(Language::Tr, "Türkçe"),
|
||||
] {
|
||||
assert_eq!(l.name(), name);
|
||||
}
|
||||
@@ -513,6 +523,11 @@ fn language_commands_switch_and_persist_without_a_document() {
|
||||
("french", Language::Fr, "fr", "Français", Some(("app.language.german", "Deutsch"))),
|
||||
("indonesian", Language::Id, "id", "Bahasa Indonesia", Some(("app.language.french", "Français"))),
|
||||
("italian", Language::It, "it", "Italiano", Some(("app.language.indonesian", "Bahasa Indonesia"))),
|
||||
("czech", Language::Cs, "cs", "Čeština", Some(("app.language.italian", "Italiano"))),
|
||||
("polish", Language::Pl, "pl", "Polski", Some(("app.language.italian", "Italiano"))),
|
||||
("swedish", Language::Sv, "sv", "Svenska", Some(("app.language.italian", "Italiano"))),
|
||||
("turkish", Language::Tr, "tr", "Türkçe", Some(("app.language.italian", "Italiano"))),
|
||||
("bosnian", Language::Bs, "bs", "Bosanski", Some(("app.language.italian", "Italiano"))),
|
||||
] {
|
||||
switch_and_check(&mut app, &ctx, name, lang, code, own, earlier);
|
||||
}
|
||||
@@ -575,10 +590,23 @@ fn system_language_picks_italian_korean_and_the_first_known() {
|
||||
for tag in ["ko", "ko-KR", "ko_KR.UTF-8"] {
|
||||
assert_eq!(l(&[tag]), Language::Ko, "{tag}");
|
||||
}
|
||||
for tag in ["cs", "CS", "cs-CZ", "cs_CZ.UTF-8"] {
|
||||
assert_eq!(l(&[tag]), Language::Cs, "{tag}");
|
||||
}
|
||||
for tag in ["sv", "SV", "sv-SE", "sv-FI", "sv-AX", "sv_SE.UTF-8", "sv_FI.UTF-8@euro", "sv_SE"] {
|
||||
assert_eq!(l(&[tag]), Language::Sv, "{tag}");
|
||||
}
|
||||
assert_eq!(l(&["fi-FI", "sv-FI", "en"]), Language::Sv, "Finnish has no catalog");
|
||||
for none in [&["swe"][..], &["s"], &["-sv"], &["da"], &["nb-NO"], &["nn"], &["no"]] {
|
||||
assert_eq!(l(none), Language::En, "{none:?}");
|
||||
}
|
||||
for tag in ["tr", "TR", "tr-TR", "tr_TR.UTF-8", "tr_TR"] {
|
||||
assert_eq!(l(&[tag]), Language::Tr, "{tag}");
|
||||
}
|
||||
for none in [&["ita"][..], &["i"], &["-it"]] {
|
||||
assert_eq!(l(none), Language::En, "{none:?}");
|
||||
}
|
||||
assert_eq!(l(&["sv-SE", "nl", "es-MX", "ja"]), Language::Es, "the first one the interface has");
|
||||
assert_eq!(l(&["da-DK", "nl", "es-MX", "ja"]), Language::Es, "the first one the interface has");
|
||||
assert_eq!(l(&["rm-CH", "de-CH", "fr-CH", "en"]), Language::De);
|
||||
assert_eq!(l(&["rm-CH", "fr-CH", "de-CH", "en"]), Language::Fr);
|
||||
assert_eq!(l(&["en-GB", "es"]), Language::En);
|
||||
@@ -707,6 +735,14 @@ fn italian_renders_with_bundled_fonts() {
|
||||
assert_bundled_fonts_cover(alphabet.chars().chain(Language::It.name().chars()).chain(letters));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn czech_renders_with_bundled_fonts() {
|
||||
let (entries, _) = catalog::parse_entries(include_str!("cs.tsv"));
|
||||
let alphabet = "ÁČĎÉĚÍŇÓŘŠŤÚŮÝŽáčďéěíňóřšťúůýž„“‚‘’";
|
||||
let letters = entries.iter().flat_map(|(_, _, cs)| cs.chars()).filter(|ch| ch.is_alphabetic());
|
||||
assert_bundled_fonts_cover(alphabet.chars().chain(Language::Cs.name().chars()).chain(letters));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn japanese_needs_an_installed_font() {
|
||||
let mut app = crate::FilmcraftApp::new(filmcraft_engine::Session::default());
|
||||
@@ -730,6 +766,7 @@ fn japanese_needs_an_installed_font() {
|
||||
for family in crate::theme::font_families() {
|
||||
let mut stack = fonts.definitions().families.get(&family).cloned().unwrap_or_default();
|
||||
stack.retain(|n| !crate::rtl::is_system_face(n));
|
||||
stack.retain(|n| n != crate::cjk::KOREAN_FACE && n != "system-chinese");
|
||||
if craft {
|
||||
assert!(stack.last().is_some_and(|n| n.starts_with("craft:")), "{family:?}: {stack:?}");
|
||||
assert!(!stack.iter().any(|n| n == JAPANESE_FONT), "{family:?}: {stack:?}");
|
||||
@@ -749,3 +786,103 @@ fn japanese_needs_an_installed_font() {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Edit ▸ Language and Settings ▸ Interface Language list the same languages in the same order.
|
||||
#[test]
|
||||
fn language_menu_and_settings_choices_are_in_the_same_order() {
|
||||
use filmcraft_engine::settings::{Kind, Row};
|
||||
fn choices(rows: &[Row], out: &mut Vec<&'static str>) {
|
||||
for r in rows {
|
||||
match r {
|
||||
Row::Field(f) if f.key == "general.interfaceLanguage" => {
|
||||
if let Kind::Choice(opts) = f.kind {
|
||||
out.extend(opts.iter().filter(|(code, _)| *code != "system").map(|(_, label)| *label));
|
||||
}
|
||||
}
|
||||
Row::Group(_, inner) => choices(inner, out),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut settings = Vec::new();
|
||||
for c in filmcraft_engine::settings::categories() {
|
||||
choices(c.rows, &mut settings);
|
||||
}
|
||||
let app = crate::FilmcraftApp::new(filmcraft_engine::Session::default());
|
||||
let menu: Vec<String> =
|
||||
crate::menus::menu_items(&app).into_iter().filter(|it| it.id.starts_with("app.language.")).map(|it| it.label).collect();
|
||||
assert!(!settings.is_empty());
|
||||
assert_eq!(menu, settings);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn polish_renders_with_bundled_fonts() {
|
||||
let (entries, errors) = catalog::parse_entries(include_str!("pl.tsv"));
|
||||
assert!(errors.is_empty(), "{errors:?}");
|
||||
// every Polish letter, even ones no entry uses yet, and every letter, curly quote, dash and
|
||||
// ellipsis the entries use
|
||||
let alphabet = "ĄĆĘŁŃÓŚŹŻąćęłńóśźż„”";
|
||||
let letters = entries.iter().flat_map(|(_, _, pl)| pl.chars()).filter(|ch| ch.is_alphabetic() || "„”“‘’–—…".contains(*ch));
|
||||
assert_bundled_fonts_cover(alphabet.chars().chain(Language::Pl.name().chars()).chain(letters));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swedish_renders_with_bundled_fonts() {
|
||||
let (entries, _) = catalog::parse_entries(include_str!("sv.tsv"));
|
||||
// every Swedish letter (é as in "idé", ü in loanwords), even ones no entry uses yet, and every
|
||||
// letter, curly quote, dash and ellipsis the entries use (symbols such as ✔ come from the
|
||||
// English sources)
|
||||
let alphabet = "ÅÄÖÉÜåäöéü";
|
||||
let letters = entries.iter().flat_map(|(_, _, sv)| sv.chars()).filter(|ch| ch.is_alphabetic() || "“”‘’–—…".contains(*ch));
|
||||
assert_bundled_fonts_cover(alphabet.chars().chain(Language::Sv.name().chars()).chain(letters));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn russian_renders_with_bundled_fonts() {
|
||||
let (entries, _) = catalog::parse_entries(include_str!("ru.tsv"));
|
||||
// the whole alphabet including «ё», even letters no entry uses yet, and every letter, quote,
|
||||
// dash and ellipsis the entries use
|
||||
let alphabet = "АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдеёжзийклмнопрстуфхцчшщъыьэюя«»";
|
||||
let letters = entries.iter().flat_map(|(_, _, ru)| ru.chars()).filter(|ch| ch.is_alphabetic() || "«»“”‘’–—…".contains(*ch));
|
||||
assert_bundled_fonts_cover(alphabet.chars().chain(Language::Ru.name().chars()).chain(letters));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn turkish_casing_and_search_folding() {
|
||||
assert_eq!(Language::Tr.to_uppercase("i"), "İ");
|
||||
assert_eq!(Language::Tr.to_uppercase("ı"), "I");
|
||||
assert_eq!(Language::Tr.to_lowercase("İ"), "i");
|
||||
assert_eq!(Language::Tr.to_lowercase("I"), "ı");
|
||||
assert_eq!(Language::Tr.to_lowercase("I\u{0307}"), "i");
|
||||
assert_eq!(to_uppercase("file"), "FILE");
|
||||
set_current(Language::Tr);
|
||||
assert_eq!(to_uppercase("i"), "İ");
|
||||
assert_eq!(to_lowercase("I"), "ı");
|
||||
assert_eq!(to_lowercase("I\u{0307}"), "i");
|
||||
assert!(matches_query("Gaussian Blur", "gaussian"));
|
||||
assert!(matches_query("Gaussian Blur", "GAUSSIAN"));
|
||||
assert!(matches_query("Inceleme", "inceleme"));
|
||||
assert!(matches_query("İnceleme", "inceleme"));
|
||||
assert!(matches_query("inceleme", "İnceleme"));
|
||||
assert!(matches_query("Işık", "ışık"));
|
||||
assert!(matches_query("ışık", "IŞIK"));
|
||||
set_current(Language::En);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn turkish_renders_with_bundled_fonts() {
|
||||
let (entries, _) = catalog::parse_entries(include_str!("tr.tsv"));
|
||||
let alphabet = "ABCÇDEFGĞHIİJKLMNOÖPRSŞTUÜVYZabcçdefgğhıijklmnoöprsştuüvyz";
|
||||
let letters = entries.iter().flat_map(|(_, _, tr)| tr.chars()).filter(|ch| ch.is_alphabetic() || "“”‘’–—…".contains(*ch));
|
||||
assert_bundled_fonts_cover(alphabet.chars().chain(Language::Tr.name().chars()).chain(letters));
|
||||
}
|
||||
|
||||
/// Every Bosnian locale, with region, script or encoding, picks the Bosnian catalog.
|
||||
#[test]
|
||||
fn system_language_picks_bosnian() {
|
||||
let l = locales;
|
||||
for tag in ["bs", "BS", "bs-BA", "bs_BA.UTF-8", "bs-Latn-BA", "bs-Cyrl-BA", "BS-ba"] {
|
||||
assert_eq!(l(&[tag]), Language::Bs, "{tag}");
|
||||
}
|
||||
assert_eq!(l(&["hr-HR", "bs-BA"]), Language::Bs, "Croatian has no catalog here, so the next preference wins");
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,8 @@
|
||||
asset: tr.tsv
|
||||
title: Turkish interface translation catalog
|
||||
author: FilmCraft contributors
|
||||
source: original work
|
||||
license: MIT OR Apache-2.0
|
||||
license-file: LICENSE-MIT and LICENSE-APACHE
|
||||
added: 2026-10-18 by MOzcelik14
|
||||
notes: Original English-to-Turkish interface translations using clean-room NLE vocabulary.
|
||||
@@ -27,16 +27,23 @@ pub const UI_COMMANDS: &[UiCommand] = &[
|
||||
uic!("window.floatPanel", "Undock Panel", [], None),
|
||||
uic!("window.closePanel", "Close Panel", [], None),
|
||||
uic!("app.language.english", "English", ["Edit", "Language"], None),
|
||||
uic!("app.language.japanese", "日本語", ["Edit", "Language"], None),
|
||||
uic!("app.language.spanish", "Español", ["Edit", "Language"], None),
|
||||
uic!("app.language.portuguese", "Português (Brasil)", ["Edit", "Language"], None),
|
||||
uic!("app.language.ukrainian", "Українська", ["Edit", "Language"], None),
|
||||
uic!("app.language.chinese", "简体中文", ["Edit", "Language"], None),
|
||||
uic!("app.language.german", "Deutsch", ["Edit", "Language"], None),
|
||||
uic!("app.language.russian", "Русский", ["Edit", "Language"], None),
|
||||
uic!("app.language.french", "Français", ["Edit", "Language"], None),
|
||||
// Alphabetical by the language's own name after English; Cyrillic, then CJK names follow
|
||||
// the Latin ones. Settings ▸ General ▸ Interface Language lists them the same way.
|
||||
uic!("app.language.indonesian", "Bahasa Indonesia", ["Edit", "Language"], None),
|
||||
uic!("app.language.bosnian", "Bosanski", ["Edit", "Language"], None),
|
||||
uic!("app.language.czech", "Čeština", ["Edit", "Language"], None),
|
||||
uic!("app.language.german", "Deutsch", ["Edit", "Language"], None),
|
||||
uic!("app.language.spanish", "Español", ["Edit", "Language"], None),
|
||||
uic!("app.language.french", "Français", ["Edit", "Language"], None),
|
||||
uic!("app.language.italian", "Italiano", ["Edit", "Language"], None),
|
||||
uic!("app.language.polish", "Polski", ["Edit", "Language"], None),
|
||||
uic!("app.language.portuguese", "Português (Brasil)", ["Edit", "Language"], None),
|
||||
uic!("app.language.swedish", "Svenska", ["Edit", "Language"], None),
|
||||
uic!("app.language.turkish", "Türkçe", ["Edit", "Language"], None),
|
||||
uic!("app.language.russian", "Русский", ["Edit", "Language"], None),
|
||||
uic!("app.language.ukrainian", "Українська", ["Edit", "Language"], None),
|
||||
uic!("app.language.japanese", "日本語", ["Edit", "Language"], None),
|
||||
uic!("app.language.chinese", "简体中文", ["Edit", "Language"], None),
|
||||
uic!("app.language.korean", "한국어", ["Edit", "Language"], None),
|
||||
uic!("source.playback.toggle", "Source Play/Stop", [], None),
|
||||
uic!("source.playback.play", "Play Source", [], None),
|
||||
@@ -202,7 +209,12 @@ pub fn invoke(app: &mut FilmcraftApp, ctx: &egui::Context, id: &str, mut params:
|
||||
| "app.language.french"
|
||||
| "app.language.indonesian"
|
||||
| "app.language.italian"
|
||||
| "app.language.bosnian"
|
||||
| "app.language.korean"
|
||||
| "app.language.czech"
|
||||
| "app.language.polish"
|
||||
| "app.language.swedish"
|
||||
| "app.language.turkish"
|
||||
) {
|
||||
// Japanese needs the craft-fonts (built with CRAFT_FONTS_DIR) or a font installed on the system
|
||||
if id == "app.language.japanese" && !crate::i18n::install_japanese_font(ctx) {
|
||||
@@ -227,7 +239,12 @@ pub fn invoke(app: &mut FilmcraftApp, ctx: &egui::Context, id: &str, mut params:
|
||||
"app.language.french" => crate::i18n::Language::Fr,
|
||||
"app.language.indonesian" => crate::i18n::Language::Id,
|
||||
"app.language.italian" => crate::i18n::Language::It,
|
||||
"app.language.bosnian" => crate::i18n::Language::Bs,
|
||||
"app.language.korean" => crate::i18n::Language::Ko,
|
||||
"app.language.czech" => crate::i18n::Language::Cs,
|
||||
"app.language.polish" => crate::i18n::Language::Pl,
|
||||
"app.language.swedish" => crate::i18n::Language::Sv,
|
||||
"app.language.turkish" => crate::i18n::Language::Tr,
|
||||
_ => crate::i18n::Language::En,
|
||||
};
|
||||
// The preference is updated in memory before it is written, so a failed write (read-only
|
||||
@@ -852,7 +869,12 @@ pub fn menu_items(app: &FilmcraftApp) -> Vec<MenuItem> {
|
||||
"app.language.french" => it.checked = Some(app.ui.language == crate::i18n::Language::Fr),
|
||||
"app.language.indonesian" => it.checked = Some(app.ui.language == crate::i18n::Language::Id),
|
||||
"app.language.italian" => it.checked = Some(app.ui.language == crate::i18n::Language::It),
|
||||
"app.language.bosnian" => it.checked = Some(app.ui.language == crate::i18n::Language::Bs),
|
||||
"app.language.korean" => it.checked = Some(app.ui.language == crate::i18n::Language::Ko),
|
||||
"app.language.czech" => it.checked = Some(app.ui.language == crate::i18n::Language::Cs),
|
||||
"app.language.polish" => it.checked = Some(app.ui.language == crate::i18n::Language::Pl),
|
||||
"app.language.swedish" => it.checked = Some(app.ui.language == crate::i18n::Language::Sv),
|
||||
"app.language.turkish" => it.checked = Some(app.ui.language == crate::i18n::Language::Tr),
|
||||
_ => {}
|
||||
}
|
||||
if it.id.starts_with("view.") {
|
||||
|
||||
@@ -32,7 +32,7 @@ pub fn show(app: &mut FilmcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
};
|
||||
let mut bui = ui.new_child(egui::UiBuilder::new().max_rect(body).id_salt("fx-body"));
|
||||
bui.set_clip_rect(body);
|
||||
let filter = q.to_lowercase();
|
||||
let filter = q.trim();
|
||||
let defs = filmcraft_project::effect_defs();
|
||||
// folder tree: top-level categories in Premiere's order
|
||||
let tops = filmcraft_project::vtransition::EFFECT_TOP_FOLDERS;
|
||||
@@ -42,19 +42,19 @@ pub fn show(app: &mut FilmcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
egui::ScrollArea::vertical().id_salt("fx-scroll").auto_shrink([false, false]).show(&mut bui, |ui| {
|
||||
for &top in tops {
|
||||
if top == "Presets" {
|
||||
let any = app.session.presets.all().iter().any(|p| crate::i18n::matches_query(&p.name, &filter));
|
||||
if any && folder_row(app, ui, top, 0, &filter) {
|
||||
preset_action = crate::panels::presets::folder_rows(app, ui, &filter).or(preset_action.take());
|
||||
let any = app.session.presets.all().iter().any(|p| crate::i18n::matches_query(&p.name, filter));
|
||||
if any && folder_row(app, ui, top, 0, filter) {
|
||||
preset_action = crate::panels::presets::folder_rows(app, ui, filter).or(preset_action.take());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if top == "Lumetri Presets" {
|
||||
if let Some(name) = lumetri_rows(app, ui, &filter) {
|
||||
if let Some(name) = lumetri_rows(app, ui, filter) {
|
||||
lumetri_apply = Some(name);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let mut items: Vec<_> = defs.iter().filter(|d| d.category.first() == Some(&top)).filter(|d| crate::i18n::matches_query(d.name, &filter)).collect();
|
||||
let mut items: Vec<_> = defs.iter().filter(|d| d.category.first() == Some(&top)).filter(|d| crate::i18n::matches_query(d.name, filter)).collect();
|
||||
// Premiere lists effect folders and effects alphabetically (obsolete ones last)
|
||||
if top == "Video Effects" || top == "Legacy" {
|
||||
items.sort_by_key(|d| (d.category.get(1) == Some(&"Obsolete"), d.category.get(1).copied().unwrap_or(""), d.name.to_ascii_lowercase()));
|
||||
@@ -62,7 +62,7 @@ pub fn show(app: &mut FilmcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
if !filter.is_empty() && items.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let open = folder_row(app, ui, top, 0, &filter);
|
||||
let open = folder_row(app, ui, top, 0, filter);
|
||||
if !open {
|
||||
continue;
|
||||
}
|
||||
@@ -79,7 +79,7 @@ pub fn show(app: &mut FilmcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
}
|
||||
seen.push(s);
|
||||
let key = format!("{top}/{s}");
|
||||
if !s.is_empty() && !folder_row(app, ui, &key, 1, &filter) {
|
||||
if !s.is_empty() && !folder_row(app, ui, &key, 1, filter) {
|
||||
continue;
|
||||
}
|
||||
for d in items.iter().filter(|d| sub(d) == s) {
|
||||
@@ -306,3 +306,27 @@ fn preset_grid(app: &mut FilmcraftApp, ui: &mut egui::Ui, area: Rect) -> Option<
|
||||
}
|
||||
apply
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn effects_search_matches_turkish_casing() {
|
||||
crate::i18n::set_current(crate::i18n::Language::Tr);
|
||||
let ctx = egui::Context::default();
|
||||
let session = filmcraft_engine::Session::default();
|
||||
let mut app = FilmcraftApp::new(session);
|
||||
let rect = Rect::from_min_size(pos2(0.0, 0.0), vec2(400.0, 600.0));
|
||||
for query in ["IŞIK", "ışık", "Işık"] {
|
||||
app.ui.effects_search = query.to_string();
|
||||
app.auto.begin_frame();
|
||||
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| {
|
||||
show(&mut app, ui, rect);
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
assert!(app.auto.find("effects.item.light_leaks").is_some(), "query `{query}` through effects search caller must find `light_leaks`",);
|
||||
}
|
||||
crate::i18n::set_current(crate::i18n::Language::En);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ pub(super) fn preset_manager(app: &mut FilmcraftApp, ctx: &egui::Context) {
|
||||
let mut pick: Option<&'static str> = None;
|
||||
let all = app.session.export_presets.all();
|
||||
let favorite = |n: &str| app.session.export_presets.is_favorite(n);
|
||||
let q = m.query.to_lowercase();
|
||||
let q = m.query.trim().to_string();
|
||||
egui::Window::new(tl!("Preset Manager"))
|
||||
.id(egui::Id::unique("Preset Manager"))
|
||||
.id(egui::Id::unique("preset-manager"))
|
||||
|
||||
@@ -196,16 +196,16 @@ fn browse(app: &mut FilmcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
install = r.clicked();
|
||||
});
|
||||
b.add_space(6.0);
|
||||
let q = st.query.to_lowercase();
|
||||
let q = st.query.trim();
|
||||
let shown: Vec<&LibraryEntry> = lib
|
||||
.iter()
|
||||
.filter(|e| {
|
||||
let tt = &e.template;
|
||||
(st.category.is_empty() || tt.category == st.category)
|
||||
&& (crate::i18n::matches_query(&tt.name, &q)
|
||||
|| crate::i18n::matches_query(&tt.category, &q)
|
||||
|| crate::i18n::matches_query(&tt.description, &q)
|
||||
|| tt.tags.iter().any(|x| crate::i18n::matches_query(x, &q)))
|
||||
&& (crate::i18n::matches_query(&tt.name, q)
|
||||
|| crate::i18n::matches_query(&tt.category, q)
|
||||
|| crate::i18n::matches_query(&tt.description, q)
|
||||
|| tt.tags.iter().any(|x| crate::i18n::matches_query(x, q)))
|
||||
})
|
||||
.collect();
|
||||
let selected = st.selected.clone();
|
||||
|
||||
@@ -608,8 +608,8 @@ fn command_list(app: &mut FilmcraftApp, ui: &mut egui::Ui, height: f32, t: &Toke
|
||||
ui.add_space(4.0);
|
||||
let rows = app.session.execute("shortcuts.list", json!({})).unwrap_or_default();
|
||||
let rows = rows.as_array().cloned().unwrap_or_default();
|
||||
let query = app.shortcut_editor.search.to_lowercase();
|
||||
let rows: Vec<_> = rows.into_iter().filter(|row| matches_search(row, &query)).collect();
|
||||
let query = app.shortcut_editor.search.trim();
|
||||
let rows: Vec<_> = rows.into_iter().filter(|row| matches_search(row, query)).collect();
|
||||
let w = ui.available_width();
|
||||
// column header
|
||||
let (hr, _) = ui.allocate_exact_size(vec2(w, 22.0), Sense::hover());
|
||||
@@ -839,4 +839,14 @@ mod localization_tests {
|
||||
}
|
||||
crate::i18n::set_current(crate::i18n::Language::En);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_search_accepts_turkish_casing() {
|
||||
crate::i18n::set_current(crate::i18n::Language::Tr);
|
||||
let row = json!({"id": "marker.in_out", "label": "Add Range Marker to In and Out", "category": "Marker", "shortcuts": []});
|
||||
for query in ["ÇIKIŞ", "çıkış", "Çıkış"] {
|
||||
assert!(matches_search(&row, query), "{query}");
|
||||
}
|
||||
crate::i18n::set_current(crate::i18n::Language::En);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -826,6 +826,19 @@ fn effects_are_indented_under_their_folders() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effects_search_matches_turkish_casing() {
|
||||
let mut d = Driver::demo();
|
||||
d.exec("prefs.set", json!({"key": "general.interfaceLanguage", "value": "tr"}));
|
||||
d.ok("ui.panel.show", json!({"panel": "Effects"}));
|
||||
d.frames(3);
|
||||
for query in ["IŞIK", "ışık", "Işık"] {
|
||||
d.ok("ui.set", json!({"effectsSearch": query}));
|
||||
d.frames(2);
|
||||
assert!(d.has("effects.item.light_leaks"), "query `{query}` through Effects search caller must find `light_leaks`");
|
||||
}
|
||||
}
|
||||
|
||||
/// The font of the first run of every text shape drawn this frame, with the text, position and colour.
|
||||
fn painted_text(d: &Driver) -> Vec<(String, egui::Pos2, egui::FontId, egui::Color32)> {
|
||||
fn walk(s: &egui::Shape, out: &mut Vec<(String, egui::Pos2, egui::FontId, egui::Color32)>) {
|
||||
|
||||
@@ -25,25 +25,110 @@ Containment rules:
|
||||
|
||||
1. The crate does not use `lints.workspace = true`. Its own `[lints]` table copies the workspace
|
||||
lints except `unsafe_code = "deny"` (not `forbid`), and adds
|
||||
`clippy::undocumented_unsafe_blocks = "deny"`. Only the FFI modules (`videotoolbox`, `media_foundation::gpu` / `media_foundation::mft`,
|
||||
`nvenc::ffi` / `nvenc::session` / `nvenc::device` on Windows and 64-bit Linux, and `vaapi::va` on Linux) carry `#[allow(unsafe_code)]`; the rest of the crate
|
||||
(the fallback logic in `hybrid`, the decoder logic in `media_foundation`, `annexb`, `biplanar`,
|
||||
the encoder logic in `nvenc`, H.264 and H.265 alike) has no `unsafe`. NVENC's H.265 support
|
||||
`clippy::undocumented_unsafe_blocks = "deny"`. Only FFI modules may opt in with
|
||||
`#[allow(unsafe_code)]`; the current allowances and deviations are recorded below. The fallback
|
||||
logic in `hybrid`, the decoder logic in `media_foundation`, `annexb`, `biplanar`, and the encoder
|
||||
logic in `nvenc` (H.264 and H.265 alike) keep `unsafe` denied. NVENC's H.265 support
|
||||
added no `unsafe` module: it reuses `nvenc::ffi` and `nvenc::session` (new data declarations and
|
||||
a second codec-configuration member of an existing union).
|
||||
2. Every `unsafe` block has a `// SAFETY:` comment saying why it is sound.
|
||||
3. The public API is safe: no `pub unsafe fn`, no raw pointers or FFI types in public signatures;
|
||||
failures are `Result`s. The crate keeps the never-crash `deny(clippy::unwrap_used, …)`
|
||||
attribute of every clean crate.
|
||||
4. No panic crosses the FFI boundary: the VideoToolbox output callback runs under `catch_unwind`
|
||||
and reports a panic as a failed frame. The Windows backend has no callbacks into Rust (it drives
|
||||
a synchronous decoder MFT with `ProcessInput` / `ProcessOutput`), so nothing can unwind into
|
||||
COM. NVENC loads the driver library at run time and has no callbacks into Rust either.
|
||||
4. No panic crosses the FFI boundary. This applies to VideoToolbox's decoder and encoder output
|
||||
callbacks, NVDEC's parser callbacks, and VA-API's error-message callback; their current unwind
|
||||
guards are described below. Media Foundation drives a synchronous decoder MFT with
|
||||
`ProcessInput` / `ProcessOutput` and registers no Rust callbacks. NVENC loads the driver library
|
||||
at run time and registers no Rust callbacks either.
|
||||
5. The crate compiles on every target. OS bindings are target-specific dependencies (the
|
||||
`objc2-*` crates on macOS, the official `windows` crate on Windows: MIT OR Apache-2.0, already
|
||||
in the lockfile through wgpu / winit, with only the Win32 features the backend uses); elsewhere
|
||||
`register()` reports `Unavailable` and does nothing. It is not part of the wasm check (L5).
|
||||
|
||||
## Current implementation (source audit, 2026-10-24)
|
||||
|
||||
Checked against `main` at
|
||||
[`552f008d2757e307194256b3e672c9c1de4fa9b2`](https://github.com/storytold/filmcraft/commit/552f008d2757e307194256b3e672c9c1de4fa9b2)
|
||||
for [#2814](https://github.com/storytold/filmcraft/issues/2814). This section records source
|
||||
facts, not new architecture approvals or evidence that builds, hardware tests or a complete
|
||||
soundness review passed. The accepted decision, its approval date and the containment rules above
|
||||
remain in effect; implementation differences are not exceptions to those rules.
|
||||
|
||||
### Unsafe allowances and module boundaries
|
||||
|
||||
[`Cargo.toml`](../../crates/platform/Cargo.toml) still denies `unsafe_code` and
|
||||
`clippy::undocumented_unsafe_blocks`. The following are the actual allowance scopes:
|
||||
|
||||
| Target | Scope carrying `#[allow(unsafe_code)]` | Work at the boundary |
|
||||
|---|---|---|
|
||||
| macOS | [`videotoolbox`](../../crates/platform/src/videotoolbox.rs) | VideoToolbox decoding, CoreMedia/CoreVideo buffers and its output callback. |
|
||||
| macOS | [`videotoolbox_encode`](../../crates/platform/src/videotoolbox_encode.rs) | VideoToolbox H.264/HEVC encoding and its output callback; `dlsym` resolves an optional VideoToolbox property. |
|
||||
| Windows | [`media_foundation::gpu`](../../crates/platform/src/media_foundation/gpu.rs), [`media_foundation::mft`](../../crates/platform/src/media_foundation/mft.rs) | COM, Media Foundation, D3D11 device setup and surface readback. |
|
||||
| Windows / 64-bit Linux | [`nvenc::ffi`](../../crates/platform/src/nvenc/ffi.rs), [`nvenc::session`](../../crates/platform/src/nvenc/session.rs), [`nvenc::device`](../../crates/platform/src/nvenc/device.rs) | NVENC declarations and calls, driver loading, D3D11 or CUDA device/context operations. |
|
||||
| 64-bit Linux | [`nvdec::cuvid`](../../crates/platform/src/nvdec/cuvid.rs) | NVDEC driver calls, parser callbacks and CUDA readback, using `nvenc::device`. |
|
||||
| Linux | [`vaapi::va`](../../crates/platform/src/vaapi/va.rs) | libva/libva-drm loading, displays, decoding, surface readback and logging. |
|
||||
| Windows | [`cursor::cursor_screen_position`](../../crates/platform/src/cursor.rs) (function only) | `GetCursorPos`; a non-media deviation, discussed below. |
|
||||
|
||||
The media allowances are on module declarations in
|
||||
[`lib.rs`](../../crates/platform/src/lib.rs) and the backend module files
|
||||
([Media Foundation](../../crates/platform/src/media_foundation.rs),
|
||||
[NVENC](../../crates/platform/src/nvenc.rs),
|
||||
[NVDEC](../../crates/platform/src/nvdec.rs), [VA-API](../../crates/platform/src/vaapi.rs)).
|
||||
They apply to the module's contents, rather than to individual blocks; the cursor allowance is
|
||||
on the Windows function itself. There is no crate-wide `allow(unsafe_code)`.
|
||||
`vaapi::ffi` and `nvdec::ffi` contain public declarations but do not carry an unsafe allowance.
|
||||
|
||||
### Callbacks into Rust and panic handling
|
||||
|
||||
The registered production callbacks are:
|
||||
|
||||
| Backend and entry point | Guard and outcome visible in the source |
|
||||
|---|---|
|
||||
| `videotoolbox::output_callback` (`extern "C-unwind"`) | `catch_unwind(AssertUnwindSafe(..))` guards decoded-picture processing. A caught panic becomes `Output::Failed`, queued through `Shared::push`. |
|
||||
| `videotoolbox_encode::output_callback` (`extern "C-unwind"`) | The same guard covers compressed-packet processing; a caught panic becomes `Output::Failed`. |
|
||||
| `nvdec::cuvid::{sequence, decode, display}` (`extern "C"`) | All three call the shared `callback` helper, whose processing closure runs under `catch_unwind(AssertUnwindSafe(..))`. The first error or caught panic is stored in `State::error` and returns `0`; `Session::parse` checks that error after the driver call. |
|
||||
| `vaapi::va::on_error` (`extern "C"`) | `catch_unwind` surrounds message conversion and logging; a caught panic is ignored. `Display::init` registers this callback and disables libva's info callback. |
|
||||
|
||||
NVDEC installs its three callbacks in `Session::create_parser`; they run synchronously within
|
||||
`cuvidParseVideoData`. The VideoToolbox callbacks deliver output asynchronously through their
|
||||
shared queues. NVENC's function-table entries are driver functions called by Rust, not callbacks
|
||||
registered with the driver. Media Foundation uses `Mft::input` / `Mft::poll` and rejects
|
||||
asynchronous MFTs in `Mft::configure`.
|
||||
|
||||
These guards do not validate foreign pointers, prevent driver faults or prove state lifetimes
|
||||
and aliasing sound. Error recording and queueing outside a guarded closure must also obey rule 4;
|
||||
in particular, the VideoToolbox callbacks call `Shared::push` after `catch_unwind` returns.
|
||||
An `extern "C-unwind"` signature does not relax the requirement that no panic reach foreign code.
|
||||
|
||||
### Known deviations and follow-up
|
||||
|
||||
- **Non-media scope:** `cursor::cursor_screen_position()` returns `Option<(i32, i32)>`, calling
|
||||
`GetCursorPos` on Windows and returning `None` elsewhere. Its purpose is to locate file drops
|
||||
when the windowing layer supplies no pointer position. It is outside the media-only decision
|
||||
above, even though its Rust API is safe and read-only. Its presence is not approval to add other
|
||||
OS integration. Removing/replacing it or granting a separate scope exception needs maintainer
|
||||
resolution; [#2725](https://github.com/storytold/filmcraft/pull/2725) contains a proposed
|
||||
window-system ADR and is still open and unmerged at this audit revision.
|
||||
- **Public FFI surface:** rule 3 is not fully satisfied. `vaapi` publicly exports `ffi`, Linux
|
||||
`va`, and the `Accel` trait. `vaapi::va::Session::new` takes `&[ffi::VAProfile]`, and
|
||||
`Accel::decode_h264` / `decode_hevc` expose VA picture, matrix and slice parameter structures.
|
||||
`nvdec` publicly exports `ffi` and `cuvid`; for example, `ffi::CUVIDPICPARAMS` exposes the raw
|
||||
`pBitstreamData` and `pSliceDataOffsets` fields, and `nvdec::ffi` callback types contain raw pointers.
|
||||
There is no production `pub unsafe fn`, but that alone does not meet the no-public-FFI-types
|
||||
rule. [#2800](https://github.com/storytold/filmcraft/issues/2800) tracks the VA-API visibility
|
||||
and driver-input validation problem: `Session::decode_into` forwards supplied parameter
|
||||
structures and slice data to libva. Restoring encapsulation and checking caller-controlled
|
||||
driver inputs are code follow-ups; this document does not waive rule 3 or fix #2800.
|
||||
- **Separate FFI hardening:** [#2812](https://github.com/storytold/filmcraft/issues/2812) remains
|
||||
open. Callback guards are not a resolution of its lifetime, COM-apartment or cleanup concerns.
|
||||
For example, `media_foundation::mft::Mft` still has a `Send` implementation based on a
|
||||
free-threaded assumption, `gpu::init_com` accepts `RPC_E_CHANGED_MODE`, and
|
||||
`gpu::Readback::read` calls `Unmap` after its Rust closure rather than through an unwind guard.
|
||||
Conversely, NVDEC's callback state now uses `Box::into_raw`; `Session::drop` reclaims it with
|
||||
`Box::from_raw` after parser destruction, or leaks it if the parser cannot be destroyed.
|
||||
That part of the older issue description must not be treated as an unchanged snapshot.
|
||||
None of these observations changes the safety requirements.
|
||||
|
||||
## The fallback guarantee
|
||||
|
||||
Registering a hardware decoder never makes a file undecodable, and never changes a picture:
|
||||
@@ -93,7 +178,8 @@ deliberate:
|
||||
|
||||
Everything else in this record applies unchanged: `unsafe` stays in `crates/platform`, every block
|
||||
has a `// SAFETY:` comment, no panic crosses the FFI boundary, the public API is safe, and the crate
|
||||
compiles everywhere (`register()` is a no-op off macOS).
|
||||
compiles everywhere (the VideoToolbox backend is macOS-only; `register()` also selects the other
|
||||
backends described in this record).
|
||||
|
||||
## Addendum (2026-10-08): NVENC HEVC Main 10 (HDR)
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ contents). Percent is ready-for-real-work for that codec: depth, profiles, speed
|
||||
| E-AC-3 (Dolby Digital Plus) | decode | own decoder (ATSC A/52 Annex E, #647): independent substream, 1–6 blocks, spectral extension; **no** adaptive hybrid transform, enhanced coupling or 7.1 dependent substreams (plays the 5.1 core) | none | SNR vs ffmpeg 65–91 dB (raw, MP4, MOV, Matroska, TS) | 75% | 10–20 h |
|
||||
| MP3 / MP2 | decode + MP3 encode | MP3: symphonia (MPL-2.0); MP1 / MP2: own decoder (ISO/IEC 11172-3 / 13818-3, #809), also the bit rate / mode combinations ffmpeg writes by default (mono 384 kbit/s); in raw, MP4, MOV (#800), Matroska, TS, PS | **none** | MP1 / MP2: SNR vs ffmpeg 73–84 dB; MP2 in MP4 / MOV within 1e-4 of ffmpeg, also after a seek (#800) | 65% | 10–20 h (MP3 encoder) |
|
||||
| Opus | — (not a Premiere import format) | own decoder, RFC 8251 range-exact | none | conformance vectors | ahead | — |
|
||||
| FLAC, ALAC, Vorbis | partial | decode (symphonia); FLAC in MP4 silent (#603) | none | — | 70% | 3–6 h |
|
||||
| FLAC, ALAC, Vorbis | partial | decode (symphonia), also FLAC in MP4 / MOV (#603) | none | — | 70% | 3–6 h |
|
||||
| WMA | decode on Windows | none | none | — | 0% | 10–20 h |
|
||||
|
||||
## Summary
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# File format parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-26 · **Change:** minor (GIF export binary transparency) · **Target:** Adobe Premiere Pro 2026 (26.5.2) and Media Encoder 2026
|
||||
> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-30 · **Change:** minor (AVI import #650 and VP8 WebM #1064 ship; GIF export binary transparency retained) · **Target:** Adobe Premiere Pro 2026 (26.5.2) and Media Encoder 2026
|
||||
|
||||
Containers, stills, audio files, project and interchange files, captions and presets. Codecs inside
|
||||
the containers are in [codec-parity.md](codec-parity.md). Our import list is
|
||||
@@ -39,8 +39,8 @@ only). "Tested" names the evidence.
|
||||
| QuickTime MOV | read + write | yes | yes (H.264, HEVC, ProRes, DNxHR, APV, MJPEG, PCM) | ffprobe | 90% |
|
||||
| MXF OP1a / OP-Atom | read + write (XDCAM, AVC-Intra, P2, AS-10 / AS-11, JPEG 2000, DNx) | yes (AVC, DNx, ProRes, MPEG-2, PCM / AES3, timecode) | OP1a (DNxHR, ProRes, H.264), OP-Atom | index-table seeking tests | 70% |
|
||||
| MPEG TS / PS (AVCHD `.mts`, `.m2ts`, `.ts`, `.mpg`, `.vob`, `.mod`) | read; write MPEG-2 / Blu-ray / DVD | yes | **no** | fixtures | 65% |
|
||||
| Matroska / WebM | read (limited) | yes (H.264, HEVC, VP9, AV1, ProRes, MJPEG + AAC, Opus, FLAC, MP3, Vorbis, PCM); WebM black report (#432), MKV multi-audio (#601) | no | fixtures; audio sample-aligned with the encoded signal (#790) | 75% |
|
||||
| **AVI** | read + write | **no** (extension listed, no demuxer, #598) | no | — | 0% |
|
||||
| Matroska / WebM | read (limited) | yes (H.264, HEVC, VP8 (#1064, fixes the black WebM of #432), VP9, AV1, ProRes, MJPEG + AAC, Opus, FLAC, MP3, Vorbis, PCM); MKV multi-audio (#601) | no | fixtures; audio sample-aligned with the encoded signal (#790) | 75% |
|
||||
| AVI | read + write | yes: AVI 1.0 and OpenDML (`crates/avi`, #650); Motion JPEG, H.264, HEVC, uncompressed RGB / YUV with PCM, MP3, MP2 or AC-3; no DivX / Xvid or DV | no | `cargo test -p filmcraft-avi` (hand-built index paths, truncation, mutation); `tests/ffmpeg_oracle.rs` (MJPEG, H.264 B-frames, BGR24, YUY2, PCM, MP3 vs ffprobe) | read only |
|
||||
| WMV / ASF | read; write on Windows | no | no | — | 0% |
|
||||
| FLV / F4V | read + write | no | no | — | 0% |
|
||||
| DCP | write (Wraptor) | no | no | — | 0% |
|
||||
@@ -85,7 +85,7 @@ only). "Tested" names the evidence.
|
||||
## Summary
|
||||
|
||||
**File formats dimension: ~60%, 140–240 h** (codecs counted separately). The beta blocker is
|
||||
`.prproj`. After it, by user impact: AVI and WMV import, EXR / PSD stills, AAF / OMF
|
||||
`.prproj`. After it, by user impact: WMV import, AVI export and DivX / Xvid / DV, EXR / PSD stills, AAF / OMF
|
||||
validation in Avid and Pro Tools, DVD / Blu-ray / DCP / broadcast MXF delivery.
|
||||
|
||||
## Revision history
|
||||
@@ -93,6 +93,7 @@ validation in Avid and Pro Tools, DVD / Blu-ray / DCP / broadcast MXF delivery.
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-13 | minor | Stills: EXIF Orientation applied to JPEG, PNG, WebP and TIFF (#1583); YCbCr TIFF and DNG not yet |
|
||||
| 2026-10-12 | minor | VP8 WebM decodes (#1064, closes #432): Matroska row no longer lists the black-picture report. AVI import ships (#650, merged 2026-10-12): row changed from "no demuxer" to read via `crates/avi`; summary no longer lists AVI import as missing; the row's percentage and the dimension figure (~60%, 140-240 h) were not re-measured |
|
||||
| 2026-10-10 | major | Created: format-by-format comparison with Premiere 26.5.2 from the import / export registries, crate READMEs, the installed bundle's exporter listings and open issues |
|
||||
| 2026-10-21 | minor | PSD stills: 8/16-bit grayscale and RGB import of the flattened composite, merged transparency unblended from white (#2116) |
|
||||
| 2026-10-26 | minor | GIF exports preserve binary transparency when requested; synthetic decode tests cover alpha threshold and animation disposal (#3396); estimates unchanged |
|
||||
|
||||
+33
-19
@@ -1,6 +1,6 @@
|
||||
# Where FilmCraft falls short of Premiere Pro
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-26 · **Change:** minor (G13: optional binary transparency in animated GIF exports) · **Target:** Adobe Premiere Pro 2026 (26.5.2)
|
||||
> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-30 · **Change:** minor (G14 catalog counts refreshed; G13 GIF binary transparency retained) · **Target:** Adobe Premiere Pro 2026 (26.5.2)
|
||||
|
||||
Every known shortfall, one entry each, ranked by what most stops an editor using FilmCraft for real
|
||||
work. This is the work list: agents should prefer it over adding checklist items. Numbers are in
|
||||
@@ -28,7 +28,7 @@ platforms** G4, G9, G17; **performance** G16; **ecosystem** G11; **measurement**
|
||||
| G12 | AI features | AI | no | 150–300 h |
|
||||
| G13 | Delivery formats | file format | no | 60–110 h |
|
||||
| G14 | Localization: 3 of the 12 key languages missing | localization | no | 150–230 h |
|
||||
| G15 | Interchange never validated in Avid / Pro Tools; slow XML import | file format | partly | 20–40 h |
|
||||
| G15 | Interchange never validated in Avid / Pro Tools | file format | partly | 20–40 h |
|
||||
| G16 | Performance reports on 0.5.0 | performance | partly | 30–60 h |
|
||||
| G17 | Audio device and playback reliability | stability | partly | 20–40 h |
|
||||
| G18 | Effects depth: approximations and missing behaviour | feature | no | 60–110 h |
|
||||
@@ -158,8 +158,8 @@ platforms** G4, G9, G17; **performance** G16; **ecosystem** G11; **measurement**
|
||||
|
||||
- **Missing:** on-screen transform handles for Motion / Transform in the Program monitor (#639);
|
||||
Effect Controls keyframe zoom bar, draggable divider, row lines (#640–#645); panel docking
|
||||
rearrangement (#493); clicking empty space to deselect (#683); filmstrip thumbnails (#614);
|
||||
marquee selection in the Project panel (#578); Freeform bins and middle-mouse pan (#579);
|
||||
rearrangement (#493); filmstrip thumbnails (#614);
|
||||
Freeform bins and middle-mouse pan (#579);
|
||||
Program monitor Button Editor (+) (#431); keyframe curves for Position (#448); transition
|
||||
editing controls (#577: Effect Controls has the transition's own timeline, drag to slide or resize,
|
||||
drag the cut to move it with the transition; the A/B previews with Start / End sliders remain); track renaming (#654); window controls on macOS (#637); UI scale on
|
||||
@@ -192,8 +192,7 @@ platforms** G4, G9, G17; **performance** G16; **ecosystem** G11; **measurement**
|
||||
from white.
|
||||
- **Missing:** free-format MPEG audio, E-AC-3 adaptive hybrid
|
||||
transform, enhanced coupling and the extra channels of 7.1 dependent substreams (played as the
|
||||
5.1 core), AVI (#598; listed as an extension but no
|
||||
demuxer), WMV / ASF, DV / DVCPRO / DV100, MPEG-4 Part 2, FLAC in MP4 (#603), camera RAW (R3D
|
||||
5.1 core), WMV / ASF, DV / DVCPRO / DV100, MPEG-4 Part 2, camera RAW (R3D
|
||||
incl. R3D NE, ARRIRAW, Sony RAW / X-OCN, Canon RAW, BRAW, ProRes RAW; #345, #383), JPEG 2000 /
|
||||
JPEG XS MXF, the rest of CinemaDNG (lossy / JPEG XL DNG, MXF-wrapped, opcode lists, Raw
|
||||
settings; DNG frames and folders import since #1065); stills: PSD layers (Merged Layers, Individual Layers, layers as a sequence) and PSB / CMYK / Lab / indexed / 32-bit / ZIP-compressed documents, EXR beyond scan-line RLE / ZIP / PXR24 (PIZ, B44, DWA,
|
||||
@@ -213,6 +212,8 @@ platforms** G4, G9, G17; **performance** G16; **ecosystem** G11; **measurement**
|
||||
[file-format-parity.md](file-format-parity.md).
|
||||
- **Have:** Matroska AAC / MP3 / AC-3 / E-AC-3 sample-aligned with the signal the encoder was
|
||||
given (the exact `CodecDelay` is trimmed, not the 1 ms-rounded one; #790).
|
||||
- **Have:** AVI / OpenDML import (#598; Motion JPEG, H.264, HEVC, uncompressed video; PCM, MP3,
|
||||
MP2, AC-3 audio) and FLAC audio in MP4 / MOV (#603; the decoder gets its STREAMINFO from `dfLa`).
|
||||
|
||||
## G11. No plugin hosting
|
||||
|
||||
@@ -247,16 +248,18 @@ platforms** G4, G9, G17; **performance** G16; **ecosystem** G11; **measurement**
|
||||
|
||||
## G14. Localization: 3 of the 12 key languages missing (Hindi, Arabic, Vietnamese)
|
||||
|
||||
- **Have:** Spanish, Japanese, Simplified Chinese, French (#498) and Portuguese (Brazil) (#859)
|
||||
catalogs cover ~99.6–100% of the ~3,500 cataloged strings; Ukrainian ~10% (menus).
|
||||
- **Have:** Spanish, Japanese, Simplified Chinese, French (#498, #804), Portuguese (Brazil) (#859,
|
||||
#866), Indonesian (#867, #878), Korean (#508), Italian, German (#630), Russian, Czech, Polish,
|
||||
Swedish, Turkish and Bosnian catalogs cover the whole interface (about 99–100% of the cataloged
|
||||
strings); Ukrainian is menus only (~10%).
|
||||
- **Missing:** Hindi, Arabic (no RTL interface layout or catalog; the interface has Arabic and Hebrew
|
||||
fallback fonts and shows captions in reading order, but other names and fields keep egui's
|
||||
left-to-right run order; Arabic titles reported broken, #395),
|
||||
Korean (#508; needs Hangul fonts in craft-fonts),
|
||||
Vietnamese; Premiere also ships Russian. Engine errors,
|
||||
Vietnamese (no catalog on main). These three are the languages with no catalog at all, which
|
||||
is what the heading counts. Engine errors,
|
||||
CLI and MCP stay English. Apart from the German menus (#630), no language has a recorded
|
||||
native-speaker review.
|
||||
- **Done:** Indonesian (#867): `id.tsv` has every entry of the Spanish catalog (~100%), in Edit ▸
|
||||
- **Done:** Indonesian (#867): `id.tsv` covers 3,562 of 3,610 strings (98.7%), in Edit ▸
|
||||
Language, Settings ▸ General and the native macOS menu; System Language picks it for `id-*` (and
|
||||
legacy `in-*`) locales. Native-speaker review welcome.
|
||||
- **Done:** Italian: `it.tsv` has every entry of the Spanish catalog (~100%), in Edit ▸ Language,
|
||||
@@ -265,13 +268,22 @@ platforms** G4, G9, G17; **performance** G16; **ecosystem** G11; **measurement**
|
||||
- **Done:** German: `de.tsv` has every entry of the Spanish catalog (~100%), beyond the menus of
|
||||
#630 (panels, dialogs, effects and their parameters, settings, status messages). The menus are
|
||||
native-reviewed; review of the rest welcome.
|
||||
- **Done:** Russian: `ru.tsv` has every entry of the Spanish catalog (100% of the reference
|
||||
catalog), translated from the English sources with Premiere-style Russian vocabulary
|
||||
(последовательность, корзина, дорожка, визуализация); Edit ▸ Language, Settings ▸ General and
|
||||
System Language (`ru-*`) reach it, a font test covers
|
||||
the Russian alphabet including «ё». Native-speaker review welcome.
|
||||
- **Done:** Turkish: `tr.tsv` has every entry of the Spanish catalog (~100%), in Edit ▸ Language,
|
||||
Settings ▸ General and the native macOS menu; System Language picks it for `tr-*` locales.
|
||||
Native-speaker review welcome.
|
||||
- **Estimate:** 150–230 h. Doc: [localization-parity.md](localization-parity.md).
|
||||
|
||||
## G15. Interchange never validated in Avid / Pro Tools; slow XML import
|
||||
## G15. Interchange never validated in Avid / Pro Tools
|
||||
|
||||
- **Missing:** AAF and OMF exports have never been opened in Media Composer or Pro Tools; FCP7 XML
|
||||
/ OTIO import of a Premiere sequence took 97 s / 66 s for three media files (#461); `file.import`
|
||||
merges into existing project content in headless round trips (#592).
|
||||
- **Missing:** AAF and OMF exports have never been opened in Media Composer or Pro Tools; `file.import`
|
||||
merges into existing project content in headless round trips unless `replace:true` is passed (#592).
|
||||
- **Done:** FCP7 XML / OTIO / EDL / AAF import links media through the streaming reader instead of
|
||||
reading each file whole into memory (#461).
|
||||
- **Estimate:** 20–40 h (needs a human with Avid / Pro Tools, both installed on the owner's Mac).
|
||||
|
||||
## G16. Performance reports on 0.5.0
|
||||
@@ -302,10 +314,11 @@ platforms** G4, G9, G17; **performance** G16; **ecosystem** G11; **measurement**
|
||||
## G18. Effects depth: approximations and missing behaviour
|
||||
|
||||
- **Missing:** Warp Stabilizer is 2-D (Premiere's is 3-D subspace), Morph Cut has no face tracking
|
||||
(and occlusion edges are soft), Lumetri HDR-aware maths, multicam paging beyond 16 angles,
|
||||
(and occlusion edges are soft), Lumetri HDR-aware maths,
|
||||
redesigned 26.0 mask tools (a stored, re-editable
|
||||
corner radius: rounding bakes fillets into the path, so a rounded corner cannot be re-rounded later).
|
||||
- **Have:** Optical Flow time interpolation synthesises motion-compensated in-between frames (dense
|
||||
- **Have:** multicam sources with more than 16 angles are paged 4 × 4 in the Multi-Camera view;
|
||||
Optical Flow time interpolation synthesises motion-compensated in-between frames (dense
|
||||
coarse-to-fine Lucas–Kanade, CPU; #780); Morph Cut morphs the subject along that optical flow
|
||||
instead of cross-dissolving (CPU; #805); Transform shutter angle motion blur (#415); Ultra Key
|
||||
Setting applied as a parameter preset (#458); rectangle and polygon masks round their corners
|
||||
@@ -325,14 +338,15 @@ platforms** G4, G9, G17; **performance** G16; **ecosystem** G11; **measurement**
|
||||
|
||||
## G20. The web app shell
|
||||
|
||||
- **Missing:** file access, WebCodecs decode, audio in the browser build beyond what exists;
|
||||
image-sequence export downloads drop frames in Chrome (#378).
|
||||
- **Missing:** file access, WebCodecs decode, audio in the browser build beyond what exists.
|
||||
- **Estimate:** 40–80 h. Doc: [web.md](web.md).
|
||||
|
||||
## Revision history
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-30 | minor | G14: three key languages have no catalog (Hindi, Arabic, Vietnamese); Korean, Italian, German and Russian are near-complete; Ukrainian is menus only; G13 GIF transparency retained |
|
||||
| 2026-10-12 | minor | Removed shipped items: AVI import (#598), FLAC in MP4 (#603), multicam paging, deselect on empty space (#683), Project marquee (#578), image-sequence downloads in Chrome (#378), slow interchange import (#461) |
|
||||
| 2026-10-10 | major | Created from ROADMAP.md's "Where we are lacking" (2026-10-05); re-ranked against Premiere 26.5.2 and the 188 open issues; added `.prproj`, 10-bit / 4:2:2 camera decode, launch crashes and missing PR CI, localization, delivery formats |
|
||||
| 2026-10-12 | minor | G10: MP1 (`A_MPEG/L1`) in Matroska imports (#1402) |
|
||||
| 2026-10-12 | minor | G10: OpenEXR PXR24 import |
|
||||
|
||||
@@ -68,3 +68,13 @@ Frame has no default key; the Premiere preset moves Shift+E to Export Frame and
|
||||
| Project panel: Next/Previous Column Field, Next/Previous Row Field (Tab, ⇧Tab, Return, ⇧Return) | The list view has no inline-editable metadata cells. |
|
||||
| Text panel: Edit Segment (Return) | No inline transcript text editing (corrections go through `transcript.set`). |
|
||||
| Text panel: Follow Active Monitor (⇧C), Show Source Transcript (⇧Z) | Only sequence transcripts exist; source clips have no transcript view. |
|
||||
|
||||
## Numpad keys
|
||||
|
||||
FilmCraft's shortcut engine runs on egui, which maps numpad digits to the same `Key::Num*` variants as the main keyboard row. This means **numpad digits cannot currently be bound independently** of their main-keyboard equivalents — binding `0` on the numpad binds `0` everywhere, and vice versa.
|
||||
|
||||
This is because egui reports numpad digits as the main-row digit keys (it does not expose distinct `Key::Numpad*` variants).
|
||||
|
||||
- `0` is bound to **Multi-Camera Record On/Off Toggle** in the Premiere-compatible preset; rebind it if you rely on numpad `0` for something else (Edit ▸ Keyboard Shortcuts…).
|
||||
|
||||
See [#2645](https://github.com/storytold/filmcraft/issues/2645) for upstream egui tracking.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Localization parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (German covers the whole interface) · **Target:** Adobe Premiere Pro 2026 (26.5.2)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (German covers the whole interface; Czech and Polish interface catalogs; Swedish and Russian cover the whole interface; Turkish catalog; Bosnian covers the whole interface) · **Target:** Adobe Premiere Pro 2026 (26.5.2)
|
||||
|
||||
Per-language status of FilmCraft's interface. Premiere 26.5.2 ships 10 interface languages (the
|
||||
installed bundle's `.lproj` folders): German, English, Spanish, French, Italian, Japanese, Korean,
|
||||
@@ -10,7 +10,7 @@ Portuguese (Brazil), Russian, Simplified Chinese.
|
||||
|
||||
Edit ▸ Language (also Settings ▸ General, `general.interfaceLanguage`) switches the interface and
|
||||
persists across restarts; System Language (the default) follows the OS's or browser's preferred
|
||||
languages and falls back to English. Commands `app.language.<english|japanese|spanish|portuguese|ukrainian|chinese|german|russian|french|indonesian|italian>`
|
||||
languages and falls back to English. Commands `app.language.<english|japanese|spanish|portuguese|ukrainian|chinese|german|russian|french|indonesian|italian|korean|czech|polish|swedish|turkish>`
|
||||
are reachable over the control channel. One catalog per language,
|
||||
`crates/ui-egui/src/i18n/<code>.tsv` (format in the header of `es.tsv`); UI code wraps strings in
|
||||
`tl!("…")` / `tlf!("…{name}", name)`, and registry names (effects and their parameters, settings,
|
||||
@@ -34,16 +34,16 @@ right to left, but Arabic titles are reported broken (#395).
|
||||
|
||||
## Status
|
||||
|
||||
Measured 2026-10-10 by counting entries in each catalog. The denominator is the union of all
|
||||
catalogs' sources (**3,498 strings**); strings not yet wrapped in `tl!` and engine messages are
|
||||
Measured 2026-10-19 by counting entries in each catalog. The denominator is the union of all
|
||||
catalogs' sources (**3,593 strings**); strings not yet wrapped in `tl!` and engine messages are
|
||||
not in it, so real coverage is somewhat lower than shown. Estimates calibrated from #449 (Japanese,
|
||||
whole interface, one PR) at ~4–8 h per language for the catalog plus tests.
|
||||
|
||||
| Language | Code | UI strings | Dialogs / tooltips / help | Script support | Native review | Status | To `full` |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| English | en | 3,498 (100%, source) | yes; no help docs in-app | Latin | n/a | **full** | — |
|
||||
| English | en | 3,593 (100%, source) | yes; no help docs in-app | Latin | n/a | **full** | — |
|
||||
| Simplified Chinese | zh-cn | 3,485 (99.6%) | dialogs, Settings, effects, shortcuts, status; engine errors English | CJK font fallback; system IME via winit | not recorded | partial | 6–10 h + review |
|
||||
| Spanish | es | 3,496 (99.9%) | as above | Latin | not recorded | partial | 4–8 h + review |
|
||||
| Spanish | es | 3,592 (the reference catalog) | as above | Latin | not recorded | partial | 4–8 h + review |
|
||||
| Hindi | hi | 0 (0%) | — | **no Devanagari shaping in the interface** | — | none | 30–50 h (shaping in UI + catalog + fonts) |
|
||||
| Arabic | ar | 0 (0%) | — | **no RTL interface layout**; fallback fonts and shaping in the interface, captions in reading order, other text in egui's left-to-right run order; titles broken (#395) | — | none | 40–70 h |
|
||||
| French | fr | 3,526 (same entries as Spanish, plus the macOS Settings item) | as Spanish | Latin (bundled fonts) | not recorded; welcome | partial (#498) | review |
|
||||
@@ -52,8 +52,14 @@ whole interface, one PR) at ~4–8 h per language for the catalog plus tests.
|
||||
| Japanese | ja | 3,487 (99.7%) | as zh-cn; system-font kanji may pick the Chinese fallback (#547) | CJK fonts, vertical title text | not recorded | partial | 4–8 h + review |
|
||||
| German | de | 3,590 (same entries as Spanish) | as Spanish | Latin (bundled fonts) | menus yes (#630); the rest not recorded, welcome | partial | review |
|
||||
| Korean | ko | 3,573 (same entries as Spanish, plus the macOS Settings item) | as Spanish | installed system Hangul font (Malgun Gothic, Apple SD Gothic Neo, Noto Sans CJK KR…); refused without one | not recorded; welcome | partial (#508) | review; Hangul in craft-fonts for the web |
|
||||
| Russian | ru | 3,592 (100%; every Spanish entry) | dialogs, Settings, effects, shortcuts, status; engine errors English | Cyrillic (bundled fonts), «ё» | not recorded; welcome | partial | native review |
|
||||
| Vietnamese | vi | 0 (0%) | — | Latin with stacked diacritics (font coverage to check) | — | none | 6–12 h + review |
|
||||
| *Other shipped:* Italian | it | 3,573 (same entries as Spanish, plus the macOS Settings item) | as Spanish | Latin (bundled fonts) | not recorded; welcome | partial | review |
|
||||
| Czech | cs | 3,617 (all Spanish source keys, plus the playback-resolution label `1/2` the two Work Area Bar commands and the seven audio-output and update strings Spanish lacks) | dialogs, Settings, effects, shortcuts, status; engine errors English | Latin (bundled fonts) | not reviewed | partial | native review |
|
||||
| *Other shipped:* Polish | pl | every Spanish entry plus the native macOS menu items; counts written count-neutral | as Spanish | Latin (bundled fonts) | not recorded; AI-drafted, review welcome | partial | review |
|
||||
| *Other shipped:* Swedish | sv | 3,581 (100%) | as Spanish | Latin (bundled fonts, å ä ö) | not recorded; welcome | partial | review |
|
||||
| *Other shipped:* Turkish | tr | 3,590 (same entries as Spanish, plus the macOS Settings item) | as Spanish | Latin (bundled fonts) | not recorded; welcome | partial | review |
|
||||
| *Other shipped:* Bosnian | bs | 3,610 (every entry of the other catalogs) | as Spanish | Latin (bundled fonts) | AI-assisted, not yet native-reviewed; welcome | partial | review |
|
||||
| *Other shipped:* Ukrainian | uk | 351 (10.0%) | menus only | Cyrillic (bundled) | not recorded | menus only | 5–9 h |
|
||||
|
||||
UI strings are catalog entries (the non-comment lines of `src/i18n/<code>.tsv`; English counts the
|
||||
@@ -62,19 +68,44 @@ Spanish and German catalogs both have 3,590 entries, German every Spanish one. A
|
||||
lacks shows in English; no test or build requires a translation. `cargo xtask i18n-report` (also
|
||||
printed by `cargo xtask ci`) lists each language's missing strings.
|
||||
|
||||
Premiere also ships Italian, which we now cover like Spanish, and Russian (6–10 h).
|
||||
Premiere also ships Italian, which we cover like Spanish, and Russian, which now covers the whole
|
||||
interface (native review welcome).
|
||||
|
||||
**Localization dimension:** 9 of the 12 key languages near-complete (English, Spanish, Japanese,
|
||||
Simplified Chinese, French, Portuguese (Brazil), Indonesian, Korean, German), 3 none (Hindi, Arabic,
|
||||
Vietnamese); against Premiere's 10 languages we cover 9 well and Russian partly. **~35%, 150–230 h** for all twelve to `full` plus Italian and Russian, of which the script
|
||||
work (RTL and complex shaping in the egui interface) is the largest and least parallel part.
|
||||
Vietnamese); against Premiere's 10 languages we cover 10 at catalog level, Russian pending native
|
||||
review. **~35%, 150–230 h** for the twelve key languages to `full` plus Russian review, of which the
|
||||
script work (RTL and complex shaping in the egui interface) is the largest and least parallel part.
|
||||
Every language needs native-speaker review (human).
|
||||
|
||||
## Czech catalog quality
|
||||
|
||||
The Czech rows were rewritten as complete phrases in batches from the English meanings,
|
||||
with French/German entries and Rust call sites consulted for ambiguous labels. This is an
|
||||
AI-assisted translation with a binding video-editing glossary, **not yet proofread by a native
|
||||
speaker**. Coverage measures lookup keys, not linguistic approval or a complete UI walkthrough.
|
||||
In particular, Bin is **Přihrádka**, Trash is **Koš**, Lift is **Zvednout**, Extract is
|
||||
**Vyjmout s posunem**, and Timeline is **Časová osa**. Delete is **Odstranit** and Remove is
|
||||
**Odebrat**; image- and audio-processing names such as Remove Fringe or Flicker Removal keep
|
||||
**Odstranit**. The original catalog header comments were preserved; their older glossary
|
||||
shorthand is superseded by these terms and the catalog rows.
|
||||
|
||||
The current catalog parser and formatter have no plural-variant selection, and none of the
|
||||
other catalogs provide plural rows. The UI's existing singular branches are retained; other
|
||||
counts use neutral wording such as “Počet klipů: {n}”, valid for 0, 1, 2–4 and larger counts
|
||||
without a new runtime API.
|
||||
Native review should concentrate on audio automation terms, Slip/Slide/Roll operations,
|
||||
creative effect names, and the shared `Subtitle` key (caption format and graphics subtitle).
|
||||
|
||||
## Revision history
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-30 | minor | Bosnian (bs, Latin, Ijekavian) covers the whole interface: 3,610 entries, Edit ▸ Language ▸ Bosanski, every `bs-*` locale; AI-assisted, not native-reviewed |
|
||||
| 2026-10-26 | minor | Swedish (sv) covers the whole interface (all 3,581 strings); System Language picks it for `sv-*` locales |
|
||||
| 2026-10-23 | minor | Missing translations never fail a test or build (they show in English); `cargo xtask i18n-report`, also run by `cargo xtask ci`, lists them per language |
|
||||
| 2026-10-19 | minor | Added Czech (`cs`) interface catalog and locale detection; rewrote all catalog rows with a binding glossary; AI-assisted, not yet proofread by a native speaker |
|
||||
| 2026-10-19 | major | Russian (ru) covers the whole interface: 3,592 entries (100% of the Spanish catalog), `ru.tsv` translated from the English sources with Premiere-style Russian vocabulary; a font test covers the Russian alphabet including «ё», System Language maps `ru-*`; native review welcome |
|
||||
| 2026-10-13 | minor | German (de) covers the whole interface: every entry of the Spanish catalog (3,590); the tests require the core menus and the macOS app menu and reject entries without a Spanish one |
|
||||
| 2026-10-11 | minor | German (de) ships the menus (#630): 371 of 3,498 strings (10.6%), native-reviewed; dimension now 2 menus-only, 5 none |
|
||||
| 2026-10-10 | major | Created from ROADMAP.md's "Interface localization" section; catalog coverage measured (es 3,496, ja 3,487, zh-cn 3,485, pt-br 348, uk 351 of 3,498); the 12-language table and script-support gaps added |
|
||||
|
||||
+7
-6
@@ -1,6 +1,6 @@
|
||||
# Roadmap detail
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-22 · **Change:** minor (M8: Fujifilm F-Log / F-Log2 / F-Log2 C colour spaces) · **Target:** Adobe Premiere Pro 2026 (26.5.2)
|
||||
> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-30 · **Change:** minor (M8: Fujifilm F-Log / F-Log2 / F-Log2 C colour spaces; E-AC-3 #788, AVI #650, Russian #610, French #804, Indonesian #878, pt-BR #866 and German #630 no longer listed as open) · **Target:** Adobe Premiere Pro 2026 (26.5.2)
|
||||
|
||||
Forward-looking plan: milestones with status, the current focus and what's next, with estimates in
|
||||
Opus 5.5 agent-hours (calibration in [target-app-parity.md](target-app-parity.md#calibration)).
|
||||
@@ -28,7 +28,7 @@ workflow would make FilmCraft pre-alpha.
|
||||
|
||||
| Workflow | Works end to end? | Evidence | Hours to pass |
|
||||
|---|---|---|---|
|
||||
| Ingest and organize media (import camera / phone / screen files, bins, metadata, proxies) | partial, not blocking | H.264 / HEVC / ProRes / DNx / MXF / AVCHD / MKV import with bit-exact decoders, Media Browser, bins, proxies, Link Media. Gaps: H.264 10-bit / 4:2:2 (Sony XAVC S-I, Canon XF-AVC) opens nowhere and HEVC 4:2:2 only through VideoToolbox (#626, [G3](gaps.md#g3-10-bit-and-422-camera-media-needs-hardware)); E-AC-3 (#647), AVI (#598), camera RAW | 0 (G3 is a beta item, 30–50 h) |
|
||||
| Ingest and organize media (import camera / phone / screen files, bins, metadata, proxies) | partial, not blocking | H.264 / HEVC / ProRes / DNx / MXF / AVCHD / MKV import with bit-exact decoders, Media Browser, bins, proxies, Link Media. Gaps: H.264 10-bit / 4:2:2 (Sony XAVC S-I, Canon XF-AVC) opens nowhere and HEVC 4:2:2 only through VideoToolbox (#626, [G3](gaps.md#g3-10-bit-and-422-camera-media-needs-hardware)); camera RAW; E-AC-3 plays its 5.1 core (#647) and AVI imports (#598) | 0 (G3 is a beta item, 30–50 h) |
|
||||
| Assemble and trim a sequence (insert / overwrite, ripple / roll / slip / slide, razor, J/K/L, multicam) | yes | Edit algebra with undo tests, Trim Monitor, Premiere keyboard preset, multicam with audio sync; UX issues (#683, #648, #668) slow it down but don't stop it | 0 |
|
||||
| Colour correct and grade (Lumetri, LUTs, scopes, colour management) | yes | Lumetri complete with CPU / WGSL parity tests, all Lumetri Scopes, LUT import, HDR export signalling checked with ffprobe | 0 |
|
||||
| Mix audio (levels, automation, Essential Sound, loudness) | yes on macOS | Mixer graph, automation, 53 effects, BS.1770 loudness matching ffmpeg; device reports are Windows / Linux (#605, #617) | 0 |
|
||||
@@ -48,7 +48,7 @@ Beta = ready for real work ≥ ~75% and `.prproj` opens reliably ([target-app-pa
|
||||
|---|---|---|---|
|
||||
| B1 | PR CI on three OSs + launch-crash fixes | ⬜ | 40–80 |
|
||||
| B2 | `.prproj` import (sequences, clips, transitions, markers, bins, common effect parameters) | ⬜ | 60–120 |
|
||||
| B3 | Camera media: H.264 High 10 / 4:2:2, HEVC 4:2:2, E-AC-3, AVI, multi-track audio confirmed | 🟡 (multi-stream audio #361) | 50–90 |
|
||||
| B3 | Camera media: H.264 High 10 / 4:2:2, HEVC 4:2:2, multi-track audio confirmed (E-AC-3 #788 and AVI #650 done) | 🟡 (multi-stream audio #361) | 50–90 |
|
||||
| B4 | Fidelity harness + `cargo xtask parity` + media corpus + workflow tests | ⬜ | 90–160 |
|
||||
| B5 | Hardware: zero-copy, more GPU effects, Windows vendor encoders, VideoToolbox Main 10 | 🟡 | 60–100 |
|
||||
| B6 | UI fidelity top issues and Windows / Linux runtime | 🟡 | 90–160 |
|
||||
@@ -69,23 +69,24 @@ Status: ✅ done · 🟡 in progress · ⬜ not started. Estimates are remaining
|
||||
| M6 | Export | ✅ | Own H.264 encoder (High/Main/Baseline, B-frames, VBR/CBR/2-pass) → MP4 or QuickTime + own AAC; ProRes, DNxHR, MJPEG, PNG/TIFF/BMP sequences (Premiere-style numbering), GIF, WAV, AIFF; background jobs; M6.5: Export mode parity (frame size / rate / scaling / pixel aspect, profile / level, CBR / VBR 1- and 2-pass, keyframe distance, audio codec / rate / channels / bitrate / sample size, multiplexer, burn-in or sidecar captions, image / name / timecode overlays, video limiter, loudness normalization with true-peak limiter, metadata, ranges, estimated size, summary), 24 built-in presets + user presets with favourites and import/export (Preset Manager), export queue (reorder, cancel, retry, several sequences / ranges), Quick Export; `export.*` commands and `filmcraft-cli export --preset` | Interlaced encoding, 5.1 audio, smart render, publishing destinations | 6–8 |
|
||||
| M7 | Audio | 🟡 | Mixer graph (tracks → submixes → Mix, pre/post-fader inserts and sends, latency-compensated, sample-accurate, ~6× realtime for 24 tracks × 3 effects on one core), Audio Track Mixer + Audio Clip Mixer panels, track automation (Off/Read/Latch/Touch/Write, recorded live while playing, thinned to keyframes, timeline lanes with pen editing), solo/solo-safe, channel mapping basics, peak + BS.1770 loudness meters (match ffmpeg), DSP crate with 16 clip/track effects, Audio Gain (set/adjust/normalize), Constant Power / Constant Gain / Exponential Fade, Essential Sound (types, Loudness auto-match, Repair incl. DeEss/DeReverb, Clarity, Creative, Ducking keyframes, presets; full Dialogue chain 22× realtime) | Music duration remix, ML speech enhancement, 5.1 panner and multichannel buses, voice-over record, effect editor windows (EQ curve), remaining effects (multiband, convolution reverb), clip-mixer automation recording | 6–9 |
|
||||
| M8 | Colour | 🟡 | Lumetri: basic, creative + looks, RGB & hue curves, wheels, HSL secondary, vignette, section bypass; Input LUT / Look LUT (.cube 1D/3D/shaper, .3dl; tetrahedral CPU + WGSL; project LUT library; built-in camera conversions); Colour Match (Oklab tonal-range statistics, skin protection, solved in Lumetri wheels); colour management: Rec. 709 / Rec. 2100 PQ / HLG working spaces + wide gamut, Interpret Footage colour space (S-Log3, V-Log, Canon Log 2/3, LogC3/4, Apple Log, D-Log, Fujifilm F-Log / F-Log2 / F-Log2 C from published specs), metadata auto-detect, BT.2390 tone mapping, gamut mapping, HDR export signalling (VUI/colr/mdcv/clli/SEI, ffprobe-verified); Lumetri Scopes: Vectorscope YUV (75/100 % targets, skin-tone line) and HLS, Histogram, Parade (RGB/YUV/RGB-White), Waveform (RGB/Luma/YC/YC no Chroma), presets, Rec. 601/709/2100, 8-bit/float/HDR (cd/m²) scales, multi-scope grid, `scopes.read` | HDR-aware Lumetri maths, macOS EDR monitors, mastering metadata → tone-map peak, D-Log M (no published formula), HSL Secondary refine | 3–5 |
|
||||
| M9 | More codecs | 🟡 | ProRes decode+encode, AAC decode+encode, HEVC Main/Main 10 decoder (bit-exact on 41 fixtures, ~225 fps 1080p), VP9 decoder (profiles 0–3, 8/10/12-bit, bit-exact on 50+ fixtures; WebM/MKV `V_VP9` and MP4 `vp09` import with key-frame-checked seeking), Matroska/WebM import (H.264/HEVC/VP9/ProRes/MJPEG + AAC/Opus/FLAC/MP3/Vorbis/PCM), Opus decoder (SILK/CELT/hybrid, 5.1/7.1 multistream; all RFC 8251 vectors range-exact; WebM/MKV/MP4), DNxHD/DNxHR decoder (all SMPTE ST 2019-1 CIDs, 8/10/12-bit, 4:2:2/4:4:4, interlaced, alpha; within IDCT precision of ffmpeg on 22 fixtures) + DNxHR LB/SQ/HQ/HQX/444 encoder and MOV `AVdh` export, AV1 decoder (Main profile, 8/10-bit, all intra/inter tools, loop filters, superres, film grain, intra BC, spatial layers; bit-exact vs libdav1d on 19 SVT fixtures + 22 libaom vectors; MP4 `av01` / WebM `V_AV1` import), MXF import (OP1a/OP-Atom; AVC with POC-ordered B pictures bit-exact, DNxHD/DNxHR, ProRes, PCM/AES3; MPEG-2 reported unsupported; index-table seeking, material package timecode), Ogg Opus/Vorbis (granule seeking, pre-skip, end trimming), image sequences, Broadcast WAV timecode | VP9 frame threading, AV1 threading/SIMD (~4 fps 1080p today), AV1 High/Professional profiles, H.264 High 10 / 4:2:2 and HEVC 4:2:2 in software (G3), E-AC-3, AVI (MPEG-2 decoder and hardware decode have landed) | 30–50 |
|
||||
| M9 | More codecs | 🟡 | ProRes decode+encode, AAC decode+encode, HEVC Main/Main 10 decoder (bit-exact on 41 fixtures, ~225 fps 1080p), VP9 decoder (profiles 0–3, 8/10/12-bit, bit-exact on 50+ fixtures; WebM/MKV `V_VP9` and MP4 `vp09` import with key-frame-checked seeking), Matroska/WebM import (H.264/HEVC/VP9/ProRes/MJPEG + AAC/Opus/FLAC/MP3/Vorbis/PCM), Opus decoder (SILK/CELT/hybrid, 5.1/7.1 multistream; all RFC 8251 vectors range-exact; WebM/MKV/MP4), DNxHD/DNxHR decoder (all SMPTE ST 2019-1 CIDs, 8/10/12-bit, 4:2:2/4:4:4, interlaced, alpha; within IDCT precision of ffmpeg on 22 fixtures) + DNxHR LB/SQ/HQ/HQX/444 encoder and MOV `AVdh` export, AV1 decoder (Main profile, 8/10-bit, all intra/inter tools, loop filters, superres, film grain, intra BC, spatial layers; bit-exact vs libdav1d on 19 SVT fixtures + 22 libaom vectors; MP4 `av01` / WebM `V_AV1` import), MXF import (OP1a/OP-Atom; AVC with POC-ordered B pictures bit-exact, DNxHD/DNxHR, ProRes, PCM/AES3; MPEG-2 reported unsupported; index-table seeking, material package timecode), Ogg Opus/Vorbis (granule seeking, pre-skip, end trimming), image sequences, Broadcast WAV timecode, AC-3 and E-AC-3 decoder (`crates/ac3`; E-AC-3 independent substream 0, #788), AVI / OpenDML import (`crates/avi`; Motion JPEG, H.264, HEVC, uncompressed video; PCM, MP3, MP2, AC-3 audio; #650) | VP9 frame threading, AV1 threading/SIMD (~4 fps 1080p today), AV1 High/Professional profiles, H.264 High 10 / 4:2:2 and HEVC 4:2:2 in software (G3), E-AC-3 adaptive hybrid transform, enhanced coupling and the 7.1 dependent substreams (played as the 5.1 core) (MPEG-2 decoder and hardware decode have landed) | 30–50 |
|
||||
| M10 | Graphics & captions | 🟡 | Caption tracks (Subtitle/CEA-608/708/Teletext formats, track style), SRT/WebVTT/SCC import+export (frame-exact, property-tested), caption editing (add/split/merge/trim/move, sync-locked insert/extract), Text panel Captions tab, burn-in in Program monitor and export; text engine (`crates/text`: bundled + system fonts, harfrust shaping, bidi, line breaking, paragraph layout, glyph cache; 3-line 1080p title ≈ 0.15 ms warm); graphic clips with text + shape layers (fill, 2 strokes, background, shadow, keyframable transform), Type tool with on-monitor editing, shape/pen tools, Properties/Essential Graphics editor, align/distribute; graphics templates (own `.fcgt` format, 8 original built-ins, Browse tab with engine-rendered thumbnails, export / install / apply / editable properties; never reads .mogrt), rolls/crawls with ease and pre/postroll, responsive pins and intro/outro, per-character styles, Upgrade Caption to Graphic, source graphics, Replace Fonts; MCC (608+708), EBU STL, TTML/IMSC1, DFXP (frame-exact, property-tested) | 608/708 embedding in video streams, speech-to-text | 12–18 |
|
||||
| M11 | Interchange & project management | ✅ | `.fcproj` schema versions + migrations, atomic saves, Save a Copy/Revert, auto-save ring + crash-recovery journal (Preferences ▸ Auto Save, recovery prompt), FCP7 XML, FCPXML, EDL, OTIO; offline media (own slate) + Link Media (fingerprint-checked relink, folder remap, search, Align Timecode, Make Offline); proxies (ProRes Proxy/LT, H.264 ¼/½ background jobs, attach/detach/reconnect, monitor toggle, export full-res) + ingest (copy/transcode/proxies); Project Manager (collect, consolidate + transcode with handles, size estimate) | Rename media to clip names, image-sequence conversion, Media Browser-driven relink, smart (cross-drive) path tracking | 1–2 |
|
||||
| M12–M16 | Multicam, web (WASM), platform, long tail | 🟡 | Multicam: audio sync (GCC-PHAT, sub-sample), Merge Clips, multi-camera source sequences, Multi-Camera view with live switching (1–9); L0–L4 crates compile to wasm32 | Web app shell (file access, WebCodecs, audio), scene detection, auto reframe, ~800 remaining commands and dialogs, performance hardening | 35–55 |
|
||||
| HW | Hardware acceleration (#30) | 🟡 | Blend modes and 34 effect ids on the GPU incl. Lumetri basic / creative / vignette (#32, GPU2–GPU7); GPU export rendering (#421); decode: VideoToolbox (#33, #41), Media Foundation / D3D11 H.264 / HEVC / VP9 / AV1 (HW2, HW3), VA-API H.264 / HEVC (#358), NVDEC H.264 / HEVC (#549); encode: VideoToolbox H.264 / HEVC, NVENC H.264 (Windows, Linux) and HEVC incl. Main 10 HDR (#328, #360) | Zero-copy, remaining GPU effects, VideoToolbox Main 10, Quick Sync / AMF, VA-API VP9 / AV1 and encode | 120–200 |
|
||||
| L10N | Localization | 🟡 | Spanish, Japanese, Simplified Chinese whole interface; Portuguese (Brazil), Ukrainian menus | Hindi, Arabic (RTL), French, Indonesian, German, Korean, Vietnamese, Italian, Russian; native review | 150–230 |
|
||||
| L10N | Localization | 🟡 | Whole interface: Spanish, Japanese, Simplified Chinese, French, Portuguese (Brazil), Indonesian, Italian, Korean, German, Russian, Czech, Polish, Swedish, Turkish, Bosnian; Ukrainian the core menus | Hindi, Arabic (RTL), Vietnamese; native review of every language | 150–230 |
|
||||
|
||||
## After beta
|
||||
|
||||
Plugin hosting (VST3 / AU, then OpenFX; owner decision, [G11](gaps.md#g11-no-plugin-hosting)),
|
||||
AI features with openly licensed models ([G12](gaps.md#g12-ai-features)), delivery formats
|
||||
([G13](gaps.md#g13-delivery-formats)), localization ([G14](gaps.md#g14-localization-8-of-the-12-key-languages-missing)),
|
||||
([G13](gaps.md#g13-delivery-formats)), localization ([G14](gaps.md#g14-localization-3-of-the-12-key-languages-missing-hindi-arabic-vietnamese)),
|
||||
the web app shell ([G20](gaps.md#g20-the-web-app-shell)).
|
||||
|
||||
## Revision history
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-30 | minor | E-AC-3 (#788), AVI (#650), Russian (#610), French (#804), Indonesian (#878), pt-BR (#866) and German (#630) moved from open to done in the Alpha gate row, B3, M9 and L10N; hour estimates and statuses not re-measured |
|
||||
| 2026-10-10 | minor | Alpha gate table added (craftrules core-workflow gate): six core workflows, all pass on macOS; stage stays alpha |
|
||||
| 2026-10-10 | major | Created: milestone table moved from ROADMAP.md (M5 and M9 remaining work updated), HW and L10N rows, beta milestones B1–B7, Current focus from the re-measure |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Parity with Adobe Premiere Pro
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** major (full re-measure against Premiere Pro 2026 26.5.2; replaces the scorecard and estimate that lived in ROADMAP.md) · **Target:** Adobe Premiere Pro 2026 (26.5.2, macOS)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (AVI import #650, E-AC-3 decode #647 and Russian menus #610 no longer counted as missing; percentages and hours not re-measured) · **Target:** Adobe Premiere Pro 2026 (26.5.2, macOS)
|
||||
|
||||
The authoritative assessment of how close FilmCraft is to Premiere Pro, and how much work is left.
|
||||
[ROADMAP.md](../ROADMAP.md) summarizes it; [gaps.md](gaps.md) lists every shortfall one at a time.
|
||||
@@ -23,7 +23,7 @@ The deep checklists are [file-format-parity.md](file-format-parity.md),
|
||||
|
||||
The previous estimate (2026-10-05) was breadth ~87%, ready ~50–60%, and "~210–325 agent-hours to
|
||||
100%". The breadth figure is one point lower now because this pass counts the file formats Premiere
|
||||
lists and we lack (AVI, WMV, camera RAW, EXR / DPX / PSD stills, `.prproj`, DVD / Blu-ray / DCP
|
||||
lists and we lack (WMV, camera RAW, EXR / DPX / PSD stills, `.prproj`, DVD / Blu-ray / DCP
|
||||
export) rather than only the formats we have. The hours went up because the old figure covered the
|
||||
checklist only and left out plugins, AI, localization, `.prproj` and the depth work that 188 open
|
||||
issues now document. That is new evidence, not drift: see [Calibration](#calibration).
|
||||
@@ -109,7 +109,7 @@ agent-hours to close the area to Premiere, sequential.
|
||||
|---|---|---|---|---|---|---|
|
||||
| Editing, timeline, trimming, multicam | 20% | ~90% | ~70% | 80–140 | Edit algebra, trim modes, Trim Monitor, multicam with audio sync, Premiere default keyboard (143 commands), Source playback and range dragging (10-08). Open: ripple-delete of a gap by column (#668), Backspace on gaps (#648), clip stretching (#653), deselect on empty space (#683), new tracks by dragging media (#483), Escape cancelling drags (#580 fixed), time remapping (#510), effects on a whole track (#623) | [ui-parity.md](ui-parity.md) |
|
||||
| Effects and transitions | 12% | 100% (measured) | ~60% | 80–140 | Every effect exists; Warp Stabilizer is 2-D, Auto Reframe approximate, Morph Cut morphs with optical flow but without face tracking (#805); Optical Flow time interpolation is motion compensated on the CPU (#780). 34 effect ids run on the GPU against ~82 of Premiere's ~92 marked Accelerated. No Object Mask (26.0, AI). | [hardware-parity.md](hardware-parity.md) |
|
||||
| Media import and codecs | 12% | ~75% | ~60% | 150–250 | Own bit-exact decoders for H.264, HEVC, VP9, AV1, ProRes, DNx, APV, MPEG-2, AAC, AC-3, Opus. But H.264 only 8-bit 4:2:0 and HEVC no 4:2:2 in software (most 10-bit cameras, #626); no camera RAW (R3D, ARRIRAW, BRAW, Sony RAW, Canon RAW, ProRes RAW, #345, #383); no E-AC-3 (#647), AVI (#598), WMV, DV, XAVC / P2 metadata; Sony start timecode (#460); WebM black (#432) | [codec-parity.md](codec-parity.md), [file-format-parity.md](file-format-parity.md) |
|
||||
| Media import and codecs | 12% | ~75% | ~60% | 150–250 | Own bit-exact decoders for H.264, HEVC, VP9, AV1, ProRes, DNx, APV, MPEG-2, AAC, AC-3, Opus. But H.264 only 8-bit 4:2:0 and HEVC no 4:2:2 in software (most 10-bit cameras, #626); no camera RAW (R3D, ARRIRAW, BRAW, Sony RAW, Canon RAW, ProRes RAW, #345, #383); E-AC-3 plays its 5.1 core only (#647), no WMV, DV, XAVC / P2 metadata; Sony start timecode (#460) | [codec-parity.md](codec-parity.md), [file-format-parity.md](file-format-parity.md) |
|
||||
| Audio | 10% | ~88% | ~65% | 50–90 | Mixer graph, automation, all 53 effects, Essential Sound, 5.1, voice-over, Remix, multi-stream container audio (#361). Device and playback reports: crackle / cut-outs (#336, #410, #602, #605, #617); Enhance Speech is DSP, not a model; ALSA fallback fixed (#254) | |
|
||||
| Panels and UI fidelity | 10% | ~88% | ~60% | 120–200 | All main panels. Missing: on-screen transform handles in the Program monitor (#639), Effect Controls keyframe zoom / divider / row lines (#640–#645), docking rearrangement (#493), multi-monitor windows (#466), filmstrip thumbnails (#614), marquee in Project panel (#578) | [ui-parity.md](ui-parity.md) |
|
||||
| Colour | 8% | ~90% | ~70% | 40–70 | Lumetri complete incl. scopes, LUTs, colour management, HDR export. Lumetri on the GPU covers basic, creative and vignette only; HDR-aware Lumetri maths, macOS EDR display, D-Log M; a Lumetri report (#628) | |
|
||||
@@ -128,7 +128,7 @@ agent-hours to close the area to Premiere, sequential.
|
||||
| File formats (containers, project, interchange, captions, stills) | ~60% | 140–240 | [file-format-parity.md](file-format-parity.md) |
|
||||
| Codecs (decode and encode, fidelity, camera media) | ~60% | 150–250 | [codec-parity.md](codec-parity.md) |
|
||||
| Hardware acceleration (decode, encode, GPU effects, I/O devices) | ~45% | 120–200 | [hardware-parity.md](hardware-parity.md) |
|
||||
| Localization (12 key languages) | ~35% of Premiere's set; 4 of 12 key languages near-complete | 150–230 | [localization-parity.md](localization-parity.md) |
|
||||
| Localization (12 key languages) | ~35% of Premiere's set; 9 of 12 key languages near-complete | 150–230 | [localization-parity.md](localization-parity.md) |
|
||||
| Performance (beyond hardware) | ~55% | 60–120 | [performance.md](performance.md) |
|
||||
| Stability | ~45% | 60–120 | [gaps.md](gaps.md#g4-crashes-at-start-up-and-no-test-ci-on-pull-requests) |
|
||||
| Platforms | ~60% | 50–90 | [gaps.md](gaps.md#g9-windows-and-linux-at-run-time) |
|
||||
@@ -211,7 +211,7 @@ full-area depth (e.g. the ~20 common effects rather than all 93 and their approx
|
||||
| Area | Weight | Depth | Why |
|
||||
|---|---|---|---|
|
||||
| Editing and trimming | 28% | 80% | Insert / overwrite, ripple / roll / slip / slide, razor, J/K/L, markers, multicam all work; gap deletion (#648, #668) and deselect (#683) are friction |
|
||||
| Importing everyday footage | 14% | 65% | Phone and 8-bit camera H.264 / HEVC, ProRes, DNx, MXF open; 10-bit / 4:2:2 H.264 (Sony XAVC S-I, Canon) does not (#626); E-AC-3, AVI missing |
|
||||
| Importing everyday footage | 14% | 65% | Phone and 8-bit camera H.264 / HEVC, ProRes, DNx, MXF open; 10-bit / 4:2:2 H.264 (Sony XAVC S-I, Canon) does not (#626); WMV and DV missing |
|
||||
| Common effects and transitions | 9% | 75% | Lumetri, Transform, Crop, blurs, Cross Dissolve, keys exist; Warp Stabilizer is 2-D, Object Mask missing |
|
||||
| Colour (Lumetri, scopes, LUTs) | 9% | 80% | Complete, parity-tested CPU / GPU; curves / wheels on CPU |
|
||||
| Audio (levels, Essential Sound, loudness, ducking) | 11% | 75% | Complete; audio-device reports off macOS |
|
||||
@@ -248,7 +248,7 @@ interchange and pro formats.
|
||||
|
||||
| Feature | Weight | Depth | Why |
|
||||
|---|---|---|---|
|
||||
| New project, import clips | 15% | 80% | Drag-and-drop and Import work for phone MP4 / MOV (H.264, HEVC); MKV multi-audio (#601), AVI (#598) fail |
|
||||
| New project, import clips | 15% | 80% | Drag-and-drop and Import work for phone MP4 / MOV (H.264, HEVC); MKV multi-audio (#601) fails |
|
||||
| Put clips on the timeline, arrange | 15% | 85% | Drop to timeline (#517), snapping, tracks; new tracks by dropping missing (#483) |
|
||||
| Trim, split, delete | 15% | 80% | Razor, trim, ripple delete; Backspace on gaps (#648) and deselect (#683) confuse |
|
||||
| Undo / redo | 5% | 95% | Snapshot undo for every command |
|
||||
@@ -267,8 +267,8 @@ Discounts a beginner actually hits:
|
||||
#687).
|
||||
- **Discoverability and UI clarity ×0.90:** a dense Premiere-style interface with no guided start;
|
||||
reports about deselecting, deleting gaps and transitions controls (#683, #648, #577).
|
||||
- **Opening files people send them ×0.95:** phone and web video mostly opens; AVI, E-AC-3 audio
|
||||
(#647), FLAC-in-MP4 (#603) and some WebM (#432) do not.
|
||||
- **Opening files people send them ×0.95:** phone and web video mostly opens; FLAC-in-MP4 (#603) and
|
||||
MKV multi-audio (#601) do not. (AVI #598, E-AC-3 #647 and WebM black #432 are closed.)
|
||||
|
||||
80.0 × 0.88 × 0.90 × 0.95 = **60.2% ≈ ~60%.**
|
||||
|
||||
@@ -310,7 +310,7 @@ Distance to beta: ~19 points of ready-for-real-work plus `.prproj` import, ~400
|
||||
| UI fidelity top issues (Program monitor handles, Effect Controls, docking, deselect) | 60–100 |
|
||||
| Hardware: zero-copy, VideoToolbox Main 10, Quick Sync / AMF encode, more GPU effects | 60–100 |
|
||||
| Windows / Linux runtime fixes, audio device issues | 30–60 |
|
||||
| Container gaps users hit: E-AC-3, AVI, multi-track audio, still formats | 30–50 |
|
||||
| Container gaps users hit: multi-track audio, still formats | 30–50 |
|
||||
| Interchange validation (AAF / OMF in Avid and Pro Tools, XML speed) | 20–40 |
|
||||
| Performance: lag / CPU reports, AV1 speed | 30–60 |
|
||||
| **Total** | **~400–730** |
|
||||
@@ -354,6 +354,7 @@ control surfaces), and validation in Avid Media Composer / Pro Tools.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-12 | minor | Outdated entries corrected from merged work: AVI import (#650) and E-AC-3 decode (#647, via #788) removed from the failing lists; VP8 WebM decodes (#432, via #1064); localization "4 of 12" now 9 of 12 (localization-parity.md); scores and hours not re-measured |
|
||||
| 2026-10-10 | minor | Readiness table: hours to ~95% for each audience (full ~1,450–2,600, mainstream ~500–850, essentials ~180–350); full number confirmed as an additive weighted sum (no change) |
|
||||
| 2026-10-10 | minor | Added the three readiness numbers: full ready-for-real-work formula written out (~55% -> ~56%, same inputs, now computed rather than rounded down), mainstream practitioner ~55% and essentials user ~60% with written weights and discounts, and counted user evidence (31 people praising, 0 switch reports, ~64 core-path vs ~29 niche open bugs) |
|
||||
| 2026-10-10 | minor | Stage checked against the new core-workflow gate (docs/roadmap.md#alpha-gate): passes, stays alpha |
|
||||
|
||||
@@ -97,3 +97,7 @@ Apple-silicon laptop CPU).
|
||||
- Transcription runs synchronously inside the command (no background job or progress bar yet).
|
||||
- Track items that refer to a subclip are looked up by the subclip's id, so a transcript made for
|
||||
the parent media is not shown through subclip clips yet.
|
||||
- Word bounds are only moved across pauses. A start that the alignment places late inside
|
||||
continuous speech (no pause of 0.2 s between the two words) stays where it is, so the word
|
||||
before it can hold the beginning of the next one: the audio level gives no boundary to move it
|
||||
to. Words that the alignment places a whole sentence away are not moved back either.
|
||||
|
||||
Reference in New Issue
Block a user