1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-04-20 17:19:01 +00:00
This commit is contained in:
Ioan Bizău 2025-04-19 08:29:12 +03:00 committed by GitHub
commit 93a5e2bd32
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 2360 additions and 2261 deletions

View File

@ -1157,7 +1157,7 @@ pub enum TranslatedString {
wipe_code__diff_from_pin = 776, // "The wipe code must be different from your PIN."
wipe_code__disabled = 777, // "Wipe code disabled."
wipe_code__enabled = 778, // "Wipe code enabled."
wipe_code__enter_new = 779, // "Enter new wipe code"
wipe_code__enter_new = 779, // "New wipe code"
wipe_code__info = 780, // "Wipe code can be used to erase all data from this device."
wipe_code__invalid = 781, // "Invalid wipe code"
wipe_code__mismatch = 782, // "The wipe codes you entered do not match."
@ -2560,7 +2560,7 @@ impl TranslatedString {
Self::wipe_code__diff_from_pin => "The wipe code must be different from your PIN.",
Self::wipe_code__disabled => "Wipe code disabled.",
Self::wipe_code__enabled => "Wipe code enabled.",
Self::wipe_code__enter_new => "Enter new wipe code",
Self::wipe_code__enter_new => "New wipe code",
Self::wipe_code__info => "Wipe code can be used to erase all data from this device.",
Self::wipe_code__invalid => "Invalid wipe code",
Self::wipe_code__mismatch => "The wipe codes you entered do not match.",

View File

@ -319,9 +319,13 @@ extern "C" fn new_confirm_properties(n_args: usize, args: *const Obj, kwargs: *m
let block = move |_args: &[Obj], kwargs: &Map| {
let title: TString = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let items: Obj = kwargs.get(Qstr::MP_QSTR_items)?;
let subtitle: Option<TString> = kwargs
.get(Qstr::MP_QSTR_subtitle)
.and_then(Obj::try_into_option)
.unwrap_or(None);
let hold: bool = kwargs.get_or(Qstr::MP_QSTR_hold, false)?;
let layout = ModelUI::confirm_properties(title, items, hold)?;
let layout = ModelUI::confirm_properties(title, subtitle, items, hold)?;
Ok(LayoutObj::new_root(layout)?.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
@ -1303,6 +1307,7 @@ pub static mp_module_trezorui_api: Module = obj_module! {
/// *,
/// title: str,
/// items: list[tuple[str | None, str | bytes | None, bool]],
/// subtitle: str | None = None,
/// hold: bool = False,
/// ) -> LayoutObj[UiResult]:
/// """Confirm list of key-value pairs. The third component in the tuple should be True if

View File

@ -111,7 +111,7 @@ impl<'a> Label<'a> {
pub fn render_with_alpha<'s>(&self, target: &mut impl Renderer<'s>, alpha: u8) {
self.text
.map(|c| self.layout.render_text_with_alpha(c, target, alpha));
.map(|c| self.layout.render_text_with_alpha(c, target, alpha, true));
}
}
@ -137,7 +137,7 @@ impl Component for Label<'_> {
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.text.map(|c| self.layout.render_text(c, target));
self.text.map(|c| self.layout.render_text(c, target, true));
}
}

View File

@ -232,21 +232,35 @@ impl TextLayout {
}
/// Draw as much text as possible on the current screen.
pub fn render_text<'s>(&self, text: &str, target: &mut impl Renderer<'s>) -> LayoutFit {
self.render_text_with_alpha(text, target, 255)
pub fn render_text<'s>(
&self,
text: &str,
target: &mut impl Renderer<'s>,
must_fit: bool,
) -> LayoutFit {
self.render_text_with_alpha(text, target, 255, must_fit)
}
/// Draw as much text as possible on the current screen.
pub fn render_text_with_alpha<'s>(
&self,
text: &str,
target: &mut impl Renderer<'s>,
alpha: u8,
must_fit: bool,
) -> LayoutFit {
self.layout_text(
let fit = self.layout_text(
text,
&mut self.initial_cursor(),
&mut TextRenderer::new(target).with_alpha(alpha),
)
);
#[cfg(feature = "ui_debug")]
if must_fit && matches!(fit, LayoutFit::OutOfBounds { .. }) {
target.raise_overflow_exception();
}
fit
}
/// Loop through the `text` and try to fit it on the current screen,

View File

@ -236,7 +236,7 @@ where
&self.visible,
self.offset,
&mut |layout, content| {
layout.render_text(content, target);
layout.render_text(content, target, false);
},
)
}

View File

@ -32,7 +32,7 @@ pub fn text_multiline<'s>(
let text_layout = TextLayout::new(text_style)
.with_bounds(area)
.with_align(alignment);
let layout_fit = text.map(|t| text_layout.render_text(t, target));
let layout_fit = text.map(|t| text_layout.render_text(t, target, false));
match layout_fit {
LayoutFit::Fitting { height, .. } => Some(area.split_top(height).1),
LayoutFit::OutOfBounds { .. } => None,
@ -60,11 +60,11 @@ pub fn text_multiline_bottom<'s>(
LayoutFit::Fitting { height, .. } => {
let (top, bottom) = area.split_bottom(height);
text_layout = text_layout.with_bounds(bottom);
text_layout.render_text(t, target);
text_layout.render_text(t, target, false);
Some(top)
}
LayoutFit::OutOfBounds { .. } => {
text_layout.render_text(t, target);
text_layout.render_text(t, target, false);
None
}
})

View File

@ -313,10 +313,21 @@ impl Layout<Result<Obj, Error>> for SwipeFlow {
self.returned_value.as_ref()
}
fn paint(&mut self) {
fn paint(&mut self) -> Result<(), Error> {
let mut overflow: bool = false;
render_on_display(None, Some(Color::black()), |target| {
self.render_state(self.state.index(), target);
#[cfg(feature = "ui_debug")]
if target.should_raise_overflow_exception() {
overflow = true;
}
});
if overflow {
Err(Error::OutOfRange)
} else {
Ok(())
}
}
}

View File

@ -3,6 +3,8 @@ use crate::ui::{
component::{base::AttachType, Event, EventCtx},
};
use crate::error::Error;
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum LayoutState {
Initial,
@ -15,7 +17,7 @@ pub trait Layout<T> {
fn place(&mut self);
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<LayoutState>;
fn value(&self) -> Option<&T>;
fn paint(&mut self);
fn paint(&mut self) -> Result<(), Error>;
}
#[cfg(feature = "micropython")]

View File

@ -44,7 +44,7 @@ use crate::{
},
display::{self, Color},
event::USBEvent,
shape::render_on_display,
shape::{render_on_display, Renderer},
CommonUI, ModelUI,
},
};
@ -146,10 +146,21 @@ where
self.returned_value.as_ref()
}
fn paint(&mut self) {
fn paint(&mut self) -> Result<(), Error> {
let mut overflow: bool = false;
render_on_display(None, Some(Color::black()), |target| {
self.inner.render(target);
#[cfg(feature = "ui_debug")]
if target.should_raise_overflow_exception() {
overflow = true;
}
});
if overflow {
Err(Error::OutOfRange)
} else {
Ok(())
}
}
}
@ -305,15 +316,14 @@ impl LayoutObjInner {
/// Run a paint pass over the component tree. Returns true if any component
/// actually requested painting since last invocation of the function.
fn obj_paint_if_requested(&mut self) -> bool {
fn obj_paint_if_requested(&mut self) -> Result<bool, Error> {
display::sync();
if self.repaint != Repaint::None {
self.repaint = Repaint::None;
self.root_mut().paint();
true
self.root_mut().paint().map(|_| true)
} else {
false
Ok(false)
}
}
@ -597,8 +607,8 @@ extern "C" fn ui_layout_timer(this: Obj, token: Obj) -> Obj {
extern "C" fn ui_layout_paint(this: Obj) -> Obj {
let block = || {
let this: Gc<LayoutObj> = this.try_into()?;
let painted = this.inner_mut().obj_paint_if_requested().into();
Ok(painted)
let painted = this.inner_mut().obj_paint_if_requested();
Ok(painted?.into())
};
unsafe { util::try_or_raise(block) }
}

View File

@ -1,7 +1,11 @@
use crate::{
strutil::TString,
ui::{
component::{image::Image, Child, Component, Event, EventCtx, Label},
component::{
image::Image,
text::paragraphs::{Paragraph, ParagraphSource, Paragraphs},
Child, Component, Event, EventCtx, Label,
},
display,
geometry::{Insets, Rect},
shape,
@ -32,7 +36,7 @@ pub enum FidoMsg {
pub struct FidoConfirm<F: Fn(usize) -> TString<'static>, U> {
page_swipe: Swipe,
app_name: Label<'static>,
account_name: Label<'static>,
account_name: Paragraphs<Paragraph<'static>>,
icon: Child<Image>,
/// Function/closure that will return appropriate page on demand.
get_account: F,
@ -65,22 +69,10 @@ where
page_swipe.allow_right = scrollbar.has_previous_page();
page_swipe.allow_left = scrollbar.has_next_page();
// NOTE: This is an ugly hotfix for the erroneous behavior of
// TextLayout used in the account_name Label. In this
// particular case, TextLayout calculates the wrong height of
// fitted text that's higher than the TextLayout bound itself.
//
// The following two lines should be swapped when the problem with
// TextLayout is fixed.
//
// See also, continuation of this hotfix in the place() function.
// let current_account = get_account(scrollbar.active_page);
let current_account = "".into();
Self {
app_name: Label::centered(app_name, theme::TEXT_DEMIBOLD),
account_name: Label::centered(current_account, theme::TEXT_DEMIBOLD),
account_name: Paragraph::new(&theme::TEXT_MONO, get_account(scrollbar.active_page))
.into_paragraphs(),
page_swipe,
icon: Child::new(Image::new(icon_data)),
get_account,
@ -107,7 +99,7 @@ where
self.page_swipe.allow_left = self.scrollbar.has_next_page();
let current_account = (self.get_account)(self.active_page());
self.account_name.set_text(current_account);
self.account_name = Paragraph::new(&theme::TEXT_MONO, current_account).into_paragraphs();
// Redraw the page.
ctx.request_paint();
@ -153,19 +145,11 @@ where
self.icon.place(image_area);
// Place the text labels.
let (app_name_area, account_name_area) = remaining_area
.inset(Insets::top(APP_NAME_PADDING))
.split_top(APP_NAME_HEIGHT);
let (app_name_area, account_name_area) = remaining_area.split_top(APP_NAME_HEIGHT);
self.app_name.place(app_name_area);
self.account_name.place(account_name_area);
// NOTE: This is a hotfix used due to the erroneous behavior of TextLayout.
// This line should be removed when the problem with TextLayout is fixed.
// See also the code for FidoConfirm::new().
self.account_name
.set_text((self.get_account)(self.scrollbar.active_page));
bounds
}
@ -194,15 +178,14 @@ where
}
// Erasing the old text content before writing the new one.
let account_name_area = self.account_name.area();
let real_area = account_name_area
.with_height(account_name_area.height() + self.account_name.font().text_baseline() + 1);
shape::Bar::new(real_area).with_bg(theme::BG).render(target);
shape::Bar::new(self.account_name.area())
.with_bg(theme::BG)
.render(target);
// Account name is optional.
// Showing it only if it differs from app name.
// (Dummy requests usually have some text as both app_name and account_name.)
let account_name = self.account_name.text();
let account_name = self.account_name.content();
let app_name = self.app_name.text();
if !account_name.is_empty() && account_name != app_name {
self.account_name.render(target);

View File

@ -376,6 +376,7 @@ impl FirmwareUI for UIBolt {
fn confirm_properties(
title: TString<'static>,
_subtitle: Option<TString<'static>>,
items: Obj,
hold: bool,
) -> Result<impl LayoutMaybeTrace, Error> {

View File

@ -412,6 +412,7 @@ impl FirmwareUI for UICaesar {
fn confirm_properties(
title: TString<'static>,
_subtitle: Option<TString<'static>>,
items: Obj,
hold: bool,
) -> Result<impl LayoutMaybeTrace, Error> {

View File

@ -402,13 +402,11 @@ fn frame_place(
bounds: Rect,
margin: usize,
) -> Rect {
let (mut header_area, mut content_area) = bounds.split_top(TITLE_HEIGHT);
content_area = content_area
let header_area = header.place(bounds);
let mut content_area = bounds
.inset(Insets::top(header_area.height().max(TITLE_HEIGHT)))
.inset(Insets::top(theme::SPACING))
.inset(Insets::top(margin as i16));
header_area = header_area.inset(Insets::sides(theme::SPACING));
header.place(header_area);
if let Some(footer) = footer {
// FIXME: spacer at the bottom might be applied also for usage without footer

View File

@ -93,7 +93,7 @@ impl Header {
pub const fn new(alignment: Alignment, title: TString<'static>) -> Self {
Self {
area: Rect::zero(),
title: Label::new(title, alignment, theme::label_title_main()).vertically_centered(),
title: Label::new(title, alignment, theme::label_title_main()),
subtitle: None,
button: None,
anim: None,
@ -185,23 +185,32 @@ impl Component for Header {
fn place(&mut self, bounds: Rect) -> Rect {
let header_area = if let Some(b) = &mut self.button {
let (rest, button_area) = bounds.split_right(TITLE_HEIGHT);
let (rest, button_area) = bounds.split_right(TITLE_HEIGHT + theme::SPACING * 2);
let (button_area, _under_button_area) = button_area.split_top(TITLE_HEIGHT);
b.place(button_area);
rest
} else {
bounds
};
}
.inset(Insets::sides(theme::SPACING));
if self.subtitle.is_some() {
let title_area = self.title.place(header_area);
let remaining = header_area.inset(Insets::top(title_area.height()));
let _subtitle_area = self.subtitle.place(remaining);
let subtitle_area = self.subtitle.place(remaining);
self.area = title_area.outset(Insets::top(subtitle_area.height()));
} else {
self.title.place(header_area);
}
let title_area = self.title.place(header_area);
if title_area.height() < header_area.height() / 10 {
self.title
.place(title_area.translate(Offset::y(title_area.height() / 2)));
} else {
self.title
.place(title_area.translate(Offset::y(-theme::SPACING)));
}
};
self.area = bounds;
bounds
self.area
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
@ -238,7 +247,7 @@ impl Component for Header {
self.button.render(target);
target.in_clip(self.area.split_left(offset.x).0, &|target| {
target.in_clip(self.title.area().split_left(offset.x).0, &|target| {
if let Some(icon) = self.icon {
let color = self.color.unwrap_or(theme::GREEN);
shape::ToifImage::new(self.title.area().left_center(), icon.toif)

View File

@ -410,12 +410,13 @@ impl FirmwareUI for UIDelizia {
fn confirm_properties(
title: TString<'static>,
subtitle: Option<TString<'static>>,
items: Obj,
hold: bool,
) -> Result<impl LayoutMaybeTrace, Error> {
let paragraphs = PropsList::new(
items,
&theme::TEXT_NORMAL,
&theme::TEXT_SUB_GREY_LIGHT,
&theme::TEXT_MONO,
&theme::TEXT_MONO,
)?;
@ -423,7 +424,7 @@ impl FirmwareUI for UIDelizia {
let flow = flow::new_confirm_action_simple(
paragraphs.into_paragraphs(),
ConfirmActionExtra::Menu(ConfirmActionMenuStrings::new()),
ConfirmActionStrings::new(title, None, None, hold.then_some(title)),
ConfirmActionStrings::new(title, subtitle, None, hold.then_some(title)),
hold,
None,
0,

View File

@ -30,6 +30,9 @@ where
bg_color: Option<Color>,
/// Drawing cache (decompression context, scratch-pad memory)
cache: &'a DrawingCache<'alloc>,
#[cfg(feature = "ui_debug")]
overflow: bool,
}
impl<'a, 'alloc, T, C> ProgressiveRenderer<'a, 'alloc, T, C>
@ -53,6 +56,8 @@ where
viewport,
bg_color,
cache,
#[cfg(feature = "ui_debug")]
overflow: false,
}
}
@ -140,4 +145,14 @@ where
unwrap!(self.shapes.push(holder), "Shape list full");
}
}
#[cfg(feature = "ui_debug")]
fn raise_overflow_exception(&mut self) {
self.overflow = true;
}
#[cfg(feature = "ui_debug")]
fn should_raise_overflow_exception(&self) -> bool {
self.overflow
}
}

View File

@ -58,6 +58,12 @@ pub trait Renderer<'a> {
inner(self);
self.set_viewport(original);
}
#[cfg(feature = "ui_debug")]
fn raise_overflow_exception(&mut self);
#[cfg(feature = "ui_debug")]
fn should_raise_overflow_exception(&self) -> bool;
}
// ==========================================================================
@ -73,6 +79,9 @@ where
canvas: &'a mut C,
/// Drawing cache (decompression context, scratch-pad memory)
cache: &'a DrawingCache<'alloc>,
#[cfg(feature = "ui_debug")]
overflow: bool,
}
impl<'a, 'alloc, C> DirectRenderer<'a, 'alloc, C>
@ -92,7 +101,12 @@ where
// TODO: consider storing original canvas.viewport
// and restoring it by drop() function
Self { canvas, cache }
Self {
canvas,
cache,
#[cfg(feature = "ui_debug")]
overflow: false,
}
}
}
@ -117,6 +131,16 @@ where
shape.cleanup(self.cache);
}
}
#[cfg(feature = "ui_debug")]
fn raise_overflow_exception(&mut self) {
self.overflow = true;
}
#[cfg(feature = "ui_debug")]
fn should_raise_overflow_exception(&self) -> bool {
self.overflow
}
}
pub struct ScopedRenderer<'alloc, 'env, T>
@ -164,4 +188,14 @@ where
{
self.renderer.render_shape(shape);
}
#[cfg(feature = "ui_debug")]
fn raise_overflow_exception(&mut self) {
self.renderer.raise_overflow_exception();
}
#[cfg(feature = "ui_debug")]
fn should_raise_overflow_exception(&self) -> bool {
self.renderer.should_raise_overflow_exception()
}
}

View File

@ -122,6 +122,7 @@ pub trait FirmwareUI {
fn confirm_properties(
title: TString<'static>,
subtitle: Option<TString<'static>>,
items: Obj, // TODO: replace Obj`
hold: bool,
) -> Result<impl LayoutMaybeTrace, Error>;

View File

@ -257,6 +257,7 @@ def confirm_properties(
*,
title: str,
items: list[tuple[str | None, str | bytes | None, bool]],
subtitle: str | None = None,
hold: bool = False,
) -> LayoutObj[UiResult]:
"""Confirm list of key-value pairs. The third component in the tuple should be True if

View File

@ -924,7 +924,7 @@ class TR:
wipe_code__diff_from_pin: str = "The wipe code must be different from your PIN."
wipe_code__disabled: str = "Wipe code disabled."
wipe_code__enabled: str = "Wipe code enabled."
wipe_code__enter_new: str = "Enter new wipe code"
wipe_code__enter_new: str = "New wipe code"
wipe_code__info: str = "Wipe code can be used to erase all data from this device."
wipe_code__invalid: str = "Invalid wipe code"
wipe_code__mismatch: str = "The wipe codes you entered do not match."

View File

@ -92,13 +92,14 @@ async def confirm_instruction(
await confirm_properties(
"confirm_instruction",
f"{instruction_index}/{instructions_count}: {instruction.ui_name}",
f"{instruction_index}/{instructions_count}",
(
(
ui_property.display_name,
property_template.format(instruction, value),
),
),
instruction.ui_name,
)
elif ui_property.account is not None:
account_template = instruction.get_account_template(ui_property.account)
@ -134,8 +135,9 @@ async def confirm_instruction(
await confirm_properties(
"confirm_instruction",
f"{instruction_index}/{instructions_count}: {instruction.ui_name}",
f"{instruction_index}/{instructions_count}",
account_data,
instruction.ui_name,
)
else:
raise ValueError # Invalid ui property
@ -158,8 +160,9 @@ async def confirm_instruction(
await confirm_properties(
"confirm_instruction",
f"{instruction_index}/{instructions_count}: {instruction.ui_name}",
f"{instruction_index}/{instructions_count}",
signers,
instruction.ui_name,
)

View File

@ -190,7 +190,7 @@ async def confirm_path_payment_strict_receive_op(
await confirm_output(
op.destination_account,
format_amount(op.destination_amount, op.destination_asset),
title=TR.stellar__path_pay,
TR.stellar__path_pay,
)
await confirm_asset_issuer(op.destination_asset)
# confirm what the sender is using to pay
@ -209,7 +209,7 @@ async def confirm_path_payment_strict_send_op(
await confirm_output(
op.destination_account,
format_amount(op.destination_min, op.destination_asset),
title=TR.stellar__path_pay_at_least,
TR.stellar__path_pay_at_least,
)
await confirm_asset_issuer(op.destination_asset)
# confirm what the sender is using to pay

View File

@ -736,12 +736,16 @@ def confirm_properties(
br_name: str,
title: str,
props: Iterable[PropertyType],
subtitle: str | None = None,
hold: bool = False,
br_code: ButtonRequestType = ButtonRequestType.ConfirmOutput,
) -> Awaitable[None]:
# Monospace flag for values that are bytes.
items = [(prop[0], prop[1], isinstance(prop[1], bytes)) for prop in props]
if subtitle:
title += ": " + subtitle
return raise_if_not_confirmed(
trezorui_api.confirm_properties(
title=title,

View File

@ -725,6 +725,7 @@ def confirm_properties(
br_name: str,
title: str,
props: Iterable[PropertyType],
subtitle: str | None = None,
hold: bool = False,
br_code: ButtonRequestType = ButtonRequestType.ConfirmOutput,
) -> Awaitable[None]:
@ -740,6 +741,9 @@ def confirm_properties(
is_data = value and " " not in value
return (key, value, bool(is_data))
if subtitle:
title += ": " + subtitle
return raise_if_not_confirmed(
trezorui_api.confirm_properties(
title=title,

View File

@ -654,6 +654,7 @@ def confirm_properties(
br_name: str,
title: str,
props: Iterable[PropertyType],
subtitle: str | None = None,
hold: bool = False,
br_code: ButtonRequestType = ButtonRequestType.ConfirmOutput,
) -> Awaitable[None]:
@ -663,6 +664,7 @@ def confirm_properties(
return raise_if_not_confirmed(
trezorui_api.confirm_properties(
title=title,
subtitle=subtitle,
items=items,
hold=hold,
),

View File

@ -565,7 +565,7 @@
"pin__mismatch": "Zadané PIN kódy se neshodují!",
"pin__pin_mismatch": "PIN se neshoduje",
"pin__please_check_again": "Zkontrolujte to znovu.",
"pin__reenter_new": "Znovu zadejte nový PIN",
"pin__reenter_new": "Zopakujte PIN",
"pin__reenter_to_confirm": "Znovu zadejte PIN pro potvrzení.",
"pin__should_be_long": "PIN musí mít 4-50 číslic.",
"pin__title_check_pin": "Zkontrolovat PIN",
@ -953,11 +953,11 @@
"wipe_code__diff_from_pin": "Kód pro vymazání se musí lišit od PIN kódu.",
"wipe_code__disabled": "Kód pro vymazání zakázán.",
"wipe_code__enabled": "Kód pro vymazání povolen.",
"wipe_code__enter_new": "Vložte nový kód vymaz.",
"wipe_code__enter_new": "Nový kód vymaz.",
"wipe_code__info": "Kód pro vymazání lze použít k vymazání všech dat ze zařízení.",
"wipe_code__invalid": "Chybný kód vymazání",
"wipe_code__mismatch": "Zadané kódy pro vymazání se neshodují.",
"wipe_code__reenter": "Zopakujte kód vymazání",
"wipe_code__reenter": "Zopakujte kód vymaz.",
"wipe_code__reenter_to_confirm": "Potvrďte dalším zadáním kódu vymazání.",
"wipe_code__title_check": "Zkont. kód vymazání",
"wipe_code__title_invalid": "Nesprávný kód pro vymazání",

View File

@ -565,7 +565,7 @@
"pin__mismatch": "Die eingegebenen PINs stimmen nicht überein.",
"pin__pin_mismatch": "PIN stimmt nicht",
"pin__please_check_again": "Bitte noch einmal prüfen.",
"pin__reenter_new": "Neue PIN neu eingeben",
"pin__reenter_new": "PIN neu eingeben",
"pin__reenter_to_confirm": "Gib die PIN zur Bestätigung erneut ein.",
"pin__should_be_long": "PIN sollte aus 4-50 Ziffern bestehen.",
"pin__title_check_pin": "PIN prüfen",

View File

@ -926,7 +926,7 @@
"wipe_code__diff_from_pin": "The wipe code must be different from your PIN.",
"wipe_code__disabled": "Wipe code disabled.",
"wipe_code__enabled": "Wipe code enabled.",
"wipe_code__enter_new": "Enter new wipe code",
"wipe_code__enter_new": "New wipe code",
"wipe_code__info": "Wipe code can be used to erase all data from this device.",
"wipe_code__invalid": "Invalid wipe code",
"wipe_code__mismatch": "The wipe codes you entered do not match.",

View File

@ -565,7 +565,7 @@
"pin__mismatch": "Los PIN introducidos no coinciden.",
"pin__pin_mismatch": "El PIN no coincide.",
"pin__please_check_again": "Vuelve a comprobarlo.",
"pin__reenter_new": "Reintroducir nuevo PIN.",
"pin__reenter_new": "Reintroducir PIN.",
"pin__reenter_to_confirm": "Vuelve a introducir el PIN para confirmar.",
"pin__should_be_long": "El PIN debe tener 4-50 dígitos.",
"pin__title_check_pin": "Revisar PIN",
@ -953,11 +953,11 @@
"wipe_code__diff_from_pin": "El código de borrar debe ser diferente del PIN.",
"wipe_code__disabled": "El código de borrar se ha desactivado.",
"wipe_code__enabled": "El código de borrar se ha activado.",
"wipe_code__enter_new": "Meter nuevo cód.borrar",
"wipe_code__enter_new": "Nuevo cód. borrar",
"wipe_code__info": "Con el código de borrar eliminarás todos los datos del dispositivo.",
"wipe_code__invalid": "Cód. borrar no válido",
"wipe_code__mismatch": "Los códigos de borrar no coinciden.",
"wipe_code__reenter": "Reingresar cód.borrar.",
"wipe_code__reenter": "Reingresar cód. bor.",
"wipe_code__reenter_to_confirm": "Reingresar cód.borrar para confirmar.",
"wipe_code__title_check": "Revisar cód. borrar",
"wipe_code__title_invalid": "Cód. borrar no válido",

View File

@ -565,7 +565,7 @@
"pin__mismatch": "Les codes PIN saisis ne correspondent pas.",
"pin__pin_mismatch": "Erreur de PIN",
"pin__please_check_again": "Revérifiez.",
"pin__reenter_new": "Réentrez nouv. PIN",
"pin__reenter_new": "Réentrez PIN",
"pin__reenter_to_confirm": "Réentrez le PIN pour conf.",
"pin__should_be_long": "Le PIN doit être composé de 4 à 50 chiffres.",
"pin__title_check_pin": "Vérifier PIN",
@ -953,7 +953,7 @@
"wipe_code__diff_from_pin": "Le code d'eff. doit être différent de votre PIN.",
"wipe_code__disabled": "Code d'eff. désactivé.",
"wipe_code__enabled": "Code d'eff. activé.",
"wipe_code__enter_new": "Entrez nouv. code eff.",
"wipe_code__enter_new": "Nouv. code d'eff.",
"wipe_code__info": "Le code d'eff. peut être utilisé pour eff. les données de ce disp.",
"wipe_code__invalid": "Code d'eff. non valide",
"wipe_code__mismatch": "Les codes d'eff. ne correspondent pas.",

View File

@ -565,7 +565,7 @@
"pin__mismatch": "I PIN immessi non corrispondono.",
"pin__pin_mismatch": "PIN non corrispondente",
"pin__please_check_again": "Ricontrolla.",
"pin__reenter_new": "Reimmetti nuovo PIN",
"pin__reenter_new": "Reimmetti PIN",
"pin__reenter_to_confirm": "Reimmettere PIN per confermare.",
"pin__should_be_long": "Lunghezza PIN 4-50 cifre.",
"pin__title_check_pin": "Controlla PIN",

View File

@ -564,7 +564,7 @@
"pin__mismatch": "Os PINs inseridos não coincidem!",
"pin__pin_mismatch": "O PIN não coincide",
"pin__please_check_again": "Verifique novamente.",
"pin__reenter_new": "Reinsira o novo PIN",
"pin__reenter_new": "Reinsira PIN",
"pin__reenter_to_confirm": "Reinsira o PIN para confirmar.",
"pin__should_be_long": "O PIN deve ter 4-50 dígitos.",
"pin__title_check_pin": "Verificar PIN",
@ -952,7 +952,7 @@
"wipe_code__diff_from_pin": "O código de limpeza deve ser diferente do seu PIN.",
"wipe_code__disabled": "Código de limpeza desativado.",
"wipe_code__enabled": "Código de limpeza ativado.",
"wipe_code__enter_new": "Inserir novo cód. limp.",
"wipe_code__enter_new": "Novo cód. limpeza",
"wipe_code__info": "O cód. limpeza pode apagar todos os dados deste dispositivo.",
"wipe_code__invalid": "Cód. limpeza inválido",
"wipe_code__mismatch": "Cód. limpeza não coincidem.",

View File

@ -1,8 +1,8 @@
{
"current": {
"merkle_root": "136cfad983788597b6218f51a94b93b14535f20111dc91aa953e6e9df462ab16",
"datetime": "2025-03-27T18:08:46.572012",
"commit": "b340d6c7b21d110b8bb4478c654b293bd3a977f0"
"merkle_root": "478ee723935569e2d267a52f12ae0a1e2a0c3876574c47b8a518b30c9a8e724b",
"datetime": "2025-04-17T10:14:37.075778",
"commit": "a8251b09f977e75f551d136cb34131ca6f1394ea"
},
"history": [
{

View File

@ -859,11 +859,11 @@
"wipe_code__diff_from_pin": "Silme kodu, PIN kodunuzdan farklı olmalıdır.",
"wipe_code__disabled": "Silme kodu devre dışı.",
"wipe_code__enabled": "Silme kodu etkin.",
"wipe_code__enter_new": "Yeni silme kodu girin",
"wipe_code__enter_new": "Yeni silme kodu",
"wipe_code__info": "Silme kodu, cihazdaki tüm verileri silmek için kullanılabilir.",
"wipe_code__invalid": "Geçersiz silme kodu",
"wipe_code__mismatch": "Girdiğiniz silme kodları uyumlu değil.",
"wipe_code__reenter": "Silme kod. yeniden girin",
"wipe_code__reenter": "Silme kod. yeniden",
"wipe_code__reenter_to_confirm": "Onaylamak için silme kodunu yeniden girin.",
"wipe_code__title_check": "Si̇lme kod. kntrl et",
"wipe_code__title_invalid": "Geçersi̇z si̇lme kodu",

File diff suppressed because it is too large Load Diff