Make the ICU SONAME configurable (#495)

This PR was tested on Ubuntu with:
```
EDIT_CFG_ICUUC_SONAME=libicuuc.so.74
EDIT_CFG_ICUI18N_SONAME=libicui18n.so.74
EDIT_CFG_ICU_RENAMING_VERSION=74
cargo build --config .cargo/release.toml --release
```

Search & Replace now works flawlessly. I hope that package maintainers
will be able to make use of this when ingesting future versions of Edit.

Closes #172
This commit is contained in:
Leonard Hecker 2025-06-19 23:26:05 +02:00 committed by GitHub
parent 70f5b73878
commit b277a1e67b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 317 additions and 81 deletions

View file

@ -19,14 +19,6 @@ You can install the latest version with WinGet:
winget install Microsoft.Edit
```
### Notes to Package Maintainers
The canonical executable name is "edit" and the alternative name is "msedit".
We're aware of the potential conflict of "edit" with existing commands and as such recommend naming packages and executables "msedit".
Names such as "ms-edit" should be avoided.
Assigning an "edit" alias is recommended if possible.
## Build Instructions
* [Install Rust](https://www.rust-lang.org/tools/install)
@ -34,3 +26,45 @@ Assigning an "edit" alias is recommended if possible.
* Alternatively, set the environment variable `RUSTC_BOOTSTRAP=1`
* Clone the repository
* For a release build, run: `cargo build --config .cargo/release.toml --release`
## Notes to Package Maintainers
### Package Naming
The canonical executable name is "edit" and the alternative name is "msedit".
We're aware of the potential conflict of "edit" with existing commands and recommend alternatively naming packages and executables "msedit".
Names such as "ms-edit" should be avoided.
Assigning an "edit" alias is recommended, if possible.
### ICU library name (SONAME)
This project _optionally_ depends on the ICU library for its Search and Replace functionality.
By default, the project will look for a SONAME without version suffix:
* Windows: `icuuc.dll`
* macOS: `libicuuc.dylib`
* UNIX, and other OS: `libicuuc.so`
If your installation uses a different SONAME, please set the following environment variable at build time:
* `EDIT_CFG_ICUUC_SONAME`:
For instance, `libicuuc.so.76`.
* `EDIT_CFG_ICUI18N_SONAME`:
For instance, `libicui18n.so.76`.
Additionally, this project assumes that the ICU exports are exported without `_` prefix and without version suffix, such as `u_errorName`.
If your installation uses versioned exports, please set:
* `EDIT_CFG_ICU_CPP_EXPORTS`:
If set to `true`, it'll look for C++ symbols such as `_u_errorName`.
Enabled by default on macOS.
* `EDIT_CFG_ICU_RENAMING_VERSION`:
If set to a version number, such as `76`, it'll look for symbols such as `u_errorName_76`.
Finally, you can set the following environment variables:
* `EDIT_CFG_ICU_RENAMING_AUTO_DETECT`:
If set to `true`, the executable will try to detect the `EDIT_CFG_ICU_RENAMING_VERSION` value at runtime.
The way it does this is not officially supported by ICU and as such is not recommended to be relied upon.
Enabled by default on UNIX (excluding macOS) if no other options are set.
To test your settings, run `cargo test` again but with the `--ignored` flag. For instance:
```sh
cargo test -- --ignored
```

View file

@ -1,9 +1,86 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use core::panic;
use std::env::VarError;
#[derive(PartialEq, Eq)]
enum TargetOs {
Windows,
MacOS,
Unix,
}
fn main() {
let target_os = match env_opt("CARGO_CFG_TARGET_OS").as_str() {
"windows" => TargetOs::Windows,
"macos" | "ios" => TargetOs::MacOS,
_ => TargetOs::Unix,
};
let icuuc_soname = env_opt("EDIT_CFG_ICUUC_SONAME");
let icui18n_soname = env_opt("EDIT_CFG_ICUI18N_SONAME");
let cpp_exports = env_opt("EDIT_CFG_ICU_CPP_EXPORTS");
let renaming_version = env_opt("EDIT_CFG_ICU_RENAMING_VERSION");
let renaming_auto_detect = env_opt("EDIT_CFG_ICU_RENAMING_AUTO_DETECT");
// If none of the `EDIT_CFG_ICU*` environment variables are set,
// we default to enabling `EDIT_CFG_ICU_RENAMING_AUTO_DETECT` on UNIX.
// This slightly improves portability at least in the cases where the SONAMEs match our defaults.
let renaming_auto_detect = if !renaming_auto_detect.is_empty() {
renaming_auto_detect.parse::<bool>().unwrap()
} else {
target_os == TargetOs::Unix
&& icuuc_soname.is_empty()
&& icui18n_soname.is_empty()
&& cpp_exports.is_empty()
&& renaming_version.is_empty()
};
if renaming_auto_detect && !renaming_version.is_empty() {
// It makes no sense to specify an explicit version and also ask for auto-detection.
panic!(
"Either `EDIT_CFG_ICU_RENAMING_AUTO_DETECT` or `EDIT_CFG_ICU_RENAMING_VERSION` must be set, but not both"
);
}
let icuuc_soname = if !icuuc_soname.is_empty() {
&icuuc_soname
} else {
match target_os {
TargetOs::Windows => "icuuc.dll",
TargetOs::MacOS => "libicucore.dylib",
TargetOs::Unix => "libicuuc.so",
}
};
let icui18n_soname = if !icui18n_soname.is_empty() {
&icui18n_soname
} else {
match target_os {
TargetOs::Windows => "icuin.dll",
TargetOs::MacOS => "libicucore.dylib",
TargetOs::Unix => "libicui18n.so",
}
};
let icu_export_prefix =
if !cpp_exports.is_empty() && cpp_exports.parse::<bool>().unwrap() { "_" } else { "" };
let icu_export_suffix =
if !renaming_version.is_empty() { format!("_{renaming_version}") } else { String::new() };
println!("cargo::rerun-if-env-changed=EDIT_CFG_ICUUC_SONAME");
println!("cargo::rustc-env=EDIT_CFG_ICUUC_SONAME={icuuc_soname}");
println!("cargo::rerun-if-env-changed=EDIT_CFG_ICUI18N_SONAME");
println!("cargo::rustc-env=EDIT_CFG_ICUI18N_SONAME={icui18n_soname}");
println!("cargo::rerun-if-env-changed=EDIT_CFG_ICU_EXPORT_PREFIX");
println!("cargo::rustc-env=EDIT_CFG_ICU_EXPORT_PREFIX={icu_export_prefix}");
println!("cargo::rerun-if-env-changed=EDIT_CFG_ICU_EXPORT_SUFFIX");
println!("cargo::rustc-env=EDIT_CFG_ICU_EXPORT_SUFFIX={icu_export_suffix}");
println!("cargo::rerun-if-env-changed=EDIT_CFG_ICU_RENAMING_AUTO_DETECT");
println!("cargo::rustc-check-cfg=cfg(edit_icu_renaming_auto_detect)");
if renaming_auto_detect {
println!("cargo::rustc-cfg=edit_icu_renaming_auto_detect");
}
#[cfg(windows)]
if std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default() == "windows" {
if target_os == TargetOs::Windows {
winresource::WindowsResource::new()
.set_manifest_file("src/bin/edit/edit.exe.manifest")
.set("FileDescription", "Microsoft Edit")
@ -13,3 +90,13 @@ fn main() {
.unwrap();
}
}
fn env_opt(name: &str) -> String {
match std::env::var(name) {
Ok(value) => value,
Err(VarError::NotPresent) => String::new(),
Err(VarError::NotUnicode(_)) => {
panic!("Environment variable `{name}` is not valid Unicode")
}
}
}

View file

@ -79,6 +79,10 @@ impl Arena {
})
}
pub fn is_empty(&self) -> bool {
self.base == NonNull::dangling()
}
pub fn offset(&self) -> usize {
self.offset.get()
}
@ -171,7 +175,7 @@ impl Arena {
impl Drop for Arena {
fn drop(&mut self) {
if self.base != NonNull::dangling() {
if !self.is_empty() {
unsafe { sys::virtual_release(self.base, self.capacity) };
}
}

View file

@ -46,6 +46,11 @@ pub fn init(capacity: usize) -> apperr::Result<()> {
/// If your function takes an [`Arena`] argument, you **MUST** pass it to `scratch_arena` as `Some(&arena)`.
pub fn scratch_arena(conflict: Option<&Arena>) -> ScratchArena<'static> {
unsafe {
#[cfg(test)]
if S_SCRATCH[0].is_empty() {
init(128 * 1024 * 1024).unwrap();
}
#[cfg(debug_assertions)]
let conflict = conflict.map(|a| a.delegate_target_unchecked());

View file

@ -4,7 +4,7 @@
//! Bindings to the ICU library.
use std::cmp::Ordering;
use std::ffi::CStr;
use std::ffi::{CStr, c_char};
use std::mem;
use std::mem::MaybeUninit;
use std::ops::Range;
@ -922,33 +922,40 @@ struct LibraryFunctions {
uregex_end64: icu_ffi::uregex_end64,
}
macro_rules! proc_name {
($s:literal) => {
concat!(env!("EDIT_CFG_ICU_EXPORT_PREFIX"), $s, env!("EDIT_CFG_ICU_EXPORT_SUFFIX"), "\0")
.as_ptr() as *const c_char
};
}
// Found in libicuuc.so on UNIX, icuuc.dll/icu.dll on Windows.
const LIBICUUC_PROC_NAMES: [&CStr; 10] = [
c"u_errorName",
c"ucasemap_open",
c"ucasemap_utf8FoldCase",
c"ucnv_getAvailableName",
c"ucnv_getStandardName",
c"ucnv_open",
c"ucnv_close",
c"ucnv_convertEx",
c"utext_setup",
c"utext_close",
const LIBICUUC_PROC_NAMES: [*const c_char; 10] = [
proc_name!("u_errorName"),
proc_name!("ucasemap_open"),
proc_name!("ucasemap_utf8FoldCase"),
proc_name!("ucnv_getAvailableName"),
proc_name!("ucnv_getStandardName"),
proc_name!("ucnv_open"),
proc_name!("ucnv_close"),
proc_name!("ucnv_convertEx"),
proc_name!("utext_setup"),
proc_name!("utext_close"),
];
// Found in libicui18n.so on UNIX, icuin.dll/icu.dll on Windows.
const LIBICUI18N_PROC_NAMES: [&CStr; 11] = [
c"ucol_open",
c"ucol_strcollUTF8",
c"uregex_open",
c"uregex_close",
c"uregex_setTimeLimit",
c"uregex_setUText",
c"uregex_reset64",
c"uregex_findNext",
c"uregex_groupCount",
c"uregex_start64",
c"uregex_end64",
const LIBICUI18N_PROC_NAMES: [*const c_char; 11] = [
proc_name!("ucol_open"),
proc_name!("ucol_strcollUTF8"),
proc_name!("uregex_open"),
proc_name!("uregex_close"),
proc_name!("uregex_setTimeLimit"),
proc_name!("uregex_setUText"),
proc_name!("uregex_reset64"),
proc_name!("uregex_findNext"),
proc_name!("uregex_groupCount"),
proc_name!("uregex_start64"),
proc_name!("uregex_end64"),
];
enum LibraryFunctionsState {
@ -971,10 +978,7 @@ fn init_if_needed() -> apperr::Result<&'static LibraryFunctions> {
unsafe {
LIBRARY_FUNCTIONS = LibraryFunctionsState::Failed;
let Ok(libicuuc) = sys::load_libicuuc() else {
return;
};
let Ok(libicui18n) = sys::load_libicui18n() else {
let Ok(icu) = sys::load_icu() else {
return;
};
@ -998,25 +1002,26 @@ fn init_if_needed() -> apperr::Result<&'static LibraryFunctions> {
let mut funcs = MaybeUninit::<LibraryFunctions>::uninit();
let mut ptr = funcs.as_mut_ptr() as *mut TransparentFunction;
#[cfg(unix)]
#[cfg(edit_icu_renaming_auto_detect)]
let scratch_outer = scratch_arena(None);
#[cfg(unix)]
let suffix = sys::icu_proc_suffix(&scratch_outer, libicuuc);
#[cfg(edit_icu_renaming_auto_detect)]
let suffix = sys::icu_detect_renaming_suffix(&scratch_outer, icu.libicuuc);
for (handle, names) in
[(libicuuc, &LIBICUUC_PROC_NAMES[..]), (libicui18n, &LIBICUI18N_PROC_NAMES[..])]
{
for name in names {
#[cfg(unix)]
for (handle, names) in [
(icu.libicuuc, &LIBICUUC_PROC_NAMES[..]),
(icu.libicui18n, &LIBICUI18N_PROC_NAMES[..]),
] {
for &name in names {
#[cfg(edit_icu_renaming_auto_detect)]
let scratch = scratch_arena(Some(&scratch_outer));
#[cfg(unix)]
let name = &sys::add_icu_proc_suffix(&scratch, name, &suffix);
#[cfg(edit_icu_renaming_auto_detect)]
let name = sys::icu_add_renaming_suffix(&scratch, name, &suffix);
let Ok(func) = sys::get_proc_address(handle, name) else {
debug_assert!(
false,
"Failed to load ICU function: {}",
name.to_string_lossy()
"Failed to load ICU function: {:?}",
CStr::from_ptr(name)
);
return;
};
@ -1314,6 +1319,12 @@ mod icu_ffi {
mod tests {
use super::*;
#[ignore]
#[test]
fn init() {
assert!(init_if_needed().is_ok());
}
#[test]
fn test_compare_strings_ascii() {
// Empty strings

View file

@ -6,8 +6,8 @@
//! Read the `windows` module for reference.
//! TODO: This reminds me that the sys API should probably be a trait.
use std::ffi::{CStr, c_int, c_void};
use std::fs::{self, File};
use std::ffi::{CStr, c_char, c_int, c_void};
use std::fs::File;
use std::mem::{self, ManuallyDrop, MaybeUninit};
use std::os::fd::{AsRawFd as _, FromRawFd as _};
use std::path::Path;
@ -433,9 +433,9 @@ pub unsafe fn virtual_commit(base: NonNull<u8>, size: usize) -> apperr::Result<(
}
}
unsafe fn load_library(name: &CStr) -> apperr::Result<NonNull<c_void>> {
unsafe fn load_library(name: *const c_char) -> apperr::Result<NonNull<c_void>> {
unsafe {
NonNull::new(libc::dlopen(name.as_ptr(), libc::RTLD_LAZY))
NonNull::new(libc::dlopen(name, libc::RTLD_LAZY))
.ok_or_else(|| errno_to_apperr(libc::ENOENT))
}
}
@ -448,9 +448,12 @@ unsafe fn load_library(name: &CStr) -> apperr::Result<NonNull<c_void>> {
/// of the function you're loading. No type checks whatsoever are performed.
//
// It'd be nice to constrain T to std::marker::FnPtr, but that's unstable.
pub unsafe fn get_proc_address<T>(handle: NonNull<c_void>, name: &CStr) -> apperr::Result<T> {
pub unsafe fn get_proc_address<T>(
handle: NonNull<c_void>,
name: *const c_char,
) -> apperr::Result<T> {
unsafe {
let sym = libc::dlsym(handle.as_ptr(), name.as_ptr());
let sym = libc::dlsym(handle.as_ptr(), name);
if sym.is_null() {
Err(errno_to_apperr(libc::ENOENT))
} else {
@ -459,20 +462,46 @@ pub unsafe fn get_proc_address<T>(handle: NonNull<c_void>, name: &CStr) -> apper
}
}
pub fn load_libicuuc() -> apperr::Result<NonNull<c_void>> {
unsafe { load_library(c"libicuuc.so") }
pub struct LibIcu {
pub libicuuc: NonNull<c_void>,
pub libicui18n: NonNull<c_void>,
}
pub fn load_libicui18n() -> apperr::Result<NonNull<c_void>> {
unsafe { load_library(c"libicui18n.so") }
pub fn load_icu() -> apperr::Result<LibIcu> {
const fn const_str_eq(a: &str, b: &str) -> bool {
let a = a.as_bytes();
let b = b.as_bytes();
let mut i = 0;
loop {
if i >= a.len() || i >= b.len() {
return a.len() == b.len();
}
if a[i] != b[i] {
return false;
}
i += 1;
}
}
const LIBICUUC: &str = concat!(env!("EDIT_CFG_ICUUC_SONAME"), "\0");
const LIBICUI18N: &str = concat!(env!("EDIT_CFG_ICUI18N_SONAME"), "\0");
if const { const_str_eq(LIBICUUC, LIBICUI18N) } {
let icu = unsafe { load_library(LIBICUUC.as_ptr() as *const _)? };
Ok(LibIcu { libicuuc: icu, libicui18n: icu })
} else {
let libicuuc = unsafe { load_library(LIBICUUC.as_ptr() as *const _)? };
let libicui18n = unsafe { load_library(LIBICUI18N.as_ptr() as *const _)? };
Ok(LibIcu { libicuuc, libicui18n })
}
}
/// ICU, by default, adds the major version as a suffix to each exported symbol.
/// They also recommend to disable this for system-level installations (`runConfigureICU Linux --disable-renaming`),
/// but I found that many (most?) Linux distributions don't do this for some reason.
/// This function returns the suffix, if any.
#[allow(clippy::not_unsafe_ptr_arg_deref)]
pub fn icu_proc_suffix(arena: &Arena, handle: NonNull<c_void>) -> ArenaString<'_> {
#[cfg(edit_icu_renaming_auto_detect)]
pub fn icu_detect_renaming_suffix(arena: &Arena, handle: NonNull<c_void>) -> ArenaString<'_> {
unsafe {
type T = *const c_void;
@ -480,7 +509,7 @@ pub fn icu_proc_suffix(arena: &Arena, handle: NonNull<c_void>) -> ArenaString<'_
// Check if the ICU library is using unversioned symbols.
// Return an empty suffix in that case.
if get_proc_address::<T>(handle, c"u_errorName").is_ok() {
if get_proc_address::<T>(handle, c"u_errorName".as_ptr()).is_ok() {
return res;
}
@ -488,7 +517,7 @@ pub fn icu_proc_suffix(arena: &Arena, handle: NonNull<c_void>) -> ArenaString<'_
// this symbol seems to be always present. This allows us to call `dladdr`.
// It's the `UCaseMap::~UCaseMap()` destructor which for some reason isn't
// in a namespace. Thank you ICU maintainers for this oversight.
let proc = match get_proc_address::<T>(handle, c"_ZN8UCaseMapD1Ev") {
let proc = match get_proc_address::<T>(handle, c"_ZN8UCaseMapD1Ev".as_ptr()) {
Ok(proc) => proc,
Err(_) => return res,
};
@ -506,7 +535,7 @@ pub fn icu_proc_suffix(arena: &Arena, handle: NonNull<c_void>) -> ArenaString<'_
Err(_) => return res,
};
let path = match fs::read_link(path) {
let path = match std::fs::read_link(path) {
Ok(path) => path,
Err(_) => path.into(),
};
@ -528,7 +557,13 @@ pub fn icu_proc_suffix(arena: &Arena, handle: NonNull<c_void>) -> ArenaString<'_
}
}
pub fn add_icu_proc_suffix<'a, 'b, 'r>(arena: &'a Arena, name: &'b CStr, suffix: &str) -> &'r CStr
#[cfg(edit_icu_renaming_auto_detect)]
#[allow(clippy::not_unsafe_ptr_arg_deref)]
pub fn icu_add_renaming_suffix<'a, 'b, 'r>(
arena: &'a Arena,
name: *const c_char,
suffix: &str,
) -> *const c_char
where
'a: 'r,
'b: 'r,
@ -538,16 +573,15 @@ where
} else {
// SAFETY: In this particular case we know that the string
// is valid UTF-8, because it comes from icu.rs.
let name = unsafe { CStr::from_ptr(name) };
let name = unsafe { name.to_str().unwrap_unchecked() };
let mut res = ArenaString::new_in(arena);
let mut res = ManuallyDrop::new(ArenaString::new_in(arena));
res.reserve(name.len() + suffix.len() + 1);
res.push_str(name);
res.push_str(suffix);
res.push('\0');
let bytes: &'a [u8] = unsafe { mem::transmute(res.as_bytes()) };
unsafe { CStr::from_bytes_with_nul_unchecked(bytes) }
res.as_ptr() as *const c_char
}
}

View file

@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use std::ffi::{CStr, OsString, c_void};
use std::ffi::{OsString, c_char, c_void};
use std::fmt::Write as _;
use std::fs::{self, File};
use std::mem::MaybeUninit;
@ -20,6 +20,35 @@ use crate::apperr;
use crate::arena::{Arena, ArenaString, scratch_arena};
use crate::helpers::*;
macro_rules! w_env {
($s:literal) => {{
const INPUT: &[u8] = env!($s).as_bytes();
const OUTPUT_LEN: usize = windows_sys::core::utf16_len(INPUT) + 1;
const OUTPUT: &[u16; OUTPUT_LEN] = {
let mut buffer = [0; OUTPUT_LEN];
let mut input_pos = 0;
let mut output_pos = 0;
while let Some((mut code_point, new_pos)) =
windows_sys::core::decode_utf8_char(INPUT, input_pos)
{
input_pos = new_pos;
if code_point <= 0xffff {
buffer[output_pos] = code_point as u16;
output_pos += 1;
} else {
code_point -= 0x10000;
buffer[output_pos] = 0xd800 + (code_point >> 10) as u16;
output_pos += 1;
buffer[output_pos] = 0xdc00 + (code_point & 0x3ff) as u16;
output_pos += 1;
}
}
&{ buffer }
};
OUTPUT.as_ptr()
}};
}
type ReadConsoleInputExW = unsafe extern "system" fn(
h_console_input: Foundation::HANDLE,
lp_buffer: *mut Console::INPUT_RECORD,
@ -109,7 +138,10 @@ pub fn init() -> apperr::Result<Deinit> {
}
unsafe fn load_read_func(module: *const u16) -> apperr::Result<ReadConsoleInputExW> {
unsafe { get_module(module).and_then(|m| get_proc_address(m, c"ReadConsoleInputExW")) }
unsafe {
get_module(module)
.and_then(|m| get_proc_address(m, c"ReadConsoleInputExW".as_ptr()))
}
}
// `kernel32.dll` doesn't exist on OneCore variants of Windows.
@ -564,21 +596,50 @@ unsafe fn load_library(name: *const u16) -> apperr::Result<NonNull<c_void>> {
/// of the function you're loading. No type checks whatsoever are performed.
//
// It'd be nice to constrain T to std::marker::FnPtr, but that's unstable.
pub unsafe fn get_proc_address<T>(handle: NonNull<c_void>, name: &CStr) -> apperr::Result<T> {
pub unsafe fn get_proc_address<T>(
handle: NonNull<c_void>,
name: *const c_char,
) -> apperr::Result<T> {
unsafe {
let ptr = LibraryLoader::GetProcAddress(handle.as_ptr(), name.as_ptr() as *const u8);
let ptr = LibraryLoader::GetProcAddress(handle.as_ptr(), name as *const u8);
if let Some(ptr) = ptr { Ok(mem::transmute_copy(&ptr)) } else { Err(get_last_error()) }
}
}
/// Loads the "common" portion of ICU4C.
pub fn load_libicuuc() -> apperr::Result<NonNull<c_void>> {
unsafe { load_library(w!("icuuc.dll")) }
pub struct LibIcu {
pub libicuuc: NonNull<c_void>,
pub libicui18n: NonNull<c_void>,
}
/// Loads the internationalization portion of ICU4C.
pub fn load_libicui18n() -> apperr::Result<NonNull<c_void>> {
unsafe { load_library(w!("icuin.dll")) }
pub fn load_icu() -> apperr::Result<LibIcu> {
const fn const_ptr_u16_eq(a: *const u16, b: *const u16) -> bool {
unsafe {
let mut a = a;
let mut b = b;
loop {
if *a != *b {
return false;
}
if *a == 0 {
return true;
}
a = a.add(1);
b = b.add(1);
}
}
}
const LIBICUUC: *const u16 = w_env!("EDIT_CFG_ICUUC_SONAME");
const LIBICUI18N: *const u16 = w_env!("EDIT_CFG_ICUI18N_SONAME");
if const { const_ptr_u16_eq(LIBICUUC, LIBICUI18N) } {
let icu = unsafe { load_library(LIBICUUC)? };
Ok(LibIcu { libicuuc: icu, libicui18n: icu })
} else {
let libicuuc = unsafe { load_library(LIBICUUC)? };
let libicui18n = unsafe { load_library(LIBICUI18N)? };
Ok(LibIcu { libicuuc, libicui18n })
}
}
/// Returns a list of preferred languages for the current user.