Merge origin/main

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas 2026-10-10 05:57:23 -04:00
commit 54f27f1f05
8 changed files with 464 additions and 6 deletions

View File

@ -9,6 +9,7 @@ iconography, images or other Adobe assets; open licences only; every asset attri
| Asset | Author | Source | Licence |
|---|---|---|---|
| `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`) |
| `assets/fonts/Inter-SemiBold.ttf` | The Inter Project Authors | https://github.com/rsms/inter | OFL-1.1 (`assets/fonts/OFL-Inter.txt`) |

View File

@ -211,4 +211,4 @@ Status: ✅ done · 🟡 in progress · ⬜ not started. Estimates are remaining
### Interface localization
Edit > Language switches English/Japanese/Spanish/Portuguese (Brazilian) and persists across restarts through `general.interfaceLanguage` (also available in Settings > General). Until a language is chosen, System Language (the default) follows the operating system's or browser's preferred languages and falls back to English. Commands `app.language.english`, `app.language.japanese`, `app.language.spanish` and `app.language.portuguese` are reachable through the control channel. Translations live in one catalog per language (`crates/ui-egui/src/i18n/<code>.tsv`); UI code wraps its strings in `tl!("…")` / `tlf!("…{name}", name)`, and names that come from registries (effects and their parameters, settings, commands, panels, workspaces, presets) are translated where they are drawn. **Spanish and Japanese cover the whole interface**: menus, panels, dialogs, the Settings pages, the Effects/Effect Controls registry, the Keyboard Shortcuts dialog and status messages; tests fail when a `tl!` literal, a menu label or a registry name has no Spanish entry (`ja.tsv` has the same entries, but the tests do not require them yet). Searches match the displayed labels as well as English sources. Brazilian Portuguese covers the core menus; untranslated labels use English. Engine error messages, the control channel, the CLI and MCP stay in English. Japanese text uses the Japanese fonts from [craft-fonts](https://github.com/storytold/craft-fonts) when FilmCraft is built with them (`CRAFT_FONTS_DIR`; all official releases, including the web build), otherwise a Japanese font already installed on the system; with neither, switching to Japanese is refused with a message. Existing vertical text support is preserved.
Edit > Language switches English/Japanese/Spanish/Portuguese (Brazilian)/Ukrainian and persists across restarts through `general.interfaceLanguage` (also available in Settings > General). Until a language is chosen, System Language (the default) follows the operating system's or browser's preferred languages and falls back to English. Commands `app.language.english`, `app.language.japanese`, `app.language.spanish`, `app.language.portuguese` and `app.language.ukrainian` are reachable through the control channel. Translations live in one catalog per language (`crates/ui-egui/src/i18n/<code>.tsv`); UI code wraps its strings in `tl!("…")` / `tlf!("…{name}", name)`, and names that come from registries (effects and their parameters, settings, commands, panels, workspaces, presets) are translated where they are drawn. **Spanish and Japanese cover the whole interface**: menus, panels, dialogs, the Settings pages, the Effects/Effect Controls registry, the Keyboard Shortcuts dialog and status messages; tests fail when a `tl!` literal, a menu label or a registry name has no Spanish entry (`ja.tsv` has the same entries, but the tests do not require them yet). Searches match the displayed labels as well as English sources. Brazilian Portuguese and Ukrainian cover the core menus; untranslated labels use English. Ukrainian uses the bundled Cyrillic interface fonts. Engine error messages, the control channel, the CLI and MCP stay in English. Japanese text uses the Japanese fonts from [craft-fonts](https://github.com/storytold/craft-fonts) when FilmCraft is built with them (`CRAFT_FONTS_DIR`; all official releases, including the web build), otherwise a Japanese font already installed on the system; with neither, switching to Japanese is refused with a message. Existing vertical text support is preserved.

View File

@ -99,6 +99,8 @@ pub fn install(app: &FilmcraftApp, ctx: egui::Context) -> (Receiver<String>, Sho
filmcraft_ui_egui::i18n::Language::Es
} else if checked("app.language.portuguese") {
filmcraft_ui_egui::i18n::Language::PtBr
} else if checked("app.language.ukrainian") {
filmcraft_ui_egui::i18n::Language::Uk
} else {
filmcraft_ui_egui::i18n::Language::En
};

View File

@ -680,7 +680,14 @@ static CATEGORIES: &[Category] = &[
f(
"general.interfaceLanguage",
"Interface Language",
Kind::Choice(&[("system", "System Language"), ("en", "English"), ("ja", "日本語"), ("es", "Español"), ("pt-br", "Português (Brasil)")]),
Kind::Choice(&[
("system", "System Language"),
("en", "English"),
("ja", "日本語"),
("es", "Español"),
("pt-br", "Português (Brasil)"),
("uk", "Українська"),
]),
true,
),
f("general.atStartup", "At Startup", Kind::Choice(STARTUP), true),

View File

@ -34,14 +34,16 @@ pub enum Language {
/// Persisted as `pt-br` (the blanket `rename_all` would produce `ptbr`).
#[serde(rename = "pt-br")]
PtBr,
Uk,
}
static JAPANESE: OnceLock<Catalog> = OnceLock::new();
static SPANISH: OnceLock<Catalog> = OnceLock::new();
static PORTUGUESE: OnceLock<Catalog> = OnceLock::new();
static UKRAINIAN: OnceLock<Catalog> = OnceLock::new();
impl Language {
pub const ALL: [Self; 4] = [Self::En, Self::Ja, Self::Es, Self::PtBr];
pub const ALL: [Self; 5] = [Self::En, Self::Ja, Self::Es, Self::PtBr, Self::Uk];
pub fn name(self) -> &'static str {
match self {
@ -49,6 +51,7 @@ impl Language {
Self::Ja => "日本語",
Self::Es => "Español",
Self::PtBr => "Português (Brasil)",
Self::Uk => "Українська",
}
}
@ -58,6 +61,7 @@ impl Language {
"ja" => Some(Self::Ja),
"es" => Some(Self::Es),
"pt-br" => Some(Self::PtBr),
"uk" => Some(Self::Uk),
_ => None,
}
}
@ -66,7 +70,7 @@ impl Language {
/// (BCP 47 or POSIX locale tags such as `es-419`, `pt_BR.UTF-8`, most preferred first) that the
/// interface has, else English. Any Portuguese gets the Brazilian catalog, the only one there is.
pub fn from_locales(tags: &[String]) -> Self {
const PRIMARY: [(&str, Language); 4] = [("en", Language::En), ("ja", Language::Ja), ("es", Language::Es), ("pt", Language::PtBr)];
const PRIMARY: [(&str, Language); 5] = [("en", Language::En), ("ja", Language::Ja), ("es", Language::Es), ("pt", Language::PtBr), ("uk", Language::Uk)];
tags.iter()
.find_map(|tag| {
let primary = tag.split(['-', '_', '.', '@']).next().unwrap_or_default();
@ -82,6 +86,7 @@ impl Language {
Self::Ja => "ja",
Self::Es => "es",
Self::PtBr => "pt-br",
Self::Uk => "uk",
}
}
@ -92,6 +97,7 @@ impl Language {
Self::Ja => Some(JAPANESE.get_or_init(|| Catalog::parse(include_str!("ja.tsv")))),
Self::Es => Some(SPANISH.get_or_init(|| Catalog::parse(include_str!("es.tsv")))),
Self::PtBr => Some(PORTUGUESE.get_or_init(|| Catalog::parse(include_str!("pt-br.tsv")))),
Self::Uk => Some(UKRAINIAN.get_or_init(|| Catalog::parse(include_str!("uk.tsv")))),
}
}
@ -246,7 +252,9 @@ mod tests {
#[test]
fn catalogs_are_well_formed() {
for (code, text) in [("es", include_str!("es.tsv")), ("ja", include_str!("ja.tsv")), ("pt-br", include_str!("pt-br.tsv"))] {
for (code, text) in
[("es", include_str!("es.tsv")), ("ja", include_str!("ja.tsv")), ("pt-br", include_str!("pt-br.tsv")), ("uk", include_str!("uk.tsv"))]
{
let (entries, errors) = catalog::parse_entries(text);
assert!(errors.is_empty(), "{code}: {errors:?}");
for (i, (ctx, en, tr)) in entries.iter().enumerate() {
@ -268,6 +276,10 @@ mod tests {
assert_eq!(Language::PtBr.tr("File"), "Arquivo");
assert_eq!(Language::PtBr.tr("meu video.mp4"), "meu video.mp4");
assert_eq!(Language::PtBr.name(), "Português (Brasil)");
assert_eq!(Language::Uk.name(), "Українська");
assert_eq!(Language::Uk.tr("File"), "Файл");
assert_eq!(Language::Uk.tr("мій кліп.mp4"), "мій кліп.mp4");
assert_eq!(Language::Uk.tr("An untranslated label"), "An untranslated label");
for l in Language::ALL {
assert_eq!(Language::parse(l.code()), Some(l));
let json = serde_json::to_string(&l).unwrap();
@ -626,6 +638,21 @@ mod tests {
assert_eq!(app.ui.language, Language::PtBr);
assert_eq!(app.session.prefs.general.interface_language, "pt-br");
assert!(crate::menus::menu_items(&app).iter().any(|it| it.id == "app.language.portuguese" && it.checked == Some(true)));
let result = crate::menus::invoke(&mut app, &ctx, "app.language.ukrainian", serde_json::json!({})).unwrap();
assert_eq!(result, serde_json::json!("uk"));
assert_eq!(app.ui.language, Language::Uk);
for item in crate::menus::menu_items(&app).iter().filter(|it| it.id.starts_with("app.language.")) {
assert_eq!(item.checked, Some(item.id == "app.language.ukrainian"), "{}", item.id);
}
let saved = serde_json::to_string(&app.ui).unwrap();
assert!(saved.contains("\"language\":\"uk\""), "{saved}");
let restored: crate::state::UiState = serde_json::from_str(&saved).unwrap();
assert_eq!(restored.language, Language::Uk);
assert_eq!(app.session.prefs.general.interface_language, "uk");
let prefs = serde_json::to_string(&app.session.prefs).unwrap();
let mut restarted = filmcraft_engine::Session::default();
restarted.prefs = serde_json::from_str(&prefs).unwrap();
assert_eq!(crate::FilmcraftApp::new(restarted).ui.language, Language::Uk);
crate::menus::invoke(&mut app, &ctx, "app.language.english", serde_json::json!({})).unwrap();
assert_eq!(app.ui.language, Language::En);
set_current(Language::En);
@ -640,6 +667,9 @@ mod tests {
assert_eq!(l(&["ES"]), Language::Es);
assert_eq!(l(&["ja-JP"]), Language::Ja);
assert_eq!(l(&["pt-BR"]), Language::PtBr);
for tag in ["uk", "uk-UA", "uk_UA.UTF-8", "UK-ua"] {
assert_eq!(l(&[tag]), Language::Uk);
}
assert_eq!(l(&["pt_PT.UTF-8@euro"]), Language::PtBr);
assert_eq!(l(&["fr-FR", "de", "es-MX", "ja"]), Language::Es, "the first one the interface has");
assert_eq!(l(&["en-GB", "es"]), Language::En);
@ -684,6 +714,57 @@ mod tests {
set_current(Language::En);
}
#[test]
fn ukrainian_entries_cover_the_original_menu_catalog() {
let app = crate::FilmcraftApp::new(filmcraft_engine::Session::default());
let items = crate::menus::menu_items(&app);
let known = |text: &str| crate::menus::MENUS.contains(&text) || items.iter().any(|it| it.label == text || it.path.iter().any(|p| p == text));
let (entries, _) = catalog::parse_entries(include_str!("uk.tsv"));
for (_, en, _) in entries {
assert!(known(&en) || en == "Settings", "not a menu label: {en}");
}
let (portuguese, _) = catalog::parse_entries(include_str!("pt-br.tsv"));
for (_, en, _) in portuguese {
assert!(Language::Uk.has(&en), "missing Ukrainian menu label: {en}");
}
for en in crate::menus::MENUS.into_iter().chain(["Audio", "Settings"]) {
assert_ne!(Language::Uk.tr(en), en, "untranslated Ukrainian menu: {en}");
}
}
#[test]
fn ukrainian_renders_with_bundled_fonts() {
let ctx = egui::Context::default();
crate::theme::install(&ctx, &crate::theme::Tokens::for_kind(crate::theme::ThemeKind::default()));
let mut output = ctx.run_ui(egui::RawInput::default(), |_| {});
output.textures_delta.clear();
let (entries, _) = catalog::parse_entries(include_str!("uk.tsv"));
ctx.fonts_mut(|fonts| {
// Include the entire alphabet, even letters not yet used by a translated menu label.
let alphabet = "АБВГҐДЕЄЖЗИІЇЙКЛМНОПРСТУФХЦЧШЩЬЮЯабвгґдеєжзиіїйклмнопрстуфхцчшщьюя";
let chars: std::collections::BTreeSet<_> = alphabet
.chars()
.chain(Language::Uk.name().chars())
.chain(entries.iter().flat_map(|(_, _, uk)| uk.chars()))
.filter(|ch| !ch.is_ascii())
.collect();
for family in crate::theme::font_families() {
let font = egui::FontId::new(13.0, family);
// has_glyph compares font faces, giving false negatives when Inter also supplies
// the replacement character (the named medium/semibold stacks). Compare the
// actual glyph texture regions instead; the noncharacter U+10FFFF is missing.
let text: String = chars.iter().copied().chain(['\u{10ffff}']).collect();
let galley = fonts.layout_no_wrap(text, font.clone(), egui::Color32::WHITE);
let glyphs = &galley.rows[0].glyphs;
assert_eq!(glyphs.len(), chars.len() + 1);
let replacement = glyphs.last().unwrap().uv_rect;
for glyph in glyphs.iter().take(chars.len()) {
assert_ne!(glyph.uv_rect, replacement, "missing {} in {font:?}", glyph.chr);
}
}
});
}
#[test]
fn japanese_needs_an_installed_font() {
let mut app = crate::FilmcraftApp::new(filmcraft_engine::Session::default());

View File

@ -0,0 +1,356 @@
# Ukrainian (uk) catalog. Columns separated by TAB: context <TAB> source <TAB> translation
# Empty context is a plain UI string. Escapes: \n, \t, \\; preserve placeholders and trailing ….
# Original clean-room translations from the meaning of FilmCraft's English menu labels,
# using Ukrainian video-editing vocabulary. No proprietary localization resources were used.
# Core menu coverage; other interface strings fall back to English.
About FilmCraft Про FilmCraft
Add Audio Submix Track Додати доріжку підміксу аудіо
Add Caption at Playhead Додати субтитр у позиції курсора відтворення
Add Chapter Marker… Додати маркер розділу…
Add Edit Додати розріз
Add Edit to All Tracks Додати розріз на всіх доріжках
Add Flash Cue Marker… Додати маркер сигналу Flash…
Add Frame Hold Додати стоп-кадр
Add Guide… Додати напрямну…
Add Marker Додати маркер
Add New Caption Track… Додати нову доріжку субтитрів…
Add Range Marker Додати маркер діапазону
Add Range Marker to In and Out Додати маркер діапазону між початковою та кінцевою позначками
Add Tracks… Додати доріжки…
Adjustment Layer… Коригувальний шар…
Align to Selection Вирівняти за виділенням
Align to Video Frame Вирівняти за відеокадром
Align to Video Frame as Group Вирівняти за відеокадром як групу
All Panels Усі панелі
Alpha Альфа
Appearance Вигляд
Appearance… Вигляд…
Apply Audio Transition Застосувати аудіоперехід
Apply Default Transitions to Selection Застосувати типові переходи до виділення
Apply Video Transition Застосувати відеоперехід
Arrange Упорядкувати
ArtCraft Website Вебсайт ArtCraft
Assembly Компонування
Attach Proxies… Приєднати проксі…
Audio Аудіо
Audio Channels… Аудіоканали…
Audio Clip Mixer Мікшер аудіокліпів
Audio Gain… Підсилення аудіо…
Audio Hardware… Аудіообладнання…
Audio In Початок аудіо
Audio Meters Індикатори рівня аудіо
Audio Options Параметри аудіо
Audio Out Кінець аудіо
Audio Track Mixer Мікшер аудіодоріжок
Audio Waveform Осцилограма аудіо
Auto Save… Автозбереження…
Automate to Sequence… Автоматично розмістити в секвенції…
Bars and Tone… Кольорові смуги й тон…
Bin Контейнер
Bin From Selection Контейнер із виділення
Black Video… Чорне відео…
Blue Синій
Bottom Низ
Breakout to Mono Розділити на моноканали
Bring Forward Перемістити вперед
Bring to Front На передній план
Brown Коричневий
Captions Субтитри
Captions and Graphics Субтитри й графіка
Captions… Субтитри…
Caribbean Карибський
Center Horizontally По центру горизонтально
Center Vertically По центру вертикально
Cerulean Лазурний
Clear Видалити
Clear Guides Видалити напрямні
Clear In Прибрати початкову позначку
Clear In and Out Прибрати початкову та кінцеву позначки
Clear Markers Видалити маркери
Clear Out Прибрати кінцеву позначку
Clear Selected Marker Видалити вибраний маркер
Clip Кліп
Close Закрити
Close All Other Projects Закрити всі інші проєкти
Close All Projects Закрити всі проєкти
Close Gap Прибрати проміжок
Close Project Закрити проєкт
Color Management… Керування кольором…
Color Matte… Кольорова підкладка…
Comparison View Режим порівняння
Composite Video Композитне відео
Consolidate Duplicates Об’єднати дублікати
Copy Копіювати
Copy Paste Includes Sequence Markers Копіювати й вставляти разом із маркерами секвенції
Create Captions from Transcript… Створити субтитри з транскрипції…
Create Multi-Camera Source Sequence… Створити вихідну мультикамерну секвенцію…
Create Proxies… Створити проксі…
Cut Вирізати
Dark Темний
Darkest Найтемніший
Delete Render Files Видалити файли рендерингу
Delete Render Files In to Out Видалити файли рендерингу між початковою та кінцевою позначками
Delete Tracks… Видалити доріжки…
Delete Transcript Видалити транскрипцію
Demo Footage Демонстраційні матеріали
Demo Project Демонстраційний проєкт
Deselect All Зняти все виділення
Detach Proxies Від’єднати проксі
Display Mode Режим відображення
Distribute Розподілити
Distribute Horizontally Розподілити горизонтально
Distribute Space Horizontally Рівні проміжки по горизонталі
Distribute Space Vertically Рівні проміжки по вертикалі
Distribute Vertically Розподілити вертикально
Duplicate Дублювати
Dynamic Audio Waveforms Динамічні осцилограми аудіо
Edit Редагування
Edit Offline… Редагувати автономно…
Edit Original Редагувати оригінал
Edit Subclip… Редагувати підкліп…
Editing Монтаж
Effect Controls Керування ефектами
Effects Ефекти
Ellipse Еліпс
Enable Увімкнути
Enable Remix Увімкнути ремікс
Essential Graphics Основна графіка
Essential Sound Основний звук
Events Події
Export Експорт
Export As Motion Graphics Template… Експортувати як шаблон анімаційної графіки…
Extract Вилучити зі зсувом
Extract Audio Видобути аудіо
Field Options… Параметри полів…
File Файл
File… Файл…
Fill frame Заповнити кадр
FilmCraft Help… Довідка FilmCraft…
FilmCraft on GitHub FilmCraft на GitHub
FilmCraft on getartcraft.com FilmCraft на getartcraft.com
Find Next Знайти далі
Find… Знайти…
Fit Умістити
Fit to frame Умістити в кадр
Flatten Звести
Forest Лісовий
Frame Blending Змішування кадрів
Frame Hold Options… Параметри стоп-кадру…
Frame Sampling Вибірка кадрів
From Bin Із контейнера
From Source Monitor Із монітора джерела
From Source Monitor, Match Frame Із монітора джерела, відповідний кадр
From file… Із файлу…
Full Повна
Generate Audio Waveform Створити осцилограму аудіо
Get Media File Properties for Показати властивості медіафайлу для
Go to Gap Перейти до проміжку
Go to In Перейти до початкової позначки
Go to Next Caption Segment Перейти до наступного сегмента субтитрів
Go to Next Marker Перейти до наступного маркера
Go to Out Перейти до кінцевої позначки
Go to Previous Caption Segment Перейти до попереднього сегмента субтитрів
Go to Previous Marker Перейти до попереднього маркера
Go to Split Перейти до розділеної позначки
Graphics and Titles Графіка й титри
Graphics… Графіка…
Green Зелений
Group Згрупувати
Guide Templates Шаблони напрямних
Help Довідка
Hide All Caption Tracks Приховати всі доріжки субтитрів
High Quality Playback Відтворення у високій якості
History Історія
Import From Імпортувати з
Import Image Sequence… Імпортувати послідовність зображень…
Import from Media Browser Імпортувати з медіабраузера
Import… Імпортувати…
Info Інформація
Ingest Settings… Налаштування завантаження медіа…
Insert Вставити
Insert Frame Hold Segment Вставити сегмент стоп-кадру
Install Motion Graphics Template… Установити шаблон анімаційної графіки…
Interpret Footage… Інтерпретувати матеріал…
Join the ArtCraft Discord… Приєднатися до Discord ArtCraft…
Keyboard Shortcuts… Клавіатурні скорочення…
Label Мітка
Labels… Мітки…
Language Мова
Lavender Лавандовий
Learning Навчання
Left Ліворуч
Libraries Бібліотеки
Lift Вилучити без зсуву
Light Світлий
Link Зв’язати
Link Media… Прив’язати медіа…
Linked Selection Пов’язане виділення
Lock Guides Заблокувати напрямні
Lumetri Color Колір Lumetri
Lumetri Scopes Відеоскопи Lumetri
Magnification Масштаб
Make Offline… Зробити автономним…
Make Subclip… Створити підкліп…
Make Subsequence Створити підсеквенцію
Manage Guides… Керувати напрямними…
Mark Clip Позначити кліп
Mark In Установити початкову позначку
Mark Out Установити кінцеву позначку
Mark Selection Позначити виділення
Mark Split Установити розділену позначку
Markers Маркери
Match Frame Знайти відповідний кадр
Media Analysis & Transcription… Аналіз медіа й транскрибування…
Media Browser Медіабраузер
Media Cache… Медіакеш…
Media… Медіа…
Memory… Пам’ять…
Merge Clips… Об’єднати кліпи…
Metadata Метадані
Modify Змінити
Motion Graphics Template… Шаблон анімаційної графіки…
Multi-Camera Мультикамера
Multi-Camera View Мультикамерний режим
Nest… Вкласти в секвенцію…
New Створити
New Layer Новий шар
New Project Panel Нова панель проєкту
Next in Sequence Наступний у секвенції
Next in Track Наступний на доріжці
Normalize Mix Track… Нормалізувати доріжку міксу…
Offline File… Автономний файл…
Open Project… Відкрити проєкт…
Optical Flow Оптичний потік
Overwrite Перезаписати
Paste Вставити
Paste Attributes… Вставити атрибути…
Paste Insert Вставити зі зсувом
Paused Resolution Роздільність під час паузи
Playback Resolution Роздільність відтворення
Playback… Відтворення…
Plugins… Плагіни…
Polygon Багатокутник
Preferences Налаштування
Previous in Sequence Попередній у секвенції
Previous in Track Попередній на доріжці
Program Програма
Progress Перебіг
Project Проєкт
Project Manager… Менеджер проєкту…
Project Settings Налаштування проєкту
Project… Проєкт…
Properties Властивості
Purple Пурпуровий
Reconnect Full Resolution Media… Повторно під’єднати медіа повної роздільності…
Recover Unsaved Changes… Відновити незбережені зміни…
Rectangle Прямокутник
Red Червоний
Redo Повторити
Reference Monitor Еталонний монітор
Remix Ремікс
Remix Properties… Властивості реміксу…
Remove Attributes… Видалити атрибути…
Remove Filler Words Видалити слова-паразити
Remove Pauses Видалити паузи
Remove Unused Видалити невикористане
Rename… Перейменувати…
Render Audio Рендерити аудіо
Render Effects In to Out Рендерити ефекти між початковою та кінцевою позначками
Render In to Out Рендерити між початковою та кінцевою позначками
Render Selection Рендерити виділення
Replace Fonts in Projects… Замінити шрифти в проєктах…
Replace With Clip Замінити кліпом
Report an Issue… Повідомити про проблему…
Reset All Parameters Скинути всі параметри
Reset Duration Скинути тривалість
Reset to Saved Layout Відновити збережене компонування
Restore Captions from Source Clip Відновити субтитри з вихідного кліпу
Reveal Log Files… Показати файли журналу…
Reverse Match Frame Знайти відповідний кадр у секвенції
Revert Повернути до збереженого
Revert Remix Скасувати ремікс
Review Перегляд
Right Праворуч
Ripple Delete Видалити зі зсувом
Ripple Sequence Markers Зсувати маркери секвенції
Rose Рожевий
Safe Margins Безпечні поля
Save Зберегти
Save All Зберегти все
Save As… Зберегти як…
Save Guides as Template… Зберегти напрямні як шаблон…
Save a Copy… Зберегти копію…
Save as Template… Зберегти як шаблон…
Scale to Frame Size Масштабувати до розміру кадру
Scene Edit Detection… Виявлення монтажних склейок…
Scratch Disks… Робочі диски…
Search Bin Пошуковий контейнер
Select Вибрати
Select All Виділити все
Select All Matching Виділити всі відповідні
Select Label Group Виділити групу міток
Select Next Graphic Вибрати наступний графічний елемент
Select Next Layer Вибрати наступний шар
Select Previous Graphic Вибрати попередній графічний елемент
Select Previous Layer Вибрати попередній шар
Selection Follows Playhead Виділення слідує за курсором відтворення
Selection as FilmCraft Project… Виділення як проєкт FilmCraft…
Selection… Виділення…
Send Backward Перемістити назад
Send to Back На задній план
Sequence Секвенція
Sequence From Clip Секвенція з кліпу
Sequence Settings… Налаштування секвенції…
Sequence… Секвенція…
Settings Налаштування
Show Active Caption Tracks Only Показати лише активні доріжки субтитрів
Show All Caption Tracks Показати всі доріжки субтитрів
Show All Marker Colors Показати всі кольори маркерів
Show Guides Показати напрямні
Show Rulers Показати лінійки
Show Through Edits Показати розрізи без зміни кадру
Simplify Sequence… Спростити секвенцію…
Snap in Program Monitor Прив’язка в моніторі програми
Snap in Timeline Прив’язка на часовій шкалі
Source Джерело
Source Settings… Налаштування джерела…
Speed/Duration… Швидкість/тривалість…
Synchronize… Синхронізувати…
System Compatibility Report… Звіт про сумісність системи…
Tan Світло-коричневий
Teal Бірюзовий
Text Текст
Time Interpolation Часова інтерполяція
Timecode Таймкод
Timecode… Таймкод…
Timeline Часова шкала
Timeline… Часова шкала…
Toggle Proxies Перемкнути проксі
Tools Інструменти
Top Верх
Transcribe Sequence… Транскрибувати секвенцію…
Transcribe… Транскрибувати…
Transcript Транскрипція
Transparent Video… Прозоре відео…
Trim Edit Підрізати склейку
Trim… Підрізання…
Undo Скасувати
Ungroup Розгрупувати
Universal Counting Leader… Універсальний зворотний відлік…
Update Metadata… Оновити метадані…
Upgrade Caption to Graphic Перетворити субтитр на графіку
Upgrade to Source Graphic Перетворити на вихідну графіку
Vertical Text Вертикальний текст
Video In Початок відео
Video Options Параметри відео
Video Out Кінець відео
Video and Audio Waveform Split Розділений вигляд відео й осцилограми аудіо
View Вигляд
Violet Фіолетовий
Window Вікно
Workspaces Робочі середовища
Yellow Жовтий
Zoom In Збільшити
Zoom Out Зменшити
Zoom to Sequence Умістити секвенцію
Can't render this file because it has a wrong number of fields in line 7.

View File

@ -0,0 +1,8 @@
asset: uk.tsv
title: Ukrainian 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-09 by dmatviichuk
notes: Original English-to-Ukrainian menu translations migrated to the TSV catalog format.

View File

@ -26,6 +26,7 @@ pub const UI_COMMANDS: &[UiCommand] = &[
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!("source.playback.toggle", "Source Play/Stop", [], None),
uic!("source.playback.play", "Play Source", [], None),
uic!("source.playback.stop", "Stop Source", [], None),
@ -169,7 +170,7 @@ pub fn invoke(app: &mut FilmcraftApp, ctx: &egui::Context, id: &str, mut params:
let object = params.as_object_mut().ok_or("command parameters must be an object")?;
object.insert("target".into(), json!("source"));
}
if matches!(id, "app.language.english" | "app.language.japanese" | "app.language.spanish" | "app.language.portuguese") {
if matches!(id, "app.language.english" | "app.language.japanese" | "app.language.spanish" | "app.language.portuguese" | "app.language.ukrainian") {
// 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) {
return Err("no Japanese font is installed on this system (for example Noto Sans CJK JP); the interface stays in English".into());
@ -178,6 +179,7 @@ pub fn invoke(app: &mut FilmcraftApp, ctx: &egui::Context, id: &str, mut params:
"app.language.japanese" => crate::i18n::Language::Ja,
"app.language.spanish" => crate::i18n::Language::Es,
"app.language.portuguese" => crate::i18n::Language::PtBr,
"app.language.ukrainian" => crate::i18n::Language::Uk,
_ => crate::i18n::Language::En,
};
// The preference is updated in memory before it is written, so a failed write (read-only
@ -548,6 +550,7 @@ pub fn menu_items(app: &FilmcraftApp) -> Vec<MenuItem> {
"app.language.japanese" => it.checked = Some(app.ui.language == crate::i18n::Language::Ja),
"app.language.spanish" => it.checked = Some(app.ui.language == crate::i18n::Language::Es),
"app.language.portuguese" => it.checked = Some(app.ui.language == crate::i18n::Language::PtBr),
"app.language.ukrainian" => it.checked = Some(app.ui.language == crate::i18n::Language::Uk),
_ => {}
}
if it.id.starts_with("view.") {