Skip to main content

icu_provider_export/
lib.rs

1// This file is part of ICU4X. For terms of use, please see the file
2// called LICENSE at the top level of the ICU4X source tree
3// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).
4
5// https://github.com/unicode-org/icu4x/blob/main/documents/process/boilerplate.md#library-annotations
6// #![cfg_attr(not(any(test, doc)), no_std)]
7// #![cfg_attr(
8//     not(test),
9//     deny(
10//         clippy::indexing_slicing,
11//         clippy::unwrap_used,
12//         clippy::expect_used,
13//         clippy::panic,
14//     )
15// )]
16#![warn(missing_docs)]
17#![allow(clippy::needless_doctest_main)]
18//! `icu_provider_export` is a library to generate data files that can be used in ICU4X data providers.
19//!
20//! For command-line usage, see the [`icu4x-datagen` binary](https://crates.io/crate/icu4x-datagen).
21//!
22//! Also see our [datagen tutorial](https://github.com/unicode-org/icu4x/blob/main/tutorials/data-management.md).
23//!
24//! # Examples
25//!
26//! ```no_run
27//! use icu_provider_export::blob_exporter::*;
28//! use icu_provider_export::prelude::*;
29//! use icu_provider_source::SourceDataProvider;
30//! use std::fs::File;
31//!
32//! let provider = SourceDataProvider::new();
33//!
34//! ExportDriver::new(
35//!     [DataLocaleFamily::FULL],
36//!     DeduplicationStrategy::None.into(),
37//!     LocaleFallbacker::try_new_unstable(&provider).unwrap(),
38//! )
39//! .with_markers([icu::list::provider::ListAndV1::INFO])
40//! .export(
41//!     &provider,
42//!     BlobExporter::new_with_sink(Box::new(
43//!         File::create("data.postcard").unwrap(),
44//!     )),
45//! )
46//! .unwrap();
47//! ```
48//!
49//! # Cargo features
50//!
51//! * `baked_exporter`
52//!   * enables the [`baked_exporter`] module, a reexport of [`icu_provider_baked::export`]
53//! * `blob_exporter`
54//!   * enables the [`blob_exporter`] module, a reexport of [`icu_provider_blob::export`]
55//! * `fs_exporter`
56//!   * enables the [`fs_exporter`] module, a reexport of [`icu_provider_fs::export`]
57//! * `rayon`
58//!   * enables parallelism during export
59
60mod export_impl;
61mod locale_family;
62use icu_provider::export::ExporterCloseMetadata;
63pub use locale_family::*;
64
65#[cfg(feature = "baked_exporter")]
66pub use icu_provider_baked::export as baked_exporter;
67#[cfg(feature = "blob_exporter")]
68pub use icu_provider_blob::export as blob_exporter;
69#[cfg(feature = "fs_exporter")]
70pub use icu_provider_fs::export as fs_exporter;
71
72/// A prelude for using the datagen API
73pub mod prelude {
74    #[doc(no_inline)]
75    pub use crate::{
76        DataLocaleFamily, DeduplicationStrategy, ExportDriver, FallbackOptions, NoFallbackOptions,
77    };
78    #[doc(no_inline)]
79    pub use icu_locale_core::{data_locale, locale};
80    #[doc(no_inline)]
81    pub use icu_locale_fallback::LocaleFallbacker;
82    #[doc(no_inline)]
83    pub use icu_provider::{DataLocale, DataMarker, DataMarkerInfo, export::DataExporter};
84}
85
86use icu_locale_fallback::LocaleFallbacker;
87use icu_provider::export::DataExporter;
88use icu_provider::export::ExportableProvider;
89use icu_provider::prelude::*;
90use std::collections::BTreeSet;
91use std::collections::HashMap;
92use std::collections::HashSet;
93use std::hash::Hash;
94use std::sync::Arc;
95
96/// Configuration for a data export operation.
97///
98/// Note that this only configures *which data* is exported. The input provider, usually
99/// `SourceDataProvider`, might expose more options about the data itself.
100///
101/// # Examples
102///
103/// ```no_run
104/// use icu_provider_export::blob_exporter::*;
105/// use icu_provider_export::prelude::*;
106/// use icu_provider_source::SourceDataProvider;
107///
108/// let provider = SourceDataProvider::new();
109///
110/// ExportDriver::new(
111///     [DataLocaleFamily::FULL],
112///     DeduplicationStrategy::None.into(),
113///     LocaleFallbacker::try_new_unstable(&provider).unwrap(),
114/// )
115/// .with_markers([icu::list::provider::ListAndV1::INFO])
116/// .export(
117///     &provider,
118///     BlobExporter::new_with_sink(Box::new(&mut Vec::new())),
119/// )
120/// .unwrap();
121/// ```
122#[derive(Clone)]
123pub struct ExportDriver {
124    markers: Option<BTreeSet<DataMarkerInfo>>,
125    requested_families: HashMap<DataLocale, DataLocaleFamilyAnnotations>,
126    #[expect(clippy::type_complexity)] // sigh
127    attributes_filters:
128        HashMap<String, Arc<Box<dyn Fn(&DataMarkerAttributes) -> bool + Send + Sync + 'static>>>,
129    fallbacker: LocaleFallbacker,
130    include_full: bool,
131    deduplication_strategy: DeduplicationStrategy,
132}
133
134impl core::fmt::Debug for ExportDriver {
135    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
136        f.debug_struct("ExportDriver")
137            .field("markers", &self.markers)
138            .field("requested_families", &self.requested_families)
139            .field("attributes_filters", &self.attributes_filters.keys())
140            .field("fallbacker", &self.fallbacker)
141            .field("include_full", &self.include_full)
142            .field("deduplication_strategy", &self.deduplication_strategy)
143            .finish()
144    }
145}
146
147impl ExportDriver {
148    /// Creates a [`ExportDriver`].
149    ///
150    /// The fallbacker is used to resolve locale families, and to dedpulicate data if requested.
151    /// Make sure to use the same fallback data when loading from the provider at runtime.
152    /// Commonly, you will export the fallback markers, in which case you should construct
153    /// your fallbacker with the source provider (i.e. [`LocaleFallbacker::try_new_unstable`]).
154    pub fn new(
155        locales: impl IntoIterator<Item = DataLocaleFamily>,
156        options: FallbackOptions,
157        fallbacker: LocaleFallbacker,
158    ) -> Self {
159        let mut include_full = false;
160        Self {
161            markers: Default::default(),
162            requested_families: locales
163                .into_iter()
164                .filter_map(|family| {
165                    Some((
166                        family.locale.or_else(|| {
167                            // Full locale family: set the bit instead of adding to the set
168                            debug_assert_eq!(
169                                family.annotations,
170                                DataLocaleFamily::FULL.annotations
171                            );
172                            include_full = true;
173                            None
174                        })?,
175                        family.annotations,
176                    ))
177                })
178                .collect(),
179            attributes_filters: Default::default(),
180            include_full,
181            fallbacker,
182            deduplication_strategy: options.deduplication_strategy,
183        }
184    }
185
186    /// Sets the filter on a particular `domain` of [`DataMarkerAttributes`].
187    ///
188    /// These are keyed by a `domain`, which is [`DataMarkerInfo::attributes_domain`] and
189    /// can thus apply to multiple data markers at once.
190    pub fn with_marker_attributes_filter(
191        mut self,
192        domain: &str,
193        filter: impl Fn(&DataMarkerAttributes) -> bool + Send + Sync + 'static,
194    ) -> Self {
195        let old_value = self
196            .attributes_filters
197            .insert(String::from(domain), Arc::new(Box::new(filter)));
198        if old_value.is_some() {
199            log::warn!(
200                "Filter applied to domain '{domain}' multiple times; ignoring all but the last filter"
201            );
202        }
203        self
204    }
205
206    /// Sets this driver to generate the given data markers.
207    ///
208    /// If this is not called, all markers supported by the provider will be exported.
209    pub fn with_markers(self, markers: impl IntoIterator<Item = DataMarkerInfo>) -> Self {
210        Self {
211            markers: Some(markers.into_iter().collect()),
212            ..self
213        }
214    }
215
216    /// This option is only relevant if using `icu::collator`.
217    ///
218    /// By default, collations starting with `search` are excluded. This method can be used to reennable them.
219    ///
220    /// The special string `"search*"` causes all search collation tables to be included.
221    pub fn with_additional_collations(
222        self,
223        additional_collations: impl IntoIterator<Item = String>,
224    ) -> Self {
225        let set = additional_collations.into_iter().collect::<HashSet<_>>();
226        self.with_marker_attributes_filter("collator", move |attrs| {
227            attrs.is_empty()
228                || set.contains(attrs.as_str())
229                || !attrs.as_str().starts_with("search")
230                || set.contains("search*")
231        })
232    }
233
234    /// This option is only relevant if using `icu::segmenter`.
235    ///
236    /// Sets this driver to generate the recommended segmentation models, to the extent required by the
237    /// chosen data markers.
238    pub fn with_recommended_segmenter_models(mut self) -> Self {
239        self.attributes_filters.remove("segmenter");
240        self
241    }
242
243    /// This option is only relevant if using `icu::segmenter`.
244    ///
245    /// Sets this driver to generate the given segmentation models, to the extent required by the
246    /// chosen data markers.
247    ///
248    /// The currently supported dictionary models are
249    /// * `cjdict`
250    /// * `burmesedict`
251    /// * `khmerdict`
252    /// * `laodict`
253    /// * `thaidict`
254    ///
255    /// The currently supported LSTM models are
256    /// * `Burmese_codepoints_exclusive_model4_heavy`
257    /// * `Khmer_codepoints_exclusive_model4_heavy`
258    /// * `Lao_codepoints_exclusive_model4_heavy`
259    /// * `Thai_codepoints_exclusive_model4_heavy`
260    ///
261    /// If a model is not included, the resulting line or word segmenter will apply rule-based
262    /// segmentation when encountering text in a script that requires the model, which will be
263    /// incorrect.
264    ///
265    /// If multiple models for the same language and segmentation type (dictionary/LSTM) are
266    /// listed, the first one will be used.
267    pub fn with_segmenter_models(self, models: impl IntoIterator<Item = String>) -> Self {
268        let set = models.into_iter().collect::<HashSet<_>>();
269        self.with_marker_attributes_filter("segmenter", move |attrs| set.contains(attrs.as_str()))
270    }
271
272    /// Exports data from the given provider to the given exporter.
273    ///
274    /// See
275    /// [`make_exportable_provider!`](icu_provider::export::make_exportable_provider),
276    /// [`BlobExporter`](icu_provider_blob::export),
277    /// [`FileSystemExporter`](icu_provider_fs::export),
278    /// and [`BakedExporter`](icu_provider_baked::export).
279    pub fn export(
280        self,
281        provider: &impl ExportableProvider,
282        mut sink: impl DataExporter,
283    ) -> Result<ExportMetadata, DataError> {
284        // Avoids multiple monomorphizations
285        self.export_dyn(provider, &mut sink)
286    }
287}
288
289#[non_exhaustive]
290#[derive(Debug)]
291/// Contains information about a successful export.
292pub struct ExportMetadata {
293    /// The metadata coming from the [`DataExporter`].
294    pub exporter: ExporterCloseMetadata,
295}
296
297/// Options bag configuring locale inclusion and behavior when runtime fallback is disabled.
298#[non_exhaustive]
299#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
300pub struct NoFallbackOptions {}
301
302/// Choices for determining the deduplication of locales for exported data payloads.
303///
304/// Deduplication affects the lookup table from locales to data payloads. If a child locale
305/// points to the same payload as its parent locale, then the child locale can be removed from
306/// the lookup table. Therefore, all deduplication strategies guarantee that data requests for
307/// selected locales will succeed so long as fallback is enabled at runtime (either internally
308/// or externally). They also do not impact which _payloads_ are included: only the lookup table.
309///
310/// Comparison of the deduplication strategies:
311///
312/// | Name | Data file size | Supported locale queries? | Needs runtime fallback? |
313/// |---|---|---|---|
314/// | [`Maximal`] | Smallest | No | Yes |
315/// | [`RetainBaseLanguages`] | Small | Yes | Yes |
316/// | [`None`] | Medium/Small | Yes | No |
317///
318/// [`Maximal`]: DeduplicationStrategy::Maximal
319/// [`RetainBaseLanguages`]: DeduplicationStrategy::RetainBaseLanguages
320/// [`None`]: DeduplicationStrategy::None
321#[non_exhaustive]
322#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
323pub enum DeduplicationStrategy {
324    /// Removes from the lookup table any locale whose parent maps to the same data.
325    Maximal,
326    /// Removes from the lookup table any locale whose parent maps to the same data, except if
327    /// the parent is `und`.
328    RetainBaseLanguages,
329    /// Keeps all selected locales in the lookup table.
330    None,
331}
332
333/// Options bag configuring locale inclusion and behavior when runtime fallback is enabled.
334#[derive(Debug, Copy, Clone, PartialEq, Eq)]
335#[non_exhaustive]
336pub struct FallbackOptions {
337    /// The aggressiveness of deduplication of data payloads.
338    pub deduplication_strategy: DeduplicationStrategy,
339}
340
341impl From<DeduplicationStrategy> for FallbackOptions {
342    fn from(deduplication_strategy: DeduplicationStrategy) -> Self {
343        Self {
344            deduplication_strategy,
345        }
346    }
347}
348
349/// Test that the last option with multiple conflicting families wins.
350#[test]
351fn test_family_precedence() {
352    let driver = ExportDriver::new(
353        [
354            "en".parse().unwrap(),
355            "%en".parse().unwrap(),
356            "@en".parse().unwrap(),
357            "%zh-TW".parse().unwrap(),
358            "^zh-TW".parse().unwrap(),
359        ],
360        DeduplicationStrategy::None.into(),
361        LocaleFallbacker::new_without_data(),
362    );
363
364    assert_eq!(
365        driver.requested_families,
366        [
367            (
368                icu::locale::data_locale!("en"),
369                DataLocaleFamilyAnnotations::single()
370            ),
371            (
372                icu::locale::data_locale!("zh-TW"),
373                DataLocaleFamilyAnnotations::without_descendants()
374            ),
375        ]
376        .into_iter()
377        .collect::<HashMap<_, _>>()
378    );
379}