Refactor `Localize` trait to use `std::io::Write`

finchie
Aug 26, 2024, 6:50 AM
3NMKD6I57ONAGHEN4PZIAV2KPYESVR4JL3DTWSHXKCMVJBEQ4GIQC

Dependencies

  • [2] CESJ4CTO Move macro-specific code into `macro_impl` module
  • [3] IRW6JACS Implement `Localize` for `RelativeTime`
  • [4] BFL2Y7GN Add relative timestamps using `jiff` and `icu_relativetime`
  • [5] 6ABVDTXZ Improve `fluent_embed_derive` test suite
  • [6] AAERM7PB Add selector tests for the `fr` locale
  • [7] 7U2DXFMP Refactor `fluent_embed::Localize` to support overriding locales
  • [8] QFPQZR4K Refactor `fluent_embed`
  • [9] HHJDRLLN Create `fluent_embed_runtime` crate
  • [10] KZLFC7OW Rename `fluent_embed_runtime` to `fluent_embed`
  • [11] XEEXWJLG Add simple end-to-end test for selectors
  • [12] 7FYXVNAB Ignore comments in Fluent source code
  • [13] D652S2N3 Rename `parse` module to `parse_fluent`
  • [14] P6FW2GGO Remove unnecessary parameters in generated `localize()` function
  • [15] 5TEX4MNU Split `fluent_embed` into `group` and `parse` modules
  • [16] F5LG7WEN Emit compilation errors from Fluent source code
  • [17] 2SITVDYW Handle common errors in Fluent code
  • [18] NO3PDO7P Refactor `fluent_embed` to support structs
  • [19] HJMYJDC7 Simplify `fluent_embed::group` module
  • [20] 4MRF5E76 Generate simple locale matching code in `localize()`
  • [*] XGNME3WR Move `Group::derive_enum` to new `crate::parse_macro` module
  • [*] UKFEFT6L Create basic `Output` proc-macro

Change contents

  • replacement in fluent_embed_derive/tests/selectors.rs at line 46
    [4.1450][4.1450:1523]()
    assert_eq!(data.message_for_locale(&language_id), expected_message);
    [4.1450]
    [4.1111]
    let mut buffer = Vec::new();
    data.message_for_locale(&mut buffer, &language_id);
    assert_eq!(String::from_utf8(buffer), Ok(expected_message));
  • replacement in fluent_embed_derive/src/macro_impl/mod.rs at line 31
    [2.834][2.834:879]()
    let locales = match &derive_input.data {
    [2.834]
    [2.879]
    let available_locales = match &derive_input.data {
  • replacement in fluent_embed_derive/src/macro_impl/mod.rs at line 48
    [2.1507][2.1507:1558]()
    impl ::fluent_embed::Localize for #ident {
    [2.1507]
    [2.1558]
    impl<W: ::std::io::Write> ::fluent_embed::Localize<W> for #ident {
  • replacement in fluent_embed_derive/src/macro_impl/mod.rs at line 52
    [2.1714][2.1714:1853]()
    fn message_for_locale(&self, locale: &::fluent_embed::icu_locid::LanguageIdentifier) -> String {
    #message_body
    [2.1714]
    [2.1853]
    fn available_locales(&self) -> Vec<::fluent_embed::icu_locid::LanguageIdentifier> {
    #available_locales
  • replacement in fluent_embed_derive/src/macro_impl/mod.rs at line 56
    [2.1868][2.1868:2149]()
    fn localize(&self) -> String {
    let available_locales = #locales;
    let selected_locale = ::fluent_embed::locale_select::match_locales(&available_locales, &Self::CANONICAL_LOCALE);
    self.message_for_locale(&selected_locale)
    [2.1868]
    [2.2149]
    fn message_for_locale(
    &self,
    writer: &mut W,
    locale: &::fluent_embed::icu_locid::LanguageIdentifier,
    ) -> Result<(), ::fluent_embed::LocalizationError> {
    #message_body
  • replacement in fluent_embed_derive/src/macro_impl/derive.rs at line 46
    [4.2509][4.1181:1275](),[4.713][4.1181:1275]()
    #(if locale.normalizing_eq(#additional_locales) { return #additional_messages })else*
    [4.2509]
    [4.2510]
    #(if locale.normalizing_eq(#additional_locales) {
    #additional_messages
    return Ok(());
    }) else*
  • edit in fluent_embed_derive/src/macro_impl/derive.rs at line 52
    [4.1302]
    [4.511]
    Ok(())
  • edit in fluent_embed_derive/src/fluent/ast.rs at line 1
    [4.33]
    [4.6411]
    use std::cell::OnceCell;
  • replacement in fluent_embed_derive/src/fluent/ast.rs at line 13
    [4.120][4.352:436](),[4.283][4.352:436](),[4.3123][4.352:436](),[4.3166][4.352:436](),[4.6657][4.352:436](),[4.352][4.352:436]()
    let mut format_body = String::new();
    let mut format_arguments = Vec::new();
    [4.6657]
    [4.436]
    let mut write_expressions: Vec<syn::Expr> =
    Vec::with_capacity(message_context.pattern.elements.len());
  • replacement in fluent_embed_derive/src/fluent/ast.rs at line 18
    [4.500][4.500:582]()
    PatternElement::TextElement { value } => format_body.push_str(value),
    [4.500]
    [4.582]
    PatternElement::TextElement { value } => {
    let byte_string_literal = proc_macro2::Literal::byte_string(value.as_bytes());
    write_expressions.push(parse_quote!(writer.write_all(#byte_string_literal)?));
    }
  • replacement in fluent_embed_derive/src/fluent/ast.rs at line 23
    [4.640][4.3167:3219]()
    let expression = match expression {
    [4.640]
    [4.3219]
    write_expressions.push(match expression {
  • replacement in fluent_embed_derive/src/fluent/ast.rs at line 25
    [4.3286][4.6714:6888]()
    let target = inline_expression(selector, message_context)?;
    let mut arms: Vec<syn::Arm> = Vec::with_capacity(variants.len());
    [4.3286]
    [4.6888]
    let match_target = inline_expression(selector, message_context)?;
    let default_arm = OnceCell::new();
    let mut additional_arms = Vec::with_capacity(variants.len());
  • replacement in fluent_embed_derive/src/fluent/ast.rs at line 30
    [4.6939][4.6939:7017]()
    let base_pattern: syn::Pat = match &variant.key {
    [4.6939]
    [4.7017]
    let variant_pattern: syn::Pat = match &variant.key {
  • replacement in fluent_embed_derive/src/fluent/ast.rs at line 32
    [4.7086][4.7086:7178](),[4.7178][4.269:371]()
    let ident = format_ident!("{}", name.to_pascal_case());
    parse_quote!(::fluent_embed::icu_plurals::PluralCategory::#ident)
    [4.7086]
    [4.7266]
    let ident_pascal_case =
    format_ident!("{}", name.to_pascal_case());
    parse_quote!(::fluent_embed::icu_plurals::PluralCategory::#ident_pascal_case)
  • replacement in fluent_embed_derive/src/fluent/ast.rs at line 38
    [4.7408][4.7408:7486]()
    // Create a new `MessageContext` for each variant
    [4.7408]
    [4.7486]
    // Create a new `MessageContext` for each variant body
  • replacement in fluent_embed_derive/src/fluent/ast.rs at line 44
    [4.7691][4.7691:7762]()
    let body = message_body(variant_context)?;
    [4.7691]
    [4.7762]
    let variant_body = message_body(variant_context)?;
    // The default pattern must go last so we don't generate invalid match stataments
  • replacement in fluent_embed_derive/src/fluent/ast.rs at line 48
    [4.7763][4.7763:8138]()
    // Default patterns match anything else
    // TODO: this can potentially generate unreachable patterns,
    // should be replaced with a more sophisticated implementation
    let pattern = if variant.default {
    parse_quote!(#base_pattern | _)
    [4.7763]
    [4.8138]
    if variant.default {
    default_arm
    .set(quote!(#variant_pattern | _ => #variant_body))
    .expect("Multiple default patterns for match statement.");
  • replacement in fluent_embed_derive/src/fluent/ast.rs at line 53
    [4.8175][4.8175:8251]()
    base_pattern
    };
    [4.8175]
    [4.3624]
    additional_arms.push(quote!(#variant_pattern => #variant_body));
    }
    }
  • replacement in fluent_embed_derive/src/fluent/ast.rs at line 57
    [4.3625][4.8252:8350]()
    arms.push(parse_quote!(#pattern => #body));
    }
    [4.3625]
    [4.8350]
    // TODO: handle this as an error
    let default_arm = default_arm.get().unwrap();
  • replacement in fluent_embed_derive/src/fluent/ast.rs at line 61
    [4.3664][4.3664:3778]()
    match plural_rules.category_for(#target) {
    #(#arms),*
    [4.3664]
    [4.3778]
    match plural_rules.category_for(#match_target) {
    #(#additional_arms,)*
    #default_arm,
  • edit in fluent_embed_derive/src/fluent/ast.rs at line 65
    [4.3808][4.3808:3809]()
  • replacement in fluent_embed_derive/src/fluent/ast.rs at line 68
    [4.3913][4.8352:8424]()
    inline_expression(expression, message_context)?
    [4.3913]
    [4.3995]
    let expression = inline_expression(expression, message_context)?;
    parse_quote!(writer.write_all(#expression)?)
  • replacement in fluent_embed_derive/src/fluent/ast.rs at line 71
    [4.4017][4.4017:4036](),[4.189][4.714:818](),[4.3208][4.714:818](),[4.4036][4.714:818](),[4.714][4.714:818]()
    };
    format_body.push_str("{}");
    format_arguments.push(quote!(#expression));
    [4.4017]
    [4.818]
    });
  • replacement in fluent_embed_derive/src/fluent/ast.rs at line 76
    [4.849][4.849:943](),[4.1018][4.1018:1019](),[4.1019][4.4037:4127]()
    let format_body_literal = proc_macro2::Literal::string(format_body.to_string().as_str());
    Ok(parse_quote!(
    format!(#format_body_literal, #(#format_arguments),*)
    ))
    [4.849]
    [4.1626]
    Ok(parse_quote! {
    {
    #(#write_expressions;)*
    }
    })
  • replacement in fluent_embed_derive/src/fluent/ast.rs at line 89
    [4.1824][4.1824:1936]()
    let string_literal = proc_macro2::Literal::string(value);
    parse_quote!(#string_literal)
    [4.1824]
    [4.1936]
    let byte_string_literal = proc_macro2::Literal::byte_string(value.as_bytes());
    parse_quote!(#byte_string_literal)
  • replacement in fluent_embed_derive/src/fluent/ast.rs at line 101
    [4.4416][4.4416:4489]()
    // let ident = format_ident!("{}", id.name.to_snake_case());
    [4.4416]
    [4.8495]
    let fluent_name = &id.name;
    let rust_name = id.name.to_snake_case();
  • replacement in fluent_embed_derive/src/fluent/ast.rs at line 107
    [4.8625][4.8625:8672]()
    .get(&id.name.to_snake_case())
    [4.8625]
    [4.4589]
    .get(&rust_name)
  • replacement in fluent_embed_derive/src/fluent/ast.rs at line 116
    [4.5925][4.5925:5977]()
    .find(&format!("${}", id.name))
    [4.5925]
    [4.5977]
    .find(&format!("${fluent_name}"))
  • replacement in fluent_embed_derive/src/fluent/ast.rs at line 121
    [4.6091][4.6091:6197]()
    fluent_name: id.name.clone(),
    rust_name: id.name.to_snake_case(),
    [4.6091]
    [4.6197]
    fluent_name: fluent_name.clone(),
    rust_name,
  • replacement in fluent_embed_derive/src/fluent/ast.rs at line 124
    [4.6230][4.6230:6305]()
    span: SourceSpan::new(location.into(), id.name.len()),
    [4.6230]
    [4.6305]
    span: SourceSpan::new(location.into(), fluent_name.len()),
  • replacement in fluent_embed_derive/src/fluent/ast.rs at line 140
    [4.3850][4.3850:3923]()
    ReferenceKind::StructField => parse_quote!(self.#ident),
    [4.3850]
    [4.3923]
    ReferenceKind::StructField => parse_quote!(message_context.#ident),
  • replacement in fluent_embed/src/time.rs at line 1
    [4.53][3.0:21]()
    use crate::Localize;
    [4.53]
    [3.21]
    use crate::{LocalizationError, Localize};
  • edit in fluent_embed/src/time.rs at line 10
    [4.350]
    [4.350]
    use writeable::Writeable;
  • edit in fluent_embed/src/time.rs at line 16
    [4.550][4.550:647]()
    /// The locale to fall back to
    pub const DEFAULT_LOCALE: LanguageIdentifier = langid!("en-US");
  • replacement in fluent_embed/src/time.rs at line 26
    [4.955][3.26:124]()
    impl Localize for RelativeTime {
    const CANONICAL_LOCALE: LanguageIdentifier = DEFAULT_LOCALE;
    [4.955]
    [3.124]
    impl<W: std::io::Write> Localize<W> for RelativeTime {
    const CANONICAL_LOCALE: LanguageIdentifier = langid!("en-US");
    fn available_locales(&self) -> Vec<LanguageIdentifier> {
    // TODO: keep track of all locales with Fluent data, and return only those
    vec![<Self as Localize<W>>::CANONICAL_LOCALE]
    }
  • replacement in fluent_embed/src/time.rs at line 34
    [3.125][3.125:199]()
    fn message_for_locale(&self, locale: &LanguageIdentifier) -> String {
    [3.125]
    [4.990]
    fn message_for_locale(
    &self,
    writer: &mut W,
    locale: &LanguageIdentifier,
    ) -> Result<(), LocalizationError> {
  • replacement in fluent_embed/src/time.rs at line 112
    [4.4306][4.4306:4358]()
    formatter.format(decimal).to_string()
    }
    [4.4306]
    [3.249]
    let formatted_text = formatter.format(decimal);
    writer.write_all(formatted_text.write_to_string().as_bytes())?;
    writer.flush()?;
  • replacement in fluent_embed/src/time.rs at line 117
    [3.250][3.250:394]()
    fn localize(&self) -> String {
    // TODO: select locale based on environment
    self.message_for_locale(&Self::CANONICAL_LOCALE)
    [3.250]
    [3.394]
    Ok(())
  • replacement in fluent_embed/src/lib.rs at line 14
    [4.406][4.406:427]()
    pub trait Localize {
    [4.406]
    [4.427]
    #[derive(thiserror::Error, Debug)]
    pub enum LocalizationError {
    #[error("invalid locale selected")]
    InvalidLocale,
    #[error("unable to write localized output")]
    IO(#[from] std::io::Error),
    }
    pub trait Localize<W: std::io::Write> {
    // TODO: this should be project-wide and tracked at build time
  • replacement in fluent_embed/src/lib.rs at line 26
    [4.512][4.2853:2960]()
    fn message_for_locale(&self, locale: &LanguageIdentifier) -> String;
    fn localize(&self) -> String;
    [4.512]
    [4.807]
    fn available_locales(&self) -> Vec<LanguageIdentifier>;
    fn message_for_locale(
    &self,
    writer: &mut W,
    locale: &LanguageIdentifier,
    ) -> Result<(), LocalizationError>;
    fn localize(&self, writer: &mut W) -> Result<(), LocalizationError> {
    let available_locales = self.available_locales();
    let selected_locale = locale_select::match_locales(
    &available_locales,
    &<Self as Localize<W>>::CANONICAL_LOCALE,
    );
    self.message_for_locale(writer, &selected_locale)
    }
  • edit in fluent_embed/Cargo.toml at line 11
    [4.4435]
    [4.4435]
    fluent_embed_derive = { path = "../fluent_embed_derive" }
  • replacement in fluent_embed/Cargo.toml at line 18
    [4.1307][4.948:1006]()
    fluent_embed_derive = { path = "../fluent_embed_derive" }
    [4.1307]
    thiserror = "1.0.61"
    writeable = "0.5.5"
  • edit in Cargo.lock at line 185
    [4.1212]
    [4.1212]
    "thiserror",
    "writeable",