feat(mxl-compile,mxl-decompile): многоязычный текст ячейки

Платформа хранит текст ячейки по элементу на язык. Декомпилятор брал ПЕРВЫЙ и терял
остальное, компилятор писал всегда ru захардкоженно. В корпусе ERP текст лежит и под
ru, и под en у 10 730 макетов из 10 924 — то есть терялось у 98%.

Форма взята готовая — конвенция ML-значений из docs/meta-dsl-spec.md §4.4, по которой
уже живут synonym, tooltip и title в метаданных и формах: строка означает русский
текст, объект даёт по надписи на язык в порядке ключей. Новых понятий не вводили,
ключи те же (text и template).

Документный ключ для языков НЕ заводили, и это измерение, а не экономия: блок
languageSettings мы и так эмитим байт в байт как платформа (currentLanguage ru,
defaultLanguage ru, один languageInfo). Отклонения редки — 8 макетов ERP с
currentLanguage en, 3 без него, 157 с объявленной парой ru+en, всего около 1,6%.
Заодно это снимает развилку из разбора PR #62: список языков компилятор дописывать
не должен, платформа его не дописывает.

Эффект на пилоте: категория row[].cell[].tl упала с 47 252 до 12 488 — минус 74%,
самое крупное улучшение кампании. Остаток частично классифицирован: 348 строк — ячейки
с ПУСТЫМ <tl> у оригинала, которых мы не эмитим вовсе; полностью разобрать мешает
обрезка диффа по 400 строк на объект.

Снэпшоты не дрейфанули: кейсы пишут текст строкой, и вывод для неё прежний.

Проверено: тесты 54/54 на обоих рантаймах, verify-snapshots 24/24, вывод портов
совпадает байт в байт и в компиляции, и в декомпиляции.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-08-10 20:52:03 +03:00
co-authored by Claude Opus 5
parent 572b75984a
commit 1e06e43e92
8 changed files with 228 additions and 45 deletions
@@ -214,8 +214,8 @@
| `style` | нет | rowStyle | Стиль ячейки (переопределяет rowStyle) |
| `param` | нет | — | Параметр заполнения |
| `detail` | нет | — | Параметр расшифровки (только с `param`) |
| `text` | нет | — | Статический текст |
| `template` | нет | — | Шаблонный текст с `[Параметр]` |
| `text` | нет | — | Статический текст. Строка или объект `{ ru, en }` — см. ниже |
| `template` | нет | — | Шаблонный текст с `[Параметр]`. Строка или объект, как `text` |
### Содержимое ячейки
@@ -225,6 +225,17 @@
- `text` — статический текст;
- ничего — пустая ячейка (нужна, например, ради рамки).
### Текст на нескольких языках
`text` и `template` принимают строку или объект «язык → текст». Строка означает русский; объект даёт по надписи на каждый язык, в порядке ключей.
```json
{ "col": 1, "text": "Наименование" }
{ "col": 2, "text": { "ru": "Поставщик", "en": "Supplier" } }
```
Языки макета объявляются отдельно (`languageSettings` в XML) и по умолчанию ограничены русским; писать текст на языке, которого нет в объявленных, допустимо — так устроено большинство типовых макетов.
## `rowStyle` — автозаполнение
Стиль применяется ко ВСЕЙ ширине строки: позиции без явных ячеек получают тот же стиль. Так в табличных строках получаются сплошные рамки.
@@ -244,8 +255,8 @@ XML не возвращается.
- рамка с разным стилем у разных сторон; стили линий кроме сплошной;
- группировки строк и колонок;
- колонтитулы, параметры печати, область печати;
- многоязычные надписи: при разборе берётся первый вариант текста, при генерации язык
всегда `ru`, остальные теряются.
- объявление языков макета: список языков и текущий язык всегда описываются как русский —
сами надписи при этом сохраняются на всех языках.
Пересборка макета из DSL — это полная перегенерация, а не точечная правка XML, поэтому
diff после round-trip обычно шире фактической доработки.
@@ -1,4 +1,4 @@
# mxl-compile v1.23 — Compile 1C spreadsheet from JSON (+write_xml_file/write_utf8_bom: общий эталон записи)
# mxl-compile v1.24 — Compile 1C spreadsheet from JSON (+write_xml_file/write_utf8_bom: общий эталон записи)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -558,6 +558,29 @@ function Esc-XmlText {
return $s.Replace('&','&amp;').Replace('<','&lt;').Replace('>','&gt;')
}
# Текст ячейки платформа хранит по элементу на язык. Конвенция ML-значений (та же, что у
# synonym/tooltip/title в метаданных и формах): строка — русский текст, объект — по элементу
# на язык В ПОРЯДКЕ КЛЮЧЕЙ.
function Emit-CellText {
param($value)
$pairs = @()
if ($value -is [System.Collections.IDictionary]) {
foreach ($k in $value.Keys) { $pairs += @{ Lang = "$k"; Text = "$($value[$k])" } }
} elseif ($value -is [System.Management.Automation.PSCustomObject]) {
foreach ($p in $value.PSObject.Properties) { $pairs += @{ Lang = $p.Name; Text = "$($p.Value)" } }
} else {
$pairs += @{ Lang = 'ru'; Text = "$value" }
}
X "`t`t`t`t`t<tl>"
foreach ($p in $pairs) {
X "`t`t`t`t`t`t<v8:item>"
X "`t`t`t`t`t`t`t<v8:lang>$($p.Lang)</v8:lang>"
X "`t`t`t`t`t`t`t<v8:content>$(Esc-XmlText $p.Text)</v8:content>"
X "`t`t`t`t`t`t</v8:item>"
}
X "`t`t`t`t`t</tl>"
}
# Helper: determine fillType from cell content
# Text НЕ эмитим: платформа его практически не пишет — на выборке корпуса 344 981 текстовая
# ячейка из 348 023 (99,1%) ссылается на формат БЕЗ fillType. Наличие <tl> и так означает
@@ -1045,23 +1068,9 @@ foreach ($area in $def.areas) {
}
}
if ($cellInfo.Text) {
X "`t`t`t`t`t<tl>"
X "`t`t`t`t`t`t<v8:item>"
X "`t`t`t`t`t`t`t<v8:lang>ru</v8:lang>"
X "`t`t`t`t`t`t`t<v8:content>$(Esc-XmlText $cellInfo.Text)</v8:content>"
X "`t`t`t`t`t`t</v8:item>"
X "`t`t`t`t`t</tl>"
}
if ($cellInfo.Text) { Emit-CellText $cellInfo.Text }
if ($cellInfo.Template) {
X "`t`t`t`t`t<tl>"
X "`t`t`t`t`t`t<v8:item>"
X "`t`t`t`t`t`t`t<v8:lang>ru</v8:lang>"
X "`t`t`t`t`t`t`t<v8:content>$(Esc-XmlText $cellInfo.Template)</v8:content>"
X "`t`t`t`t`t`t</v8:item>"
X "`t`t`t`t`t</tl>"
}
if ($cellInfo.Template) { Emit-CellText $cellInfo.Template }
X "`t`t`t`t</c>"
X "`t`t`t</c>"
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# mxl-compile v1.23 — Compile 1C spreadsheet from JSON (+write_xml_file/write_utf8_bom: общий эталон записи)
# mxl-compile v1.24 — Compile 1C spreadsheet from JSON (+write_xml_file/write_utf8_bom: общий эталон записи)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import hashlib
@@ -606,6 +606,22 @@ def main():
col_format_map = column_layouts[0]['FormatMap']
# 6c. Helper: determine fillType from cell content
# Текст ячейки платформа хранит по элементу на язык. Конвенция ML-значений (та же, что у
# synonym/tooltip/title в метаданных и формах): строка — русский текст, объект — по элементу
# на язык В ПОРЯДКЕ КЛЮЧЕЙ.
def emit_cell_text(lines, value):
if isinstance(value, dict):
pairs = [(str(k), str(v)) for k, v in value.items()]
else:
pairs = [('ru', str(value))]
lines.append('\t\t\t\t\t<tl>')
for lang, content in pairs:
lines.append('\t\t\t\t\t\t<v8:item>')
lines.append(f'\t\t\t\t\t\t\t<v8:lang>{lang}</v8:lang>')
lines.append(f'\t\t\t\t\t\t\t<v8:content>{esc_xml_text(content)}</v8:content>')
lines.append('\t\t\t\t\t\t</v8:item>')
lines.append('\t\t\t\t\t</tl>')
def get_fill_type(cell):
"""Text НЕ эмитим: платформа его практически не пишет — на выборке корпуса 344 981
текстовая ячейка из 348 023 (99,1%) ссылается на формат БЕЗ fillType. Наличие <tl>
@@ -1033,20 +1049,10 @@ def main():
lines.append(f'\t\t\t\t\t<detailParameter>{cell_info["Detail"]}</detailParameter>')
if cell_info['Text']:
lines.append('\t\t\t\t\t<tl>')
lines.append('\t\t\t\t\t\t<v8:item>')
lines.append('\t\t\t\t\t\t\t<v8:lang>ru</v8:lang>')
lines.append(f'\t\t\t\t\t\t\t<v8:content>{esc_xml_text(cell_info["Text"])}</v8:content>')
lines.append('\t\t\t\t\t\t</v8:item>')
lines.append('\t\t\t\t\t</tl>')
emit_cell_text(lines, cell_info['Text'])
if cell_info['Template']:
lines.append('\t\t\t\t\t<tl>')
lines.append('\t\t\t\t\t\t<v8:item>')
lines.append('\t\t\t\t\t\t\t<v8:lang>ru</v8:lang>')
lines.append(f'\t\t\t\t\t\t\t<v8:content>{esc_xml_text(cell_info["Template"])}</v8:content>')
lines.append('\t\t\t\t\t\t</v8:item>')
lines.append('\t\t\t\t\t</tl>')
emit_cell_text(lines, cell_info['Template'])
lines.append('\t\t\t\t</c>')
lines.append('\t\t\t</c>')
@@ -1,4 +1,4 @@
# mxl-decompile v1.5 — Decompile 1C spreadsheet to JSON
# mxl-decompile v1.6 — Decompile 1C spreadsheet to JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -248,9 +248,22 @@ foreach ($riNode in $root.SelectNodes("d:rowsItem", $ns)) {
$dNode = $cContent.SelectSingleNode("d:detailParameter", $ns)
if ($dNode) { $detail = $dNode.InnerText }
# Текст ячейки платформа хранит по элементу на язык. Раньше брался ПЕРВЫЙ, и всё
# остальное терялось — в корпусе ERP 98% макетов держат текст и под ru, и под en.
# Конвенция ML-значений: только ru → строка, иначе объект «язык → текст».
$text = $null
$tNode = $cContent.SelectSingleNode("d:tl/v8:item/v8:content", $ns)
if ($tNode) { $text = $tNode.InnerText }
$items = $cContent.SelectNodes("d:tl/v8:item", $ns)
if ($items -and $items.Count -gt 0) {
$byLang = [ordered]@{}
foreach ($it in $items) {
$langNode = $it.SelectSingleNode("v8:lang", $ns)
$contentNode = $it.SelectSingleNode("v8:content", $ns)
$lang = if ($langNode) { $langNode.InnerText } else { '' }
$byLang[$lang] = if ($contentNode) { $contentNode.InnerText } else { '' }
}
if ($byLang.Count -eq 1 -and $byLang.Contains('ru')) { $text = $byLang['ru'] }
else { $text = $byLang }
}
$cells += @{
Col = $col
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# mxl-decompile v1.5 — Decompile 1C spreadsheet to JSON
# mxl-decompile v1.6 — Decompile 1C spreadsheet to JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -428,10 +428,22 @@ def main():
if d_node is not None and d_node.text:
detail = d_node.text
# Текст ячейки платформа хранит по элементу на язык. Раньше брался ПЕРВЫЙ, и всё
# остальное терялось — в корпусе ERP 98% макетов держат текст и под ru, и под en.
# Конвенция ML-значений: только ru → строка, иначе объект «язык → текст».
text = None
t_node = find(c_content, "d:tl/v8:item/v8:content")
if t_node is not None and t_node.text:
text = t_node.text
items = findall(c_content, "d:tl/v8:item")
if items:
by_lang = OrderedDict()
for it in items:
lang_node = find(it, "v8:lang")
content_node = find(it, "v8:content")
lang = (text_of(lang_node) or '') if lang_node is not None else ''
by_lang[lang] = (text_of(content_node) or '') if content_node is not None else ''
if len(by_lang) == 1 and 'ru' in by_lang:
text = by_lang['ru']
else:
text = by_lang
cells.append({
"Col": col,
+15 -4
View File
@@ -214,8 +214,8 @@
| `style` | нет | rowStyle | Стиль ячейки (переопределяет rowStyle) |
| `param` | нет | — | Параметр заполнения |
| `detail` | нет | — | Параметр расшифровки (только с `param`) |
| `text` | нет | — | Статический текст |
| `template` | нет | — | Шаблонный текст с `[Параметр]` |
| `text` | нет | — | Статический текст. Строка или объект `{ ru, en }` — см. ниже |
| `template` | нет | — | Шаблонный текст с `[Параметр]`. Строка или объект, как `text` |
### Содержимое ячейки
@@ -225,6 +225,17 @@
- `text` — статический текст;
- ничего — пустая ячейка (нужна, например, ради рамки).
### Текст на нескольких языках
`text` и `template` принимают строку или объект «язык → текст». Строка означает русский; объект даёт по надписи на каждый язык, в порядке ключей.
```json
{ "col": 1, "text": "Наименование" }
{ "col": 2, "text": { "ru": "Поставщик", "en": "Supplier" } }
```
Языки макета объявляются отдельно (`languageSettings` в XML) и по умолчанию ограничены русским; писать текст на языке, которого нет в объявленных, допустимо — так устроено большинство типовых макетов.
## `rowStyle` — автозаполнение
Стиль применяется ко ВСЕЙ ширине строки: позиции без явных ячеек получают тот же стиль. Так в табличных строках получаются сплошные рамки.
@@ -244,8 +255,8 @@ XML не возвращается.
- рамка с разным стилем у разных сторон; стили линий кроме сплошной;
- группировки строк и колонок;
- колонтитулы, параметры печати, область печати;
- многоязычные надписи: при разборе берётся первый вариант текста, при генерации язык
всегда `ru`, остальные теряются.
- объявление языков макета: список языков и текущий язык всегда описываются как русский —
сами надписи при этом сохраняются на всех языках.
Пересборка макета из DSL — это полная перегенерация, а не точечная правка XML, поэтому
diff после round-trip обычно шире фактической доработки.
@@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8"?>
<document xmlns="http://v8.1c.ru/8.2/data/spreadsheet" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<languageSettings>
<currentLanguage>ru</currentLanguage>
<defaultLanguage>ru</defaultLanguage>
<languageInfo>
<id>ru</id>
<code>Русский</code>
<description>Русский</description>
</languageInfo>
</languageSettings>
<columns>
<size>3</size>
</columns>
<rowsItem>
<index>0</index>
<row>
<c>
<i>0</i>
<c>
<f>1</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Только по-русски</v8:content>
</v8:item>
</tl>
</c>
</c>
<c>
<i>1</i>
<c>
<f>1</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Поставщик</v8:content>
</v8:item>
<v8:item>
<v8:lang>en</v8:lang>
<v8:content>Supplier</v8:content>
</v8:item>
</tl>
</c>
</c>
<c>
<i>2</i>
<c>
<f>2</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Итого: [Сумма]</v8:content>
</v8:item>
<v8:item>
<v8:lang>en</v8:lang>
<v8:content>Total: [Сумма]</v8:content>
</v8:item>
</tl>
</c>
</c>
</row>
</rowsItem>
<templateMode>true</templateMode>
<defaultFormatIndex>1</defaultFormatIndex>
<height>1</height>
<vgRows>1</vgRows>
<namedItem xsi:type="NamedItemCells">
<name>Шапка</name>
<area>
<type>Rows</type>
<beginRow>0</beginRow>
<endRow>0</endRow>
<beginColumn>-1</beginColumn>
<endColumn>-1</endColumn>
</area>
</namedItem>
<font faceName="Arial" height="10" bold="false" italic="false" underline="false" strikeout="false" kind="Absolute" scale="100"/>
<format>
<width>10</width>
</format>
<format>
<font>0</font>
<fillType>Template</fillType>
</format>
</document>
@@ -0,0 +1,35 @@
{
"name": "Многоязычный текст ячейки: строка = ru, объект = по элементу на язык",
"input": {
"columns": 3,
"areas": [
{
"name": "Шапка",
"rows": [
{
"cells": [
{ "col": 1, "text": "Только по-русски" },
{ "col": 2, "text": { "ru": "Поставщик", "en": "Supplier" } },
{ "col": 3, "template": { "ru": "Итого: [Сумма]", "en": "Total: [Сумма]" } }
]
}
]
}
]
},
"params": {
"outputPath": "Template.xml"
},
"validatePath": "Template.xml",
"expect": {
"files": ["Template.xml"],
"fileContains": {
"file": "Template.xml",
"text": [
"<v8:lang>en</v8:lang>",
"<v8:content>Supplier</v8:content>",
"<v8:content>Поставщик</v8:content>"
]
}
}
}