feat(meta-compile,meta-decompile): мультиязычный ML для синонима и подсказки реквизита (v1.18/v0.4)

Раундтрип-находка (класс-1 синоним + класс-3 подсказка). Корпус: у реквизитов 21677 мультиязычных
синонимов (ru+en) и 16063 непустых подсказки (10689 мультиязычных). meta-compile писал ВСЁ ML только ru
(Emit-MLText брал строку) и хардкодил <ToolTip/> пустым → массовая потеря en + подсказок.

- meta-compile (дуал-порт): Emit-MLText/emit_mltext принимают строку (→ru) ИЛИ объект {lang:content}
  (→<v8:item> на язык, в порядке ключей) через новый Emit-MLItems/emit_ml_items. Parse object-форма
  реквизита пробрасывает synonym/tooltip без стрингификации; Emit-Attribute эмитит <ToolTip> из tooltip.
- meta-decompile: Get-MLValue → строка (ru-only) | {ru,en} (мультиязычно, порядок из XML); object-форма
  реквизита при кастомном синониме ИЛИ наличии подсказки.
- spec meta-dsl-spec.md v2.3: §4.2 (tooltip) + §4.4 ML-значения (строка/{ru,en}), консистентно с form-compile.
- тест-кейс catalog-attr-ml (мультиязычный синоним + подсказка, обе формы).

Валидация: PS==PY паритет; полный прогон −228748 строк (447285→218537, >половины); регресс 35/35 (ps+py);
1С-cert зелёный.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-07-01 17:18:01 +03:00
co-authored by Claude Opus 4.8
parent ed42d5e9cb
commit 58bc95264e
10 changed files with 584 additions and 23 deletions
@@ -197,15 +197,27 @@ lines = []
def X(text):
lines.append(text)
# ML-значение: строка → один <v8:item> ru; dict {lang: content} → item на язык (в порядке ключей).
def emit_ml_items(indent, val):
if isinstance(val, dict):
for k, v in val.items():
X(f'{indent}<v8:item>')
X(f'{indent}\t<v8:lang>{k}</v8:lang>')
X(f'{indent}\t<v8:content>{esc_xml(str(v))}</v8:content>')
X(f'{indent}</v8:item>')
else:
X(f'{indent}<v8:item>')
X(f'{indent}\t<v8:lang>ru</v8:lang>')
X(f'{indent}\t<v8:content>{esc_xml(str(val))}</v8:content>')
X(f'{indent}</v8:item>')
def emit_mltext(indent, tag, text):
if not text:
# Пусто (None / '') → самозакрывающийся тег.
if text is None or (isinstance(text, str) and text == ''):
X(f'{indent}<{tag}/>')
return
X(f'{indent}<{tag}>')
X(f'{indent}\t<v8:item>')
X(f'{indent}\t\t<v8:lang>ru</v8:lang>')
X(f'{indent}\t\t<v8:content>{esc_xml(text)}</v8:content>')
X(f'{indent}\t</v8:item>')
emit_ml_items(f'{indent}\t', text)
X(f'{indent}</{tag}>')
# ---------------------------------------------------------------------------
@@ -615,12 +627,13 @@ def parse_attribute_shorthand(val):
parsed['type'] = colon_parts[1].strip()
parsed['synonym'] = split_camel_case(parsed['name'])
return parsed
# Object form
# Object form. synonym/tooltip — сквозной проброс (строка ИЛИ dict {ru,en}), НЕ стрингифаим.
name = str(val.get('name', ''))
return {
'name': name,
'type': build_type_str(val),
'synonym': str(val['synonym']) if val.get('synonym') else split_camel_case(name),
'synonym': val['synonym'] if val.get('synonym') is not None else split_camel_case(name),
'tooltip': val.get('tooltip'),
'comment': str(val['comment']) if val.get('comment') else '',
'flags': list(val.get('flags', [])),
'fillChecking': str(val['fillChecking']) if val.get('fillChecking') else '',
@@ -957,7 +970,7 @@ def emit_attribute(indent, parsed, context):
X(f'{indent}\t\t<PasswordMode>false</PasswordMode>')
X(f'{indent}\t\t<Format/>')
X(f'{indent}\t\t<EditFormat/>')
X(f'{indent}\t\t<ToolTip/>')
emit_mltext(f'{indent}\t\t', 'ToolTip', parsed.get('tooltip'))
X(f'{indent}\t\t<MarkNegatives>false</MarkNegatives>')
X(f'{indent}\t\t<Mask/>')
multi_line = 'true' if (parsed.get('multiLine') is True or 'multiline' in parsed.get('flags', [])) else 'false'