From 576f6dda8aee9658a3dd3f84d2c46ea115986a03 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Sat, 25 Apr 2026 18:30:37 +0300 Subject: [PATCH] =?UTF-8?q?feat(skd-compile):=20=D0=BC=D0=BD=D0=BE=D0=B3?= =?UTF-8?q?=D0=BE=D1=8F=D0=B7=D1=8B=D1=87=D0=BD=D1=8B=D0=B9=20title/presen?= =?UTF-8?q?tation=20(=D0=BE=D0=B1=D1=8A=D0=B5=D0=BA=D1=82=D0=BD=D0=B0?= =?UTF-8?q?=D1=8F=20=D1=84=D0=BE=D1=80=D0=BC=D0=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Везде, где DSL принимает title/presentation, теперь поддерживается объектная форма с языками: "title": { "ru": "...", "en": "..." }. Строка по-прежнему работает как ru-only. Покрыто: field title, calculatedField title, parameter title/presentation, settingsVariant title/presentation (root и в structure-items), availableValue title/presentation, userSettingPresentation в filter/dataParameter, mltext-значения в conditionalAppearance.appearance (ключи Текст/Заголовок/Формат). Реализация: - Хелпер emit_mltext / Emit-MLText расширен — принимает string|dict и итерирует по языкам. - 8 inline-блоков LocalStringType в каждом скрипте заменены на вызовы хелпера (унификация — побочный эффект, бенефит на будущее). - На входе сняты str()/"$()" коэрции для title/presentation, чтобы dict доходил до хелпера живым. - SKILL.md: одна строка про объектную форму title. - tests: новый snapshot-кейс multi-lang-title (5 узлов с ru+en). - Версия v1.21. Co-Authored-By: Claude Opus 4.7 (1M context) --- .claude/skills/skd-compile/SKILL.md | 2 + .../skd-compile/scripts/skd-compile.ps1 | 97 +++++++----------- .../skills/skd-compile/scripts/skd-compile.py | 88 ++++++----------- .../cases/skd-compile/multi-lang-title.json | 33 +++++++ .../snapshots/multi-lang-title/Template.xml | 98 +++++++++++++++++++ 5 files changed, 196 insertions(+), 122 deletions(-) create mode 100644 tests/skills/cases/skd-compile/multi-lang-title.json create mode 100644 tests/skills/cases/skd-compile/snapshots/multi-lang-title/Template.xml diff --git a/.claude/skills/skd-compile/SKILL.md b/.claude/skills/skd-compile/SKILL.md index ab902fdc..581c74d7 100644 --- a/.claude/skills/skd-compile/SKILL.md +++ b/.claude/skills/skd-compile/SKILL.md @@ -86,6 +86,8 @@ powershell.exe -NoProfile -File .claude/skills/skd-compile/scripts/skd-compile.p ``` `dataPath` автоматически берётся из `field`, если не указан явно. +Многоязычный заголовок: `"title": { "ru": "...", "en": "..." }`. Применимо везде, где принимается title/presentation (поля, calculatedFields, parameters, settingsVariants, availableValues и пр.). Строка эквивалентна `{ "ru": "..." }`. + Типы: `string`, `string(N)`, `decimal(D,F)`, `boolean`, `date`, `dateTime`, `CatalogRef.X`, `DocumentRef.X`, `EnumRef.X`, `StandardPeriod`. Ссылочные типы эмитируются с inline namespace `d5p1:` (`http://v8.1c.ru/8.1/data/enterprise/current-config`). Сборка EPF со ссылочными типами требует базу с соответствующей конфигурацией. Составной тип (несколько типов значений) — массив в объектной форме: `"type": ["CatalogRef.A", "CatalogRef.B"]`. Квалификаторы (`(N)`, `(D,F)`) применяются к каждому элементу. diff --git a/.claude/skills/skd-compile/scripts/skd-compile.ps1 b/.claude/skills/skd-compile/scripts/skd-compile.ps1 index 1f95d0a9..3ce1b65d 100644 --- a/.claude/skills/skd-compile/scripts/skd-compile.ps1 +++ b/.claude/skills/skd-compile/scripts/skd-compile.ps1 @@ -1,4 +1,4 @@ -# skd-compile v1.20 — Compile 1C DCS from JSON +# skd-compile v1.21 — Compile 1C DCS from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [string]$DefinitionFile, @@ -80,12 +80,25 @@ function Resolve-QueryValue { } function Emit-MLText { - param([string]$tag, [string]$text, [string]$indent) + param([string]$tag, $text, [string]$indent) X "$indent<$tag xsi:type=`"v8:LocalStringType`">" - X "$indent`t" - X "$indent`t`tru" - X "$indent`t`t$(Esc-Xml $text)" - X "$indent`t" + # Multi-lang: object form { ru: "...", en: "..." } → one per language + if ($text -is [System.Management.Automation.PSCustomObject] -or $text -is [hashtable] -or $text -is [System.Collections.IDictionary]) { + $props = if ($text -is [System.Management.Automation.PSCustomObject]) { $text.PSObject.Properties } else { $text.GetEnumerator() | ForEach-Object { @{ Name = $_.Key; Value = $_.Value } } } + foreach ($p in $props) { + $lang = if ($p -is [hashtable]) { $p.Name } else { $p.Name } + $content = if ($p -is [hashtable]) { $p.Value } else { $p.Value } + X "$indent`t" + X "$indent`t`t$(Esc-Xml "$lang")" + X "$indent`t`t$(Esc-Xml "$content")" + X "$indent`t" + } + } else { + X "$indent`t" + X "$indent`t`tru" + X "$indent`t`t$(Esc-Xml "$text")" + X "$indent`t" + } X "$indent" } @@ -610,7 +623,7 @@ function Emit-Field { $f = @{ dataPath = if ($fieldDef.dataPath) { "$($fieldDef.dataPath)" } elseif ($fieldDef.field) { "$($fieldDef.field)" } else { "" } field = if ($fieldDef.field) { "$($fieldDef.field)" } else { "$($fieldDef.dataPath)" } - title = if ($fieldDef.title) { "$($fieldDef.title)" } else { "" } + title = if ($fieldDef.title) { $fieldDef.title } else { "" } type = if ($fieldDef.type) { if ($fieldDef.type -is [array] -or $fieldDef.type -is [System.Collections.IList]) { @($fieldDef.type | ForEach-Object { Resolve-TypeStr "$_" }) @@ -845,7 +858,7 @@ function Emit-CalcFields { $parsed = Parse-CalcShorthand $cf $dataPath = "$($parsed.dataPath)" $expression = "$($parsed.expression)" - $title = "$($parsed.title)" + $title = $parsed.title $typeStr = "$($parsed.type)" if ($parsed.restrict) { $restrictTokens = @($parsed.restrict) } } else { @@ -853,7 +866,7 @@ function Emit-CalcFields { elseif ($cf.field) { "$($cf.field)" } else { "$($cf.name)" } $expression = "$($cf.expression)" - if ($cf.title) { $title = "$($cf.title)" } + if ($cf.title) { $title = $cf.title } if ($cf.type) { $typeStr = Resolve-TypeStr "$($cf.type)" } $restrictVal = if ($cf.restrict) { $cf.restrict } elseif ($cf.useRestriction) { $cf.useRestriction } else { $null } @@ -955,11 +968,11 @@ function Emit-SingleParam { # a synonym — 1C UI labels a parameter's caption "Представление"). $title = "" if ($parsed.title) { - $title = "$($parsed.title)" + $title = $parsed.title } elseif ($p -isnot [string] -and $p.title) { - $title = "$($p.title)" + $title = $p.title } elseif ($p -isnot [string] -and $p.presentation) { - $title = "$($p.presentation)" + $title = $p.presentation } if ($title) { Emit-MLText -tag "title" -text $title -indent "`t`t" @@ -1012,14 +1025,9 @@ function Emit-SingleParam { X "`t`t" X "`t`t`t$(Esc-Xml $avVal)" # `title` accepted as synonym of `presentation` — both map to the same UI label. - $avPres = if ($av.presentation) { "$($av.presentation)" } elseif ($av.title) { "$($av.title)" } else { "" } + $avPres = if ($av.presentation) { $av.presentation } elseif ($av.title) { $av.title } else { "" } if ($avPres) { - X "`t`t`t" - X "`t`t`t`t" - X "`t`t`t`t`tru" - X "`t`t`t`t`t$(Esc-Xml $avPres)" - X "`t`t`t`t" - X "`t`t`t" + Emit-MLText -tag "presentation" -text $avPres -indent "`t`t`t" } X "`t`t" } @@ -1413,12 +1421,7 @@ function Emit-AreaTemplateDSL { } else { # Static text X "`t`t`t`t`t" - X "`t`t`t`t`t`t" - X "`t`t`t`t`t`t`t" - X "`t`t`t`t`t`t`t`tru" - X "`t`t`t`t`t`t`t`t$(Esc-Xml $cellStr)" - X "`t`t`t`t`t`t`t" - X "`t`t`t`t`t`t" + Emit-MLText -tag "dcsat:value" -text $cellStr -indent "`t`t`t`t`t`t" X "`t`t`t`t`t" } } @@ -1635,12 +1638,7 @@ function Emit-FilterItem { } if ($item.presentation) { - X "$indent`t" - X "$indent`t`t" - X "$indent`t`t`tru" - X "$indent`t`t`t$(Esc-Xml "$($item.presentation)")" - X "$indent`t`t" - X "$indent`t" + Emit-MLText -tag "dcsset:presentation" -text $item.presentation -indent "$indent`t" } if ($item.viewMode) { @@ -1653,12 +1651,7 @@ function Emit-FilterItem { } if ($item.userSettingPresentation) { - X "$indent`t" - X "$indent`t`t" - X "$indent`t`t`tru" - X "$indent`t`t`t$(Esc-Xml "$($item.userSettingPresentation)")" - X "$indent`t`t" - X "$indent`t" + Emit-MLText -tag "dcsset:userSettingPresentation" -text $item.userSettingPresentation -indent "$indent`t" } X "$indent" @@ -1747,12 +1740,7 @@ function Emit-AppearanceValue { } elseif ($actualVal -eq "true" -or $actualVal -eq "false") { X "$indent`t$actualVal" } elseif ($key -eq "Текст" -or $key -eq "Заголовок" -or $key -eq "Формат") { - X "$indent`t" - X "$indent`t`t" - X "$indent`t`t`tru" - X "$indent`t`t`t$(Esc-Xml $actualVal)" - X "$indent`t`t" - X "$indent`t" + Emit-MLText -tag "dcscor:value" -text $actualVal -indent "$indent`t" } else { X "$indent`t$(Esc-Xml $actualVal)" } @@ -1831,12 +1819,7 @@ function Emit-OutputParameters { X "$indent`t" X "$indent`t`t$(Esc-Xml $key)" if ($ptype -eq "mltext") { - X "$indent`t`t" - X "$indent`t`t`t" - X "$indent`t`t`t`tru" - X "$indent`t`t`t`t$(Esc-Xml $val)" - X "$indent`t`t`t" - X "$indent`t`t" + Emit-MLText -tag "dcscor:value" -text $val -indent "$indent`t`t" } else { X "$indent`t`t$(Esc-Xml $val)" } @@ -1925,12 +1908,7 @@ function Emit-DataParameters { } if ($dp.userSettingPresentation) { - X "$indent`t`t" - X "$indent`t`t`t" - X "$indent`t`t`t`tru" - X "$indent`t`t`t`t$(Esc-Xml "$($dp.userSettingPresentation)")" - X "$indent`t`t`t" - X "$indent`t`t" + Emit-MLText -tag "dcsset:userSettingPresentation" -text $dp.userSettingPresentation -indent "$indent`t`t" } X "$indent`t" @@ -2166,13 +2144,8 @@ function Emit-SettingsVariants { X "`t" X "`t`t$(Esc-Xml "$($v.name)")" - $pres = if ($v.presentation) { "$($v.presentation)" } elseif ($v.title) { "$($v.title)" } else { "$($v.name)" } - X "`t`t" - X "`t`t`t" - X "`t`t`t`tru" - X "`t`t`t`t$(Esc-Xml $pres)" - X "`t`t`t" - X "`t`t" + $pres = if ($v.presentation) { $v.presentation } elseif ($v.title) { $v.title } else { "$($v.name)" } + Emit-MLText -tag "dcsset:presentation" -text $pres -indent "`t`t" X "`t`t" diff --git a/.claude/skills/skd-compile/scripts/skd-compile.py b/.claude/skills/skd-compile/scripts/skd-compile.py index 67ecec32..9a32ca18 100644 --- a/.claude/skills/skd-compile/scripts/skd-compile.py +++ b/.claude/skills/skd-compile/scripts/skd-compile.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# skd-compile v1.20 — Compile 1C DCS from JSON +# skd-compile v1.21 — Compile 1C DCS from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import json @@ -41,10 +41,18 @@ def emit_mltext(lines, indent, tag, text): lines.append(f"{indent}<{tag}/>") return lines.append(f'{indent}<{tag} xsi:type="v8:LocalStringType">') - lines.append(f"{indent}\t") - lines.append(f"{indent}\t\tru") - lines.append(f"{indent}\t\t{esc_xml(text)}") - lines.append(f"{indent}\t") + # Multi-lang: object form { ru: "...", en: "..." } -- one per language + if isinstance(text, dict): + for lang, content in text.items(): + lines.append(f"{indent}\t") + lines.append(f"{indent}\t\t{esc_xml(str(lang))}") + lines.append(f"{indent}\t\t{esc_xml(str(content))}") + lines.append(f"{indent}\t") + else: + lines.append(f"{indent}\t") + lines.append(f"{indent}\t\tru") + lines.append(f"{indent}\t\t{esc_xml(str(text))}") + lines.append(f"{indent}\t") lines.append(f"{indent}") @@ -496,7 +504,7 @@ def emit_field(lines, field_def, indent): f = { 'dataPath': str(field_def.get('dataPath', '')) or str(field_def.get('field', '')), 'field': str(field_def.get('field', '')) or str(field_def.get('dataPath', '')), - 'title': str(field_def.get('title', '')) if field_def.get('title') else '', + 'title': field_def.get('title') if field_def.get('title') else '', 'type': ( [resolve_type_str(str(t)) for t in field_def['type']] if isinstance(field_def['type'], list) @@ -698,7 +706,7 @@ def emit_calc_fields(lines, defn): data_path = str(cf.get('dataPath') or cf.get('field') or cf.get('name') or '') expression = str(cf.get('expression', '')) if cf.get('title'): - title = str(cf['title']) + title = cf['title'] if cf.get('type'): type_str = resolve_type_str(str(cf['type'])) @@ -823,11 +831,11 @@ def emit_single_param(lines, p, parsed): # a synonym — 1C UI labels a parameter's caption "Представление"). title = '' if parsed.get('title'): - title = str(parsed['title']) + title = parsed['title'] elif p is not None and not isinstance(p, str) and p.get('title'): - title = str(p['title']) + title = p['title'] elif p is not None and not isinstance(p, str) and p.get('presentation'): - title = str(p['presentation']) + title = p['presentation'] if title: emit_mltext(lines, '\t\t', 'title', title) @@ -873,14 +881,9 @@ def emit_single_param(lines, p, parsed): lines.append('\t\t') lines.append(f'\t\t\t{esc_xml(av_val)}') # `title` accepted as synonym of `presentation` — both map to the same UI label. - av_pres = str(av.get('presentation') or av.get('title') or '') + av_pres = av.get('presentation') or av.get('title') or '' if av_pres: - lines.append('\t\t\t') - lines.append('\t\t\t\t') - lines.append('\t\t\t\t\tru') - lines.append(f'\t\t\t\t\t{esc_xml(av_pres)}') - lines.append('\t\t\t\t') - lines.append('\t\t\t') + emit_mltext(lines, '\t\t\t', 'presentation', av_pres) lines.append('\t\t') # DenyIncompleteValues @@ -1208,12 +1211,7 @@ def _emit_area_template_dsl(lines, t): cell_extra_items.append('\t\t\t\t\t') else: lines.append('\t\t\t\t\t') - lines.append('\t\t\t\t\t\t') - lines.append('\t\t\t\t\t\t\t') - lines.append('\t\t\t\t\t\t\t\tru') - lines.append(f'\t\t\t\t\t\t\t\t{esc_xml(cell_str)}') - lines.append('\t\t\t\t\t\t\t') - lines.append('\t\t\t\t\t\t') + emit_mltext(lines, '\t\t\t\t\t\t', 'dcsat:value', cell_str) lines.append('\t\t\t\t\t') h = min_height if r == 0 else 0 _emit_cell_appearance(lines, style, w, False, False, h, cell_extra_items or None) @@ -1397,12 +1395,7 @@ def emit_filter_item(lines, item, indent): lines.append(f'{indent}\t{v_str}') if item.get('presentation'): - lines.append(f'{indent}\t') - lines.append(f'{indent}\t\t') - lines.append(f'{indent}\t\t\tru') - lines.append(f'{indent}\t\t\t{esc_xml(str(item["presentation"]))}') - lines.append(f'{indent}\t\t') - lines.append(f'{indent}\t') + emit_mltext(lines, f'{indent}\t', 'dcsset:presentation', item["presentation"]) if item.get('viewMode'): lines.append(f'{indent}\t{esc_xml(str(item["viewMode"]))}') @@ -1412,12 +1405,7 @@ def emit_filter_item(lines, item, indent): lines.append(f'{indent}\t{esc_xml(uid)}') if item.get('userSettingPresentation'): - lines.append(f'{indent}\t') - lines.append(f'{indent}\t\t') - lines.append(f'{indent}\t\t\tru') - lines.append(f'{indent}\t\t\t{esc_xml(str(item["userSettingPresentation"]))}') - lines.append(f'{indent}\t\t') - lines.append(f'{indent}\t') + emit_mltext(lines, f'{indent}\t', 'dcsset:userSettingPresentation', item["userSettingPresentation"]) lines.append(f'{indent}') @@ -1492,12 +1480,7 @@ def emit_appearance_value(lines, key, val, indent): elif actual_val == 'true' or actual_val == 'false': lines.append(f'{indent}\t{actual_val}') elif key in ('\u0422\u0435\u043a\u0441\u0442', '\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a', '\u0424\u043e\u0440\u043c\u0430\u0442'): - lines.append(f'{indent}\t') - lines.append(f'{indent}\t\t') - lines.append(f'{indent}\t\t\tru') - lines.append(f'{indent}\t\t\t{esc_xml(actual_val)}') - lines.append(f'{indent}\t\t') - lines.append(f'{indent}\t') + emit_mltext(lines, f'{indent}\t', 'dcscor:value', actual_val) else: lines.append(f'{indent}\t{esc_xml(actual_val)}') lines.append(f'{indent}') @@ -1562,12 +1545,7 @@ def emit_output_parameters(lines, params, indent): lines.append(f'{indent}\t') lines.append(f'{indent}\t\t{esc_xml(key)}') if ptype == 'mltext': - lines.append(f'{indent}\t\t') - lines.append(f'{indent}\t\t\t') - lines.append(f'{indent}\t\t\t\tru') - lines.append(f'{indent}\t\t\t\t{esc_xml(val_str)}') - lines.append(f'{indent}\t\t\t') - lines.append(f'{indent}\t\t') + emit_mltext(lines, f'{indent}\t\t', 'dcscor:value', val_str) else: lines.append(f'{indent}\t\t{esc_xml(val_str)}') lines.append(f'{indent}\t') @@ -1637,12 +1615,7 @@ def emit_data_parameters(lines, items, indent): lines.append(f'{indent}\t\t{esc_xml(uid)}') if dp.get('userSettingPresentation'): - lines.append(f'{indent}\t\t') - lines.append(f'{indent}\t\t\t') - lines.append(f'{indent}\t\t\t\tru') - lines.append(f'{indent}\t\t\t\t{esc_xml(str(dp["userSettingPresentation"]))}') - lines.append(f'{indent}\t\t\t') - lines.append(f'{indent}\t\t') + emit_mltext(lines, f'{indent}\t\t', 'dcsset:userSettingPresentation', dp["userSettingPresentation"]) lines.append(f'{indent}\t') lines.append(f'{indent}') @@ -1824,13 +1797,8 @@ def emit_settings_variants(lines, defn): lines.append('\t') lines.append(f'\t\t{esc_xml(str(v["name"]))}') - pres = str(v.get('presentation', '')) or str(v.get('title', '')) or str(v['name']) - lines.append('\t\t') - lines.append('\t\t\t') - lines.append('\t\t\t\tru') - lines.append(f'\t\t\t\t{esc_xml(pres)}') - lines.append('\t\t\t') - lines.append('\t\t') + pres = v.get('presentation') or v.get('title') or v['name'] + emit_mltext(lines, '\t\t', 'dcsset:presentation', pres) lines.append('\t\t') diff --git a/tests/skills/cases/skd-compile/multi-lang-title.json b/tests/skills/cases/skd-compile/multi-lang-title.json new file mode 100644 index 00000000..656384ef --- /dev/null +++ b/tests/skills/cases/skd-compile/multi-lang-title.json @@ -0,0 +1,33 @@ +{ + "name": "Многоязычные title и presentation (ru + en)", + "params": { "outputPath": "Template.xml" }, + "input": { + "dataSets": [{ + "name": "Основной", + "query": "ВЫБРАТЬ Т.Сумма ИЗ Регистр КАК Т", + "fields": [ + { "field": "Сумма", "title": { "ru": "Сумма продажи", "en": "Sale amount" }, "type": "decimal(15,2)" } + ] + }], + "calculatedFields": [ + { "name": "Маржа", "title": { "ru": "Маржа", "en": "Margin" }, "expression": "Сумма * 0.2" } + ], + "totalFields": [ + { "dataPath": "Сумма", "title": { "ru": "Итого, руб.", "en": "Total, RUB" }, "expression": "Сумма(Сумма)" } + ], + "parameters": [ + { "name": "Период", "title": { "ru": "Период", "en": "Period" }, "type": "StandardPeriod" } + ], + "settingsVariants": [{ + "name": "Основной", + "title": { "ru": "Продажи", "en": "Sales" }, + "settings": { + "selection": ["Сумма", "Маржа", "Auto"] + } + }] + }, + "validatePath": "Template.xml", + "expect": { + "files": ["Template.xml"] + } +} diff --git a/tests/skills/cases/skd-compile/snapshots/multi-lang-title/Template.xml b/tests/skills/cases/skd-compile/snapshots/multi-lang-title/Template.xml new file mode 100644 index 00000000..ba89d296 --- /dev/null +++ b/tests/skills/cases/skd-compile/snapshots/multi-lang-title/Template.xml @@ -0,0 +1,98 @@ + + + + ИсточникДанных1 + Local + + + Основной + + Сумма + Сумма + + <v8:item> + <v8:lang>ru</v8:lang> + <v8:content>Сумма продажи</v8:content> + </v8:item> + <v8:item> + <v8:lang>en</v8:lang> + <v8:content>Sale amount</v8:content> + </v8:item> + + + xs:decimal + + 15 + 2 + Any + + + + ИсточникДанных1 + ВЫБРАТЬ Т.Сумма ИЗ Регистр КАК Т + + + Маржа + Сумма * 0.2 + + <v8:item> + <v8:lang>ru</v8:lang> + <v8:content>Маржа</v8:content> + </v8:item> + <v8:item> + <v8:lang>en</v8:lang> + <v8:content>Margin</v8:content> + </v8:item> + + + + Сумма + Сумма(Сумма) + + + Период + + <v8:item> + <v8:lang>ru</v8:lang> + <v8:content>Период</v8:content> + </v8:item> + <v8:item> + <v8:lang>en</v8:lang> + <v8:content>Period</v8:content> + </v8:item> + + + v8:StandardPeriod + + + + Основной + + + ru + Продажи + + + en + Sales + + + + + + Сумма + + + Маржа + + + + +