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$tag>"
}
@@ -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}{tag}>")
@@ -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
+
+
+ Основной
+
+ Сумма
+ Сумма
+
+
+ ru
+ Сумма продажи
+
+
+ en
+ Sale amount
+
+
+
+ xs:decimal
+
+ 15
+ 2
+ Any
+
+
+
+ ИсточникДанных1
+ ВЫБРАТЬ Т.Сумма ИЗ Регистр КАК Т
+
+
+ Маржа
+ Сумма * 0.2
+
+
+ ru
+ Маржа
+
+
+ en
+ Margin
+
+
+
+
+ Сумма
+ Сумма(Сумма)
+
+
+ Период
+
+
+ ru
+ Период
+
+
+ en
+ Period
+
+
+
+ v8:StandardPeriod
+
+
+
+ Основной
+
+
+ ru
+ Продажи
+
+
+ en
+ Sales
+
+
+
+
+
+ Сумма
+
+
+ Маржа
+
+
+
+
+