Skip to main content

eska_presentation/localization/
platform_values.rs

1//! Contextual presentation of platform tokens, never a replacement for source values.
2
3use eska_project_core::project::metadata_model::{MetadataKind, PropertyKey};
4
5use super::{Localizer, platform_value_types::PROPERTY_ENUMS};
6
7const MD: &str = "http://v8.1c.ru/8.3/MDClasses";
8const CORE: &str = "http://v8.1c.ru/8.1/data/core";
9const APP: &str = "http://v8.1c.ru/8.2/managed-application/core";
10const READABLE: &str = "http://v8.1c.ru/8.3/xcf/readable";
11
12impl Localizer {
13    /// Translate only a token belonging to the field's known enum domain.
14    #[must_use]
15    pub fn property_value_caption(
16        &self,
17        owner: MetadataKind,
18        root: &PropertyKey,
19        path: &[&PropertyKey],
20        key: &PropertyKey,
21        text: &str,
22    ) -> Option<String> {
23        if key.namespace.as_deref() == Some(READABLE)
24            && key.name == "Ref"
25            && path.len() == 1
26            && root.namespace.as_deref() == Some(MD)
27            && root.name == "Picture"
28            && let Some(name) = text.strip_prefix("StdPicture.")
29        {
30            return self.optional_text(&format!("platform-picture-{name}"));
31        }
32        let domain = if key.namespace.as_deref() == Some("http://v8.1c.ru/8.3/xcf/predef")
33            && key.name == "AccountType"
34        {
35            Some("AccountType")
36        } else if path.is_empty() && key.namespace.as_deref() == Some(MD) {
37            top_level_type(owner, &key.name)
38        } else {
39            nested_type(owner, root, path, key)
40        }?;
41        self.enum_caption(domain, text)
42    }
43
44    /// Preserve unknown enum tokens, including future platform additions.
45    #[must_use]
46    pub fn enum_caption(&self, domain: &str, token: &str) -> Option<String> {
47        if !token
48            .bytes()
49            .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
50        {
51            return None;
52        }
53        let token = if domain == "BorderStyle" && token == "WithoutBorder" {
54            "None"
55        } else {
56            token
57        };
58        self.optional_text(&format!("platform-value-{domain}-{token}"))
59    }
60
61    /// XML value annotations select a closed enum domain independently of prefix spelling.
62    #[must_use]
63    pub fn typed_value_caption(&self, key: &PropertyKey, text: &str) -> Option<String> {
64        let domain = match (key.namespace.as_deref()?, key.name.as_str()) {
65            ("http://v8.1c.ru/8.1/data/enterprise", "AccountType") => "AccountType",
66            (APP, "ApplicationUsePurpose") => "ApplicationUsePurpose",
67            _ => return None,
68        };
69        self.enum_caption(domain, text)
70    }
71
72    /// Resolve built-in XML type names using namespace URIs, independent of prefix spelling.
73    #[must_use]
74    pub fn type_caption(&self, key: &PropertyKey) -> Option<String> {
75        let known = match key.namespace.as_deref()? {
76            "http://www.w3.org/2001/XMLSchema" => matches!(
77                key.name.as_str(),
78                "string"
79                    | "decimal"
80                    | "boolean"
81                    | "dateTime"
82                    | "base64Binary"
83                    | "int"
84                    | "integer"
85                    | "date"
86            ),
87            CORE => matches!(
88                key.name.as_str(),
89                "ValueStorage"
90                    | "UUID"
91                    | "Undefined"
92                    | "Null"
93                    | "Array"
94                    | "FixedArray"
95                    | "Structure"
96                    | "FixedStructure"
97                    | "Map"
98                    | "FixedMap"
99                    | "StandardPeriod"
100                    | "ValueTable"
101                    | "ValueListType"
102                    | "ValueTree"
103            ),
104            "http://v8.1c.ru/8.1/data-composition-system/settings" => {
105                key.name == "SettingsComposer"
106            }
107            "http://v8.1c.ru/8.2/data/chart" => key.name == "Chart",
108            "http://v8.1c.ru/8.1/data/enterprise/current-config" => {
109                matches!(key.name.as_str(), "ConstantsSet" | "ReportBuilder")
110            }
111            _ => false,
112        };
113        known
114            .then(|| self.enum_caption("PrimitiveType", &key.name))
115            .flatten()
116    }
117
118    /// Standard-attribute identities are platform vocabulary; arbitrary object names are not.
119    #[must_use]
120    pub fn qualifier_caption(
121        &self,
122        field: &PropertyKey,
123        qualifier: &PropertyKey,
124        text: &str,
125    ) -> Option<String> {
126        if field.namespace.as_deref() == Some(READABLE)
127            && field.name == "StandardAttribute"
128            && qualifier.namespace.is_none()
129            && qualifier.name == "name"
130        {
131            self.enum_caption("StandardAttribute", text)
132        } else {
133            None
134        }
135    }
136}
137
138/// Some identically named metadata fields use different enums according to their owner.
139fn top_level_type(owner: MetadataKind, name: &str) -> Option<&'static str> {
140    use MetadataKind as K;
141    let contextual = match (name, owner) {
142        ("Event", K::EventSubscription) => "MetadataEvent",
143        ("Group", K::Command | K::CommonCommand) => "StandardCommandsGroup",
144        ("Type", K::StyleItem) => "StyleElementType",
145        ("CodeType", K::ChartOfCalculationTypes) => "ChartOfCalculationTypesCodeType",
146        (
147            "CodeType",
148            K::Catalog | K::ChartOfAccounts | K::ChartOfCharacteristicTypes | K::ExchangePlan,
149        ) => "CatalogCodeType",
150        ("CodeSeries", K::ChartOfAccounts) => "CharOfAccountCodeSeries",
151        ("CodeSeries", K::ChartOfCharacteristicTypes) => "CharacteristicKindCodesSeries",
152        ("CodeSeries", K::Catalog) => "CatalogCodesSeries",
153        ("DefaultPresentation", K::Catalog) => "CatalogMainPresentation",
154        ("DefaultPresentation", K::ChartOfAccounts) => "AccountMainPresentation",
155        ("DefaultPresentation", K::ChartOfCalculationTypes) => "CalculationTypeMainPresentation",
156        ("DefaultPresentation", K::ChartOfCharacteristicTypes) => {
157            "CharacteristicTypeMainPresentation"
158        }
159        ("DefaultPresentation", K::ExchangePlan) => "DataExchangeMainPresentation",
160        ("DefaultPresentation", K::Task) => "TaskMainPresentation",
161        ("NumberType", K::Document | K::DocumentNumerator) => "DocumentNumberType",
162        ("NumberType", K::BusinessProcess) => "BusinessProcessNumberType",
163        ("NumberType", K::Task) => "TaskNumberType",
164        ("NumberPeriodicity", K::Document | K::DocumentNumerator) => "DocumentNumberPeriodicity",
165        ("NumberPeriodicity", K::BusinessProcess) => "BusinessProcessNumberPeriodicity",
166        _ => {
167            return PROPERTY_ENUMS
168                .iter()
169                .find_map(|(field, domain)| (*field == name).then_some(*domain));
170        }
171    };
172    Some(contextual)
173}
174
175/// Nested scalar names such as Value and functionality require an enclosing semantic context.
176fn nested_type(
177    owner: MetadataKind,
178    root: &PropertyKey,
179    path: &[&PropertyKey],
180    key: &PropertyKey,
181) -> Option<&'static str> {
182    let namespace = key.namespace.as_deref()?;
183    let parent = path.last()?;
184    if namespace == CORE && parent.namespace.as_deref() == Some(CORE) {
185        return match (parent.name.as_str(), key.name.as_str()) {
186            ("StringQualifiers" | "BinaryDataQualifiers", "AllowedLength") => Some("AllowedLength"),
187            ("NumberQualifiers", "AllowedSign") => Some("AllowedSign"),
188            ("DateQualifiers", "DateFractions") => Some("DateFractions"),
189            _ => None,
190        };
191    }
192    if root.namespace.as_deref() != Some(MD) {
193        return None;
194    }
195    match (root.name.as_str(), namespace, key.name.as_str()) {
196        ("Content", READABLE, "Use") if owner == MetadataKind::CommonAttribute => {
197            Some("CommonAttributeUse")
198        }
199        ("UsePurposes", CORE, "Value") => Some("ApplicationUsePurpose"),
200        ("UsedMobileApplicationFunctionalities", APP, "functionality") => {
201            Some("MobileApplicationFunctionalities")
202        }
203        ("UsedMobileApplicationFunctionalities", APP, "permission") => {
204            Some("RequiredMobileApplicationPermissionMessages")
205        }
206        (
207            "RequiredMobileApplicationPermissions" | "RequiredMobileApplicationPermissions8315",
208            APP,
209            "permission",
210        ) => Some("RequiredMobileApplicationPermissions"),
211        ("AllowedIncomingShareRequestTypes", APP, "processingVariant") => {
212            Some("AllowedIncomingShareRequestTypeProcessingVariant")
213        }
214        ("ChoiceParameterLinks" | "StandardAttributes", READABLE, "ValueChange") => {
215            Some("LinkedValueChangeMode")
216        }
217        ("StandardAttributes" | "StandardTabularSections", READABLE, name) => match name {
218            "QuickChoice" => Some("UseQuickChoice"),
219            "FillChecking" => Some("FillChecking"),
220            "FullTextSearch" => Some("FullTextSearchUsing"),
221            "CreateOnInput" => Some("CreateOnInput"),
222            "ChoiceHistoryOnInput" => Some("ChoiceHistoryOnInput"),
223            "TypeReductionMode" => Some("TypeReductionMode"),
224            "DataHistory" => Some("DataHistoryUse"),
225            _ => None,
226        },
227        _ => None,
228    }
229}
230
231#[cfg(test)]
232mod tests {
233    use super::*;
234    use crate::localization::Locale;
235
236    /// The frozen EDT enum inventory and supplemental Designer tokens have both captions.
237    #[test]
238    fn all_catalog_values_have_translations() {
239        for locale in [Locale::RuRu, Locale::EnUs] {
240            let localizer = Localizer::try_new(locale).unwrap();
241            for line in include_str!("../../locales/platform-values.tsv")
242                .lines()
243                .skip(1)
244            {
245                let cells: Vec<_> = line.split('\t').collect();
246                assert_eq!(cells.len(), 4);
247                assert_eq!(
248                    localizer.enum_caption(cells[0], cells[1]),
249                    localizer.optional_text(cells[3]),
250                    "{line}"
251                );
252                assert!(
253                    localizer
254                        .optional_text(cells[3])
255                        .is_some_and(|text| !text.trim().is_empty())
256                );
257                // EDT's Russian palette contains English placeholders; do not reimport them.
258                if locale == Locale::RuRu && cells[0] != "HTTPMethod" {
259                    let caption = localizer.optional_text(cells[3]).unwrap();
260                    assert!(
261                        caption
262                            .chars()
263                            .any(|c| ('\u{0400}'..='\u{04ff}').contains(&c))
264                            || ["NFC", "SMS", "Null"].contains(&caption.as_str()),
265                        "Untranslated Russian caption: {line} = {caption}"
266                    );
267                }
268            }
269        }
270    }
271
272    /// The same text in Comment or a foreign namespace remains a user string.
273    #[test]
274    fn enum_translation_requires_its_property_domain() {
275        let localizer = Localizer::try_new(Locale::RuRu).unwrap();
276        let mut key = PropertyKey {
277            namespace: Some(MD.into()),
278            name: "CompatibilityMode".into(),
279        };
280        assert_eq!(
281            localizer
282                .property_value_caption(
283                    MetadataKind::Configuration,
284                    &key,
285                    &[],
286                    &key,
287                    "Version8_3_27"
288                )
289                .as_deref(),
290            Some("Версия 8.3.27")
291        );
292        key.name = "Comment".into();
293        assert_eq!(
294            localizer.property_value_caption(
295                MetadataKind::Configuration,
296                &key,
297                &[],
298                &key,
299                "Version8_3_27"
300            ),
301            None
302        );
303        key.name = "CompatibilityMode".into();
304        key.namespace = Some("urn:custom".into());
305        assert_eq!(
306            localizer.property_value_caption(
307                MetadataKind::Configuration,
308                &key,
309                &[],
310                &key,
311                "Version8_3_27"
312            ),
313            None
314        );
315        key.namespace = Some(MD.into());
316        assert_eq!(
317            localizer.property_value_caption(
318                MetadataKind::Configuration,
319                &key,
320                &[],
321                &key,
322                "FutureMode"
323            ),
324            None
325        );
326    }
327
328    /// A generic nested Value field translates only inside the relevant platform property.
329    #[test]
330    fn nested_values_are_contextual() {
331        let localizer = Localizer::try_new(Locale::RuRu).unwrap();
332        let mut root = PropertyKey {
333            namespace: Some(MD.into()),
334            name: "UsedMobileApplicationFunctionalities".into(),
335        };
336        let key = PropertyKey {
337            namespace: Some(APP.into()),
338            name: "functionality".into(),
339        };
340        assert_eq!(
341            localizer
342                .property_value_caption(
343                    MetadataKind::Configuration,
344                    &root,
345                    &[&root, &key],
346                    &key,
347                    "Biometrics"
348                )
349                .as_deref(),
350            Some("Биометрия")
351        );
352        root.name = "Comment".into();
353        assert_eq!(
354            localizer.property_value_caption(
355                MetadataKind::Configuration,
356                &root,
357                &[&root, &key],
358                &key,
359                "Biometrics"
360            ),
361            None
362        );
363    }
364
365    /// Reference-like platform values are translated only for their owning metadata kinds.
366    #[test]
367    fn command_groups_and_subscription_events_have_scoped_captions() {
368        let localizer = Localizer::try_new(Locale::RuRu).unwrap();
369        for (owner, name, token, caption) in [
370            (
371                MetadataKind::CommonCommand,
372                "Group",
373                "NavigationPanelOrdinary",
374                "Панель навигации: Обычное",
375            ),
376            (
377                MetadataKind::EventSubscription,
378                "Event",
379                "BeforeWrite",
380                "Перед записью",
381            ),
382        ] {
383            let key = PropertyKey {
384                namespace: Some(MD.into()),
385                name: name.into(),
386            };
387            assert_eq!(
388                localizer
389                    .property_value_caption(owner, &key, &[], &key, token)
390                    .as_deref(),
391                Some(caption)
392            );
393            assert!(
394                localizer
395                    .property_value_caption(MetadataKind::Catalog, &key, &[], &key, token)
396                    .is_none()
397            );
398        }
399    }
400}