Merge pull request #2637 from ludovic111/feat/launcher-language-home

🤖 Agent: launcher-features-1: base main, merge-tree clean against main, CI verify + launcher green; queued.
This commit is contained in:
Ludovic Marie authored and GitHub committed 2026-10-27 08:01:36 +01:00
commit 6af60956ec
8 files changed
+180 -47

No files matched your search

+63 -1
View File
@@ -91,5 +91,67 @@
"Landscape": "Paysage",
"Your picture, behind Home, cropped to fill it.": "Votre image, derrière l'accueil, recadrée pour le remplir.",
"Horizon's landscape. Pick a PNG, JPEG or WebP picture of your own.": "Le paysage d'Horizon. Choisissez votre propre image PNG, JPEG ou WebP.",
"Home background": "Fond de l'accueil"
"Home background": "Fond de l'accueil",
"Play": "Jouer",
"Running": "En cours",
"Stopped": "Arrêté",
"Start the game, or run a server for others to join.": "Lancez le jeu, ou ouvrez un serveur que d'autres peuvent rejoindre.",
"No version installed yet.": "Aucune version installée pour l'instant.",
"Choose a version": "Choisir une version",
"Host a server": "Héberger un serveur",
"Preparing…": "Préparation…",
"Prepare": "Préparer",
"Prepare again": "Préparer à nouveau",
"Not found. Choose the folder where your launcher keeps Minecraft.": "Introuvable. Choisissez le dossier où votre launcher garde Minecraft.",
"Ready to play": "Prêt à jouer",
"Resources": "Ressources",
"Minecraft folder": "Dossier Minecraft",
"Choose…": "Choisir…",
"Playing offline": "Jeu hors ligne",
"Online servers need a Microsoft account.": "Les serveurs en ligne demandent un compte Microsoft.",
"Accounts…": "Comptes…",
"The first preparation takes a few minutes.": "La première préparation prend quelques minutes.",
"Horizon crashed": "Horizon a planté",
"When you report it on Discord, attach the crash report file (or paste it with Copy report) and say what you were doing.": "Pour le signaler sur Discord, joignez le rapport de plantage (ou collez-le avec Copier le rapport) et dites ce que vous faisiez.",
"The game ended without a crash report. When you report it on Discord, attach latest.log from the logs folder and say what you were doing.": "Le jeu s'est arrêté sans rapport de plantage. Pour le signaler sur Discord, joignez latest.log du dossier des journaux et dites ce que vous faisiez.",
"Open report": "Ouvrir le rapport",
"Copied": "Copié",
"Copy report": "Copier le rapport",
"Open logs folder": "Ouvrir le dossier des journaux",
"Report on Discord": "Signaler sur Discord",
"Dismiss": "Fermer",
"INSTANCES": "INSTANCES",
"SERVERS": "SERVEURS",
"HIDE": "MASQUER",
"Website": "Site web",
"Ready": "Prêt",
"Needs preparing": "À préparer",
"{version} is not installed": "{version} n'est pas installée",
"No version installed": "Aucune version installée",
"Modpack": "Modpack",
"From Horizon's registry": "Du registre d'Horizon",
"Instance": "Instance",
"Edit…": "Modifier…",
"Copy…": "Copier…",
"Export…": "Exporter…",
"Close export": "Fermer l'export",
"Launch…": "Lancement…",
"Close launch": "Fermer le lancement",
"Open folder": "Ouvrir le dossier",
"Join a friend": "Rejoindre un ami",
"Paste an invite code": "Collez un code d'invitation",
"Paste the code a friend shows when they invite you.": "Collez le code qu'un ami affiche quand il vous invite.",
"Join": "Rejoindre",
"Shortcut": "Raccourci",
"Remove shortcut": "Retirer le raccourci",
"today": "aujourd'hui",
"yesterday": "hier",
"{days} days ago": "il y a {days} jours",
"{time} in total · last played {when}": "{time} au total · joué pour la dernière fois {when}",
"Not played yet": "Pas encore joué",
"under a minute": "moins d'une minute",
"PARTNER": "PARTENAIRE",
"Not prepared. Horizon needs the files of your own Minecraft {version}.": "Pas préparé. Horizon a besoin des fichiers de votre propre Minecraft {version}.",
"Prepared for Minecraft {version}": "Préparé pour Minecraft {version}",
"Prepared for Minecraft {version}, without its sounds: the game will be silent. Prepare again to copy them from your Minecraft install.": "Préparé pour Minecraft {version}, sans ses sons : le jeu sera muet. Préparez à nouveau pour les copier depuis votre installation de Minecraft."
}
+63 -1
View File
@@ -91,5 +91,67 @@
"Landscape": "Пейзаж",
"Your picture, behind Home, cropped to fill it.": "Ваша картинка за главной страницей, обрезанная по размеру.",
"Horizon's landscape. Pick a PNG, JPEG or WebP picture of your own.": "Пейзаж Horizon. Выберите свою картинку PNG, JPEG или WebP.",
"Home background": "Фон главной"
"Home background": "Фон главной",
"Play": "Играть",
"Running": "Запущено",
"Stopped": "Остановлен",
"Start the game, or run a server for others to join.": "Запустите игру или сервер, к которому смогут подключиться другие.",
"No version installed yet.": "Пока не установлено ни одной версии.",
"Choose a version": "Выбрать версию",
"Host a server": "Открыть сервер",
"Preparing…": "Подготовка…",
"Prepare": "Подготовить",
"Prepare again": "Подготовить заново",
"Not found. Choose the folder where your launcher keeps Minecraft.": "Не найдено. Выберите папку, где ваш лаунчер хранит Minecraft.",
"Ready to play": "Готово к игре",
"Resources": "Ресурсы",
"Minecraft folder": "Папка Minecraft",
"Choose…": "Выбрать…",
"Playing offline": "Игра офлайн",
"Online servers need a Microsoft account.": "Для онлайн-серверов нужен аккаунт Microsoft.",
"Accounts…": "Аккаунты…",
"The first preparation takes a few minutes.": "Первая подготовка занимает несколько минут.",
"Horizon crashed": "Horizon вылетел",
"When you report it on Discord, attach the crash report file (or paste it with Copy report) and say what you were doing.": "Сообщая об этом в Discord, приложите файл отчёта о сбое (или вставьте его через «Скопировать отчёт») и расскажите, что вы делали.",
"The game ended without a crash report. When you report it on Discord, attach latest.log from the logs folder and say what you were doing.": "Игра закрылась без отчёта о сбое. Сообщая об этом в Discord, приложите latest.log из папки журналов и расскажите, что вы делали.",
"Open report": "Открыть отчёт",
"Copied": "Скопировано",
"Copy report": "Скопировать отчёт",
"Open logs folder": "Открыть папку журналов",
"Report on Discord": "Сообщить в Discord",
"Dismiss": "Закрыть",
"INSTANCES": "СБОРКИ",
"SERVERS": "СЕРВЕРЫ",
"HIDE": "СКРЫТЬ",
"Website": "Сайт",
"Ready": "Готово",
"Needs preparing": "Нужна подготовка",
"{version} is not installed": "{version} не установлена",
"No version installed": "Версия не установлена",
"Modpack": "Сборка модов",
"From Horizon's registry": "Из каталога Horizon",
"Instance": "Сборка",
"Edit…": "Изменить…",
"Copy…": "Копировать…",
"Export…": "Экспорт…",
"Close export": "Закрыть экспорт",
"Launch…": "Запуск…",
"Close launch": "Закрыть запуск",
"Open folder": "Открыть папку",
"Join a friend": "Присоединиться к другу",
"Paste an invite code": "Вставьте код приглашения",
"Paste the code a friend shows when they invite you.": "Вставьте код, который друг показывает, когда приглашает вас.",
"Join": "Войти",
"Shortcut": "Ярлык",
"Remove shortcut": "Убрать ярлык",
"today": "сегодня",
"yesterday": "вчера",
"{days} days ago": "{days} дн. назад",
"{time} in total · last played {when}": "всего {time} · последняя игра: {when}",
"Not played yet": "Ещё не запускалась",
"under a minute": "меньше минуты",
"PARTNER": "ПАРТНЁР",
"Not prepared. Horizon needs the files of your own Minecraft {version}.": "Не подготовлено. Horizon нужны файлы вашего собственного Minecraft {version}.",
"Prepared for Minecraft {version}": "Подготовлено для Minecraft {version}",
"Prepared for Minecraft {version}, without its sounds: the game will be silent. Prepare again to copy them from your Minecraft install.": "Подготовлено для Minecraft {version}, но без звуков: игра будет беззвучной. Подготовьте заново, чтобы скопировать их из вашей установки Minecraft."
}
+9 -6
View File
@@ -651,14 +651,17 @@ fn sounds_in(resources: &Path) -> bool {
/// What the Play page says of a version's resources.
pub fn resources_summary(prepared: bool, sounds: bool, version: &str) -> String {
use crate::i18n::trf;
match (prepared, sounds) {
(false, _) => {
format!("Not prepared. Horizon needs the files of your own Minecraft {version}.")
}
(true, true) => format!("Prepared for Minecraft {version}"),
(true, false) => format!(
(false, _) => trf(
"Not prepared. Horizon needs the files of your own Minecraft {version}.",
&[("version", version)],
),
(true, true) => trf("Prepared for Minecraft {version}", &[("version", version)]),
(true, false) => trf(
"Prepared for Minecraft {version}, without its sounds: the game will be silent. \
Prepare again to copy them from your Minecraft install."
Prepare again to copy them from your Minecraft install.",
&[("version", version)],
),
}
}
+1 -1
View File
@@ -448,7 +448,7 @@ fn record_play_in(
pub fn play_time(played: Duration) -> String {
let minutes = played.as_secs() / 60;
match (minutes / 60, minutes % 60) {
(0, 0) => "under a minute".into(),
(0, 0) => crate::i18n::tr("under a minute").into(),
(0, m) => format!("{m} min"),
(h, m) if h < 100 => format!("{h} h {m:02} min"),
(h, _) => format!("{h} h"),
+15 -13
View File
@@ -190,11 +190,11 @@ impl Launcher {
InstanceKind::Default | InstanceKind::Own => (
self.page == Page::Play && selected.as_ref() == Some(&i.id),
match (&tag, &i.version) {
(None, Some(own)) => format!("{own} is not installed"),
(None, None) => "No version installed".into(),
(Some(_), _) if !prepared => "Needs preparing".into(),
(Some(tag), Some(_)) => format!("Ready · {tag}"),
(Some(_), None) => "Ready".into(),
(None, Some(own)) => trf("{version} is not installed", &[("version", own)]),
(None, None) => tr("No version installed").into(),
(Some(_), _) if !prepared => tr("Needs preparing").into(),
(Some(tag), Some(_)) => format!("{} · {tag}", tr("Ready")),
(Some(_), None) => tr("Ready").into(),
},
),
InstanceKind::Port => {
@@ -202,10 +202,10 @@ impl Launcher {
let summary = self.packs.iter().find(|p| p.slug == slug);
(
on_packs && self.pack_open.as_deref() == Some(slug),
summary.map_or_else(|| "Modpack".into(), |p| p.summary()),
summary.map_or_else(|| tr("Modpack").into(), |p| p.summary()),
)
}
InstanceKind::Pack => (false, "From Horizon's registry".into()),
InstanceKind::Pack => (false, tr("From Horizon's registry").into()),
};
let dot = matches!(i.kind, InstanceKind::Default | InstanceKind::Own).then(|| {
div()
@@ -621,7 +621,7 @@ impl Launcher {
);
Some(index_row(
n,
"Instance",
tr("Instance"),
Some(
div()
.flex()
@@ -651,12 +651,14 @@ impl Launcher {
/// "3 h 05 min in total · last played today", or "Not played yet".
fn played_line(instance: &Instance) -> String {
match instance.last_played {
Some(when) => format!(
"{} in total · last played {}",
instances::play_time(instance.played),
worlds_page::ago(when)
Some(when) => trf(
"{time} in total · last played {when}",
&[
("time", &instances::play_time(instance.played)),
("when", &worlds_page::ago(when)),
],
),
None => "Not played yet".into(),
None => tr("Not played yet").into(),
}
}
+25 -21
View File
@@ -1755,7 +1755,7 @@ impl Launcher {
.flex()
.flex_col()
.gap(px(4.))
.child(heading("INSTANCES".into()).child(add))
.child(heading(tr("INSTANCES").into()).child(add))
.children(self.instance_items(heading, cx));
let servers: Vec<_> = self
.servers
@@ -1764,7 +1764,7 @@ impl Launcher {
.unwrap_or_else(|| self.side_servers.clone());
let mut list = list;
if !servers.is_empty() {
list = list.child(heading("SERVERS".into()));
list = list.child(heading(tr("SERVERS").into()));
}
for e in servers {
let up = self.servers.is_up(&e.dir);
@@ -1790,7 +1790,7 @@ impl Launcher {
.flex_col()
.min_w_0()
.child(title(e.name.clone()))
.child(sub(if up { "Running" } else { "Stopped" }.into())),
.child(sub(tr(if up { "Running" } else { "Stopped" }).into())),
)
.on_click(cx.listener(move |this, _: &ClickEvent, _, cx| {
this.servers.open = Some(dir.clone());
@@ -1874,7 +1874,7 @@ impl Launcher {
text(theme::MICRO)
.text_color(t.muted)
.hover(move |s| s.text_color(t.ink))
.child("HIDE"),
.child(tr("HIDE")),
)
.on_click(cx.listener(|this, _: &ClickEvent, _, cx| {
this.settings.partner_hidden = true;
@@ -1896,7 +1896,7 @@ impl Launcher {
if !self.links.website.is_empty() {
foot = foot.child(link(
"website",
"Website",
tr("Website"),
"globe",
self.links.website.clone(),
));
@@ -2061,7 +2061,7 @@ impl Launcher {
"Start the game, or run a server for others to join.",
&t,
))
.child(centered("No version installed yet.", &t).child(
.child(centered(tr("No version installed yet."), &t).child(
button("go-versions", "Choose a version", Kind::Secondary, &t).on_click(
cx.listener(|this, _: &ClickEvent, _, cx| {
this.go(Page::Versions);
@@ -2077,10 +2077,12 @@ impl Launcher {
let hero = self.hero(&tag, &version, prepared, cx);
let running = self.client.is_some() || self.starting_client.is_some();
let play_label = if running { "Running" } else { "Play" };
// The label clears the icon in every language ("Играть" is wider than "Play").
let mut play = button("play", play_label, Kind::Primary, &t)
.min_w(px(200.))
.h(px(60.))
.px(px(28.))
.pl(px(60.))
.pr(px(28.))
.text_size(px(28.))
.font_weight(FontWeight::SEMIBOLD)
.child(
@@ -2121,7 +2123,7 @@ impl Launcher {
);
let resources_value = if self.preparing {
"Preparing…".to_string()
tr("Preparing…").to_string()
} else {
backend::resources_summary(prepared, !silent, &version)
};
@@ -2146,7 +2148,9 @@ impl Launcher {
let minecraft = self.settings.minecraft_dir.as_deref().map_or_else(
|| {
div()
.child("Not found. Choose the folder where your launcher keeps Minecraft.")
.child(tr(
"Not found. Choose the folder where your launcher keeps Minecraft.",
))
.into_any_element()
},
|p| {
@@ -2165,14 +2169,14 @@ impl Launcher {
.children(self.launch_panel(cx))
.child(index_row(
2,
"Resources",
tr("Resources"),
Some(div().child(resources_value).into_any_element()),
Some(prepare.into_any_element()),
&t,
))
.child(index_row(
3,
"Minecraft folder",
tr("Minecraft folder"),
Some(minecraft),
Some(
button("choose-mc", "Choose…", Kind::Ghost, &t)
@@ -2188,10 +2192,10 @@ impl Launcher {
if self.accounts.selected().is_none() && account::client_id().is_some() {
rows = rows.child(index_row(
5,
"Playing offline",
tr("Playing offline"),
Some(
div()
.child("Online servers need a Microsoft account.")
.child(tr("Online servers need a Microsoft account."))
.into_any_element(),
),
Some(
@@ -2276,7 +2280,7 @@ impl Launcher {
.child(
text(theme::CAPTION)
.text_color(t.muted)
.child("The first preparation takes a few minutes."),
.child(tr("The first preparation takes a few minutes.")),
),
);
}
@@ -2370,11 +2374,11 @@ impl Launcher {
},
)),
);
let help = if crash.report.is_some() {
let help = tr(if crash.report.is_some() {
"When you report it on Discord, attach the crash report file (or paste it with Copy report) and say what you were doing."
} else {
"The game ended without a crash report. When you report it on Discord, attach latest.log from the logs folder and say what you were doing."
};
});
let mut body = div()
.flex_1()
.min_w_0()
@@ -2384,7 +2388,7 @@ impl Launcher {
.child(
text(theme::HEADING)
.text_color(t.ink)
.child("Horizon crashed"),
.child(tr("Horizon crashed")),
);
if !crash.summary.is_empty() {
body = body.child(div().text_color(t.ink_soft).child(crash.summary));
@@ -2460,7 +2464,7 @@ impl Launcher {
.child(caps(format!("Minecraft {version}")).text_color(t.ink_soft))
.child(div().size(px(4.)).bg(t.muted))
.child(
caps(if prepared { "Ready" } else { "Needs preparing" })
caps(tr(if prepared { "Ready" } else { "Needs preparing" }))
.text_color(if prepared { t.success } else { t.warn }),
),
)
@@ -3153,7 +3157,7 @@ impl Launcher {
.text_size(px(12.))
.cursor_text()
.child(if self.friend_code.is_empty() && !focused {
div().text_color(t.muted).child("Paste an invite code")
div().text_color(t.muted).child(tr("Paste an invite code"))
} else {
// The end of a long code stays in view while typing.
let shown: String = {
@@ -3203,10 +3207,10 @@ impl Launcher {
}
index_row(
4,
"Join a friend",
tr("Join a friend"),
Some(
div()
.child("Paste the code a friend shows when they invite you.")
.child(tr("Paste the code a friend shows when they invite you."))
.into_any_element(),
),
Some(
+1 -1
View File
@@ -38,7 +38,7 @@ pub fn banner(id: &'static str, width: f32, t: &Theme) -> Stateful<Div> {
crate::text(crate::theme::MICRO)
.text_color(t.muted)
.group_hover(id, move |s| s.text_color(t.ink))
.child("PARTNER"),
.child(horizon_launcher::i18n::tr("PARTNER")),
)
.child(
div().relative().group_hover(id, |s| s.top(px(-2.))).child(
+3 -3
View File
@@ -32,9 +32,9 @@ pub(super) fn ago(when: std::time::SystemTime) -> String {
let when: chrono::DateTime<chrono::Local> = when.into();
let days = (chrono::Local::now().date_naive() - when.date_naive()).num_days();
match days {
..=0 => "today".into(),
1 => "yesterday".into(),
2..=13 => format!("{days} days ago"),
..=0 => tr("today").into(),
1 => tr("yesterday").into(),
2..=13 => trf("{days} days ago", &[("days", &days.to_string())]),
_ => when.format("%Y-%m-%d").to_string(),
}
}