1use 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 #[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 #[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 #[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 #[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 #[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
138fn 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
175fn 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 #[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 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 #[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 #[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 #[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}