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}