From 28e2f42bf36b3b650c5c81d0edd0fd01e402e6db Mon Sep 17 00:00:00 2001 From: Rizzen Yazston Date: Wed, 28 Jun 2023 11:39:08 +0200 Subject: [PATCH] Added coomands feature to 'Message' crate, and updated documentation, 'Cargo.toml' files, tests of all crates of the 'i18n' project. Corrected various localisation strings for en-ZA in Sqlite3, tests files and examples using localisation string. --- CHANGELOG.asciidoc | 23 +- Cargo.toml | 3 +- README.asciidoc | 13 +- crates/i18n/Cargo.toml | 19 +- crates/i18n/README.asciidoc | 18 +- crates/i18n/src/lib.rs | 22 +- crates/icu/Cargo.toml | 59 +---- crates/icu/README.asciidoc | 36 ++- crates/icu/src/error.rs | 4 +- crates/icu/src/icu.rs | 5 + crates/icu/src/lib.rs | 12 +- crates/lexer/Cargo.toml | 53 ++--- crates/lexer/README.asciidoc | 57 +---- crates/lexer/i18n/i18n_lexer.sqlite3 | Bin 94208 -> 0 bytes crates/lexer/src/lexer.rs | 8 +- crates/lexer/src/lib.rs | 2 +- crates/message/Cargo.toml | 62 +++--- crates/message/README.asciidoc | 34 ++- crates/message/i18n/i18n_message.sqlite3 | Bin 94208 -> 94208 bytes .../message/i18n/supported_languages.asciidoc | 34 +++ crates/message/src/error.rs | 8 +- crates/message/src/lib.rs | 7 +- crates/message/src/message.rs | 39 ++-- crates/message/tests/message.rs | 14 +- crates/pattern/Cargo.toml | 55 +++-- crates/pattern/README.asciidoc | 97 ++++---- crates/pattern/docs/pattern strings.asciidoc | 4 +- crates/pattern/i18n/i18n_pattern.sqlite3 | Bin 94208 -> 94208 bytes .../pattern/i18n/supported_languages.asciidoc | 34 +++ crates/pattern/src/command.rs | 207 ++++++++++++++++++ crates/pattern/src/error.rs | 97 +++++--- crates/pattern/src/formatter.rs | 161 ++++++++++++-- crates/pattern/src/lib.rs | 53 ++++- crates/pattern/src/parser.rs | 159 +++++++++----- crates/pattern/src/types.rs | 29 ++- crates/pattern/tests/command.rs | 36 +++ crates/pattern/tests/formatter.rs | 93 +++++++- crates/provider/core/Cargo.toml | 17 +- crates/provider/core/README.asciidoc | 4 +- crates/provider/core/src/error.rs | 9 +- crates/provider/core/src/lib.rs | 9 +- crates/provider/core/src/provider.rs | 24 +- crates/provider/sqlite3/Cargo.toml | 18 +- crates/provider/sqlite3/README.asciidoc | 4 + .../i18n/i18n_provider_sqlite3.sqlite3 | Bin 94208 -> 94208 bytes .../sqlite3/i18n/supported_languages.asciidoc | 34 +++ crates/provider/sqlite3/src/error.rs | 8 +- crates/provider/sqlite3/src/lib.rs | 11 +- crates/provider/sqlite3/src/provider.rs | 48 ++-- crates/provider/sqlite3/tests/sqlite3.rs | 6 +- crates/registry/Cargo.toml | 33 --- crates/registry/README.asciidoc | 38 ---- crates/registry/src/lib.rs | 44 ---- crates/utility/Cargo.toml | 24 ++ .../LICENSE-BSD-3-Clause | 0 crates/utility/README.asciidoc | 81 +++++++ crates/{registry => utility}/src/error.rs | 6 +- crates/utility/src/lib.rs | 79 +++++++ .../src/lib.rs => utility/src/lstring.rs} | 61 ++---- crates/{registry => utility}/src/registry.rs | 51 +++-- 60 files changed, 1444 insertions(+), 722 deletions(-) delete mode 100644 crates/lexer/i18n/i18n_lexer.sqlite3 create mode 100644 crates/message/i18n/supported_languages.asciidoc create mode 100644 crates/pattern/i18n/supported_languages.asciidoc create mode 100644 crates/pattern/src/command.rs create mode 100644 crates/pattern/tests/command.rs create mode 100644 crates/provider/sqlite3/i18n/supported_languages.asciidoc delete mode 100644 crates/registry/Cargo.toml delete mode 100644 crates/registry/README.asciidoc delete mode 100644 crates/registry/src/lib.rs create mode 100644 crates/utility/Cargo.toml rename crates/{registry => utility}/LICENSE-BSD-3-Clause (100%) create mode 100644 crates/utility/README.asciidoc rename crates/{registry => utility}/src/error.rs (72%) create mode 100644 crates/utility/src/lib.rs rename crates/{lstring/src/lib.rs => utility/src/lstring.rs} (66%) rename crates/{registry => utility}/src/registry.rs (88%) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 4af77d0..c664c1c 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -1,7 +1,7 @@ = Changelog Rizzen Yazston -== i18n 0.6.0 (2023-03-??) +== i18n 0.6.0 (2023-06-??) WARNING: This update has API breaking changes for some `i18n` crates. @@ -39,14 +39,22 @@ Breaking change is the result of changing how ICU data providers are used and pa * Updated the `i18n_pattern` crate: -** Updated `Formatter` to use `IcuDataProvider` +** Updated `Formatter` to use `IcuDataProvider`. + +** Added `CommandRegistry` and `CommandError` for the command patterns. + +** Added command callback function `file_path`. ** Updated `Cargo.toml`, tests, examples and documentation. -* Updated `i18n_lstring` crate: +* Updated `i18n_lstring` crate to merge with `i18n_registry` crate: ** Added `Clone` to `#[Derive()]` to allow cloning. +** Renamed `lib.rs` to 'lstring.rs', and moved to `i18n_registry` crate. + +** Removed `i18n_lstring` crate + * Added the `i18n_message` crate: ** Added `Message`, `MessageError`. @@ -55,6 +63,14 @@ Breaking change is the result of changing how ICU data providers are used and pa ** Added the `Cargo.toml`, license, and documentation. +* Updated `i18n_registry` crate: + +** Renamed crate to `i18n_utility` + +** Added `lstring` entries in `lib.rs` + +** Updated `Cargo.toml`, tests, examples and documentation. + == i18n 0.5.0 (2023-03-16) WARNING: This update has many API breaking changes for all existing `i18n` crates. @@ -149,7 +165,6 @@ Breaking change is the result of changing the implementation of handling error a * Removed `i18n_error` crate as it is no longer needed after update of error handling. - == i18n 0.4.0 (2023-02-24) WARNING: This update has many API breaking changes for all existing `i18n` crates. diff --git a/Cargo.toml b/Cargo.toml index ca1b3de..c713f83 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,9 +5,8 @@ resolver = "2" members = [ "crates/icu", - "crates/lstring", "crates/lexer", - "crates/registry", + "crates/utility", "crates/provider/core", "crates/provider/sqlite3", "crates/pattern", diff --git a/README.asciidoc b/README.asciidoc index 8d02ed9..85a2454 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -2,17 +2,16 @@ Rizzen Yazston :url-unicode: https://unicode.org/ :icu4x: https://github.com/unicode-org/icu4x -:BCP_47_Language_Tag: https://www.rfc-editor.org/rfc/bcp/bcp47.txt Welcome to the Internationalisation (I18n) project. -The *Internationalisation* project provides alternative components for the {icu4x}[*ICU4X*] project (maintained by the {url-unicode}[*Unicode Consortium*]), yet still depends on many core components of ICU4X for internal functionality, thus can also be seen as an extension library to ICU4X. The Internationalisation project consists of an alternative messaging system, and adding macro functionality to patterns when formatting strings for locales. +The *Internationalisation* project provides alternative components for the {icu4x}[*ICU4X*] project (maintained by the {url-unicode}[*Unicode Consortium*]), yet still depends on many core components of ICU4X for internal functionality, thus can also be seen as an extension library to ICU4X. The Internationalisation project consists of an alternative messaging system, and adding macro functionality to patterns when formatting strings for locales (language tags). The repository contains the following crates: - `i18n`: The convenience meta crate that contains selected available crates, -- `i18n_lstring`: Associating a {BCP_47_Language_Tag}[_BCP 47 Language Tag_] to a `String`, +- `i18n_icu`: Contains ICU4X data provider helper, - `i18n_lexer`: A simple lexer to tokenise a string, @@ -24,9 +23,13 @@ The repository contains the following crates: - `i18n_provider_sqlite3`: Implementation of `i18n_provider` using Sqlite3 as its data store, -- `i18n_registry`: Language tag registry, that caches language tag strings and their `Locale` objects. +- `i18n_utility`: Contains the Language tag registry, and the LString type (language tagged string). -Note:: All these crates on `crates.io` have the names appended with the suffix `-rizzen-yazston` to distinguish them from internationalisation crates created by other authors. +NOTE: All these crates on `crates.io` have the names appended with the suffix `-rizzen-yazston` to distinguish them from internationalisation crates created by other authors. + +== Acknowledgement + +Stefano Angeleri for advice on various design aspects of implementing the components of the internationalisation project, and also providing the Italian translation of error message strings. == Usage diff --git a/crates/i18n/Cargo.toml b/crates/i18n/Cargo.toml index bd3ee46..75abfae 100644 --- a/crates/i18n/Cargo.toml +++ b/crates/i18n/Cargo.toml @@ -13,27 +13,18 @@ license = "BSD-3-Clause" readme = "README.asciidoc" # false prevents publishing while developing, else string of repository identfier publish = false - -# these keys are used when preparing to publish to 'crates.io' -#documentation = "" -#homepage = "` to prevent unnecessary duplication. +== Acknowledgement + +Stefano Angeleri for advice on various design aspects of implementing the components of the internationalisation project, and also providing the Italian translation of error message strings. + == Cargo.toml ``` [dependencies] -i18n_icu-rizzen-yazston = "0.5.0" +icu_provider = "1.1.0" + +# These are required for the DataProvider. +icu_properties = "1.1.0" +icu_segmenter = "0.8.0" +icu_plurals = "1.1.0" +icu_decimal = "1.1.0" +icu_calendar = "1.1.0" +icu_datetime = "1.1.0" + +# This is required for the DataProvider. +[dependencies.fixed_decimal] +version = "0.5.2" +# Needed for floating point support. +features = [ "ryu" ] ``` == Examples -See various examples of `i18n_lexer`, `i18n_pattern`, and `i18n_message` crates. +See various examples of the `i18n_lexer`, `i18n_pattern`, and `i18n_message` crates. diff --git a/crates/icu/src/error.rs b/crates/icu/src/error.rs index d512878..17da8ec 100644 --- a/crates/icu/src/error.rs +++ b/crates/icu/src/error.rs @@ -14,10 +14,8 @@ pub enum IcuError { } impl Display for IcuError { - - /// Simply call the display formatter of embedded error. fn fmt( &self, formatter: &mut Formatter ) -> Result { - match *self { + match self { IcuError::Properties( ref error ) => error.fmt( formatter ), IcuError::Segmenter( ref error ) => error.fmt( formatter ), } diff --git a/crates/icu/src/icu.rs b/crates/icu/src/icu.rs index 47c7f8f..64dea6d 100644 --- a/crates/icu/src/icu.rs +++ b/crates/icu/src/icu.rs @@ -77,6 +77,7 @@ where /// `DataProviderWrapper` type, which is provided by this crate. Besides storing the `DataProvider`, it also /// obtains and stores the Pattern_Syntax character set, the Pattern_White_Space character set, and the Grapheme /// Cluster Segmenter required for the `Lexer` types to function. + /// See `i18n_lexer` crate on usage. pub fn try_new( data_provider: &'a P ) -> Result { let syntax = load_pattern_syntax( data_provider )?; let white_space = load_pattern_white_space( data_provider )?; @@ -92,21 +93,25 @@ where /// Get the `DataProviderWrapper` object that can be used in any ICU function that accepts a `DataProvider` as a /// parameter, as `data_provider().0`. + /// See `i18n_lexer` crate on usage. pub fn data_provider( &self ) -> &DataProviderWrapper

{ &self.data_provider } /// Get the preloaded Pattern_Syntax character set. + /// See `i18n_lexer` crate on usage. pub fn pattern_syntax( &self ) -> &CodePointSetData { &self.pattern_syntax } /// Get the preloaded Pattern_White_Space character set. + /// See `i18n_lexer` crate on usage. pub fn pattern_white_space( &self ) -> &CodePointSetData { &self.pattern_white_space } /// Get the Grapheme Cluster Segmenter with preloaded character data set. + /// See `i18n_lexer` crate on usage. pub fn grapheme_segmenter( &self ) -> &GraphemeClusterSegmenter { &self.grapheme_segmenter } diff --git a/crates/icu/src/lib.rs b/crates/icu/src/lib.rs index 776815e..cd8e17a 100644 --- a/crates/icu/src/lib.rs +++ b/crates/icu/src/lib.rs @@ -1,10 +1,10 @@ // This file is part of `i18n_icu-rizzen-yazston` crate. For the terms of use, please see the file // called LICENSE-BSD-3-Clause at the top level of the `i18n_icu-rizzen-yazston` crate. -//! ICU4X data provider helper. +//! [`ICU4X`] project (maintained by the [Unicode Consortium]) data provider helper. //! -//! The `IcuDataProvider` type contains a member `data_provider` holding the `&DataProvider` as a `DataProviderWrapper` -//! type. +//! The `IcuDataProvider` type contains a member `data_provider` holding the ICU4X [`&DataProvider`] as a +//! `DataProviderWrapper` type. //! //! The `IcuDataProvider` type also contains non-locale based data used within the `i18n_lexer` crate. //! @@ -12,7 +12,11 @@ //! //! # Examples //! -//! See various examples of `i18n_lexer`, `i18n_pattern`, and `i18n_message` crates. +//! See various examples of the `i18n_lexer`, `i18n_pattern`, and `i18n_message` crates. +//! +//! [`ICU4X`]: https://github.com/unicode-org/icu4x +//! [Unicode Consortium]: https://home.unicode.org/ +//! [`&DataProvider`] icu_provider::DataProvider pub mod icu; pub use icu::*; diff --git a/crates/lexer/Cargo.toml b/crates/lexer/Cargo.toml index 6c4661a..b1a2ed0 100644 --- a/crates/lexer/Cargo.toml +++ b/crates/lexer/Cargo.toml @@ -13,64 +13,57 @@ license = "BSD-3-Clause" readme = "README.asciidoc" # false prevents publishing while developing, else string of repository identfier publish = false - -# these keys are used when preparing to publish to 'crates.io' -#documentation = "" -#homepage = "(+&moDZS6L0+-;st@-=QAwVUS4 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, P: ?Sized + DataProvider + DataProvider diff --git a/crates/lexer/src/lib.rs b/crates/lexer/src/lib.rs index a7127fd..ab19663 100644 --- a/crates/lexer/src/lib.rs +++ b/crates/lexer/src/lib.rs @@ -45,7 +45,7 @@ //! } //! ``` //! -//! [`BufferProvider`]: https://docs.rs/icu_provider/latest/icu_provider/buf/trait.BufferProvider.html +//! [`DataProvider`] icu_provider::DataProvider //! [Unicode Consortium]: https://home.unicode.org/ //! [CLDR]: https://cldr.unicode.org/ //! [ICU4X]: https://github.com/unicode-org/icu4x diff --git a/crates/message/Cargo.toml b/crates/message/Cargo.toml index 2038cef..23e2dac 100644 --- a/crates/message/Cargo.toml +++ b/crates/message/Cargo.toml @@ -13,73 +13,63 @@ license = "BSD-3-Clause" readme = "README.asciidoc" # false prevents publishing while developing, else string of repository identfier publish = false - -# these keys are used when preparing to publish to 'crates.io' -#documentation = "" -#homepage = " Result<(), Box> { let lstring_provider = ProviderSqlite3::try_new( "./i18n/", &language_tag_registry )?; + let command_registry = Rc::new( CommandRegistry::new() ); let message_system = Message::try_new( - &icu_data_provider, &language_tag_registry, &lstring_provider, true, true + &icu_data_provider, &language_tag_registry, &lstring_provider, &command_registry, true, true )?; let mut values = HashMap::::new(); values.insert( diff --git a/crates/message/i18n/i18n_message.sqlite3 b/crates/message/i18n/i18n_message.sqlite3 index 9ae63e898b102a310e3fcb09998d8885b2bf8843..c3b251a0285843f1694417b30cd8d0849a2d43f4 100644 GIT binary patch delta 116 zcmZp8z}oPDb%Hdb`9v9KM)Qpci}JZb7?^nN8F=maV>j~@81r&A2{Cdo=!!ShPpa3S zZW_X<$f1y!m!eRTkvch`UYEH%v3PP#eG3Z%10(0;h`PB Result { match self { MessageError::Registry( ref error ) => error.fmt( formatter ), @@ -34,14 +32,14 @@ impl Display for MessageError { }; write!( formatter, - "No string was found for identifier ‘{}’ and language tag ‘{}’. Fallback used: {}.", + "No string was found for the identifier ‘{}’ and the language tag ‘{}’. Fallback was used: {}.", identifier, language_tag, string, ) }, MessageError::NoDefaultLanguageTag( identifier ) => - write!( formatter, "No default language tag was found for identifier ‘{}’.", identifier ) + write!( formatter, "No default language tag was found for the identifier ‘{}’.", identifier ) } } } diff --git a/crates/message/src/lib.rs b/crates/message/src/lib.rs index befa169..73a1dd6 100644 --- a/crates/message/src/lib.rs +++ b/crates/message/src/lib.rs @@ -16,9 +16,9 @@ //! //! ``` //! use i18n_icu::IcuDataProvider; -//! use i18n_registry::LanguageTagRegistry; +//! use i18n_utility::LanguageTagRegistry; //! use i18n_provider_sqlite3::ProviderSqlite3; -//! use i18n_pattern::PlaceholderValue; +//! use i18n_pattern::{ PlaceholderValue, CommandRegistry }; //! use i18n_message::Message; //! use icu_testdata::buffer; //! use icu_provider::serde::AsDeserializingBufferProvider; @@ -36,8 +36,9 @@ //! let lstring_provider = ProviderSqlite3::try_new( //! "./i18n/", &language_tag_registry //! )?; +//! let command_registry = Rc::new( CommandRegistry::new() ); //! let message_system = Message::try_new( -//! &icu_data_provider, &language_tag_registry, &lstring_provider, true, true +//! &icu_data_provider, &language_tag_registry, &lstring_provider, &command_registry, true, true //! )?; //! let mut values = HashMap::::new(); //! values.insert( diff --git a/crates/message/src/message.rs b/crates/message/src/message.rs index 3e615a4..604df35 100644 --- a/crates/message/src/message.rs +++ b/crates/message/src/message.rs @@ -5,9 +5,8 @@ use crate::MessageError; use i18n_icu::IcuDataProvider; use i18n_lexer::tokenise; use i18n_provider::{ LStringProvider, LStringProviderWrapper }; -use i18n_registry::LanguageTagRegistry; -use i18n_lstring::LString; -use i18n_pattern::{ parse, Formatter, PlaceholderValue }; +use i18n_utility::{ LanguageTagRegistry, LString }; +use i18n_pattern::{ parse, Formatter, PlaceholderValue, CommandRegistry }; use icu_provider::DataProvider; use icu_properties::{ provider::{ PatternSyntaxV1Marker, PatternWhiteSpaceV1Marker } }; use icu_segmenter::provider::GraphemeClusterBreakDataV1Marker; @@ -53,6 +52,7 @@ where icu_data_provider: Rc>, language_tag_registry: Rc, lstring_provider: LStringProviderWrapper<'a, L>, + command_registry: Rc, fallback: bool, caching: bool, cache: RefCell, HashMap>>>, @@ -76,10 +76,10 @@ where L: ?Sized + LStringProvider, { - /// Create a new `Message` instance, that is connected to a language string provider `LStringProvider`. A - /// reference to the language tag registry `Rc` instance and reference to the ICU data - /// provider `Rc` are stored within the `Message` to facilitate the parsing of language string - /// patterns, and for formatting strings. + /// Create a new `Message` instance, that is connected to a language string provider [`LStringProvider`]. A + /// reference to the language tag registry [`Rc`]`<`[`LanguageTagRegistry`]`>` instance and reference to the ICU + /// data provider [`Rc`]`<`[`IcuDataProvider`]`>` are stored within the `Message` to facilitate the parsing of + /// language string patterns, and for formatting strings. /// /// Two boolean flags `fallback` and `caching` are also set to be the defaults of the `Message` instance. These /// flags govern whether parsed strings are cached for reuse, and if no string is found for the specified language @@ -89,9 +89,9 @@ where /// /// ``` /// use i18n_icu::IcuDataProvider; - /// use i18n_registry::LanguageTagRegistry; + /// use i18n_utility::LanguageTagRegistry; /// use i18n_provider_sqlite3::ProviderSqlite3; - /// use i18n_pattern::PlaceholderValue; + /// use i18n_pattern::{ PlaceholderValue, CommandRegistry }; /// use i18n_message::Message; /// use icu_testdata::buffer; /// use icu_provider::serde::AsDeserializingBufferProvider; @@ -109,8 +109,9 @@ where /// let lstring_provider = ProviderSqlite3::try_new( /// "./i18n/", &language_tag_registry /// )?; + /// let command_registry = Rc::new( CommandRegistry::new() ); /// let message_system = Message::try_new( - /// &icu_data_provider, &language_tag_registry, &lstring_provider, true, true + /// &icu_data_provider, &language_tag_registry, &lstring_provider, &command_registry, true, true /// )?; /// let mut values = HashMap::::new(); /// values.insert( @@ -140,11 +141,16 @@ where /// Ok( () ) /// } /// ``` - // TODO: Add struct contain callback functions for commands + /// + /// [`LStringProvider`]: i18n_provider::LStringProvider + /// [`Rc`]: std::rc::Rc + /// [`LanguageTagRegistry`]: i18n_utility::LanguageTagRegistry + /// [`IcuDataProvider`]: i18n_icu::IcuDataProvider pub fn try_new( icu_data_provider: &Rc>, language_tag_registry: &Rc, lstring_provider: &'a L, + command_registry: &Rc, fallback: bool, //true = fallback to default language caching: bool, ) -> Result { @@ -152,6 +158,7 @@ where icu_data_provider: Rc::clone( icu_data_provider ), language_tag_registry: Rc::clone( language_tag_registry ), lstring_provider: LStringProviderWrapper( lstring_provider ), + command_registry: Rc::clone( command_registry ), fallback, caching, cache: RefCell::new( HashMap::, HashMap>>::new() ), @@ -168,9 +175,9 @@ where /// /// ``` /// use i18n_icu::IcuDataProvider; - /// use i18n_registry::LanguageTagRegistry; + /// use i18n_utility::LanguageTagRegistry; /// use i18n_provider_sqlite3::ProviderSqlite3; - /// use i18n_pattern::PlaceholderValue; + /// use i18n_pattern::{ PlaceholderValue, CommandRegistry }; /// use i18n_message::Message; /// use icu_testdata::buffer; /// use icu_provider::serde::AsDeserializingBufferProvider; @@ -188,8 +195,9 @@ where /// let lstring_provider = ProviderSqlite3::try_new( /// "./i18n/", &language_tag_registry /// )?; + /// let command_registry = Rc::new( CommandRegistry::new() ); /// let message_system = Message::try_new( - /// &icu_data_provider, &language_tag_registry, &lstring_provider, true, true + /// &icu_data_provider, &language_tag_registry, &lstring_provider, &command_registry, true, true /// )?; /// let mut values = HashMap::::new(); /// values.insert( @@ -313,7 +321,8 @@ where &self.icu_data_provider, language_tag, &self.language_tag_registry.get_locale( language_tag.as_str() )?, - &tree + &tree, + &self.command_registry, )?; // If caching is not allowed, simple use `Formatter` to get the LString. diff --git a/crates/message/tests/message.rs b/crates/message/tests/message.rs index c7f5648..eb57687 100644 --- a/crates/message/tests/message.rs +++ b/crates/message/tests/message.rs @@ -4,9 +4,9 @@ //! Testing `Message`. use i18n_icu::IcuDataProvider; -use i18n_registry::LanguageTagRegistry; +use i18n_utility::LanguageTagRegistry; use i18n_provider_sqlite3::ProviderSqlite3; -use i18n_pattern::PlaceholderValue; +use i18n_pattern::{ PlaceholderValue, CommandRegistry }; use i18n_message::Message; use icu_testdata::buffer; use icu_provider::serde::AsDeserializingBufferProvider; @@ -25,8 +25,14 @@ fn message() -> Result<(), Box> { let lstring_provider = ProviderSqlite3::try_new( "./i18n/", &language_tag_registry )?; + let command_registry = Rc::new( CommandRegistry::new() ); let message_system = Message::try_new( - &icu_data_provider, &language_tag_registry, &lstring_provider, true, true + &icu_data_provider, + &language_tag_registry, + &lstring_provider, + &command_registry, + true, + true )?; let mut values = HashMap::::new(); values.insert( @@ -50,7 +56,7 @@ fn message() -> Result<(), Box> { )?; assert_eq!( lstring.as_str(), - "No string was found for identifier ‘i18n_message/string_not_found’ and language tag ‘en-ZA’. Fallback used: True.", + "No string was found for the identifier ‘i18n_message/string_not_found’ and the language tag ‘en-ZA’. Fallback was used: True.", "Check placeholder values." ); Ok( () ) diff --git a/crates/pattern/Cargo.toml b/crates/pattern/Cargo.toml index b41f9dd..a1ad298 100644 --- a/crates/pattern/Cargo.toml +++ b/crates/pattern/Cargo.toml @@ -13,68 +13,67 @@ license = "BSD-3-Clause" readme = "README.asciidoc" # false prevents publishing while developing, else string of repository identfier publish = false - -# these keys are used when preparing to publish to 'crates.io' -#documentation = "" -#homepage = " Result<(), Box> { let tree = parse( tokens.0 )?; let locale: Rc = Rc::new( "en-ZA".parse()? ); let language_tag = Rc::new( locale.to_string() ); - let mut formatter = Formatter::try_new( &icu_data_provider, &language_tag, &locale, &tree )?; + let mut formatter = Formatter::try_new( + &icu_data_provider, &language_tag, &locale, &tree, &command_registry + )?; let mut values = HashMap::::new(); values.insert( "dogs_number".to_string(), @@ -96,4 +77,38 @@ fn pattern_plural() -> Result<(), Box> { Ok( () ) } +fn command_delayed() -> Result<(), Box> { + let buffer_provider = buffer(); + let data_provider = buffer_provider.as_deserializing(); + let icu_data_provider = + Rc::new( IcuDataProvider::try_new( &data_provider )? ); + let tokens = tokenise( + "At night {#english_a_or_an# hunter} {hunter} stalked {#english_a_or_an # prey} {prey}.", + &vec![ '{', '}', '`', '#' ], + &icu_data_provider, + ); + let tree = parse( tokens.0 )?; + let locale: Rc = Rc::new( "en-ZA".parse()? ); + let language_tag = Rc::new( locale.to_string() ); + let command_registry = Rc::new( CommandRegistry::new() ); + command_registry.insert( "english_a_or_an", english_a_or_an )?; + let mut formatter = + Formatter::try_new( &icu_data_provider, &language_tag, &locale, &tree, &command_registry )?; + let mut values = HashMap::::new(); + values.insert( + "hunter".to_string(), + PlaceholderValue::String( "owl".to_string() ) + ); + values.insert( + "prey".to_string(), + PlaceholderValue::String( "mouse".to_string() ) + ); + let result = formatter.format( &values )?; + assert_eq!( + result.as_str(), + "At night an owl stalked a mouse.", + "Strings must be the same." + ); + Ok( () ) +} ``` diff --git a/crates/pattern/docs/pattern strings.asciidoc b/crates/pattern/docs/pattern strings.asciidoc index 2dc5bdd..1702528 100644 --- a/crates/pattern/docs/pattern strings.asciidoc +++ b/crates/pattern/docs/pattern strings.asciidoc @@ -17,8 +17,8 @@ syntax = literal / `#` / pattern literal = backtick , [ backtick / `#` / `{` / `}` ] pattern = `{` , type , `}` ; Future: may allow {} to be short hand for `{`} type = literal_text / command / placeholder -command = `#` , identifier+ , [ PWS+ , argument ]+ -argument = literal_text / identifier+ +command = `#` , identifier+ , [ PWS* , `#` ] , [ PWS+ , parameter ]+ +parameter = literal_text / identifier+ placeholder = identifier+, [ PWS+ , simple / complex ]? simple = decimal / date_time ; add other types when available in ICU, such as `currency` decimal = `decimal` , [ PWS+ , decimal_keyboard ]* diff --git a/crates/pattern/i18n/i18n_pattern.sqlite3 b/crates/pattern/i18n/i18n_pattern.sqlite3 index 610611965f81c52479304bd9b98dbfc6a007944d..d49d4842ad38357a9837596b93d31abbd52a397d 100644 GIT binary patch delta 2362 zcmbVNTTmNi6y8lXWD|0uDc!VOvI_#GluIubMVtXCWm-!M4G>6xBqj^&G-ShODIj37 z(yAQ`%AB^KJ}`C07wVK&-t?glj-yk@u@Cj~ptjTT%^AUV93LE}{`*7H7N~W6%E^D( z^PTTI=lc_Pbcs8Fhdk;F|x^@owz|VIyh?ZJy=~xlA4*E)z9I!Z1p&&|&?L zdUx6nY4y7E)D6QfI;g*`FHRG5w{&gPEJbT4&{aZ6(-Q>@U&jfNQ-QD$@=2m7^N~_d zZA%}|%8=gGNN4M4qL@)C!QWK|mzNBhGIr)pHJJxjx~l1Xibfw$8sT2oOUczd5V{@o z7A;MbGfEbm?S5V>_wh5AGRdtF^ku{E-5k9`LlZ@eQUZ*x!Zs9$u|48oB*X?}_K~AC zdLOALDw$fC_hsu;L^1f%_dI1hPc|$LHPw*@rNL$(o@JEdFzH@BaSG<$d1^km@)M)) z5heA5U~@ZQ#eIk_vtqc%;pO&1job%fyTgw5*=RtPc`1TWte(Ho%))I~27KOLKwB*s zY8EWF7yavDS4Xb?HM2_SC=7R$V-Rzmov`fE!*?Byvbc#RI7ZRBxNsmQ`+|K!IF#aA zU1ISPEB0W_a~=j3yiPdi$u(xA)5JbT(FHlUKIMQjo%ReBwk)3HAA+9q95Y&sG_eQe z2ypQ86a!|j-KHK41>``gxH->$==3_cF@qinJJ7*J#1QX`h_Vlhkb=a+w6|-IqM>Ob zALVZ4dIV_$ZoIZR@P{6Gn*m^T^QbSe~JR?3LI*` zqy`4~t^ExYXyY*0F{}n!_@g6gBnZtrh>*dDe85Fd8`{@UuWve8F9Iix_{YoER_Y~5l&Y$ZiSi+Ja^pK)vV>|b-dnME zUungG(!B>1CyR%z(wlOZ6k{rw6klv)u>TcaVnjn;W}&dgOidMjRZ7s5gdM`vZwM2Y&6+;6K&?le%y zXeVH(DQ-}VS=gX6V63SK7QeTXJS;RFp||b8+HME3*|8N184d*bJ`tI&#G==3aE5Ck z9dNa27xXme({_6Rk+bSc2LndFci|w#rQBEltn4#6Cxpg z7+0ev!T;D%{4>rDZ5%2{oT#UB8En%<@Xd+cC2BJC@bs&1P-tCRG=j(Qbhw1xh<_aZ zZgxVYE8keVjV4MNC3up9g~?2PlE>374nI3K_ahws<;sVfEfbXWIpPesTNQ(H)UIms zItiVvjWf||l7bAkLnHRgEZE6R#Jdyjx9-gIY{93@L#bkYSnpJ=s@7Qsyrz1{Ojk9@ zfY_Ew?S#+T%2nsx57kQ*#Y#pFZUv`{+r;x!^49zb-tWpgjF|k79#p$(p!L{4H@54C H%ZYyg)`=8f delta 192 zcmV;x06+hL;01u-1&|v7RFND*0aUSIp-&9}000{Ra{v#c4-O81vkq|W53~DU1Ry6H z3J)y+91Q&h4{#194vr1Q4nPk)4XO`T4Id7r51$PE4c-p*4ULm7fMAngfC#gWfCmf) zK}i4r0UeVetss*x_XPw*Cj>{6v6mN?UjYpjNB{r;%K>RIIBqU%VQy!2VP|DubhG`8 u1XBqB0003GH~ Result<(), Box> { +/// let registry = CommandRegistry::new(); +/// registry.insert( "file_path", file_path )?; +/// let mut strings = Vec::::new(); +/// strings.push( PlaceholderValue::String( "file_path".to_string() ) ); +/// strings.push( PlaceholderValue::String( "tests/command.rs".to_string() ) ); +/// let function = registry.get( "file_path" )?; +/// let string = function( strings )?; +/// assert_eq!( string.as_str(), "tests/command.rs", "Strings must be the same." ); +/// Ok( () ) +/// } +/// ``` +pub struct CommandRegistry { + registry: RefCell ) -> Result>>, +} + +impl CommandRegistry { + /// Creates an empty registry. + /// + /// # Examples + /// + /// ``` + /// use i18n_pattern::{CommandRegistry, file_path, PlaceholderValue}; + /// use std::error::Error; + /// + /// fn command_registry() -> Result<(), Box> { + /// let registry = CommandRegistry::new(); + /// registry.insert( "file_path", file_path )?; + /// let mut strings = Vec::::new(); + /// strings.push( PlaceholderValue::String( "file_path".to_string() ) ); + /// strings.push( PlaceholderValue::String( "tests/command.rs".to_string() ) ); + /// let function = registry.get( "file_path" )?; + /// let string = function( strings )?; + /// assert_eq!( string.as_str(), "tests/command.rs", "Strings must be the same." ); + /// Ok( () ) + /// } + /// ``` + pub fn new() -> Self { + CommandRegistry { + registry: RefCell::new( + HashMap:: ) -> Result>::new() + ), + } + } + + /// Insert a new the command callback function to the registry. + /// + /// The [`Vec`]`<`[`String`]`>` passed to callback has following index meaning: + /// - 0: the command identifier, + /// - 1-: are the parameters used by the command callback. + /// + /// # Examples + /// + /// ``` + /// use i18n_pattern::{CommandRegistry, file_path, PlaceholderValue}; + /// use std::error::Error; + /// + /// fn command_registry() -> Result<(), Box> { + /// let registry = CommandRegistry::new(); + /// registry.insert( "file_path", file_path )?; + /// let mut strings = Vec::::new(); + /// strings.push( PlaceholderValue::String( "file_path".to_string() ) ); + /// strings.push( PlaceholderValue::String( "tests/command.rs".to_string() ) ); + /// let function = registry.get( "file_path" )?; + /// let string = function( strings )?; + /// assert_eq!( string.as_str(), "tests/command.rs", "Strings must be the same." ); + /// Ok( () ) + /// } + /// ``` + /// + /// [`Vec`]: std::vec::Vec + /// [`String`]: alloc::string::String + pub fn insert>( + &self, + command: T, + function: fn( Vec ) -> Result, + ) -> Result<(), CommandError> { + { + if self.registry.borrow().contains_key( command.as_ref() ) { + return Err( CommandError::AlreadyExists( command.as_ref().to_string() ) ); + } + } + self.registry.borrow_mut().insert( command.as_ref().to_string(), function ); + Ok( () ) + } + + /// Get the callback function for the command. + /// + /// # Examples + /// + /// ``` + /// use i18n_pattern::{CommandRegistry, file_path, PlaceholderValue}; + /// use std::error::Error; + /// + /// fn command_registry() -> Result<(), Box> { + /// let registry = CommandRegistry::new(); + /// registry.insert( "file_path", file_path )?; + /// let mut strings = Vec::::new(); + /// strings.push( PlaceholderValue::String( "file_path".to_string() ) ); + /// strings.push( PlaceholderValue::String( "tests/command.rs".to_string() ) ); + /// let function = registry.get( "file_path" )?; + /// let string = function( strings )?; + /// assert_eq!( string.as_str(), "tests/command.rs", "Strings must be the same." ); + /// Ok( () ) + /// } + /// ``` + pub fn get>( + &self, + command: T, + ) -> Result ) -> Result, CommandError> { + let binding = self.registry.borrow(); + let Some( result ) = binding.get( command.as_ref() ) else { + return Err( CommandError::NotFound( command.as_ref().to_string() ) ); + }; + Ok( result.to_owned() ) + } + + /// Returns a [`Vec`]`<`[`String`]`>` of the identifiers of all the registered commands. + /// + /// # Examples + /// + /// ``` + /// use i18n_pattern::{CommandRegistry, file_path}; + /// use std::error::Error; + /// + /// fn command_registry() -> Result<(), Box> { + /// let registry = CommandRegistry::new(); + /// registry.insert( "file_path", file_path )?; + /// let list = registry.list().iter().count(); + /// assert_eq!( list, 1, "Only 1 command." ); + /// Ok( () ) + /// } + /// ``` + /// + /// [`Vec`]: std::vec::Vec + /// [`String`]: alloc::string::String + pub fn list( &self ) -> Vec { + Vec::from_iter( self.registry.borrow().keys().map( |x| x.to_string() ) ) + } +} + +/// Format a file path according to the OS being used. +/// Requires 1 parameter having type of [`PlaceholderValue`]`::String`. +/// Additional parameters are simply ignored. +/// +/// [`PlaceholderValue`]: crate::PlaceholderValue +pub fn file_path( parameters: Vec ) -> Result { + let command = match ¶meters[ 0 ] { + PlaceholderValue::String( string ) => string, + + // Called from within `Formatter` methods, this is never reached. + _ => return Err( CommandError::InvalidType( "command identifier".to_string(), 0 ) ) + }; + if parameters.len() < 2 { + return Err( CommandError::ParameterMissing( command.clone(), 1 ) ); + } + let string = match ¶meters[ 1 ] { + PlaceholderValue::String( string ) => string, + _ => return Err( CommandError::InvalidType( command.clone(), 1 ) ) + }; + let path = PathBuf::from_str( &string ).unwrap(); + Ok( path.display().to_string() ) +} + +/// Select a or an for english words passed as parameter. +/// Requires 1 parameter having type of [`PlaceholderValue`]`::String`. +/// Additional parameters are simply ignored. +/// +/// [`PlaceholderValue`]: crate::PlaceholderValue +pub fn english_a_or_an( parameters: Vec ) -> Result { + let command = match ¶meters[ 0 ] { + PlaceholderValue::String( string ) => string, + + // Called from within `Formatter` methods, this is never reached. + _ => return Err( CommandError::InvalidType( "command identifier".to_string(), 0 ) ) + }; + if parameters.len() < 2 { + return Err( CommandError::ParameterMissing( command.clone(), 1 ) ); + } + let string = match ¶meters[ 1 ] { + PlaceholderValue::String( string ) => string, + _ => return Err( CommandError::InvalidType( command.clone(), 1 ) ) + }; + let mut chars = string.chars(); + match chars.next().unwrap() { + 'a' | 'e' | 'i' | 'o' | 'u' => return Ok( "an".to_string() ), + _ => return Ok( "a".to_string() ) + } +} diff --git a/crates/pattern/src/error.rs b/crates/pattern/src/error.rs index fe3e16d..5a490bf 100644 --- a/crates/pattern/src/error.rs +++ b/crates/pattern/src/error.rs @@ -27,19 +27,45 @@ pub enum ParserError { impl Error for ParserError {} impl Display for ParserError { - - /// Write to the formatter the default preformatted error message. fn fmt( &self, formatter: &mut Formatter ) -> Result { match self { - ParserError::EndedAbruptly => write!( formatter, "String ended abruptly." ), + ParserError::EndedAbruptly => write!( formatter, "The string ended abruptly." ), ParserError::UniqueNamed( identifier) => - write!( formatter, "Named substrings must have unique identifiers. ‘{}’ already exists.", identifier ), + write!( formatter, "Named substrings must have unique identifiers. The identifier ‘{}’ already exists.", identifier ), ParserError::InvalidToken( position, token ) => - write!( formatter, "Invalid token ‘{}’ found at position {} of the string.", token.string, position ), + write!( formatter, "Invalid token ‘{}’ was found at the position {} of the string.", token.string, position ), ParserError::MultiNumberSign( position ) => - write!( formatter, "Found sequential number signs at ‘{}’ of the string.", position ), + write!( formatter, "Found sequential number signs at the position {} of the string.", position ), ParserError::UniquePattern( identifier) => - write!( formatter, "Pattern identifiers must have unique. ‘{}’ already exists.", identifier ), + write!( formatter, "Pattern identifiers must be unique. The identifier ‘{}’ already exists.", identifier ), + } + } +} + +#[derive( Debug )] +#[non_exhaustive] +pub enum CommandError { + AlreadyExists( String ), + NotFound( String ), + ParameterMissing( String, usize ), + InvalidType( String, usize ), + Other( Box ), // For custom commands returning errors not of this enum. +} + +impl Error for CommandError {} + +impl Display for CommandError { + fn fmt( &self, formatter: &mut Formatter ) -> Result { + match self { + CommandError::AlreadyExists( command ) => + write!( formatter, "The command ‘{}’ already exists in the CommandRegistry.", command ), + CommandError::NotFound( command ) => + write!( formatter, "The command ‘{}’ was not found in the CommandRegistry.", command ), + CommandError::ParameterMissing( command, index ) => + write!( formatter, "The parameter number {} is missing for the command ‘{}’.", index, command ), + CommandError::InvalidType( command, index ) => + write!( formatter, "The parameter number {} has invalid type for the command ‘{}’.", index, command ), + CommandError::Other( ref error ) => error.fmt( formatter ), } } } @@ -77,56 +103,56 @@ pub enum FormatterError { PluralRules( PluralError ), FixedDecimal( FixedDecimalError ), NamedStringIdentifier( String ), + Command( CommandError ), + NeverReached, } impl Error for FormatterError {} impl Display for FormatterError { - - /// Write to the formatter the default preformatted error message. fn fmt( &self, formatter: &mut Formatter ) -> Result { match self { - FormatterError::InvalidRoot => write!( formatter, "Tree root must be a ‘Root’ node." ), + FormatterError::InvalidRoot => write!( formatter, "The tree root must be a ‘Root’ node." ), FormatterError::RetrieveChildren( node_type ) => - write!( formatter, "Failed to retrieve children for ‘{}’ node.", node_type ), + write!( formatter, "Failed to retrieve the children for the ‘{}’ node.", node_type ), FormatterError::NodeNotFound( node_type ) => - write!( formatter, "Expected node ‘{}’ was not found.", node_type ), + write!( formatter, "The expected node ‘{}’ was not found.", node_type ), FormatterError::FirstChild( node_type ) => - write!( formatter, "First child of ‘{}’ node not found.", node_type ), + write!( formatter, "The first child of the ‘{}’ node was not found.", node_type ), FormatterError::RetrieveNodeData( node_type ) => - write!( formatter, "Failed to retrieve data for ‘{}’ node.", node_type ), + write!( formatter, "Failed to retrieve the data for the ‘{}’ node.", node_type ), FormatterError::RetrieveNodeToken( node_type ) => - write!( formatter, "Failed to retrieve token for ‘{}’ node.", node_type ), + write!( formatter, "Failed to retrieve the token for the ‘{}’ node.", node_type ), FormatterError::LastChild( node_type ) => - write!( formatter, "Last child of ‘{}’ node not found.", node_type ), + write!( formatter, "The last child of the ‘{}’ node was not found.", node_type ), FormatterError::InvalidNode( node_type ) => - write!( formatter, "Invalid child node found in ‘{}’ node.", node_type ), + write!( formatter, "Invalid child node found in the ‘{}’ node.", node_type ), FormatterError::PatternNamed( identifier ) => - write!( formatter, "Failed to retrieve pattern for named string ‘{}’.", identifier ), + write!( formatter, "Failed to retrieve the pattern for the named string ‘{}’.", identifier ), FormatterError::PatternPart( identifier, index ) => write!( formatter, - "Failed to retrieve part ‘{}’ of pattern for named string ‘{}’.", + "Failed to retrieve the part ‘{}’ of the pattern for the named string ‘{}’.", identifier, index ), FormatterError::InvalidOptionValue( value, option, keyword ) => write!( formatter, - "Value ‘{}’ is invalid for option ‘{}’ for keyword ‘{}’.", + "The value ‘{}’ is invalid for the option ‘{}’ for the keyword ‘{}’.", value, option, keyword ), FormatterError::InvalidKeyword( keyword, placeholder ) => - write!( formatter, "Invalid keyword ‘{}’ for placeholder ‘{}’.", keyword, placeholder ), + write!( formatter, "Invalid keyword ‘{}’ for the placeholder ‘{}’.", keyword, placeholder ), FormatterError::SelectorNamed( named, selector, identifier ) => write!( formatter, - "Named string identifier ‘{}’ is not found for selector ‘{}’ of placeholder ‘{}’.", + "The named string identifier ‘{}’ was not found for the selector ‘{}’ of the placeholder ‘{}’.", named, selector, identifier ), FormatterError::SelectorOther( keyword, placeholder ) => write!( formatter, - "The required ‘other’ selector was not found for keyword ‘{}’ of placeholder ‘{}’.", + "The required ‘other’ selector was not found for the keyword ‘{}’ of the placeholder ‘{}’.", keyword, placeholder ), FormatterError::NoChildren( node_type) => @@ -134,32 +160,32 @@ impl Display for FormatterError { FormatterError::InvalidOption( option, keyword, placeholder ) => write!( formatter, - "Invalid for option ‘{}’ for keyword ‘{}’ of placeholder ‘{}’.", + "Invalid option ‘{}’ for the keyword ‘{}’ of the placeholder ‘{}’.", option, keyword, placeholder ), FormatterError::InvalidSelector( option, keyword, placeholder ) => write!( formatter, - "Invalid for selector ‘{}’ for keyword ‘{}’ of placeholder ‘{}’.", + "Invalid selector ‘{}’ for the keyword ‘{}’ of the placeholder ‘{}’.", option, keyword, placeholder ), FormatterError::Locale( ref error ) => error.fmt( formatter ), FormatterError::Calendar( ref error ) => error.fmt( formatter ), FormatterError::ParseInt( ref error ) => error.fmt( formatter ), FormatterError::NumberSignString( index ) => - write!( formatter, "Unable to retrieve formatted string for NumberSign index {}.", index ), + write!( formatter, "Unable to retrieve the formatted string for the NumberSign index {}.", index ), FormatterError::SelectorsIndex( index ) => - write!( formatter, "Index {} is not found in collected selectors.", index ), + write!( formatter, "The index {} is not found in the collected selectors.", index ), FormatterError::SelectorsIndexNamed( identifier, index ) => write!( formatter, - "Failed to retrieve string for named string ‘{}’ of the selectors index {}.", + "Failed to retrieve the string for the named string ‘{}’ of the selectors index {}.", identifier, index ), FormatterError::PlaceholderValue( part ) => - write!( formatter, "Placeholder value is not found for pattern part ‘{}’.", part ), + write!( formatter, "The placeholder value was not found for the pattern part ‘{}’.", part ), FormatterError::InvalidValue( part ) => - write!( formatter, "Invalid value type provided for pattern part ‘{}’.", part ), + write!( formatter, "Invalid value type was provided for the pattern part ‘{}’.", part ), FormatterError::Decimal( ref error ) => error.fmt( formatter ), FormatterError::DateTime( ref error ) => error.fmt( formatter ), FormatterError::PluralRules( ref error ) => error.fmt( formatter ), @@ -167,9 +193,12 @@ impl Display for FormatterError { FormatterError::NamedStringIdentifier( identifier ) => write!( formatter, - "Named string identifier ‘{}’ already exists. Identifiers must be unique and not ‘_’.", + "The named string identifier ‘{}’ already exists. The identifiers must be unique and not ‘_’.", identifier ), + FormatterError::Command( ref error ) => error.fmt( formatter ), + FormatterError::NeverReached => + write!( formatter, "Should never have reached this match branch." ), } } } @@ -215,3 +244,9 @@ impl From for FormatterError { FormatterError::FixedDecimal( error ) } } + +impl From for FormatterError { + fn from( error: CommandError ) -> FormatterError { + FormatterError::Command( error ) + } +} diff --git a/crates/pattern/src/formatter.rs b/crates/pattern/src/formatter.rs index 99aba6b..52b12f2 100644 --- a/crates/pattern/src/formatter.rs +++ b/crates/pattern/src/formatter.rs @@ -1,19 +1,18 @@ // This file is part of `i18n_pattern-rizzen-yazston` crate. For the terms of use, please see the file // called `LICENSE-BSD-3-Clause` at the top level of the `i18n_pattern-rizzen-yazston` crate. -// Outstanding: Command (to be done during or after `message` crate), experimental ICU4X components and options. // Feature option: language tag wrapping -use crate::{ NodeType, FormatterError, PlaceholderValue }; +use crate::{ NodeType, FormatterError, PlaceholderValue, CommandRegistry }; use i18n_icu::IcuDataProvider; use i18n_lexer::Token; -use i18n_lstring::LString; +use i18n_utility::LString; use tree::Tree; use icu_provider::prelude::*; use icu_locid::Locale; use icu_plurals::{ PluralCategory, PluralRules }; use icu_decimal::{ FixedDecimalFormatter, options }; -use fixed_decimal::{FixedDecimal, DoublePrecision, SignDisplay }; +use fixed_decimal::{ FixedDecimal, DoublePrecision, SignDisplay }; use icu_calendar::{ types::{ Time, IsoHour, IsoMinute, IsoSecond, NanoSecond }, DateTime, Date, Iso @@ -70,6 +69,7 @@ where patterns: HashMap>, numbers: Vec, selectors: Vec>, + command_registry: Rc, } impl<'a, P> Formatter<'a, P> @@ -88,14 +88,14 @@ where + DataProvider + DataProvider, { /// Creates a Formatter for a language string using parsing results. - /// During the creation of the formatter for the supplied Tree, the semantic analyse is done. + /// During the creation of the formatter for the supplied [`Tree`], the semantic analyse is done. /// /// # Examples /// /// ``` /// use i18n_icu::IcuDataProvider; /// use i18n_lexer::{Token, TokenType, tokenise}; - /// use i18n_pattern::{parse, NodeType, Formatter, FormatterError, PlaceholderValue}; + /// use i18n_pattern::{parse, NodeType, Formatter, FormatterError, PlaceholderValue, CommandRegistry}; /// use icu_testdata::buffer; /// use icu_provider::serde::AsDeserializingBufferProvider; /// use icu_locid::Locale; @@ -116,7 +116,10 @@ where /// let tree = parse( tokens.0 ).expect( "Failed to parse tokens." ); /// let locale: Rc = Rc::new( "en-ZA".parse().expect( "Failed to parse language tag." ) ); /// let language_tag = Rc::new( locale.to_string() ); - /// let mut formatter = Formatter::try_new( &icu_data_provider, &language_tag, &locale, &tree )?; + /// let command_registry = Rc::new( CommandRegistry::new() ); + /// let mut formatter = Formatter::try_new( + /// &icu_data_provider, &language_tag, &locale, &tree, &command_registry + /// )?; /// let mut values = HashMap::::new(); /// values.insert( /// "dogs_number".to_string(), @@ -131,11 +134,14 @@ where /// Ok( () ) /// } /// ``` + /// + /// [`Tree`]: tree::Tree pub fn try_new( data_provider: &Rc>, language_tag: &Rc, locale: &Rc, tree: &Tree, + command_registry: &Rc, ) -> Result, FormatterError> { let mut patterns = HashMap::>::new(); patterns.insert( "_".to_string(), Vec::::new() ); // Insert empty main pattern. @@ -149,6 +155,7 @@ where patterns, numbers, selectors, + command_registry: Rc::clone( command_registry ), } ); } let option_selectors = OptionSelectors { @@ -220,7 +227,7 @@ where locale, )?; } else if check_node_type( tree, *child, NodeType::Command ) { - // TODO: create a method for Command pattern as both NamedString and main String have it. + part_command( &mut pattern, tree, *child, command_registry )?; } else { return Err( FormatterError::InvalidNode( NodeType::String ) ); } @@ -255,7 +262,7 @@ where locale, )?; } else if check_node_type( tree, *child, NodeType::Command ) { - // TODO: create a method for Command pattern as both NamedString and main String have it. + part_command( &mut pattern, tree, *child, command_registry )?; } else { return Err( FormatterError::InvalidNode( NodeType::String ) ); } @@ -268,17 +275,18 @@ where patterns, numbers, selectors, + command_registry: Rc::clone( command_registry ), } ) } - /// Format the language string with supplied values. + /// Format the language string with supplied values as [`HashMap`]`<`[`String`]`, `[`PlaceholderValue`]`>`. /// /// # Examples /// /// ``` /// use i18n_icu::IcuDataProvider; /// use i18n_lexer::{Token, TokenType, tokenise}; - /// use i18n_pattern::{parse, NodeType, Formatter, FormatterError, PlaceholderValue}; + /// use i18n_pattern::{parse, NodeType, Formatter, FormatterError, PlaceholderValue, CommandRegistry}; /// use icu_testdata::buffer; /// use icu_provider::serde::AsDeserializingBufferProvider; /// use icu_locid::Locale; @@ -299,7 +307,10 @@ where /// let tree = parse( tokens.0 ).expect( "Failed to parse tokens." ); /// let locale: Rc = Rc::new( "en-ZA".parse().expect( "Failed to parse language tag." ) ); /// let language_tag = Rc::new( locale.to_string() ); - /// let mut formatter = Formatter::try_new( &icu_data_provider, &language_tag, &locale, &tree )?; + /// let command_registry = Rc::new( CommandRegistry::new() ); + /// let mut formatter = Formatter::try_new( + /// &icu_data_provider, &language_tag, &locale, &tree, &command_registry + /// )?; /// let mut values = HashMap::::new(); /// values.insert( /// "dogs_number".to_string(), @@ -314,6 +325,10 @@ where /// Ok( () ) /// } /// ``` + /// + /// [`HashMap`]: std::collections::HashMap + /// [`String`]: alloc::string::String + /// [`PlaceholderValue`]: crate::PlaceholderValue pub fn format( &mut self, values: &HashMap ) -> Result { if self.patterns.get( "_" ).unwrap().len() == 0 { return Ok( LString::new( String::new(), &self.language_tag ) ); @@ -650,6 +665,37 @@ where return Err( FormatterError::NumberSignString( *index ) ); }; string.push_str( number_string.as_str() ); + }, + PatternPart::Command{ strings } => { + let mut parameters = Vec::::new(); + let mut iterator = strings.iter(); + + // First string is always command identifier. + let first = iterator.next().unwrap(); + let command = match first { + PlaceholderValue::String( string ) => string, + + // Never reached. Always PlaceholderValue::String. + _ => return Err( FormatterError::NeverReached ) + }; + parameters.push( first.clone() ); + + // If parameter is same as placeholder, take the placeholder value instead. + while let Some( parameter ) = iterator.next() { + let string = match parameter { + PlaceholderValue::String( string ) => string, + + // Never reached. Always PlaceholderValue::String. + _ => return Err( FormatterError::NeverReached ) + }; + if let Some( value ) = values.get( string ) { + parameters.push( value.clone() ); + } else { + parameters.push( parameter.clone() ); + } + } + let function = self.command_registry.get( command )?; + string.push_str( &function( parameters )? ); } } i += 1; @@ -751,7 +797,7 @@ where } } -/// Decomposes an ISO 8601 date string into a `Date` struct. +/// Decomposes an ISO 8601 date string into a [`Date`]`<`[`Iso`]`>` struct. /// /// Supported ISO 8601 extended and basic formats: /// YYYY-MM-DD or YYYYMMDD @@ -782,6 +828,9 @@ where /// /// ISO 8601 _Week_ and _Ordinal date_ formats are not supported as there are currently no methods available for /// ICU4X `Date` for creating structs using the week number or the ordinal day of the year. +/// +/// [`Date`]: icu_calendar::Date +/// [`Iso`]: icu_calendar::Iso pub fn decompose_iso_date( string: &str ) -> Result, FormatterError> { let no_plus = string.trim_start_matches( '+' ); let mut year: i32 = 0; @@ -839,7 +888,7 @@ pub fn decompose_iso_date( string: &str ) -> Result, FormatterError> { Ok( result ) } -/// Decomposes an ISO time string into a `Time` struct. +/// Decomposes an ISO time string into a [`Time`] struct. /// /// Supported ISO 8601 extended and basic formats: /// Thh:mm:ss.nnn or Thhmmss.nnn @@ -859,7 +908,8 @@ pub fn decompose_iso_date( string: &str ) -> Result, FormatterError> { /// Time zones are not supported by ICU4X `Time`, thus will be ignored. /// - time zones ( Z (for UCT 00:00), +hh:mm, -hh:mm, +hhmm, -hhmm ). /// -00:00 or -0000 are not supported by ISO 8601. - +/// +/// [`Time`]: icu_calendar::types::Time pub fn decompose_iso_time( string: &str ) -> Result { let no_t = string.trim_start_matches( 'T' ); let no_plus = match no_t.find( '+' ) { @@ -914,7 +964,7 @@ fn check_node_type( tree: &Tree, index: usize, node_type: NodeType ) -> bool { let Ok( node_type_data ) = tree.node_type( index ) else { return false; }; - let Some( node_type2 ) = node_type_data.downcast_ref::() else { + let Some( node_type2 ) = node_type_data.as_ref().unwrap().downcast_ref::() else { return false; }; if node_type != *node_type2 { @@ -1184,6 +1234,82 @@ fn part_pattern( Ok( () ) } +// Commands always returns static text +fn part_command( + pattern: &mut Vec, + tree: &Tree, + index: usize, + command_registry: &Rc, +) -> Result<(), FormatterError> { + let mut delay = false; + let mut parameters = Vec::::new(); + let Ok( children ) = tree.children( index ) else { + return Err( FormatterError::RetrieveChildren( NodeType::Pattern ) ); + }; + let mut iterator = children.iter(); + + // Identifier - first node + let Some( command ) = iterator.next() else { + return Err( FormatterError::NoChildren( NodeType::Pattern ) ); + }; + if !check_node_type( tree, *command, NodeType::Identifier ) { + return Err( FormatterError::NodeNotFound( NodeType::Identifier ) ); + } + let Ok( command_data ) = tree.data_ref( *command ) else { + return Err( FormatterError::RetrieveNodeData( NodeType::Identifier ) ); + }; + let Some( command_token ) = command_data.first().unwrap().downcast_ref::>() else { + return Err( FormatterError::RetrieveNodeToken( NodeType::Identifier ) ); + }; + parameters.push( PlaceholderValue::String( command_token.as_ref().string.to_string() ) ); + + // Check if delay command marker `#` is present. + //peek ahead so not to interfere with while if not present. + let mut iterator_peeking = iterator.clone(); + let command_next = iterator_peeking.next(); + if command_next.is_some() && check_node_type( tree, *command_next.unwrap(), NodeType::NumberSign ) { + delay = true; + iterator = iterator_peeking; + } + + // Rest can be either Identifier or Text nodes. + while let Some( parameter ) = iterator.next() { + if check_node_type( tree, *parameter, NodeType::Identifier ) { + let Ok( identifier_data ) = tree.data_ref( *parameter ) else { + return Err( FormatterError::RetrieveNodeData( NodeType::Identifier ) ); + }; + let Some( identifier_token ) = identifier_data.first().unwrap().downcast_ref::>() + else { + return Err( FormatterError::RetrieveNodeToken( NodeType::Identifier ) ); + }; + parameters.push( PlaceholderValue::String( identifier_token.as_ref().string.to_string() ) ); + } else if check_node_type( tree, *parameter, NodeType::Text ) { + let mut string = String::new(); + let Ok( text_data ) = tree.data_ref( *parameter ) else { + return Err( FormatterError::RetrieveNodeData( NodeType::Text ) ); + }; + for token_data in text_data.iter() { + let Some( token ) = token_data.downcast_ref::>() else { + return Err( FormatterError::RetrieveNodeToken( NodeType::Text ) ); + }; + string.push_str( token.string.as_str() ); + } + parameters.push( PlaceholderValue::String( string ) ); + } else { + return Err( FormatterError::InvalidNode( NodeType::Command ) ); + } + } + if delay { + pattern.push( PatternPart::Command{ strings: parameters } ); + } else { + let function = command_registry.get( + command_token.as_ref().string.to_string() + )?; + pattern.push( PatternPart::Text( function( parameters )? ) ); + } + Ok( () ) +} + fn pattern_selectors( tree: &Tree, index: usize, @@ -1320,4 +1446,7 @@ enum PatternPart { complex: ComplexType, selectors: usize, }, + Command{ + strings: Vec, + } } diff --git a/crates/pattern/src/lib.rs b/crates/pattern/src/lib.rs index 7a1de2b..5569c85 100644 --- a/crates/pattern/src/lib.rs +++ b/crates/pattern/src/lib.rs @@ -18,7 +18,9 @@ //! ``` //! use i18n_icu::IcuDataProvider; //! use i18n_lexer::{Token, TokenType, tokenise}; -//! use i18n_pattern::{ parse, NodeType, Formatter, FormatterError, PlaceholderValue }; +//! use i18n_pattern::{ +//! parse, NodeType, Formatter, FormatterError, PlaceholderValue, CommandRegistry, english_a_or_an +//! }; //! use icu_testdata::buffer; //! use icu_provider::serde::AsDeserializingBufferProvider; //! use icu_locid::Locale; @@ -38,7 +40,10 @@ //! let tree = parse( tokens.0 )?; //! let locale: Rc = Rc::new( "en-ZA".parse()? ); //! let language_tag = Rc::new( locale.to_string() ); -//! let mut formatter = Formatter::try_new( &icu_data_provider, &language_tag, &locale, &tree )?; +//! let command_registry = Rc::new( CommandRegistry::new() ); +//! let mut formatter = Formatter::try_new( +//! &icu_data_provider, &language_tag, &locale, &tree, &command_registry +//! )?; //! let mut values = HashMap::::new(); //! values.insert( //! "dogs_number".to_string(), @@ -48,14 +53,50 @@ //! assert_eq!( result.as_str(), "There are 3 dogs in the park.", "Strings must be the same." ); //! Ok( () ) //! } +//! +//! fn command_delayed() -> Result<(), Box> { +//! let buffer_provider = buffer(); +//! let data_provider = buffer_provider.as_deserializing(); +//! let icu_data_provider = +//! Rc::new( IcuDataProvider::try_new( &data_provider )? ); +//! let tokens = tokenise( +//! "At night {#english_a_or_an# hunter} {hunter} stalked {#english_a_or_an # prey} {prey}.", +//! &vec![ '{', '}', '`', '#' ], +//! &icu_data_provider, +//! ); +//! let tree = parse( tokens.0 )?; +//! let locale: Rc = Rc::new( "en-ZA".parse()? ); +//! let language_tag = Rc::new( locale.to_string() ); +//! let command_registry = Rc::new( CommandRegistry::new() ); +//! command_registry.insert( "english_a_or_an", english_a_or_an )?; +//! let mut formatter = +//! Formatter::try_new( &icu_data_provider, &language_tag, &locale, &tree, &command_registry )?; +//! let mut values = HashMap::::new(); +//! values.insert( +//! "hunter".to_string(), +//! PlaceholderValue::String( "owl".to_string() ) +//! ); +//! values.insert( +//! "prey".to_string(), +//! PlaceholderValue::String( "mouse".to_string() ) +//! ); +//! let result = formatter.format( &values )?; +//! assert_eq!( +//! result.as_str(), +//! "At night an owl stalked a mouse.", +//! "Strings must be the same." +//! ); +//! Ok( () ) +//! } //! ``` pub mod types; -pub mod parser; -pub mod formatter; -pub mod error; - pub use types::*; +pub mod parser; pub use parser::*; +pub mod command; +pub use command::*; +pub mod formatter; pub use formatter::*; +pub mod error; pub use error::*; diff --git a/crates/pattern/src/parser.rs b/crates/pattern/src/parser.rs index 0158fac..16ccd60 100644 --- a/crates/pattern/src/parser.rs +++ b/crates/pattern/src/parser.rs @@ -4,13 +4,13 @@ use crate::ParserError; use crate::types::*; use i18n_lexer::{ Token, TokenType }; -use tree::{ Tree, NodeFeatures }; +use tree::{ Tree, ALLOW_CHILDREN, ALLOW_DATA }; use std::rc::Rc; use std::collections::HashMap; use core::fmt::{ Display, Formatter, Result as FmtResult }; -/// Constructs a valid syntax tree from the supplied `Vec>`. Any grammar error that occurs will result in -/// an `Err()` result being returned. +/// Constructs a valid syntax tree [`Tree`] from the supplied [`Vec`]`<`[`Rc`]`<`[`Token`]`>>`. Any grammar error that +/// occurs will result in an `Err()` result being returned. /// /// Implicit syntax tokens and optional whitespace tokens are not included in syntax trees. /// @@ -21,7 +21,7 @@ use core::fmt::{ Display, Formatter, Result as FmtResult }; /// ``` /// use i18n_icu::IcuDataProvider; /// use i18n_lexer::{Token, TokenType, tokenise}; -/// use i18n_pattern::{parse, NodeType, Formatter, FormatterError, PlaceholderValue}; +/// use i18n_pattern::{parse, NodeType, Formatter, FormatterError, PlaceholderValue, CommandRegistry}; /// use icu_testdata::buffer; /// use icu_provider::serde::AsDeserializingBufferProvider; /// use icu_locid::Locale; @@ -41,7 +41,10 @@ use core::fmt::{ Display, Formatter, Result as FmtResult }; /// let tree = parse( tokens.0 )?; /// let locale: Rc = Rc::new( "en-ZA".parse()? ); /// let language_tag = Rc::new( locale.to_string() ); -/// let mut formatter = Formatter::try_new( &icu_data_provider, &language_tag, &locale, &tree )?; +/// let command_registry = Rc::new( CommandRegistry::new() ); +/// let mut formatter = Formatter::try_new( +/// &icu_data_provider, &language_tag, &locale, &tree, &command_registry +/// )?; /// let mut values = HashMap::::new(); /// values.insert( /// "dogs_number".to_string(), @@ -52,17 +55,28 @@ use core::fmt::{ Display, Formatter, Result as FmtResult }; /// Ok( () ) /// } /// ``` +/// +/// [`Tree`]: tree::Tree +/// [`Vec`]: std::vec::Vec +/// [`Rc`]: std::rc::Rc +/// [`Token`]: i18n_lexer::Token pub fn parse( tokens: Vec> ) -> Result { let mut tree = Tree::new(); if tokens.len() == 0 { return Ok( tree ); } - tree.insert( 0, CONTAINER, Box::new( NodeType::Root ) ).ok(); + tree.insert( + 0, + ALLOW_CHILDREN, + Some( Box::new( NodeType::Root ) ), + None, + ).ok(); let mut parser = Parser { current: tree.insert( 0, - CONTAINER, - Box::new( NodeType::String ) + ALLOW_CHILDREN, + Some( Box::new( NodeType::String ) ), + None, ).ok(), state: ParserStates::String, nested_states: Vec::::new(), @@ -84,7 +98,7 @@ pub fn parse( tokens: Vec> ) -> Result { &mut tree, &mut parser, NodeType::Text, - LEAF, + ALLOW_DATA, token_next ); iterator = iterator_peeking; // Skip over ` token. @@ -102,7 +116,7 @@ pub fn parse( tokens: Vec> ) -> Result { iterator = iterator_peeking; // Skip over { and next token. } else if string == "#" { parser.current = Some( 0 ); // Move to root. - create_node( &mut tree, &mut parser, NodeType::NamedGroup, CONTAINER ); + create_node( &mut tree, &mut parser, NodeType::NamedGroup, ALLOW_CHILDREN ); parser.state = ParserStates::NamedGroup; } else { return Err( ParserError::InvalidToken( token.position_grapheme, Rc::clone( &token ) ) ); @@ -112,7 +126,7 @@ pub fn parse( tokens: Vec> ) -> Result { &mut tree, &mut parser, NodeType::Text, - LEAF, + ALLOW_DATA, token ); } @@ -123,7 +137,8 @@ pub fn parse( tokens: Vec> ) -> Result { if string == "#" { let parent = tree.parent( parser.current.unwrap() ).unwrap(); if let Some( last ) = tree.last( parent ).ok() { - let node_type_data = tree.node_type( last ).ok().unwrap(); + let node_type_data = + tree.node_type( last ).ok().unwrap().as_ref().unwrap(); let node_type = node_type_data.downcast_ref::().unwrap(); if *node_type == NodeType::NumberSign { return Err( ParserError::MultiNumberSign( token.position_grapheme ) ); @@ -133,7 +148,7 @@ pub fn parse( tokens: Vec> ) -> Result { &mut tree, &mut parser, NodeType::NumberSign, - LEAF, + ALLOW_DATA, token ); parser.state = ParserStates::SubString; @@ -146,7 +161,7 @@ pub fn parse( tokens: Vec> ) -> Result { &mut tree, &mut parser, NodeType::Text, - LEAF, + ALLOW_DATA, token_next ); iterator = iterator_peeking; // Skip over ` token. @@ -168,7 +183,9 @@ pub fn parse( tokens: Vec> ) -> Result { if tree.children( *parser.current.as_ref().unwrap() ).ok().as_ref().unwrap().len() != 2 { - return Err( ParserError::InvalidToken( token.position_grapheme, Rc::clone( &token ) ) ); + return Err( + ParserError::InvalidToken( token.position_grapheme, Rc::clone( &token ) ) + ); } // Ends NamedString, and returns to NamedGroup @@ -181,7 +198,7 @@ pub fn parse( tokens: Vec> ) -> Result { &mut tree, &mut parser, NodeType::Text, - LEAF, + ALLOW_DATA, token ); } @@ -192,7 +209,7 @@ pub fn parse( tokens: Vec> ) -> Result { &mut tree, &mut parser, NodeType::Identifier, - LEAF, + ALLOW_DATA, token ); move_to_container( &tree, &mut parser ); @@ -212,12 +229,12 @@ pub fn parse( tokens: Vec> ) -> Result { }, ParserStates::Keyword => {// Valid tokens: PWS (separator - ignore), }, Identifier if token.token_type == TokenType::Identifier { - create_node( &mut tree, &mut parser, NodeType::Selector, CONTAINER ); + create_node( &mut tree, &mut parser, NodeType::Selector, ALLOW_CHILDREN ); add_token( &mut tree, &mut parser, NodeType::Identifier, - LEAF, + ALLOW_DATA, token ); move_to_container( &tree, &mut parser ); @@ -237,7 +254,7 @@ pub fn parse( tokens: Vec> ) -> Result { &mut tree, &mut parser, NodeType::Identifier, - LEAF, + ALLOW_DATA, token_next_2nd ); move_to_container( &tree, &mut parser ); @@ -263,7 +280,7 @@ pub fn parse( tokens: Vec> ) -> Result { &mut tree, &mut parser, NodeType::Text, - LEAF, + ALLOW_DATA, token_next ); iterator = iterator_peeking; // Skip over 1st ` token. @@ -274,7 +291,7 @@ pub fn parse( tokens: Vec> ) -> Result { continue; } } - add_token( &mut tree, &mut parser, NodeType::Text, LEAF, token ); + add_token( &mut tree, &mut parser, NodeType::Text, ALLOW_DATA, token ); }, ParserStates::Literal => {// Valid tokens: } if token.token_type == TokenType::Grammar { @@ -285,24 +302,25 @@ pub fn parse( tokens: Vec> ) -> Result { } return Err( ParserError::InvalidToken( token.position_grapheme, Rc::clone( &token ) ) ); }, - ParserStates::Command => {// Valid tokens: PWS (separator - ignore), `, }, Identifier + ParserStates::Command => {// Valid tokens: PWS (separator - ignore), `, }, #, Identifier if token.token_type == TokenType::Identifier { create_node_add_token( &mut tree, &mut parser, NodeType::Identifier, - LEAF, + ALLOW_DATA, token ); move_to_container( &tree, &mut parser ); } else if token.token_type == TokenType::Grammar { let string = token.string.as_str(); - if string == "{" { - // None (only identifier provide with default type of preformatted string) + if string == "}" { if tree.children( *parser.current.as_ref().unwrap() - ).ok().as_ref().unwrap().len() != 1 { - return Err( ParserError::InvalidToken( token.position_grapheme, Rc::clone( &token ) ) ); + ).ok().as_ref().unwrap().len() < 1 { + return Err( + ParserError::InvalidToken( token.position_grapheme, Rc::clone( &token ) ) + ); } end_nested_state( &tree, &mut parser ); } else if string == "`" { @@ -313,12 +331,28 @@ pub fn parse( tokens: Vec> ) -> Result { create_node_add_token( &mut tree,&mut parser, NodeType::Text, - LEAF, + ALLOW_DATA, token_next ); iterator = iterator_peeking; // Skip over ` token. parser.nested_states.push( ParserStates::Command ); parser.state = ParserStates::LiteralText; + } else if string == "#" { + // Only valid after command identifier. + if tree.children( + *parser.current.as_ref().unwrap() + ).ok().as_ref().unwrap().len() != 1 { + return Err( + ParserError::InvalidToken( token.position_grapheme, Rc::clone( &token ) ) + ); + } + create_node_add_token( + &mut tree, + &mut parser, + NodeType::NumberSign, + ALLOW_DATA, + token + ); } else { return Err( ParserError::InvalidToken( token.position_grapheme, Rc::clone( &token ) ) ); } @@ -341,7 +375,7 @@ pub fn parse( tokens: Vec> ) -> Result { &mut tree, &mut parser, NodeType::Identifier, - LEAF, + ALLOW_DATA, token ); move_to_container( &tree, &mut parser ); @@ -355,13 +389,13 @@ pub fn parse( tokens: Vec> ) -> Result { } } } else if len == 1 { - create_node( &mut tree, &mut parser, NodeType::String, CONTAINER ); + create_node( &mut tree, &mut parser, NodeType::String, ALLOW_CHILDREN ); parser.nested_states.push( ParserStates::NamedString ); create_node_add_token( &mut tree, &mut parser, NodeType::Text, - LEAF, + ALLOW_DATA, token ); parser.state = ParserStates::SubString; @@ -375,15 +409,17 @@ pub fn parse( tokens: Vec> ) -> Result { if tree.children( *parser.current.as_ref().unwrap() ).ok().as_ref().unwrap().len() != 1 { - return Err( ParserError::InvalidToken( token.position_grapheme, Rc::clone( &token ) ) ); + return Err( + ParserError::InvalidToken( token.position_grapheme, Rc::clone( &token ) ) + ); } - create_node( &mut tree, &mut parser, NodeType::String, CONTAINER ); + create_node( &mut tree, &mut parser, NodeType::String, ALLOW_CHILDREN ); parser.nested_states.push( ParserStates::NamedString ); create_node_add_token( &mut tree, &mut parser, NodeType::NumberSign, - LEAF, + ALLOW_DATA, token ); parser.state = ParserStates::SubString; @@ -392,7 +428,9 @@ pub fn parse( tokens: Vec> ) -> Result { if tree.children( *parser.current.as_ref().unwrap() ).ok().as_ref().unwrap().len() != 1 { - return Err( ParserError::InvalidToken( token.position_grapheme, Rc::clone( &token ) ) ); + return Err( + ParserError::InvalidToken( token.position_grapheme, Rc::clone( &token ) ) + ); } let mut iterator_peeking = iterator.clone(); let Some( token_next ) = iterator_peeking.next() else { @@ -413,19 +451,21 @@ pub fn parse( tokens: Vec> ) -> Result { if tree.children( *parser.current.as_ref().unwrap() ).ok().as_ref().unwrap().len() != 1 { - return Err( ParserError::InvalidToken( token.position_grapheme, Rc::clone( &token ) ) ); + return Err( + ParserError::InvalidToken( token.position_grapheme, Rc::clone( &token ) ) + ); } let mut iterator_peeking = iterator.clone(); let Some( token_next ) = iterator_peeking.next() else { return Err( ParserError::EndedAbruptly ); }; - create_node( &mut tree, &mut parser, NodeType::String, CONTAINER ); + create_node( &mut tree, &mut parser, NodeType::String, ALLOW_CHILDREN ); parser.nested_states.push( ParserStates::NamedString ); create_node_add_token( &mut tree, &mut parser, NodeType::Text, - LEAF, + ALLOW_DATA, token_next ); iterator = iterator_peeking; // Skip over ` token. @@ -440,13 +480,13 @@ pub fn parse( tokens: Vec> ) -> Result { ).ok().as_ref().unwrap().len() != 1 { return Err( ParserError::InvalidToken( token.position_grapheme, Rc::clone( &token ) ) ); } - create_node( &mut tree, &mut parser, NodeType::String, CONTAINER ); + create_node( &mut tree, &mut parser, NodeType::String, ALLOW_CHILDREN ); parser.nested_states.push( ParserStates::NamedString ); create_node_add_token( &mut tree, &mut parser, NodeType::Text, - LEAF, + ALLOW_DATA, token ); } else if token.token_type == TokenType::WhiteSpace { @@ -468,7 +508,7 @@ pub fn parse( tokens: Vec> ) -> Result { } // start of NamedString - create_node( &mut tree, &mut parser, NodeType::NamedString, CONTAINER ); + create_node( &mut tree, &mut parser, NodeType::NamedString, ALLOW_CHILDREN ); parser.nested_states.push( ParserStates::NamedGroup ); parser.state = ParserStates::NamedString; } else if token.token_type == TokenType::WhiteSpace { @@ -486,9 +526,6 @@ pub fn parse( tokens: Vec> ) -> Result { // Internal structures, enums, etc. -const CONTAINER: NodeFeatures = NodeFeatures { allow_children: true, allow_data: false }; -const LEAF: NodeFeatures = NodeFeatures { allow_children: false, allow_data: true }; - // Various ParserStates the tokens may be in. #[derive( PartialEq, Copy, Clone )] enum ParserStates { @@ -526,12 +563,12 @@ struct Parser { nested_states: Vec, } -// Move `current` to its parent node only if `current` is a leaf node. -// Usually this signals the leaf has all its tokens. +// Move `current` to its parent node only if `current` is a ALLOW_DATA node. +// Usually this signals the ALLOW_DATA has all its tokens. fn move_to_container( tree: &Tree, parser: &mut Parser, ) { let node_index = *parser.current.as_ref().unwrap(); - if !tree.features( node_index ).ok().unwrap().allow_children { - parser.current = tree.parent( node_index ).ok(); // Root node always a container. + if tree.features( node_index ).unwrap() & ALLOW_CHILDREN != ALLOW_CHILDREN { + parser.current = tree.parent( node_index ).ok(); // Root node always a ALLOW_CHILDREN. } } @@ -541,13 +578,14 @@ fn create_node( tree: &mut Tree, parser: &mut Parser, node_type: NodeType, - features: NodeFeatures, + features: u8, ) { move_to_container( tree, parser ); parser.current = tree.insert( parser.current.take().unwrap(), features, - Box::new( node_type ) + Some( Box::new( node_type ) ), + None, ).ok(); } @@ -557,14 +595,15 @@ fn create_node_add_token( tree: &mut Tree, parser: &mut Parser, node_type: NodeType, - features: NodeFeatures, + features: u8, token: &Rc ) { move_to_container( tree, parser ); parser.current = tree.insert( parser.current.take().unwrap(), features, - Box::new( node_type ) + Some( Box::new( node_type ) ), + None, ).ok(); tree.data_mut( *parser.current.as_ref().unwrap() ).unwrap().push( Box::new( Rc::clone( &token ) ) @@ -578,11 +617,11 @@ fn add_token( tree: &mut Tree, parser: &mut Parser, node_type: NodeType, - features: NodeFeatures, + features: u8, token: &Rc ) { let current = *parser.current.as_ref().unwrap(); - let node_type_ref = tree.node_type( current ).ok().unwrap().as_ref(); + let node_type_ref = tree.node_type( current ).ok().unwrap().as_ref().unwrap(); let node_type_is = node_type_ref.downcast_ref::().unwrap(); if *node_type_is == node_type { tree.data_mut( current ).ok().unwrap().push( Box::new( Rc::clone( &token ) ) ); @@ -609,12 +648,12 @@ fn pattern_start( ) -> Result<(), ParserError> { if token_next.token_type == TokenType::Identifier { // Multilingual pattern - create_node( tree, parser, NodeType::Pattern, CONTAINER ); + create_node( tree, parser, NodeType::Pattern, ALLOW_CHILDREN ); if patterns.contains_key( &token_next.string ) { return Err( ParserError::UniquePattern( token_next.string.as_str().to_string() ) ); } patterns.insert( token_next.string.as_str().to_string(), *parser.current.as_ref().unwrap() ); - create_node_add_token( tree, parser, NodeType::Identifier, LEAF, token_next ); + create_node_add_token( tree, parser, NodeType::Identifier, ALLOW_DATA, token_next ); move_to_container( tree, parser ); // Move back to Pattern node. parser.nested_states.push( parser.state ); parser.state = ParserStates::Pattern; @@ -622,13 +661,13 @@ fn pattern_start( // Future: may allow empty {} to be treated as literal {} if token_next.string.as_str() == "`" { // Literal pattern. - create_node( tree, parser, NodeType::Text, LEAF ); + create_node( tree, parser, NodeType::Text, ALLOW_DATA ); parser.nested_states.push( parser.state ); parser.nested_states.push( ParserStates::Literal ); parser.state = ParserStates::LiteralText; } else if token_next.string.as_str() == "#" { // Command pattern. - create_node( tree, parser, NodeType::Command, CONTAINER ); + create_node( tree, parser, NodeType::Command, ALLOW_CHILDREN ); parser.nested_states.push( parser.state ); parser.state = ParserStates::Command; } else { @@ -645,6 +684,6 @@ fn pattern_start( #[allow( dead_code )] fn node_type_to_string( tree: &Tree, parser: &mut Parser, ) -> String { let current = *parser.current.as_ref().unwrap(); - let node_type_ref = tree.node_type( current ).ok().unwrap().as_ref(); + let node_type_ref = tree.node_type( current ).ok().unwrap().as_ref().unwrap(); node_type_ref.downcast_ref::().unwrap().to_string() } diff --git a/crates/pattern/src/types.rs b/crates/pattern/src/types.rs index ce6eb3a..fd3080f 100644 --- a/crates/pattern/src/types.rs +++ b/crates/pattern/src/types.rs @@ -3,7 +3,7 @@ //! Collection of types used by `parser` and `formatter` modules. -use i18n_lstring::LString; +use i18n_utility::LString; use fixed_decimal::FixedDecimal; use icu_calendar::{Iso, Date, DateTime, types::Time }; use core::fmt::{ Debug, Display, Formatter, Result as FmtResult }; @@ -51,10 +51,29 @@ impl Display for NodeType { } } -// --- placeholder value types --- - -/// An enum consists of a selection of Rust primitives, ICU4X types, and `LString` for messages. -#[derive( Debug )] +/// An enum consists of a selection of Rust primitives, ICU4X types, and [`LString`] for messages. +/// The following are types are available: +/// * String( [`String`] ), Can also be used for date (ISO format), time (ISO format), fixed decimal. +/// * Integer( [`i128`] ), +/// * Unsigned( [`u128`] ), +/// * Float( [`f64`] ), +/// * LString( [`LString`] ), +/// * FixedDecimal( [`FixedDecimal`] ), +/// * DateTime( [`DateTime`]`<`[`Iso`]`>` ), +/// * Date( [`Date`]`<`[`Iso`]`>` ), +/// * Time( [`Time`] ), +/// +/// [`LString`]: i18n_utility::LString +/// [`String`]: alloc::string::String +/// [`i128`]: core::i128 +/// [`u128`]: core::u128 +/// [`f64`]: core::f64 +/// [`FixedDecimal`]: fixed_decimal::FixedDecimal +/// [`DateTime`]: icu_calendar::DateTime +/// [`Iso`]: icu_calendar::Iso +/// [`Date`]: icu_calendar::Date +/// [`Time`]: icu_calendar::types::Time +#[derive( Debug, Clone )] pub enum PlaceholderValue { String( String ), // Can also be used for date (ISO format), time (ISO format), fixed decimal. Integer( i128 ), diff --git a/crates/pattern/tests/command.rs b/crates/pattern/tests/command.rs new file mode 100644 index 0000000..a50669c --- /dev/null +++ b/crates/pattern/tests/command.rs @@ -0,0 +1,36 @@ +// This file is part of `i18n_pattern-rizzen-yazston` crate. For the terms of use, please see the file +// called `LICENSE-BSD-3-Clause` at the top level of the `i18n_pattern-rizzen-yazston` crate. + +//! Testing command functions. + +use i18n_pattern::{ CommandRegistry, file_path, PlaceholderValue }; +use os_info; +use std::error::Error; + +#[test] +fn test_file_path() -> Result<(), Box> { + let mut strings = Vec::::new(); + strings.push( PlaceholderValue::String( "file_path".to_string() ) ); + strings.push( PlaceholderValue::String( "tests/command.rs".to_string() ) ); + let string = file_path( strings )?; + let info = os_info::get(); + if info.os_type() == os_info::Type::Windows { + assert_eq!( string.as_str(), "tests\\command.rs", "Should be Windows path." ); + } else { + assert_eq!( string.as_str(), "tests/command.rs", "Should be non-Windows path." ); + } + Ok( () ) +} + +#[test] +fn command_registry() -> Result<(), Box> { + let registry = CommandRegistry::new(); + registry.insert( "file_path", file_path )?; + let mut strings = Vec::::new(); + strings.push( PlaceholderValue::String( "file_path".to_string() ) ); + strings.push( PlaceholderValue::String( "tests/command.rs".to_string() ) ); + let function = registry.get( "file_path" )?; + let string = function( strings )?; + assert_eq!( string.as_str(), "tests/command.rs", "Strings must be the same." ); + Ok( () ) +} diff --git a/crates/pattern/tests/formatter.rs b/crates/pattern/tests/formatter.rs index 8c4c1c8..2d74056 100644 --- a/crates/pattern/tests/formatter.rs +++ b/crates/pattern/tests/formatter.rs @@ -5,11 +5,12 @@ use i18n_icu::IcuDataProvider; use i18n_lexer::tokenise; -use i18n_pattern::{ parse, Formatter, PlaceholderValue }; +use i18n_pattern::{ parse, Formatter, PlaceholderValue, CommandRegistry, file_path, english_a_or_an }; use icu_testdata::buffer; use icu_provider::serde::AsDeserializingBufferProvider; use icu_locid::Locale; use icu_calendar::{ Iso, DateTime, Date, types::Time }; +use os_info; use std::collections::HashMap; use std::{ rc::Rc, error::Error }; @@ -26,8 +27,9 @@ fn plain_text() -> Result<(), Box> { let tree = parse( tokens.0 )?; let locale: Rc = Rc::new( "en-ZA".parse()? ); let language_tag = Rc::new( locale.to_string() ); + let command_registry = Rc::new( CommandRegistry::new() ); let mut formatter = - Formatter::try_new( &icu_data_provider, &language_tag, &locale, &tree )?; + Formatter::try_new( &icu_data_provider, &language_tag, &locale, &tree, &command_registry )?; let values = HashMap::::new(); let result = formatter.format( &values )?; assert_eq!( result.as_str(), "A simple plain text string.", "Strings must be the same." ); @@ -47,8 +49,9 @@ fn pattern_string() -> Result<(), Box> { let tree = parse( tokens.0 )?; let locale: Rc = Rc::new( "en-ZA".parse()? ); let language_tag = Rc::new( locale.to_string() ); + let command_registry = Rc::new( CommandRegistry::new() ); let mut formatter = - Formatter::try_new( &icu_data_provider, &language_tag, &locale, &tree )?; + Formatter::try_new( &icu_data_provider, &language_tag, &locale, &tree, &command_registry )?; let mut values = HashMap::::new(); values.insert( "string".to_string(), @@ -77,8 +80,9 @@ fn pattern_plural() -> Result<(), Box> { let tree = parse( tokens.0 )?; let locale: Rc = Rc::new( "en-ZA".parse()? ); let language_tag = Rc::new( locale.to_string() ); + let command_registry = Rc::new( CommandRegistry::new() ); let mut formatter = - Formatter::try_new( &icu_data_provider, &language_tag, &locale, &tree )?; + Formatter::try_new( &icu_data_provider, &language_tag, &locale, &tree, &command_registry )?; let mut values = HashMap::::new(); values.insert( "dogs_number".to_string(), @@ -107,8 +111,9 @@ fn pattern_decimal() -> Result<(), Box> { let tree = parse( tokens.0 )?; let locale: Rc = Rc::new( "en-ZA".parse()? ); let language_tag = Rc::new( locale.to_string() ); + let command_registry = Rc::new( CommandRegistry::new() ); let mut formatter = - Formatter::try_new( &icu_data_provider, &language_tag, &locale, &tree )?; + Formatter::try_new( &icu_data_provider, &language_tag, &locale, &tree, &command_registry )?; let mut values = HashMap::::new(); values.insert( "amount".to_string(), @@ -137,8 +142,9 @@ fn pattern_decimal_with_option() -> Result<(), Box> { let tree = parse( tokens.0 )?; let locale: Rc = Rc::new( "en-ZA".parse()? ); let language_tag = Rc::new( locale.to_string() ); + let command_registry = Rc::new( CommandRegistry::new() ); let mut formatter = - Formatter::try_new( &icu_data_provider, &language_tag, &locale, &tree )?; + Formatter::try_new( &icu_data_provider, &language_tag, &locale, &tree, &command_registry )?; let mut values = HashMap::::new(); values.insert( "amount".to_string(), @@ -154,7 +160,7 @@ fn pattern_decimal_with_option() -> Result<(), Box> { } #[test] -fn pattern_dateime() -> Result<(), Box> { +fn pattern_datetime() -> Result<(), Box> { let buffer_provider = buffer(); let data_provider = buffer_provider.as_deserializing(); let icu_data_provider = @@ -167,8 +173,9 @@ fn pattern_dateime() -> Result<(), Box> { let tree = parse( tokens.0 )?; let locale: Rc = Rc::new( "en-ZA".parse()? ); let language_tag = Rc::new( locale.to_string() ); + let command_registry = Rc::new( CommandRegistry::new() ); let mut formatter = - Formatter::try_new( &icu_data_provider, &language_tag, &locale, &tree )?; + Formatter::try_new( &icu_data_provider, &language_tag, &locale, &tree, &command_registry )?; let mut values = HashMap::::new(); values.insert( "time".to_string(), @@ -187,7 +194,7 @@ fn pattern_dateime() -> Result<(), Box> { } #[test] -fn pattern_dateime_string() -> Result<(), Box> { +fn pattern_datetime_string() -> Result<(), Box> { let buffer_provider = buffer(); let data_provider = buffer_provider.as_deserializing(); let icu_data_provider = @@ -200,8 +207,9 @@ fn pattern_dateime_string() -> Result<(), Box> { let tree = parse( tokens.0 )?; let locale: Rc = Rc::new( "en-ZA".parse()? ); let language_tag = Rc::new( locale.to_string() ); + let command_registry = Rc::new( CommandRegistry::new() ); let mut formatter = - Formatter::try_new( &icu_data_provider, &language_tag, &locale, &tree )?; + Formatter::try_new( &icu_data_provider, &language_tag, &locale, &tree, &command_registry )?; let mut values = HashMap::::new(); values.insert( "time".to_string(), @@ -215,3 +223,68 @@ fn pattern_dateime_string() -> Result<(), Box> { ); Ok( () ) } + +#[test] +fn command_static() -> Result<(), Box> { + let buffer_provider = buffer(); + let data_provider = buffer_provider.as_deserializing(); + let icu_data_provider = + Rc::new( IcuDataProvider::try_new( &data_provider )? ); + let tokens = tokenise( + "The file ‘{#file_path `tests/formatter.rs`}’ failed to open.", + &vec![ '{', '}', '`', '#' ], + &icu_data_provider, + ); + let tree = parse( tokens.0 )?; + let locale: Rc = Rc::new( "en-ZA".parse()? ); + let language_tag = Rc::new( locale.to_string() ); + let command_registry = Rc::new( CommandRegistry::new() ); + command_registry.insert( "file_path", file_path )?; + let mut formatter = + Formatter::try_new( &icu_data_provider, &language_tag, &locale, &tree, &command_registry )?; + let values = HashMap::::new(); + let result = formatter.format( &values )?; + let info = os_info::get(); + if info.os_type() == os_info::Type::Windows { + assert_eq!( result.as_str(), "The file ‘tests\\formatter.rs’ failed to open.", "Should be Windows path." ); + } else { + assert_eq!( result.as_str(), "The file ‘tests/formatter.rs’ failed to open.", "Should be non-Windows path." ); + } + Ok( () ) +} + +#[test] +fn command_delayed() -> Result<(), Box> { + let buffer_provider = buffer(); + let data_provider = buffer_provider.as_deserializing(); + let icu_data_provider = + Rc::new( IcuDataProvider::try_new( &data_provider )? ); + let tokens = tokenise( + "At night {#english_a_or_an# hunter} {hunter} stalked {#english_a_or_an # prey} {prey}.", + &vec![ '{', '}', '`', '#' ], + &icu_data_provider, + ); + let tree = parse( tokens.0 )?; + let locale: Rc = Rc::new( "en-ZA".parse()? ); + let language_tag = Rc::new( locale.to_string() ); + let command_registry = Rc::new( CommandRegistry::new() ); + command_registry.insert( "english_a_or_an", english_a_or_an )?; + let mut formatter = + Formatter::try_new( &icu_data_provider, &language_tag, &locale, &tree, &command_registry )?; + let mut values = HashMap::::new(); + values.insert( + "hunter".to_string(), + PlaceholderValue::String( "owl".to_string() ) + ); + values.insert( + "prey".to_string(), + PlaceholderValue::String( "mouse".to_string() ) + ); + let result = formatter.format( &values )?; + assert_eq!( + result.as_str(), + "At night an owl stalked a mouse.", + "Strings must be the same." + ); + Ok( () ) +} diff --git a/crates/provider/core/Cargo.toml b/crates/provider/core/Cargo.toml index ef66153..5ca0126 100644 --- a/crates/provider/core/Cargo.toml +++ b/crates/provider/core/Cargo.toml @@ -13,21 +13,12 @@ license = "BSD-3-Clause" readme = "README.asciidoc" # false prevents publishing while developing, else string of repository identfier publish = false - -# these keys are used when preparing to publish to 'crates.io' -#documentation = "" -#homepage = "` vector, and obtaining the default language tag used for the crate's messages. For an implementation example, see the `i18n_provider_sqlite3-rizzen-yazston` crate, which uses Sqlite3 for its data store. diff --git a/crates/provider/core/src/error.rs b/crates/provider/core/src/error.rs index 96ad268..e0c8098 100644 --- a/crates/provider/core/src/error.rs +++ b/crates/provider/core/src/error.rs @@ -6,8 +6,13 @@ use std::error::Error; /// Contains the error that occurred within the provider. /// -/// Due to the nature of `Box` opaquing the error type, the error type is stored as a `String` in -/// `error_type` to facilitate in downcasting the error to original error type for further processing. +/// Due to the nature of [`Box`]`` opaquing the error type, the error type is stored as a +/// `&'static `[`str`] in /// `error_type` to facilitate in downcasting the error to original error type for further +/// processing. +/// +/// [`Box`]: std::boxed::Box +/// [`Error`]: std::error::Error +/// [`str`]: core::str #[derive( Debug )] pub struct ProviderError { pub error_type: &'static str, diff --git a/crates/provider/core/src/lib.rs b/crates/provider/core/src/lib.rs index 786c5df..b9b50bd 100644 --- a/crates/provider/core/src/lib.rs +++ b/crates/provider/core/src/lib.rs @@ -1,13 +1,16 @@ // This file is part of `i18n_provider-rizzen-yazston` crate. For the terms of use, please see the file // called `LICENSE-BSD-3-Clause` at the top level of the `i18n_provider-rizzen-yazston` crate. -//! LString provider. +//! [`LString`] provider. //! -//! A trait for providing language strings in the form of a `LString` vector, and obtaining the default language tag -//! used for the crate's messages. +//! A trait for providing language strings in the form of [`Vec`]`<`[`LString`]`>`, and obtaining the default language +//! tag used for the crate's messages. //! //! For an implementation example, see the `i18n_provider_sqlite3-rizzen-yazston` crate, which uses Sqlite3 for its //! data store. +//! +//! [`Vec`]: std::vec::Vec +//! [`LString`]: i18n_utility::LString pub mod provider; pub use provider::*; diff --git a/crates/provider/core/src/provider.rs b/crates/provider/core/src/provider.rs index 7e769f3..36f263d 100644 --- a/crates/provider/core/src/provider.rs +++ b/crates/provider/core/src/provider.rs @@ -2,31 +2,38 @@ // called `LICENSE-BSD-3-Clause` at the top level of the `i18n_provider-rizzen-yazston` crate. use crate::ProviderError; -use i18n_lstring::LString; +use i18n_utility::LString; use std::rc::Rc; -/// A trait for providing language strings in the form of a `LString` vector, and obtaining the default language tag -/// used for the crate's messages. +/// A trait for providing language strings in the form of [`Vec`]`<`[`LString`]`>`, and obtaining the default language +/// tag used for the crate's messages. /// /// For an implementation example, see the `i18n_provider_sqlite3-rizzen-yazston` crate, which uses Sqlite3 for its /// data store. +/// +/// [`Vec`]: std::vec::Vec +/// [`LString`]: i18n_utility::LString pub trait LStringProvider { /// Ideally a single exact match should be returned, yet may not be for the requested language tag. If no strings - /// is found for the requested tag, the right most subtag is removed sequentially until there are no more subtags. - /// Multiple `LString`s may be returned when there are multiple entries of language tags having additional subtags - /// than the requested language tag. + /// are found for the requested tag, the right most subtag is removed sequentially until there are no more subtags. + /// Multiple [`LString`]'s may be returned when there are multiple entries of language tags having additional + /// subtags than the requested language tag. /// /// Return of `ProviderError` indicates there was an error, usually in the data store. + /// + /// [`LString`]: i18n_utility::LString fn get>( &self, identifier: T, language_tag: &Rc ) -> Result, ProviderError>; - /// Similar to `get()` method, except that `get_one()` will only return a single `LString` if multiple strings are - /// available. + /// Similar to `get()` method, except that `get_one()` will only return a single [`LString`] if multiple strings + /// are available. /// /// `None` is returned when there is no strings available for the language tag. + /// + /// [`LString`]: i18n_utility::LString fn get_one>( &self, identifier: T, language_tag: &Rc @@ -42,5 +49,4 @@ pub trait LStringProvider { /// A wrapper struct tuple to hold a reference to an `impl LStringProvider`, so that the Provider can be stored in /// structs. -//pub struct LStringProviderWrapper<'a, P: ?Sized>( pub impl P ); pub struct LStringProviderWrapper<'a, P: ?Sized>( pub &'a P ); diff --git a/crates/provider/sqlite3/Cargo.toml b/crates/provider/sqlite3/Cargo.toml index d796e00..772984b 100644 --- a/crates/provider/sqlite3/Cargo.toml +++ b/crates/provider/sqlite3/Cargo.toml @@ -13,23 +13,13 @@ license = "BSD-3-Clause" readme = "README.asciidoc" # false prevents publishing while developing, else string of repository identfier publish = false - -# these keys are used when preparing to publish to 'crates.io' -#documentation = "" -#homepage = "MmAZ*m}XXl0X?e`}L{eTD=NRse952Yze^T>%FGB^N=H3Vt91 ncVTmrDSkByfdK>u05KP7V(uqF6dpCqs4hq>$*6tQFjLRSz{ptFz)07~oNpV$cU5s$2F2pSoXnEc z_|&4J{G!y!wY6<5k&NsNliBNXCQq)lG=S&;YBFPBV33g&w`EYw%!jJZ(~WZU^j83C iZDeF&XD}4cOjew{p Result { match *self { ProviderSqlite3Error::Io( ref error ) => error.fmt( formatter ), @@ -32,10 +30,10 @@ impl Display for ProviderSqlite3Error { ProviderSqlite3Error::NoSqlite3Files( ref path ) => write!( formatter, "No ‘.sqlite3’ files was found in ‘{}’.", path.display() ), ProviderSqlite3Error::MissingIdentifierPart( ref string ) => - write!( formatter, "Missing either string or crate identifier part for ‘{}’.", string ), + write!( formatter, "Missing either the string or the crate identifier part for ‘{}’.", string ), ProviderSqlite3Error::LanguageTagRegistry( ref error ) => error.fmt( formatter ), ProviderSqlite3Error::InvalidPath => - write!( formatter, "Invalid path provided." ), + write!( formatter, "Invalid path was provided." ), } } } diff --git a/crates/provider/sqlite3/src/lib.rs b/crates/provider/sqlite3/src/lib.rs index 2086699..4cbdedb 100644 --- a/crates/provider/sqlite3/src/lib.rs +++ b/crates/provider/sqlite3/src/lib.rs @@ -1,9 +1,9 @@ // This file is part of `i18n_provider_sqlite3-rizzen-yazston` crate. For the terms of use, please see the file // called `LICENSE-BSD-3-Clause` at the top level of the `i18n_provider_sqlite3-rizzen-yazston` crate. -//! Sqlite3 provider for `LString`s. +//! Sqlite3 provider for [`LString`]s. //! -//! This crate implements `LStringProvider` using Sqlite3 as the data store for language strings. As a directory path +//! This crate implements [`LStringProvider`] using Sqlite3 as the data store for language strings. As a directory path //! is used at the time of creating a `ProviderSqlite3` object, it means that an application can have multiple data //! stores for both application language strings, and also for data packages' language strings. //! @@ -12,7 +12,7 @@ //! ``` //! use i18n_provider_sqlite3::ProviderSqlite3; //! use i18n_provider::LStringProvider; -//! use i18n_registry::LanguageTagRegistry; +//! use i18n_utility::LanguageTagRegistry; //! use std::rc::Rc; //! use std::error::Error; //! @@ -29,11 +29,14 @@ //! &tag //! )?; //! assert_eq!( strings.len(), 1, "There should be 1 string." ); -//! assert_eq!( strings[ 0 ].as_str(), "Invalid path provided.", "Not correct string." ); +//! assert_eq!( strings[ 0 ].as_str(), "Invalid path was provided.", "Not correct string." ); //! assert_eq!( strings[ 0 ].language_tag().as_str(), "en-ZA", "Must be en-ZA." ); //! Ok( () ) //! } //! ``` +//! +//! [`LString`]: i18n_utility::LString +//! [`LStringProvider`]: i18n_provider::LStringProvider pub mod provider; pub use provider::*; diff --git a/crates/provider/sqlite3/src/provider.rs b/crates/provider/sqlite3/src/provider.rs index f4955dc..833a317 100644 --- a/crates/provider/sqlite3/src/provider.rs +++ b/crates/provider/sqlite3/src/provider.rs @@ -2,8 +2,7 @@ // called `LICENSE-BSD-3-Clause` at the top level of the `i18n_provider_sqlite3-rizzen-yazston` crate. use crate::ProviderSqlite3Error; -use i18n_registry::registry::LanguageTagRegistry; -use i18n_lstring::LString; +use i18n_utility::{ LanguageTagRegistry, LString }; use i18n_provider::{ LStringProvider, ProviderError }; use rusqlite::{ Connection, OpenFlags }; use std::collections::HashMap; @@ -11,7 +10,7 @@ use std::rc::Rc; use std::path::PathBuf; use core::cell::RefCell; -/// `ProviderSqlite3` struct is an implementation of the `LStringProvider` trait, and uses Sqlite3 as the data store +/// `ProviderSqlite3` struct is an implementation of the [`LStringProvider`] trait, and uses Sqlite3 as the data store /// for language strings. As the directory path of the data store is embedded in the `ProviderSqlite3` struct upon /// creating an instance of `ProviderSqlite3`, one can have multiple `ProviderSqlite3` objects representing the /// application itself, and for data packages that supports internationalisation. @@ -21,7 +20,7 @@ use core::cell::RefCell; /// ``` /// use i18n_provider_sqlite3::ProviderSqlite3; /// use i18n_provider::LStringProvider; -/// use i18n_registry::LanguageTagRegistry; +/// use i18n_utility::LanguageTagRegistry; /// use std::rc::Rc; /// use std::error::Error; /// fn main() -> Result<(), Box> { @@ -37,11 +36,13 @@ use core::cell::RefCell; /// &tag /// )?; /// assert_eq!( strings.len(), 1, "There should be 1 string." ); -/// assert_eq!( strings[ 0 ].as_str(), "Invalid path provided.", "Not correct string." ); +/// assert_eq!( strings[ 0 ].as_str(), "Invalid path was provided.", "Not correct string." ); /// assert_eq!( strings[ 0 ].language_tag().as_str(), "en-ZA", "Must be en-ZA." ); /// Ok( () ) /// } /// ``` +/// +/// [`LStringProvider`]: i18n_provider::LStringProvider pub struct ProviderSqlite3 { directory: PathBuf, language_tag_registry: Rc, @@ -58,8 +59,8 @@ impl ProviderSqlite3 { /// /// Returns a `ProviderSqlite3` struct, which indicates a valid path to directory containing `.sqlite3` files. /// - /// Returns a `ErrorMessage` when there is an error in verifying the path is a directory and contains `.sqlite3` - /// files. + /// Returns a `ProviderSqlite3Error` when there is an error in verifying the path is a directory and contains + /// `.sqlite3` files. pub fn try_new>( directory_path: T, language_tag_registry: &Rc @@ -109,21 +110,21 @@ impl ProviderSqlite3 { impl LStringProvider for ProviderSqlite3 { - /// Retrieve a vector of possible `LString` for requested identifier that matches a language tag. + /// Retrieve a vector of possible [`LString`] for requested identifier that matches a language tag. /// /// Ideally a single exact match should be returned, yet may not be for the requested language tag. If no strings /// is found for the requested tag, the right most subtag is removed sequentially until there are no more subtags. - /// Multiple `LString`s may be returned when there are multiple entries of language tags having additional subtags - /// than the requested language tag. + /// Multiple [`LString`]s may be returned when there are multiple entries of language tags having additional + /// subtags than the requested language tag. /// - /// Return of `ErrorMessage` indicates there was a Sqlite3 error. + /// Return of `ProviderSqlite3Errore` indicates there was a Sqlite3 error. /// /// # Examples /// /// ``` /// use i18n_provider_sqlite3::ProviderSqlite3; /// use i18n_provider::LStringProvider; - /// use i18n_registry::LanguageTagRegistry; + /// use i18n_utility::LanguageTagRegistry; /// use std::rc::Rc; /// use std::error::Error; /// fn main() -> Result<(), Box> { @@ -139,11 +140,13 @@ impl LStringProvider for ProviderSqlite3 { /// &tag /// )?;//.expect( "No string found for language tag." ); /// assert_eq!( strings.len(), 1, "There should be 1 string." ); - /// assert_eq!( strings[ 0 ].as_str(), "Invalid path provided.", "Not correct string." ); + /// assert_eq!( strings[ 0 ].as_str(), "Invalid path was provided.", "Not correct string." ); /// assert_eq!( strings[ 0 ].language_tag().as_str(), "en-ZA", "Must be en-ZA." ); /// Ok( () ) /// } /// ``` + /// + /// [`LString`]: i18n_utility::LString fn get>( &self, identifier: T, @@ -244,17 +247,19 @@ impl LStringProvider for ProviderSqlite3 { Ok( result ) } - /// Similar to `get()` method, except that `get_one()` will only return a single `LString` if multiple strings are - /// available. + /// Similar to `get()` method, except that `get_one()` will only return a single [`LString`] if multiple strings + /// are available. /// /// `None` is returned when there is no strings available for the language tag. /// + /// Return of [`ProviderError`] indicates there was a Sqlite3 error. + /// /// # Examples /// /// ``` /// use i18n_provider_sqlite3::ProviderSqlite3; /// use i18n_provider::LStringProvider; - /// use i18n_registry::LanguageTagRegistry; + /// use i18n_utility::LanguageTagRegistry; /// use core::cell::RefCell; /// use std::rc::Rc; /// use std::error::Error; @@ -271,11 +276,14 @@ impl LStringProvider for ProviderSqlite3 { /// &tag /// )?;//.expect( "No string found for language tag." ); /// assert_eq!( strings.len(), 1, "There should be 1 string." ); - /// assert_eq!( strings[ 0 ].as_str(), "Invalid path provided.", "Not correct string." ); + /// assert_eq!( strings[ 0 ].as_str(), "Invalid path was provided.", "Not correct string." ); /// assert_eq!( strings[ 0 ].language_tag().as_str(), "en-ZA", "Must be en-ZA." ); /// Ok( () ) /// } /// ``` + /// + /// [`LString`]: i18n_utility::LString + /// [`ProviderError`]: i18n_provider::ProviderError fn get_one>( &self, identifier: T, language_tag: &Rc @@ -289,14 +297,14 @@ impl LStringProvider for ProviderSqlite3 { /// /// Return of `None` indicates no default language tag was found. /// - /// Return of `ErrorMessage` indicates there was a Sqlite3 error. + /// Return of [`ProviderError`] indicates there was a Sqlite3 error. /// /// # Examples /// /// ``` /// use i18n_provider_sqlite3::ProviderSqlite3; /// use i18n_provider::LStringProvider; - /// use i18n_registry::LanguageTagRegistry; + /// use i18n_utility::LanguageTagRegistry; /// use core::cell::RefCell; /// use std::rc::Rc; /// use std::error::Error; @@ -314,6 +322,8 @@ impl LStringProvider for ProviderSqlite3 { /// Ok( () ) /// } /// ``` + /// + /// [`ProviderError`]: i18n_provider::ProviderError fn default_language_tag>( &self, identifier: T ) -> Result, ProviderError> { let mut have = false; { diff --git a/crates/provider/sqlite3/tests/sqlite3.rs b/crates/provider/sqlite3/tests/sqlite3.rs index 3d336bb..d35ecf4 100644 --- a/crates/provider/sqlite3/tests/sqlite3.rs +++ b/crates/provider/sqlite3/tests/sqlite3.rs @@ -5,7 +5,7 @@ use i18n_provider::LStringProvider; use i18n_provider_sqlite3::ProviderSqlite3; -use i18n_registry::LanguageTagRegistry; +use i18n_utility::LanguageTagRegistry; use std::{ rc::Rc, error::Error }; #[test] @@ -22,7 +22,7 @@ fn get_for_en() -> Result<(), Box> { &tag )?; assert_eq!( strings.len(), 1, "There should be 1 string." ); - assert_eq!( strings[ 0 ].as_str(), "Invalid path provided.", "Not correct string." ); + assert_eq!( strings[ 0 ].as_str(), "Invalid path was provided.", "Not correct string." ); Ok( () ) } @@ -40,7 +40,7 @@ fn get_for_en_za_u_ca_julian() -> Result<(), Box> { &tag )?; assert_eq!( strings.len(), 1, "There should be 1 string." ); - assert_eq!( strings[ 0 ].as_str(), "Invalid path provided.", "Not correct string." ); + assert_eq!( strings[ 0 ].as_str(), "Invalid path was provided.", "Not correct string." ); Ok( () ) } diff --git a/crates/registry/Cargo.toml b/crates/registry/Cargo.toml deleted file mode 100644 index 17fe1ba..0000000 --- a/crates/registry/Cargo.toml +++ /dev/null @@ -1,33 +0,0 @@ -# This file is part of `i18n_registry-rizzen-yazston` crate. For the terms of use, please see the file -# called `LICENSE-BSD-3-Clause` at the top level of the `i18n_registry-rizzen-yazston` crate. - -[package] -# suffix '-rizzen-yazston' appended to prevent package name clashes on 'crates.io' -name = "i18n_registry-rizzen-yazston" -version = "0.5.0" -authors = ["Rizzen Yazston"] -edition = "2021" -rust-version = "1.67.0" -description = "The `i18n_registry` crate of the Internationalisation project." -license = "BSD-3-Clause" -readme = "README.asciidoc" -# false prevents publishing while developing, else string of repository identfier -publish = false - -# these keys are used when preparing to publish to 'crates.io' -#documentation = "" -#homepage = "`). + +The specified language is expected to be a {BCP_47_Language_Tag}[BCP 47 Language Tag] string, though any identifier could be used. + + +== Acknowledgements + +Stefano Angeleri for advice on various design aspects of implementing the library of the Internationalisation project, and also providing the Italian translation of error message strings. + + +== Usage + +Simply include the `i18n_utility-rizzen-yazston` crate in the `Cargo.toml` to make it available to the application or library. + + +=== Cargo.toml + +``` +[dependencies] +icu_locid = "1.1.0" +i18n_utility-rizzen-yazston = "0.5.0" +``` + + +=== Examples + +==== `LanguageTagRegistry` +``` +use icu_locid::Locale; +use std::rc::Rc; +use i18n_utility::registry::LanguageTagRegistry; + +let mut registry = LanguageTagRegistry::new(); +let result = registry.get( "en_ZA" ).expect( "Failed to parse language tag." ); +let tags = registry.list().iter().count(); + +assert_eq!( result.0.as_str(), "en-ZA", "Did not convert en_ZA to en-ZA BCP 47 format." ); +assert_eq!( tags, 1, "Supposed to be 1 entries: en-ZA." ) +``` + +==== `LString` +``` +use icu_locid::Locale; +use std::rc::Rc; +use i18n_utility::LString; + +let string = "This is a test string."; +let tag = Rc::new( + Locale::canonicalize( "en-ZA" ).expect( "Failed to canonicalise language tag." ) +); +let lang_string = LString::new( string, &tag ); +assert_eq!( lang_string.as_str(), string, "String failed." ); +assert_eq!( lang_string.language_tag(), &tag, "Language tag failed." ); +``` diff --git a/crates/registry/src/error.rs b/crates/utility/src/error.rs similarity index 72% rename from crates/registry/src/error.rs rename to crates/utility/src/error.rs index 074388c..11a44a5 100644 --- a/crates/registry/src/error.rs +++ b/crates/utility/src/error.rs @@ -1,5 +1,5 @@ -// This file is part of `i18n_registry-rizzen-yazston` crate. For the terms of use, please see the file -// called `LICENSE-BSD-3-Clause` at the top level of the `i18n_registry-rizzen-yazston` crate. +// This file is part of `i18n_utility-rizzen-yazston` crate. For the terms of use, please see the file +// called `LICENSE-BSD-3-Clause` at the top level of the `i18n_utility-rizzen-yazston` crate. use icu_locid::ParserError; use std::error::Error; // Experimental in `core` crate. @@ -12,8 +12,6 @@ pub enum RegistryError { } impl Display for RegistryError { - - /// Simply call the display formatter of embedded error. fn fmt( &self, formatter: &mut Formatter ) -> Result { match *self { RegistryError::Locale( ref error ) => error.fmt( formatter ), diff --git a/crates/utility/src/lib.rs b/crates/utility/src/lib.rs new file mode 100644 index 0000000..41bd2a2 --- /dev/null +++ b/crates/utility/src/lib.rs @@ -0,0 +1,79 @@ +// This file is part of `i18n_utility-rizzen-yazston` crate. For the terms of use, please see the file +// called LICENSE-BSD-3-Clause at the top level of the `i18n_utility-rizzen-yazston` crate. + +//! Welcome to the **utility** crate of the *Internationalisation* (i18n) project. +//! +//! This crate consists of two models: +//! +//! * `registry`: Registry for Language Tags and ICU4X's `Locale` instances, +//! +//! * `lstring`: Simple tagged string type. +//! +//! +//! # Registry for holding ICU4X `Locale` objects. +//! +//! This module contains the `LanguageTagRegistry` type, to provide a simple container that caches the +//! [BCP 47 Language Tag] string and the [`Locale`] type for querying language tags. The purpose of the registry is to +//! reduce the need of parsing language tags repeatedly, by storing the result `Locale` for querying language tag in +//! the registry, and uses the existing `Locale` for the querying language tag when requested. +//! +//! The `Locale` type can be provided by either the [`icu_locid`] crate or the `icu` meta-crate. These two crates +//! are part of the [`ICU4X`] protect developed by the [Unicode Consortium]. +//! +//! This crate makes use of the `Locale` type instead of the [`LanguageIdentifier`] type due to that the `Locale` +//! type supports the entire BCP 47 Language Tag specification, where as the `LanguageIdentifier` type excludes the +//! **extension** subtags of the BCP 47 Language Tag specification. +//! +//! ## Examples +//! +//! ``` +//! use icu_locid::Locale; +//! use std::rc::Rc; +//! use i18n_utility::registry::LanguageTagRegistry; +//! +//! let mut registry = LanguageTagRegistry::new(); +//! let result = registry.get( "en_ZA" ).expect( "Failed to parse language tag." ); +//! let tags = registry.list().iter().count(); +//! +//! assert_eq!( result.0.as_str(), "en-ZA", "Did not convert en_ZA to en-ZA BCP 47 format." ); +//! assert_eq!( tags, 1, "Supposed to be 1 entries: en-ZA." ) +//! ``` +//! +//! # Language string. +//! +//! This crate contains the `LString` type (aka LanguageString), for associating a text string ([`String`]) to a +//! specific language ([`Rc`]``). +//! +//! The specified language is expected to be a [BCP 47 Language Tag] string, though any identifier could be used. +//! +//! ## Examples +//! +//! ``` +//! use icu_locid::Locale; +//! use std::rc::Rc; +//! use i18n_utility::LString; +//! +//! let string = "This is a test string."; +//! let tag = Rc::new( +//! Locale::canonicalize( "en-ZA" ).expect( "Failed to canonicalise language tag." ) +//! ); +//! let lang_string = LString::new( string, &tag ); +//! assert_eq!( lang_string.as_str(), string, "String failed." ); +//! assert_eq!( lang_string.language_tag(), &tag, "Language tag failed." ); +//! ``` +//! +//! [`Locale`]: icu_locid::Locale +//! [`icu_locid`]: icu_locid +//! [`ICU4X`]: https://github.com/unicode-org/icu4x +//! [Unicode Consortium]: https://home.unicode.org/ +//! [`LanguageIdentifier`]: icu_locid::LanguageIdentifier +//! [BCP 47 Language Tag]: https://www.rfc-editor.org/rfc/bcp/bcp47.txt +//! [`String`]: alloc::string::String +//! [`Rc`]: std::rc::Rc + +pub mod lstring; +pub use lstring::*; +pub mod registry; +pub use registry::*; +pub mod error; +pub use error::*; diff --git a/crates/lstring/src/lib.rs b/crates/utility/src/lstring.rs similarity index 66% rename from crates/lstring/src/lib.rs rename to crates/utility/src/lstring.rs index f5f44f1..6255027 100644 --- a/crates/lstring/src/lib.rs +++ b/crates/utility/src/lstring.rs @@ -1,32 +1,5 @@ -// This file is part of `i18n_lstring-rizzen-yazston` crate. For the terms of use, please see the file -// called `LICENSE-BSD-3-Clause` at the top level of the `i18n_lstring-rizzen-yazston` crate. - -//! Language string. -//! -//! This crate contains the `LString` type (aka LanguageString), for associating a text string ([`String`]) to a -//! specific language ([`Rc`]``). -//! -//! The specified language is expected to be a [BCP 47 Language Tag] string, though any identifier could be used. -//! -//! # Examples -//! -//! ``` -//! use icu_locid::Locale; -//! use std::rc::Rc; -//! use i18n_lstring::LString; -//! -//! let string = "This is a test string."; -//! let tag = Rc::new( -//! Locale::canonicalize( "en-ZA" ).expect( "Failed to canonicalise language tag." ) -//! ); -//! let lang_string = LString::new( string, &tag ); -//! assert_eq!( lang_string.as_str(), string, "String failed." ); -//! assert_eq!( lang_string.language_tag(), &tag, "Language tag failed." ); -//! ``` -//! -//! [`String`]: https://doc.rust-lang.org/std/string/struct.String.html -//! [`Rc`]: https://doc.rust-lang.org/std/rc/struct.Rc.html -//! [BCP 47 Language Tag]: https://www.rfc-editor.org/rfc/bcp/bcp47.txt +// This file is part of `i18n_utility-rizzen-yazston` crate. For the terms of use, please see the file +// called `LICENSE-BSD-3-Clause` at the top level of the `i18n_utility-rizzen-yazston` crate. use std::rc::Rc; @@ -42,7 +15,7 @@ use std::rc::Rc; /// ``` /// use icu_locid::Locale; /// use std::rc::Rc; -/// use i18n_lstring::LString; +/// use i18n_utility::LString; /// /// let string = "This is a test string."; /// let tag = Rc::new( @@ -53,8 +26,8 @@ use std::rc::Rc; /// assert_eq!( lang_string.language_tag(), &tag, "Language tag failed." ); /// ``` /// -/// [`String`]: https://doc.rust-lang.org/std/string/struct.String.html -/// [`Rc`]: https://doc.rust-lang.org/std/rc/struct.Rc.html +/// [`String`]: alloc::string::String +/// [`Rc`]: std::rc::Rc /// [BCP 47 Language Tag]: https://www.rfc-editor.org/rfc/bcp/bcp47.txt #[derive( PartialEq, Debug, Clone )] pub struct LString { @@ -82,7 +55,7 @@ impl LString { /// ``` /// use icu_locid::Locale; /// use std::rc::Rc; - /// use i18n_lstring::LString; + /// use i18n_utility::LString; /// /// let string = "This is a test string."; /// let tag = Rc::new( @@ -93,12 +66,12 @@ impl LString { /// assert_eq!( lang_string.language_tag(), &tag, "Language tag failed." ); /// ``` /// - /// [`str`]: https://doc.rust-lang.org/core/primitive.str.html - /// [`Rc`]: https://doc.rust-lang.org/std/rc/struct.Rc.html - /// [`String`]: https://doc.rust-lang.org/std/string/struct.String.html + /// [`str`]: core::str + /// [`String`]: alloc::string::String + /// [`Rc`]: std::rc::Rc /// [BCP 47 Language Tag]: https://www.rfc-editor.org/rfc/bcp/bcp47.txt - /// [`Locale`]: https://docs.rs/icu/latest/icu/locid/struct.Locale.html - /// [`icu_locid`]: https://crates.io/crates/icu_locid + /// [`Locale`]: icu_locid::Locale + /// [`icu_locid`]: icu_locid pub fn new>( string: T, language_tag: &Rc ) -> Self { LString { string: string.into(), language_tag: Rc::clone( language_tag ) } } @@ -110,7 +83,7 @@ impl LString { /// ``` /// use icu_locid::Locale; /// use std::rc::Rc; - /// use i18n_lstring::LString; + /// use i18n_utility::LString; /// /// let string = "This is a test string."; /// let tag = Rc::new( @@ -119,8 +92,8 @@ impl LString { /// let lang_string = LString::new( string, &tag ); /// assert_eq!( lang_string.as_str(), string, "String failed." ); /// ``` - /// [`str`]: https://doc.rust-lang.org/core/primitive.str.html - /// [`String`]: https://doc.rust-lang.org/std/string/struct.String.html + /// [`str`]: core::str + /// [`String`]: alloc::string::String pub fn as_str( &self ) -> &str { &self.string } @@ -132,7 +105,7 @@ impl LString { /// ``` /// use icu_locid::Locale; /// use std::rc::Rc; - /// use i18n_lstring::LString; + /// use i18n_utility::LString; /// /// let tag = Rc::new( /// Locale::canonicalize( "en-ZA" ).expect( "Failed to canonicalise language tag." ) @@ -140,8 +113,8 @@ impl LString { /// let lang_string = LString::new( "This is a test string.", &tag ); /// assert_eq!( lang_string.language_tag(), &tag, "Locale failed." ); /// ``` - /// [`Rc`]: https://doc.rust-lang.org/std/rc/struct.Rc.html - /// [`String`]: https://doc.rust-lang.org/std/string/struct.String.html + /// [`Rc`]: std::rc::Rc + /// [`String`]: alloc::string::String pub fn language_tag( &self ) -> &Rc { &self.language_tag } diff --git a/crates/registry/src/registry.rs b/crates/utility/src/registry.rs similarity index 88% rename from crates/registry/src/registry.rs rename to crates/utility/src/registry.rs index 5ea5232..364f54a 100644 --- a/crates/registry/src/registry.rs +++ b/crates/utility/src/registry.rs @@ -1,5 +1,5 @@ -// This file is part of `i18n_registry-rizzen-yazston` crate. For the terms of use, please see the file -// called `LICENSE-BSD-3-Clause` at the top level of the `i18n_registry-rizzen-yazston` crate. +// This file is part of `i18n_utility-rizzen-yazston` crate. For the terms of use, please see the file +// called `LICENSE-BSD-3-Clause` at the top level of the `i18n_utility-rizzen-yazston` crate. use crate::RegistryError; use icu_locid::Locale; @@ -8,14 +8,14 @@ use std::rc::Rc; use std::collections::HashMap; use std::iter::FromIterator; -/// Registry for holding [`ICU4X`] [`Locale`] objects. +/// Registry for holding `ICU4X` `Locale` objects. /// /// This module contains the `LanguageTagRegistry` type, to provide a simple container that caches the -/// [BCP 47 Language Tag] strings and the [`Locale`] objects for querying language tags. The purpose of the registry is +/// [BCP 47 Language Tag] strings and the [`Locale`] types for querying language tags. The purpose of the registry is /// to reduce the need of parsing language tags repeatedly, by storing the result `Locale` for querying language tag in /// the registry, and uses the existing `Locale` for the querying language tag when requested. /// -/// The `Locale` type can be provided by either the [`icu_locid`] crate or the [`icu`] meta-crate. These two crates +/// The `Locale` type can be provided by either the [`icu_locid`] crate or the `icu` meta-crate. These two crates /// are part of the [`ICU4X`] protect developed by the [Unicode Consortium]. /// /// This crate makes use of the `Locale` type instead of the [`LanguageIdentifier`] type due to that the `Locale` @@ -27,7 +27,7 @@ use std::iter::FromIterator; /// ``` /// use icu_locid::Locale; /// use std::rc::Rc; -/// use i18n_registry::registry::LanguageTagRegistry; +/// use i18n_utility::LanguageTagRegistry; /// /// let registry = LanguageTagRegistry::new(); /// let result = registry.get( "en_ZA" ).expect( "Failed to parse language tag." ); @@ -37,12 +37,11 @@ use std::iter::FromIterator; /// assert_eq!( tags, 1, "Supposed to be 1 entries: en-ZA." ) /// ``` /// -/// [`Locale`]: https://docs.rs/icu/latest/icu/locid/struct.Locale.html -/// [`icu_locid`]: https://crates.io/crates/icu_locid -/// [`icu`]: https://crates.io/crates/icu +/// [`Locale`]: icu_locid::Locale +/// [`icu_locid`]: icu_locid /// [`ICU4X`]: https://github.com/unicode-org/icu4x /// [Unicode Consortium]: https://home.unicode.org/ -/// [`LanguageIdentifier`]: https://docs.rs/icu/latest/icu/locid/struct.LanguageIdentifier.html +/// [`LanguageIdentifier`]: icu_locid::LanguageIdentifier /// [BCP 47 Language Tag]: https://www.rfc-editor.org/rfc/bcp/bcp47.txt pub struct LanguageTagRegistry { bcp47: RefCell, Rc )>>, @@ -60,7 +59,7 @@ impl LanguageTagRegistry { /// ``` /// use icu_locid::Locale; /// use std::rc::Rc; - /// use i18n_registry::registry::LanguageTagRegistry; + /// use i18n_utility::LanguageTagRegistry; /// /// let registry = LanguageTagRegistry::new(); /// let result = registry.get( "en_ZA" ).expect( "Failed to parse language tag." ); @@ -92,7 +91,7 @@ impl LanguageTagRegistry { /// ``` /// use icu_locid::Locale; /// use std::rc::Rc; - /// use i18n_registry::registry::LanguageTagRegistry; + /// use i18n_utility::LanguageTagRegistry; /// /// let registry = LanguageTagRegistry::new(); /// let result = registry.get( "en_ZA" ).expect( "Failed to parse language tag." ); @@ -102,9 +101,9 @@ impl LanguageTagRegistry { /// assert_eq!( tags, 1, "Supposed to be 1 entries: en-ZA." ) /// ``` /// - /// [`Rc`]: https://doc.rust-lang.org/std/rc/struct.Rc.html - /// [`String`]: https://doc.rust-lang.org/std/string/struct.String.html - /// [`Locale`]: https://docs.rs/icu/latest/icu/locid/struct.Locale.html + /// [`String`]: alloc::string::String + /// [`Rc`]: std::rc::Rc + /// [`Locale`]: icu_locid::Locale /// [BCP 47 Language Tag]: https://www.rfc-editor.org/rfc/bcp/bcp47.txt pub fn get>( &self, language_tag: T ) -> Result<( Rc, Rc ), RegistryError> { if let Some( result ) = self.bcp47.borrow().get( language_tag.as_ref() ) { @@ -154,7 +153,7 @@ impl LanguageTagRegistry { /// ``` /// use icu_locid::Locale; /// use std::rc::Rc; - /// use i18n_registry::registry::LanguageTagRegistry; + /// use i18n_utility::LanguageTagRegistry; /// /// let registry = LanguageTagRegistry::new(); /// let tag = registry.get_language_tag( "en_ZA" ).expect( "Failed to parse language tag." ); @@ -164,9 +163,9 @@ impl LanguageTagRegistry { /// assert_eq!( tags, 1, "Supposed to be 1 entries: en-ZA." ) /// ``` /// - /// [`Rc`]: https://doc.rust-lang.org/std/rc/struct.Rc.html - /// [`String`]: https://doc.rust-lang.org/std/string/struct.String.html - /// [`Locale`]: https://docs.rs/icu/latest/icu/locid/struct.Locale.html + /// [`String`]: alloc::string::String + /// [`Rc`]: std::rc::Rc + /// [`Locale`]: icu_locid::Locale /// [BCP 47 Language Tag]: https://www.rfc-editor.org/rfc/bcp/bcp47.txt pub fn get_language_tag>( &self, language_tag: T ) -> Result, RegistryError> { let result = self.get( language_tag.as_ref() )?; @@ -188,7 +187,7 @@ impl LanguageTagRegistry { /// ``` /// use icu_locid::Locale; /// use std::rc::Rc; - /// use i18n_registry::registry::LanguageTagRegistry; + /// use i18n_utility::LanguageTagRegistry; /// /// let registry = LanguageTagRegistry::new(); /// let locale = registry.get_locale( "en_ZA" ).expect( "Failed to parse language tag." ); @@ -198,9 +197,9 @@ impl LanguageTagRegistry { /// assert_eq!( tags, 1, "Supposed to be 1 entries: en-ZA." ) /// ``` /// - /// [`Rc`]: https://doc.rust-lang.org/std/rc/struct.Rc.html - /// [`String`]: https://doc.rust-lang.org/std/string/struct.String.html - /// [`Locale`]: https://docs.rs/icu/latest/icu/locid/struct.Locale.html + /// [`String`]: alloc::string::String + /// [`Rc`]: std::rc::Rc + /// [`Locale`]: icu_locid::Locale /// [BCP 47 Language Tag]: https://www.rfc-editor.org/rfc/bcp/bcp47.txt pub fn get_locale>( &self, language_tag: T ) -> Result, RegistryError> { let result = self.get( language_tag.as_ref() )?; @@ -214,7 +213,7 @@ impl LanguageTagRegistry { /// ``` /// use icu_locid::Locale; /// use std::rc::Rc; - /// use i18n_registry::registry::LanguageTagRegistry; + /// use i18n_utility::LanguageTagRegistry; /// /// let registry = LanguageTagRegistry::new(); /// let result = registry.get( "en_ZA" ).expect( "Failed to parse language tag." ); // Just adding deprecated tag. @@ -236,7 +235,7 @@ impl LanguageTagRegistry { /// ``` /// use icu_locid::Locale; /// use std::rc::Rc; - /// use i18n_registry::registry::LanguageTagRegistry; + /// use i18n_utility::LanguageTagRegistry; /// /// let registry = LanguageTagRegistry::new(); /// let result = registry.get( "en_ZA" ).expect( "Failed to parse language tag." ); // Just adding deprecated tag. @@ -259,7 +258,7 @@ impl LanguageTagRegistry { /// ``` /// use icu_locid::Locale; /// use std::rc::Rc; - /// use i18n_registry::registry::LanguageTagRegistry; + /// use i18n_utility::LanguageTagRegistry; /// /// let registry = LanguageTagRegistry::new(); /// let result = registry.get( "en_ZA" ).expect( "Failed to parse language tag." ); // Just adding deprecated tag.