Add `layout` module for localizing with separators and vertical/horizontal padding

finchie
Aug 26, 2025, 7:36 AM
LYOV6ZIRUE34ZJG6X6BVPZ6R4LLHLMMWC5FHVLGW73HIHQUHDJYQC

Dependencies

  • [2] HHJDRLLN Create `fluent_embed_runtime` crate
  • [3] C6W7N6N5 Implement `Localize` for `FixedDecimal` and primitive number types
  • [4] USKESL6X Add examples for using common types in `l10n_embed`
  • [5] 2HHBS7VW Add rudimentary support for localizing lists
  • [6] XPGOKS6X Add rudimentary support for built-in `LEN()` function
  • [7] 5TEX4MNU Split `fluent_embed` into `group` and `parse` modules

Change contents

  • edit in l10n_embed_derive/tests/functions.rs at line 7
    [6.449]
    [6.449]
    use l10n_embed::layout::Lines;
  • replacement in l10n_embed_derive/tests/functions.rs at line 15
    [6.621][6.621:728]()
    Length { items: Vec<String> },
    Numbers { items: Vec<String> },
    Plurals { items: Vec<String> },
    [6.621]
    [6.728]
    Length { items: Lines<String> },
    Numbers { items: Lines<String> },
    Plurals { items: Lines<String> },
  • replacement in l10n_embed_derive/tests/functions.rs at line 22
    [6.796][6.796:820]()
    items: Vec<String>,
    [6.796]
    [6.820]
    items: Lines<String>,
  • replacement in l10n_embed_derive/tests/functions.rs at line 27
    [6.889][6.889:913]()
    items: Vec<String>,
    [6.889]
    [6.913]
    items: Lines<String>,
  • replacement in l10n_embed_derive/tests/functions.rs at line 32
    [6.982][6.982:1006]()
    items: Vec<String>,
    [6.982]
    [6.1006]
    items: Lines<String>,
  • replacement in l10n_embed_derive/tests/functions.rs at line 46
    [6.1267][6.1267:1301]()
    items: items.clone(),
    [6.1267]
    [6.1301]
    items: Lines::new(items.clone()),
  • replacement in l10n_embed_derive/tests/functions.rs at line 52
    [6.1409][6.1409:1435]()
    Length { items },
    [6.1409]
    [6.1435]
    Length {
    items: Lines::new(items),
    },
  • replacement in l10n_embed_derive/tests/functions.rs at line 71
    [6.1836][6.1836:1870]()
    items: items.clone(),
    [6.1836]
    [6.1870]
    items: Lines::new(items.clone()),
  • replacement in l10n_embed_derive/tests/functions.rs at line 76
    [6.1930][6.1930:1996]()
    compare_message(Numbers { items }, expected, DEFAULT_LOCALE);
    [6.1930]
    [6.1996]
    compare_message(
    Numbers {
    items: Lines::new(items),
    },
    expected,
    DEFAULT_LOCALE,
    );
  • replacement in l10n_embed_derive/tests/functions.rs at line 96
    [6.2320][6.2320:2354]()
    items: items.clone(),
    [6.2320]
    [6.2354]
    items: Lines::new(items.clone()),
    },
    expected,
    DEFAULT_LOCALE,
    );
    compare_message(
    Plurals {
    items: Lines::new(items),
  • edit in l10n_embed_derive/tests/functions.rs at line 108
    [6.2414][6.2414:2480]()
    compare_message(Plurals { items }, expected, DEFAULT_LOCALE);
  • replacement in l10n_embed_derive/src/fluent/ast.rs at line 326
    [6.6420][6.6420:6479]()
    parse_quote!(Vec::len(#vec_reference))
    [6.6420]
    [6.6479]
    parse_quote!(l10n_embed::list::Length::len(#vec_reference))
  • edit in l10n_embed/src/lib.rs at line 2
    [3.2501]
    [5.552]
    pub mod layout;
  • file addition: layout.rs (----------)
    [2.49]
    //! Implementations of `Localize` for various layout helpers
    use icu_locale::Locale;
    use crate::Localize;
    use crate::list::Length;
    pub struct Lines<L: Localize, const SEPARATOR_COUNT: usize = 1> {
    messages: Vec<L>,
    }
    impl<L: Localize, const SEPARATOR_COUNT: usize> Lines<L, SEPARATOR_COUNT> {
    pub fn new(messages: Vec<L>) -> Self {
    Self { messages }
    }
    }
    impl<L: Localize, const SEPARATOR_COUNT: usize> Length for Lines<L, SEPARATOR_COUNT> {
    fn len(&self) -> usize {
    self.messages.len()
    }
    }
    impl<L: Localize, const SEPARATOR_COUNT: usize> Localize for Lines<L, SEPARATOR_COUNT> {
    fn localize_for(&self, locale: &Locale) -> String {
    let localized_items: Vec<String> = self
    .messages
    .iter()
    .map(|item| item.localize_for(locale))
    .collect();
    localized_items.join(&"\n".repeat(SEPARATOR_COUNT))
    }
    }
    pub struct Padded<L: Localize, const VERTICAL_PADDING: usize, const HORIZONTAL_PADDING: usize> {
    message: L,
    }
    impl<L: Localize, const VERTICAL_PADDING: usize, const HORIZONTAL_PADDING: usize>
    Padded<L, VERTICAL_PADDING, HORIZONTAL_PADDING>
    {
    pub fn new(message: L) -> Self {
    Self { message }
    }
    }
    impl<L: Localize, const VERTICAL_PADDING: usize, const HORIZONTAL_PADDING: usize> Localize
    for Padded<L, VERTICAL_PADDING, HORIZONTAL_PADDING>
    {
    fn canonical_locale(&self) -> Locale {
    self.message.canonical_locale()
    }
    fn available_locales(&self) -> Vec<Locale> {
    self.message.available_locales()
    }
    fn localize_for(&self, locale: &Locale) -> String {
    let mut output = String::new();
    // Only apply the vertical padding above the message, not below
    output.push_str(&"\n".repeat(VERTICAL_PADDING));
    let localized_message = self.message.localize_for(locale);
    // Apply the horizontal padding to each line
    let horizontal_padding = " ".repeat(HORIZONTAL_PADDING);
    for (index, line) in localized_message.lines().enumerate() {
    // Add a newline on every additional line
    if index > 0 {
    output.push('\n');
    }
    output.push_str(&horizontal_padding);
    output.push_str(line);
    }
    output
    }
    }
  • file addition: layout.rs (----------)
    [4.20]
    //! Example showing how to localize with various layouts
    use icu_locale::{Locale, locale};
    use l10n_embed::Localize;
    use l10n_embed::layout::{Lines, Padded};
    const DEFAULT_LOCALE: Locale = locale!("en-US");
    fn main() {
    let items = vec!["item 1", "item 2", "item 3", "item 4"];
    let message = "a secret message";
    // Create different layouts
    let single_newline: Lines<&str, 1> = Lines::new(items.clone());
    let double_newlines: Lines<&str, 2> = Lines::new(items.clone());
    let vertical_padding: Padded<&str, 1, 0> = Padded::new(message);
    let horizontal_padding: Padded<&str, 0, 4> = Padded::new(message);
    let combined_padding: Padded<&str, 1, 4> = Padded::new(message);
    let padded_newlines: Lines<Padded<&str, 0, 4>, 1> =
    Lines::new(items.iter().map(|item| Padded::new(*item)).collect());
    // Localize these layouts
    let layouts = [
    (
    "Single newline",
    single_newline.localize_for(&DEFAULT_LOCALE),
    ),
    (
    "Double newlines",
    double_newlines.localize_for(&DEFAULT_LOCALE),
    ),
    (
    "Vertical padding",
    vertical_padding.localize_for(&DEFAULT_LOCALE),
    ),
    (
    "Horizontal padding",
    horizontal_padding.localize_for(&DEFAULT_LOCALE),
    ),
    (
    "Combined padding",
    combined_padding.localize_for(&DEFAULT_LOCALE),
    ),
    (
    "Padded newlines",
    padded_newlines.localize_for(&DEFAULT_LOCALE),
    ),
    ];
    // Print each layout with a separator to make things clear
    for (name, layout) in layouts {
    println!("----- {name} -----");
    println!("{layout}");
    }
    }