Skip to main content

atmos/os_lib/js/builtins/
intl.rs

1//! `Intl` (ECMAScript Internationalization API Specification) 実装。
2//!
3//! `Intl.NumberFormat`, `Intl.DateTimeFormat`, `Intl.Collator`, `Intl.PluralRules`,
4//! `Intl.RelativeTimeFormat`, `Intl.Segmenter`, `Intl.ListFormat`, `Intl.DisplayNames`,
5//! および `Intl.getCanonicalLocales` を提供する。
6
7use super::*;
8use alloc::format;
9use alloc::rc::Rc;
10use alloc::string::{String, ToString};
11use alloc::vec;
12use alloc::vec::Vec;
13
14/// `Intl` グローバルオブジェクトを作成して返す。
15pub(crate) fn create_intl_object() -> Value {
16    let intl = Obj::plain();
17
18    // Intl.getCanonicalLocales(locales)
19    intl.borrow_mut().props.insert(
20        "getCanonicalLocales".into(),
21        nv("Intl.getCanonicalLocales", intl_get_canonical_locales),
22    );
23
24    // Intl.NumberFormat
25    let number_format = create_intl_constructor(
26        "NumberFormat",
27        intl_number_format_ctor,
28        vec![
29            ("format", intl_number_format_format),
30            ("formatToParts", intl_number_format_format_to_parts),
31            ("resolvedOptions", intl_number_format_resolved_options),
32        ],
33    );
34    intl.borrow_mut().props.insert("NumberFormat".into(), number_format);
35
36    // Intl.DateTimeFormat
37    let datetime_format = create_intl_constructor(
38        "DateTimeFormat",
39        intl_datetime_format_ctor,
40        vec![
41            ("format", intl_datetime_format_format),
42            ("formatToParts", intl_datetime_format_format_to_parts),
43            ("resolvedOptions", intl_datetime_format_resolved_options),
44        ],
45    );
46    intl.borrow_mut().props.insert("DateTimeFormat".into(), datetime_format);
47
48    // Intl.Collator
49    let collator = create_intl_constructor(
50        "Collator",
51        intl_collator_ctor,
52        vec![
53            ("compare", intl_collator_compare),
54            ("resolvedOptions", intl_collator_resolved_options),
55        ],
56    );
57    intl.borrow_mut().props.insert("Collator".into(), collator);
58
59    // Intl.PluralRules
60    let plural_rules = create_intl_constructor(
61        "PluralRules",
62        intl_plural_rules_ctor,
63        vec![
64            ("select", intl_plural_rules_select),
65            ("resolvedOptions", intl_plural_rules_resolved_options),
66        ],
67    );
68    intl.borrow_mut().props.insert("PluralRules".into(), plural_rules);
69
70    // Intl.RelativeTimeFormat
71    let relative_time_format = create_intl_constructor(
72        "RelativeTimeFormat",
73        intl_relative_time_format_ctor,
74        vec![
75            ("format", intl_relative_time_format_format),
76            ("formatToParts", intl_relative_time_format_format_to_parts),
77            ("resolvedOptions", intl_relative_time_format_resolved_options),
78        ],
79    );
80    intl.borrow_mut().props.insert("RelativeTimeFormat".into(), relative_time_format);
81
82    // Intl.Segmenter
83    let segmenter = create_intl_constructor(
84        "Segmenter",
85        intl_segmenter_ctor,
86        vec![
87            ("segment", intl_segmenter_segment),
88            ("resolvedOptions", intl_segmenter_resolved_options),
89        ],
90    );
91    intl.borrow_mut().props.insert("Segmenter".into(), segmenter);
92
93    // Intl.ListFormat
94    let list_format = create_intl_constructor(
95        "ListFormat",
96        intl_list_format_ctor,
97        vec![
98            ("format", intl_list_format_format),
99            ("formatToParts", intl_list_format_format_to_parts),
100            ("resolvedOptions", intl_list_format_resolved_options),
101        ],
102    );
103    intl.borrow_mut().props.insert("ListFormat".into(), list_format);
104
105    // Intl.DisplayNames
106    let display_names = create_intl_constructor(
107        "DisplayNames",
108        intl_display_names_ctor,
109        vec![
110            ("of", intl_display_names_of),
111            ("resolvedOptions", intl_display_names_resolved_options),
112        ],
113    );
114    intl.borrow_mut().props.insert("DisplayNames".into(), display_names);
115
116    // Intl.DurationFormat
117    let duration_format = create_intl_constructor(
118        "DurationFormat",
119        intl_duration_format_ctor,
120        vec![
121            ("format", intl_duration_format_format),
122            ("formatToParts", intl_duration_format_format_to_parts),
123            ("resolvedOptions", intl_duration_format_resolved_options),
124        ],
125    );
126    intl.borrow_mut().props.insert("DurationFormat".into(), duration_format);
127
128    Value::Object(intl)
129}
130
131fn create_intl_constructor(
132    name: &str,
133    ctor_fn: NativeFn,
134    methods: Vec<(&'static str, NativeFn)>,
135) -> Value {
136    let proto = Obj::plain();
137    for (m_name, m_fn) in methods {
138        proto.borrow_mut().props.insert(m_name.into(), nv(m_name, m_fn));
139    }
140    let ctor = Obj::native(name, ctor_fn);
141    ctor.borrow_mut().props.insert("prototype".into(), Value::Object(proto));
142    Value::Object(ctor)
143}
144
145fn parse_locale(val: &Value) -> String {
146    let s = val.to_js_string();
147    if s.is_empty() || s == "undefined" {
148        String::from("en-US")
149    } else {
150        match s.as_str() {
151            "en" => String::from("en-US"),
152            "ja" => String::from("ja-JP"),
153            "zh" => String::from("zh-CN"),
154            "fr" => String::from("fr-FR"),
155            "de" => String::from("de-DE"),
156            "es" => String::from("es-ES"),
157            _ => s,
158        }
159    }
160}
161
162// ===== Intl.getCanonicalLocales =====
163pub(crate) fn intl_get_canonical_locales(_it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
164    let arg0 = arg(a, 0);
165    let mut locales = Vec::new();
166    if let Value::Object(o) = &arg0 {
167        if let ObjKind::Array(items) = &o.borrow().kind {
168            for item in items {
169                let loc = parse_locale(item);
170                if !locales.contains(&loc) {
171                    locales.push(loc);
172                }
173            }
174        } else {
175            locales.push(parse_locale(&arg0));
176        }
177    } else {
178        locales.push(parse_locale(&arg0));
179    }
180    let res_items: Vec<Value> = locales.into_iter().map(|s| Value::Str(Rc::new(s))).collect();
181    Ok(Value::Object(Obj::array(res_items)))
182}
183
184/// `new`を介さない裸呼び出し(`Intl.NumberFormat()`。`Intl`系コンストラクタは
185/// 仕様上どちらの呼び出し形式でも動作可能でなければならない)を正しく扱う。
186/// メンバ呼出(`Intl.NumberFormat(...)`)の`this`は「メンバ式のレシーバ」
187/// (`interp/calls.rs`の`eval_call`のコメントどおり)、つまり`Intl`
188/// 名前空間オブジェクト自身になる。以前は「`this`がオブジェクトなら
189/// そのまま使う」という判定だったため、`new`を省略すると`Intl`オブジェクト
190/// 自身に`_intl_type`/`_locale`を書き込んでしまい(`Intl`名前空間の汚染)、
191/// かつ`Intl`のproto連鎖には`.format`等が無いため常に`undefined`になる
192/// バグだった。正しくは`this`のprotoが既に対象コンストラクタの
193/// `prototype`と一致する場合(`new`経由。`construct_object`が設定済み)
194/// のみそのオブジェクトを再利用し、それ以外(裸呼び出し)は`Intl.<name>.
195/// prototype`へ明示的に繋いだ新規オブジェクトを都度生成する。
196/// 2026-07-18 発見・実装。
197fn resolve_intl_this(it: &Interp, this: Value, ctor_name: &str) -> super::super::value::ObjRef {
198    let intl_val = it.global.borrow().vars.get("Intl").cloned();
199    let proto = intl_val.and_then(|v| match v {
200        Value::Object(intl) => match intl.borrow().props.get(ctor_name) {
201            Some(Value::Object(ctor)) => match ctor.borrow().props.get("prototype") {
202                Some(Value::Object(p)) => Some(p.clone()),
203                _ => None,
204            },
205            _ => None,
206        },
207        _ => None,
208    });
209    if let Value::Object(o) = &this {
210        let is_own_instance = match (&o.borrow().proto, &proto) {
211            (Some(op), Some(p)) => Rc::ptr_eq(op, p),
212            _ => false,
213        };
214        if is_own_instance {
215            return o.clone();
216        }
217    }
218    let obj = Obj::plain();
219    if let Some(p) = proto {
220        obj.borrow_mut().proto = Some(p);
221    }
222    obj
223}
224
225// ===== Intl.NumberFormat =====
226pub(crate) fn intl_number_format_ctor(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
227    let locale = parse_locale(&arg(a, 0));
228    let obj = resolve_intl_this(it, this, "NumberFormat");
229    obj.borrow_mut().props.insert("_intl_type".into(), Value::Str(Rc::new("NumberFormat".into())));
230    obj.borrow_mut().props.insert("_locale".into(), Value::Str(Rc::new(locale)));
231    Ok(Value::Object(obj))
232}
233
234/// 千の位区切り(`,`)を挿入する。この処理系には本格的なCLDR数値書式
235/// データベースが無いため、`en-US`をはじめとする「3桁ごとにカンマ区切り、
236/// 小数点は`.`」というASCII圏で最も一般的な書式のみを既定で適用する簡略
237/// 実装(`de-DE`のようなカンマ/ピリオドが逆の書式や桁区切り位置が異なる
238/// ロケール、`useGrouping:false`等のオプションは非対応。既存の
239/// `oklch()`/`Float16Array`等の「1ビットの誤りが静かに間違った数値を
240/// 生み続けるリスク」とは異なり、整数の3桁区切りは目視でも容易に検証
241/// でき、かつ最もよく使われる書式のため実装した)。
242fn group_thousands(num: f64) -> String {
243    if !num.is_finite() {
244        return format!("{}", num);
245    }
246    let negative = num.is_sign_negative() && num != 0.0;
247    let abs = libm::fabs(num);
248    let s = format!("{}", abs);
249    let (int_part, frac_part) = match s.split_once('.') {
250        Some((i, f)) => (i, Some(f)),
251        None => (s.as_str(), None),
252    };
253    let bytes = int_part.as_bytes();
254    let mut grouped = String::new();
255    for (i, b) in bytes.iter().enumerate() {
256        if i > 0 && (bytes.len() - i) % 3 == 0 {
257            grouped.push(',');
258        }
259        grouped.push(*b as char);
260    }
261    let mut out = String::new();
262    if negative {
263        out.push('-');
264    }
265    out.push_str(&grouped);
266    if let Some(f) = frac_part {
267        out.push('.');
268        out.push_str(f);
269    }
270    out
271}
272
273pub(crate) fn intl_number_format_format(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
274    let num = arg(a, 0).to_number();
275    let locale = if let Value::Object(o) = &this {
276        o.borrow().props.get("_locale").cloned().unwrap_or(Value::Str(Rc::new("en-US".into()))).to_js_string()
277    } else {
278        String::from("en-US")
279    };
280
281    let _ = locale;
282    let formatted = group_thousands(num);
283    Ok(Value::Str(Rc::new(formatted)))
284}
285
286pub(crate) fn intl_number_format_format_to_parts(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
287    let num = arg(a, 0).to_number();
288    let num_str = format!("{}", num);
289    let parts = vec![
290        {
291            let p = Obj::plain();
292            p.borrow_mut().props.insert("type".into(), Value::Str(Rc::new("integer".into())));
293            p.borrow_mut().props.insert("value".into(), Value::Str(Rc::new(num_str)));
294            Value::Object(p)
295        }
296    ];
297    let _ = this;
298    Ok(Value::Object(Obj::array(parts)))
299}
300
301pub(crate) fn intl_number_format_resolved_options(_it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
302    let locale = if let Value::Object(o) = &this {
303        o.borrow().props.get("_locale").cloned().unwrap_or(Value::Str(Rc::new("en-US".into()))).to_js_string()
304    } else {
305        String::from("en-US")
306    };
307    let res = Obj::plain();
308    res.borrow_mut().props.insert("locale".into(), Value::Str(Rc::new(locale)));
309    res.borrow_mut().props.insert("numberingSystem".into(), Value::Str(Rc::new("latn".into())));
310    res.borrow_mut().props.insert("style".into(), Value::Str(Rc::new("decimal".into())));
311    Ok(Value::Object(res))
312}
313
314// ===== Intl.DateTimeFormat =====
315pub(crate) fn intl_datetime_format_ctor(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
316    let locale = parse_locale(&arg(a, 0));
317    let obj = match this {
318        Value::Object(o) => o,
319        _ => Obj::plain(),
320    };
321    obj.borrow_mut().props.insert("_intl_type".into(), Value::Str(Rc::new("DateTimeFormat".into())));
322    obj.borrow_mut().props.insert("_locale".into(), Value::Str(Rc::new(locale)));
323    Ok(Value::Object(obj))
324}
325
326pub(crate) fn intl_datetime_format_format(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
327    let val = arg(a, 0);
328    let _ = this;
329    let formatted = val.to_js_string();
330    Ok(Value::Str(Rc::new(formatted)))
331}
332
333pub(crate) fn intl_datetime_format_format_to_parts(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
334    let val = arg(a, 0);
335    let formatted = val.to_js_string();
336    let parts = vec![
337        {
338            let p = Obj::plain();
339            p.borrow_mut().props.insert("type".into(), Value::Str(Rc::new("literal".into())));
340            p.borrow_mut().props.insert("value".into(), Value::Str(Rc::new(formatted)));
341            Value::Object(p)
342        }
343    ];
344    let _ = this;
345    Ok(Value::Object(Obj::array(parts)))
346}
347
348pub(crate) fn intl_datetime_format_resolved_options(_it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
349    let locale = if let Value::Object(o) = &this {
350        o.borrow().props.get("_locale").cloned().unwrap_or(Value::Str(Rc::new("en-US".into()))).to_js_string()
351    } else {
352        String::from("en-US")
353    };
354    let res = Obj::plain();
355    res.borrow_mut().props.insert("locale".into(), Value::Str(Rc::new(locale)));
356    res.borrow_mut().props.insert("calendar".into(), Value::Str(Rc::new("gregory".into())));
357    res.borrow_mut().props.insert("timeZone".into(), Value::Str(Rc::new("UTC".into())));
358    Ok(Value::Object(res))
359}
360
361// ===== Intl.Collator =====
362pub(crate) fn intl_collator_ctor(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
363    let locale = parse_locale(&arg(a, 0));
364    let obj = match this {
365        Value::Object(o) => o,
366        _ => Obj::plain(),
367    };
368    obj.borrow_mut().props.insert("_intl_type".into(), Value::Str(Rc::new("Collator".into())));
369    obj.borrow_mut().props.insert("_locale".into(), Value::Str(Rc::new(locale)));
370    Ok(Value::Object(obj))
371}
372
373pub(crate) fn intl_collator_compare(_it: &mut Interp, _this: Value, a: &[Value]) -> Result<Value, Value> {
374    let s1 = arg(a, 0).to_js_string();
375    let s2 = arg(a, 1).to_js_string();
376    let cmp = s1.cmp(&s2) as i32;
377    Ok(Value::Number(cmp as f64))
378}
379
380pub(crate) fn intl_collator_resolved_options(_it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
381    let locale = if let Value::Object(o) = &this {
382        o.borrow().props.get("_locale").cloned().unwrap_or(Value::Str(Rc::new("en-US".into()))).to_js_string()
383    } else {
384        String::from("en-US")
385    };
386    let res = Obj::plain();
387    res.borrow_mut().props.insert("locale".into(), Value::Str(Rc::new(locale)));
388    res.borrow_mut().props.insert("usage".into(), Value::Str(Rc::new("sort".into())));
389    res.borrow_mut().props.insert("sensitivity".into(), Value::Str(Rc::new("variant".into())));
390    Ok(Value::Object(res))
391}
392
393// ===== Intl.PluralRules =====
394pub(crate) fn intl_plural_rules_ctor(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
395    let locale = parse_locale(&arg(a, 0));
396    let obj = match this {
397        Value::Object(o) => o,
398        _ => Obj::plain(),
399    };
400    obj.borrow_mut().props.insert("_intl_type".into(), Value::Str(Rc::new("PluralRules".into())));
401    obj.borrow_mut().props.insert("_locale".into(), Value::Str(Rc::new(locale)));
402    Ok(Value::Object(obj))
403}
404
405pub(crate) fn intl_plural_rules_select(_it: &mut Interp, _this: Value, a: &[Value]) -> Result<Value, Value> {
406    let n = arg(a, 0).to_number();
407    let category = if n == 1.0 { "one" } else { "other" };
408    Ok(Value::Str(Rc::new(category.into())))
409}
410
411pub(crate) fn intl_plural_rules_resolved_options(_it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
412    let locale = if let Value::Object(o) = &this {
413        o.borrow().props.get("_locale").cloned().unwrap_or(Value::Str(Rc::new("en-US".into()))).to_js_string()
414    } else {
415        String::from("en-US")
416    };
417    let res = Obj::plain();
418    res.borrow_mut().props.insert("locale".into(), Value::Str(Rc::new(locale)));
419    res.borrow_mut().props.insert("type".into(), Value::Str(Rc::new("cardinal".into())));
420    Ok(Value::Object(res))
421}
422
423// ===== Intl.RelativeTimeFormat =====
424pub(crate) fn intl_relative_time_format_ctor(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
425    let locale = parse_locale(&arg(a, 0));
426    let obj = match this {
427        Value::Object(o) => o,
428        _ => Obj::plain(),
429    };
430    obj.borrow_mut().props.insert("_intl_type".into(), Value::Str(Rc::new("RelativeTimeFormat".into())));
431    obj.borrow_mut().props.insert("_locale".into(), Value::Str(Rc::new(locale)));
432    Ok(Value::Object(obj))
433}
434
435pub(crate) fn intl_relative_time_format_format(_it: &mut Interp, _this: Value, a: &[Value]) -> Result<Value, Value> {
436    let value = arg(a, 0).to_number();
437    let unit = arg(a, 1).to_js_string();
438    let formatted = if value < 0.0 {
439        format!("{} {}s ago", -value, unit)
440    } else {
441        format!("in {} {}s", value, unit)
442    };
443    Ok(Value::Str(Rc::new(formatted)))
444}
445
446pub(crate) fn intl_relative_time_format_format_to_parts(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
447    let res = intl_relative_time_format_format(_it, this, a)?;
448    let parts = vec![
449        {
450            let p = Obj::plain();
451            p.borrow_mut().props.insert("type".into(), Value::Str(Rc::new("literal".into())));
452            p.borrow_mut().props.insert("value".into(), res);
453            Value::Object(p)
454        }
455    ];
456    Ok(Value::Object(Obj::array(parts)))
457}
458
459pub(crate) fn intl_relative_time_format_resolved_options(_it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
460    let locale = if let Value::Object(o) = &this {
461        o.borrow().props.get("_locale").cloned().unwrap_or(Value::Str(Rc::new("en-US".into()))).to_js_string()
462    } else {
463        String::from("en-US")
464    };
465    let res = Obj::plain();
466    res.borrow_mut().props.insert("locale".into(), Value::Str(Rc::new(locale)));
467    res.borrow_mut().props.insert("style".into(), Value::Str(Rc::new("long".into())));
468    res.borrow_mut().props.insert("numeric".into(), Value::Str(Rc::new("always".into())));
469    Ok(Value::Object(res))
470}
471
472// ===== Intl.Segmenter =====
473pub(crate) fn intl_segmenter_ctor(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
474    let locale = parse_locale(&arg(a, 0));
475    let obj = match this {
476        Value::Object(o) => o,
477        _ => Obj::plain(),
478    };
479    obj.borrow_mut().props.insert("_intl_type".into(), Value::Str(Rc::new("Segmenter".into())));
480    obj.borrow_mut().props.insert("_locale".into(), Value::Str(Rc::new(locale)));
481    Ok(Value::Object(obj))
482}
483
484pub(crate) fn intl_segmenter_segment(_it: &mut Interp, _this: Value, a: &[Value]) -> Result<Value, Value> {
485    let str_val = arg(a, 0).to_js_string();
486    let segments: Vec<Value> = str_val
487        .chars()
488        .enumerate()
489        .map(|(idx, ch)| {
490            let seg = Obj::plain();
491            seg.borrow_mut().props.insert("segment".into(), Value::Str(Rc::new(ch.to_string())));
492            seg.borrow_mut().props.insert("index".into(), Value::Number(idx as f64));
493            seg.borrow_mut().props.insert("input".into(), Value::Str(Rc::new(str_val.clone())));
494            Value::Object(seg)
495        })
496        .collect();
497    let result = Obj::plain();
498    result.borrow_mut().props.insert("containing".into(), nv("containing", |_it, _this, _a| Ok(Value::Undefined)));
499    result.borrow_mut().props.insert("_segments".into(), Value::Object(Obj::array(segments)));
500    Ok(Value::Object(result))
501}
502
503pub(crate) fn intl_segmenter_resolved_options(_it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
504    let locale = if let Value::Object(o) = &this {
505        o.borrow().props.get("_locale").cloned().unwrap_or(Value::Str(Rc::new("en-US".into()))).to_js_string()
506    } else {
507        String::from("en-US")
508    };
509    let res = Obj::plain();
510    res.borrow_mut().props.insert("locale".into(), Value::Str(Rc::new(locale)));
511    res.borrow_mut().props.insert("granularity".into(), Value::Str(Rc::new("grapheme".into())));
512    Ok(Value::Object(res))
513}
514
515// ===== Intl.ListFormat =====
516pub(crate) fn intl_list_format_ctor(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
517    let locale = parse_locale(&arg(a, 0));
518    let obj = match this {
519        Value::Object(o) => o,
520        _ => Obj::plain(),
521    };
522    obj.borrow_mut().props.insert("_intl_type".into(), Value::Str(Rc::new("ListFormat".into())));
523    obj.borrow_mut().props.insert("_locale".into(), Value::Str(Rc::new(locale)));
524    Ok(Value::Object(obj))
525}
526
527pub(crate) fn intl_list_format_format(_it: &mut Interp, _this: Value, a: &[Value]) -> Result<Value, Value> {
528    let arg0 = arg(a, 0);
529    let mut items = Vec::new();
530    if let Value::Object(o) = &arg0 {
531        if let ObjKind::Array(list) = &o.borrow().kind {
532            for it in list {
533                items.push(it.to_js_string());
534            }
535        }
536    }
537    let formatted = items.join(", ");
538    Ok(Value::Str(Rc::new(formatted)))
539}
540
541pub(crate) fn intl_list_format_format_to_parts(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
542    let res = intl_list_format_format(_it, this, a)?;
543    let parts = vec![
544        {
545            let p = Obj::plain();
546            p.borrow_mut().props.insert("type".into(), Value::Str(Rc::new("element".into())));
547            p.borrow_mut().props.insert("value".into(), res);
548            Value::Object(p)
549        }
550    ];
551    Ok(Value::Object(Obj::array(parts)))
552}
553
554pub(crate) fn intl_list_format_resolved_options(_it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
555    let locale = if let Value::Object(o) = &this {
556        o.borrow().props.get("_locale").cloned().unwrap_or(Value::Str(Rc::new("en-US".into()))).to_js_string()
557    } else {
558        String::from("en-US")
559    };
560    let res = Obj::plain();
561    res.borrow_mut().props.insert("locale".into(), Value::Str(Rc::new(locale)));
562    res.borrow_mut().props.insert("type".into(), Value::Str(Rc::new("conjunction".into())));
563    res.borrow_mut().props.insert("style".into(), Value::Str(Rc::new("long".into())));
564    Ok(Value::Object(res))
565}
566
567// ===== Intl.DisplayNames =====
568pub(crate) fn intl_display_names_ctor(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
569    let locale = parse_locale(&arg(a, 0));
570    let obj = match this {
571        Value::Object(o) => o,
572        _ => Obj::plain(),
573    };
574    obj.borrow_mut().props.insert("_intl_type".into(), Value::Str(Rc::new("DisplayNames".into())));
575    obj.borrow_mut().props.insert("_locale".into(), Value::Str(Rc::new(locale)));
576    Ok(Value::Object(obj))
577}
578
579pub(crate) fn intl_display_names_of(_it: &mut Interp, _this: Value, a: &[Value]) -> Result<Value, Value> {
580    let code = arg(a, 0).to_js_string();
581    Ok(Value::Str(Rc::new(code)))
582}
583
584pub(crate) fn intl_display_names_resolved_options(_it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
585    let locale = if let Value::Object(o) = &this {
586        o.borrow().props.get("_locale").cloned().unwrap_or(Value::Str(Rc::new("en-US".into()))).to_js_string()
587    } else {
588        String::from("en-US")
589    };
590    let res = Obj::plain();
591    res.borrow_mut().props.insert("locale".into(), Value::Str(Rc::new(locale)));
592    res.borrow_mut().props.insert("style".into(), Value::Str(Rc::new("long".into())));
593    res.borrow_mut().props.insert("fallback".into(), Value::Str(Rc::new("code".into())));
594    Ok(Value::Object(res))
595}
596
597// ===== Intl.DurationFormat =====
598pub(crate) fn intl_duration_format_ctor(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
599    let locale = parse_locale(&arg(a, 0));
600    let mut style = String::from("long");
601    if let Value::Object(opts) = arg(a, 1) {
602        if let Some(s) = opts.borrow().props.get("style") {
603            style = s.to_js_string();
604        }
605    }
606    let obj = match this {
607        Value::Object(o) => o,
608        _ => Obj::plain(),
609    };
610    obj.borrow_mut().props.insert("_intl_type".into(), Value::Str(Rc::new("DurationFormat".into())));
611    obj.borrow_mut().props.insert("_locale".into(), Value::Str(Rc::new(locale)));
612    obj.borrow_mut().props.insert("_style".into(), Value::Str(Rc::new(style)));
613    Ok(Value::Object(obj))
614}
615
616pub(crate) fn intl_duration_format_format(_it: &mut Interp, _this: Value, a: &[Value]) -> Result<Value, Value> {
617    let dur_obj = arg(a, 0);
618    let units = [
619        ("years", "year", "years"),
620        ("months", "month", "months"),
621        ("weeks", "week", "weeks"),
622        ("days", "day", "days"),
623        ("hours", "hour", "hours"),
624        ("minutes", "minute", "minutes"),
625        ("seconds", "second", "seconds"),
626        ("milliseconds", "millisecond", "milliseconds"),
627        ("microseconds", "microsecond", "microseconds"),
628        ("nanoseconds", "nanosecond", "nanoseconds"),
629    ];
630
631    let mut parts = Vec::new();
632    if let Value::Object(o) = &dur_obj {
633        let props = &o.borrow().props;
634        for &(key, singular, plural) in &units {
635            if let Some(val) = props.get(key) {
636                let num = val.to_number();
637                if num.is_finite() && num != 0.0 {
638                    let unit_str = if num.abs() == 1.0 { singular } else { plural };
639                    parts.push(format!("{} {}", num, unit_str));
640                }
641            }
642        }
643    }
644    let formatted = if parts.is_empty() {
645        String::from("0 seconds")
646    } else {
647        parts.join(", ")
648    };
649    Ok(Value::Str(Rc::new(formatted)))
650}
651
652pub(crate) fn intl_duration_format_format_to_parts(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
653    let formatted = intl_duration_format_format(it, this, a)?;
654    let p = Obj::plain();
655    p.borrow_mut().props.insert("type".into(), Value::Str(Rc::new("integer".into())));
656    p.borrow_mut().props.insert("value".into(), formatted);
657    Ok(Value::Object(Obj::array(vec![Value::Object(p)])))
658}
659
660pub(crate) fn intl_duration_format_resolved_options(_it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
661    let (locale, style) = if let Value::Object(o) = &this {
662        let l = o.borrow().props.get("_locale").cloned().unwrap_or(Value::Str(Rc::new("en-US".into()))).to_js_string();
663        let s = o.borrow().props.get("_style").cloned().unwrap_or(Value::Str(Rc::new("long".into()))).to_js_string();
664        (l, s)
665    } else {
666        (String::from("en-US"), String::from("long"))
667    };
668    let res = Obj::plain();
669    res.borrow_mut().props.insert("locale".into(), Value::Str(Rc::new(locale)));
670    res.borrow_mut().props.insert("style".into(), Value::Str(Rc::new(style)));
671    Ok(Value::Object(res))
672}