Skip to main content

phemios_core/encyclopedia/
translation.rs

1//! Resolve counterpart identities through Wikipedia's own interlanguage relation, never title search.
2use super::{Client, article_result, checked_response, language, page_id, title};
3use serde_json::Value;
4use std::time::{SystemTime, UNIX_EPOCH};
5const MISSING: &str = "Cet article n’a pas de version dans la langue demandée.";
6fn counterpart_title<'a>(value: &'a Value, id: u64, target: &str) -> Result<&'a str, String> {
7    let pages = checked_response(value)?["pages"]
8        .as_array()
9        .filter(|pages| pages.len() == 1)
10        .ok_or("Réponse Wikipédia invalide.")?;
11    let page = &pages[0];
12    if page["pageid"].as_u64() != Some(id)
13        || page["ns"].as_u64() != Some(0)
14        || page.get("missing").is_some()
15        || page.get("invalid").is_some()
16    {
17        return Err("Article Wikipédia indisponible.".into());
18    }
19    let Some(links) = page.get("langlinks") else {
20        return Err(MISSING.into());
21    };
22    let links = links.as_array().ok_or("Réponse Wikipédia invalide.")?;
23    if links.is_empty() {
24        return Err(MISSING.into());
25    }
26    if links.len() != 1 || links[0]["lang"].as_str() != Some(target) {
27        return Err("Réponse Wikipédia invalide.".into());
28    }
29    let name = title(&links[0])?;
30    // Neither multi-page query separators nor section fragments identify one complete article.
31    if name.contains(['|', '#']) {
32        return Err("Réponse Wikipédia invalide.".into());
33    }
34    Ok(name)
35}
36fn counterpart_result(value: &Value, target: &str, at: u64) -> Result<Value, String> {
37    let pages = checked_response(value)?["pages"]
38        .as_array()
39        .filter(|pages| pages.len() == 1)
40        .ok_or("Réponse Wikipédia invalide.")?;
41    let id = page_id(
42        pages[0]["pageid"]
43            .as_u64()
44            .ok_or("Article Wikipédia indisponible.")?,
45    )?;
46    article_result(value, target, id, at)
47}
48pub(super) fn checked_article_title(name: &str) -> Result<&str, String> {
49    if name.trim().is_empty()
50        || name.len() > 2048
51        || name.chars().any(char::is_control)
52        || name.contains(['|', '#'])
53    {
54        return Err("Titre Wikipédia invalide.".into());
55    }
56    Ok(name)
57}
58impl Client {
59    /// Read one exact attributed title through fixed FR/EN hosts; never perform a name search.
60    pub fn named_article(&self, lang: &str, name: &str) -> Result<Value, String> {
61        language(lang)?;
62        let name = checked_article_title(name)?;
63        let article = self.request_article(
64            lang,
65            &[
66                ("titles", name),
67                ("redirects", "1"),
68                ("prop", "extracts|info"),
69                ("explaintext", "1"),
70                ("exsectionformat", "wiki"),
71            ],
72        )?;
73        let at = SystemTime::now()
74            .duration_since(UNIX_EPOCH)
75            .map_err(|_| "Horloge système invalide.")?
76            .as_millis();
77        counterpart_result(
78            &article,
79            lang,
80            u64::try_from(at).map_err(|_| "Horloge système invalide.")?,
81        )
82    }
83    /// Explicitly follow one FR/EN interlanguage link and read its canonical main-namespace page.
84    /// Both requests retain fixed public hosts, limits and provenance, without executing returned URLs.
85    pub fn translated_article(&self, source: &str, id: u64, target: &str) -> Result<Value, String> {
86        language(source)?;
87        language(target)?;
88        let id = page_id(id)?;
89        if source == target {
90            return self.article(source, id);
91        }
92        let relation = self.request(
93            source,
94            &[
95                ("pageids", &id.to_string()),
96                ("prop", "langlinks"),
97                ("lllang", target),
98                ("lllimit", "1"),
99            ],
100        )?;
101        let name = counterpart_title(&relation, id, target)?;
102        let article = self.request_article(
103            target,
104            &[
105                ("titles", name),
106                ("redirects", "1"),
107                ("prop", "extracts|info"),
108                ("explaintext", "1"),
109                ("exsectionformat", "wiki"),
110            ],
111        )?;
112        let at = SystemTime::now()
113            .duration_since(UNIX_EPOCH)
114            .map_err(|_| "Horloge système invalide.")?
115            .as_millis();
116        counterpart_result(
117            &article,
118            target,
119            u64::try_from(at).map_err(|_| "Horloge système invalide.")?,
120        )
121    }
122}
123#[cfg(test)]
124#[path = "translation_tests.rs"]
125mod tests;