Skip to content

Commit 6cb032d

Browse files
Expose BCP-47 locale variant APIs over FFI (unicode-org#7519)
This PR adds FFI support for accessing and mutating BCP-47 locale variants so that non-Rust consumers (C/C++/JS/Dart) can work with variant subtags in a safe and consistent way. It exposes a small and focused set of Locale APIs to query, add, remove, and clear variants. Mutation operations are implemented by operating on a temporary collection, followed by sorting and deduplication before reconstructing the internal representation, ensuring canonical BCP-47 ordering is preserved. The change is intentionally scoped only to locale variants and does not alter existing Locale semantics. Unit tests are included to cover add/remove behavior, duplicate handling, canonical sorting, invalid variant input, and bounds checking. This work is intended to improve feature parity for FFI consumers while keeping the API surface minimal and maintainable. Closes unicode-org#6671. Co-authored-by: Robert Bastian <4706271+robertbastian@users.noreply.github.com>
1 parent 8ef6120 commit 6cb032d

File tree

13 files changed

+888
-0
lines changed

13 files changed

+888
-0
lines changed

components/locale_core/src/subtags/variants.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,58 @@ impl Variants {
119119
self.0.is_empty()
120120
}
121121

122+
/// Adds a variant to the list, maintaining sorted order.
123+
///
124+
/// Returns `true` if the variant was added, `false` if it was already present.
125+
///
126+
/// ✨ *Enabled with the `alloc` Cargo feature.*
127+
///
128+
/// # Examples
129+
///
130+
/// ```
131+
/// use icu::locale::subtags::{variant, Variants};
132+
///
133+
/// let mut variants = Variants::new();
134+
/// assert!(variants.push(variant!("posix")));
135+
/// assert!(!variants.push(variant!("posix"))); // Already present
136+
/// assert!(variants.push(variant!("macos")));
137+
/// assert_eq!(variants.to_string(), "macos-posix");
138+
/// ```
139+
#[cfg(feature = "alloc")]
140+
pub fn push(&mut self, variant: Variant) -> bool {
141+
match self.binary_search(&variant) {
142+
Ok(_) => false, // Already present
143+
Err(i) => {
144+
self.0.insert(i, variant);
145+
true
146+
}
147+
}
148+
}
149+
150+
/// Removes a variant from the list.
151+
///
152+
/// Returns `true` if the variant was removed, `false` if it was not present.
153+
///
154+
/// # Examples
155+
///
156+
/// ```
157+
/// use icu::locale::subtags::{variant, Variants};
158+
///
159+
/// let mut variants = Variants::from_variant(variant!("posix"));
160+
/// assert!(variants.remove(&variant!("posix")));
161+
/// assert!(!variants.remove(&variant!("posix"))); // Already removed
162+
/// assert!(variants.is_empty());
163+
/// ```
164+
pub fn remove(&mut self, variant: &Variant) -> bool {
165+
match self.binary_search(variant) {
166+
Ok(i) => {
167+
self.0.remove(i);
168+
true
169+
}
170+
Err(_) => false,
171+
}
172+
}
173+
122174
pub(crate) fn for_each_subtag_str<E, F>(&self, f: &mut F) -> Result<(), E>
123175
where
124176
F: FnMut(&str) -> Result<(), E>,

examples/cpp/locale.cpp

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,5 +105,60 @@ int main() {
105105
return 1;
106106
}
107107

108+
// --- Variant Tests ---
109+
locale = Locale::from_string("en-US-valencia").ok().value();
110+
if (locale->variant_count() != 1) {
111+
std::cout << "Expected 1 variant, got " << locale->variant_count() << std::endl;
112+
return 1;
113+
}
114+
if (!locale->has_variant("valencia")) {
115+
std::cout << "Expected locale to have 'valencia' variant" << std::endl;
116+
return 1;
117+
}
118+
119+
// Add a variant (returns Result<bool, Error>)
120+
auto added = locale->add_variant("posix").ok().value();
121+
if (!added) {
122+
std::cout << "Expected add_variant to return true for new variant" << std::endl;
123+
return 1;
124+
}
125+
if (locale->variant_count() != 2) {
126+
std::cout << "After add, expected 2 variants, got " << locale->variant_count() << std::endl;
127+
return 1;
128+
}
129+
130+
// Add duplicate (should return false)
131+
auto addedDup = locale->add_variant("posix").ok().value();
132+
if (addedDup) {
133+
std::cout << "Expected add_variant to return false for duplicate" << std::endl;
134+
return 1;
135+
}
136+
137+
// Remove a variant (returns bool)
138+
auto removed = locale->remove_variant("posix");
139+
if (!removed) {
140+
std::cout << "Expected remove_variant to return true" << std::endl;
141+
return 1;
142+
}
143+
if (locale->variant_count() != 1) {
144+
std::cout << "After remove, expected 1 variant" << std::endl;
145+
return 1;
146+
}
147+
148+
// Remove non-existent (returns false)
149+
auto removedNone = locale->remove_variant("posix");
150+
if (removedNone) {
151+
std::cout << "Expected remove_variant to return false for non-existent" << std::endl;
152+
return 1;
153+
}
154+
155+
// Clear variants
156+
locale->clear_variants();
157+
if (locale->variant_count() != 0) {
158+
std::cout << "After clear, expected 0 variants" << std::endl;
159+
return 1;
160+
}
161+
std::cout << "Variant tests passed!" << std::endl;
162+
108163
return 0;
109164
}

ffi/capi/bindings/c/Locale.h

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ffi/capi/bindings/cpp/icu4x/Locale.d.hpp

Lines changed: 59 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ffi/capi/bindings/cpp/icu4x/Locale.hpp

Lines changed: 74 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ffi/capi/src/locale_core.rs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,69 @@ pub mod ffi {
159159
Ok(())
160160
}
161161

162+
// --- Variants ---
163+
164+
/// Returns a string representation of the [`Locale`] variants.
165+
#[diplomat::rust_link(icu::locale::Variants, Struct)]
166+
pub fn variants(&self, write: &mut diplomat_runtime::DiplomatWrite) {
167+
let _infallible = self.0.id.variants.write_to(write);
168+
}
169+
170+
/// Returns the number of variants in this [`Locale`].
171+
#[diplomat::rust_link(icu::locale::Variants, Struct)]
172+
#[diplomat::attr(auto, getter)]
173+
pub fn variant_count(&self) -> usize {
174+
self.0.id.variants.len()
175+
}
176+
177+
/// Returns the variant at the given index, or nothing if the index is out of bounds.
178+
#[diplomat::rust_link(icu::locale::Variants, Struct)]
179+
pub fn variant_at(
180+
&self,
181+
index: usize,
182+
write: &mut diplomat_runtime::DiplomatWrite,
183+
) -> Option<()> {
184+
let _infallible = self.0.id.variants.get(index)?.write_to(write);
185+
Some(())
186+
}
187+
188+
/// Returns whether the [`Locale`] has a specific variant.
189+
#[diplomat::rust_link(icu::locale::Variants, Struct)]
190+
pub fn has_variant(&self, s: &DiplomatStr) -> bool {
191+
icu_locale_core::subtags::Variant::try_from_utf8(s)
192+
.map(|v| self.0.id.variants.contains(&v))
193+
.unwrap_or(false)
194+
}
195+
196+
/// Adds a variant to the [`Locale`].
197+
///
198+
/// Returns an error if the variant string is invalid.
199+
/// Returns `true` if the variant was added, `false` if already present.
200+
#[diplomat::rust_link(icu::locale::Variants::push, FnInStruct)]
201+
pub fn add_variant(&mut self, s: &DiplomatStr) -> Result<bool, LocaleParseError> {
202+
let variant = icu_locale_core::subtags::Variant::try_from_utf8(s)?;
203+
Ok(self.0.id.variants.push(variant))
204+
}
205+
206+
/// Removes a variant from the [`Locale`].
207+
///
208+
/// Returns `true` if the variant was removed, `false` if not present.
209+
/// Returns `false` for invalid variant strings (they cannot exist in the locale).
210+
#[diplomat::rust_link(icu::locale::Variants::remove, FnInStruct)]
211+
pub fn remove_variant(&mut self, s: &DiplomatStr) -> bool {
212+
icu_locale_core::subtags::Variant::try_from_utf8(s)
213+
.map(|v| self.0.id.variants.remove(&v))
214+
.unwrap_or(false)
215+
}
216+
217+
/// Clears all variants from the [`Locale`].
218+
#[diplomat::rust_link(icu::locale::Variants::clear, FnInStruct)]
219+
pub fn clear_variants(&mut self) {
220+
self.0.id.variants.clear();
221+
}
222+
223+
// --- Other ---
224+
162225
/// Normalizes a locale string.
163226
#[diplomat::rust_link(icu::locale::Locale::normalize, FnInStruct)]
164227
#[diplomat::rust_link(icu::locale::Locale::normalize_utf8, FnInStruct, hidden)]

0 commit comments

Comments
 (0)