From 1e06e43e9237448a4522ca1fe0606676511b5d81 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Mon, 10 Aug 2026 20:52:03 +0300 Subject: [PATCH] =?UTF-8?q?feat(mxl-compile,mxl-decompile):=20=D0=BC=D0=BD?= =?UTF-8?q?=D0=BE=D0=B3=D0=BE=D1=8F=D0=B7=D1=8B=D1=87=D0=BD=D1=8B=D0=B9=20?= =?UTF-8?q?=D1=82=D0=B5=D0=BA=D1=81=D1=82=20=D1=8F=D1=87=D0=B5=D0=B9=D0=BA?= =?UTF-8?q?=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Платформа хранит текст ячейки по элементу на язык. Декомпилятор брал ПЕРВЫЙ и терял остальное, компилятор писал всегда 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 строк — ячейки с ПУСТЫМ у оригинала, которых мы не эмитим вовсе; полностью разобрать мешает обрезка диффа по 400 строк на объект. Снэпшоты не дрейфанули: кейсы пишут текст строкой, и вывод для неё прежний. Проверено: тесты 54/54 на обоих рантаймах, verify-snapshots 24/24, вывод портов совпадает байт в байт и в компиляции, и в декомпиляции. Co-Authored-By: Claude Opus 5 (1M context) --- .../skills/mxl-compile/reference/dsl-spec.md | 19 +++- .../mxl-compile/scripts/mxl-compile.ps1 | 43 ++++++---- .../skills/mxl-compile/scripts/mxl-compile.py | 32 ++++--- .../mxl-decompile/scripts/mxl-decompile.ps1 | 19 +++- .../mxl-decompile/scripts/mxl-decompile.py | 20 ++++- docs/mxl-dsl-spec.md | 19 +++- .../snapshots/text-multilang/Template.xml | 86 +++++++++++++++++++ .../cases/mxl-compile/text-multilang.json | 35 ++++++++ 8 files changed, 228 insertions(+), 45 deletions(-) create mode 100644 tests/skills/cases/mxl-compile/snapshots/text-multilang/Template.xml create mode 100644 tests/skills/cases/mxl-compile/text-multilang.json diff --git a/.claude/skills/mxl-compile/reference/dsl-spec.md b/.claude/skills/mxl-compile/reference/dsl-spec.md index 595f1c5c..80055230 100644 --- a/.claude/skills/mxl-compile/reference/dsl-spec.md +++ b/.claude/skills/mxl-compile/reference/dsl-spec.md @@ -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 обычно шире фактической доработки. diff --git a/.claude/skills/mxl-compile/scripts/mxl-compile.ps1 b/.claude/skills/mxl-compile/scripts/mxl-compile.ps1 index 98825528..1eff2ed1 100644 --- a/.claude/skills/mxl-compile/scripts/mxl-compile.ps1 +++ b/.claude/skills/mxl-compile/scripts/mxl-compile.ps1 @@ -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('&','&').Replace('<','<').Replace('>','>') } +# Текст ячейки платформа хранит по элементу на язык. Конвенция 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" + foreach ($p in $pairs) { + X "`t`t`t`t`t`t" + X "`t`t`t`t`t`t`t$($p.Lang)" + X "`t`t`t`t`t`t`t$(Esc-XmlText $p.Text)" + X "`t`t`t`t`t`t" + } + X "`t`t`t`t`t" +} + # Helper: determine fillType from cell content # Text НЕ эмитим: платформа его практически не пишет — на выборке корпуса 344 981 текстовая # ячейка из 348 023 (99,1%) ссылается на формат БЕЗ fillType. Наличие и так означает @@ -1045,23 +1068,9 @@ foreach ($area in $def.areas) { } } - if ($cellInfo.Text) { - X "`t`t`t`t`t" - X "`t`t`t`t`t`t" - X "`t`t`t`t`t`t`tru" - X "`t`t`t`t`t`t`t$(Esc-XmlText $cellInfo.Text)" - X "`t`t`t`t`t`t" - X "`t`t`t`t`t" - } + if ($cellInfo.Text) { Emit-CellText $cellInfo.Text } - if ($cellInfo.Template) { - X "`t`t`t`t`t" - X "`t`t`t`t`t`t" - X "`t`t`t`t`t`t`tru" - X "`t`t`t`t`t`t`t$(Esc-XmlText $cellInfo.Template)" - X "`t`t`t`t`t`t" - X "`t`t`t`t`t" - } + if ($cellInfo.Template) { Emit-CellText $cellInfo.Template } X "`t`t`t`t" X "`t`t`t" diff --git a/.claude/skills/mxl-compile/scripts/mxl-compile.py b/.claude/skills/mxl-compile/scripts/mxl-compile.py index ca69b851..8880f1f8 100644 --- a/.claude/skills/mxl-compile/scripts/mxl-compile.py +++ b/.claude/skills/mxl-compile/scripts/mxl-compile.py @@ -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') + for lang, content in pairs: + lines.append('\t\t\t\t\t\t') + lines.append(f'\t\t\t\t\t\t\t{lang}') + lines.append(f'\t\t\t\t\t\t\t{esc_xml_text(content)}') + lines.append('\t\t\t\t\t\t') + lines.append('\t\t\t\t\t') + def get_fill_type(cell): """Text НЕ эмитим: платформа его практически не пишет — на выборке корпуса 344 981 текстовая ячейка из 348 023 (99,1%) ссылается на формат БЕЗ fillType. Наличие @@ -1033,20 +1049,10 @@ def main(): lines.append(f'\t\t\t\t\t{cell_info["Detail"]}') if cell_info['Text']: - lines.append('\t\t\t\t\t') - lines.append('\t\t\t\t\t\t') - lines.append('\t\t\t\t\t\t\tru') - lines.append(f'\t\t\t\t\t\t\t{esc_xml_text(cell_info["Text"])}') - lines.append('\t\t\t\t\t\t') - lines.append('\t\t\t\t\t') + emit_cell_text(lines, cell_info['Text']) if cell_info['Template']: - lines.append('\t\t\t\t\t') - lines.append('\t\t\t\t\t\t') - lines.append('\t\t\t\t\t\t\tru') - lines.append(f'\t\t\t\t\t\t\t{esc_xml_text(cell_info["Template"])}') - lines.append('\t\t\t\t\t\t') - lines.append('\t\t\t\t\t') + emit_cell_text(lines, cell_info['Template']) lines.append('\t\t\t\t') lines.append('\t\t\t') diff --git a/.claude/skills/mxl-decompile/scripts/mxl-decompile.ps1 b/.claude/skills/mxl-decompile/scripts/mxl-decompile.ps1 index b7f792dd..999d03e5 100644 --- a/.claude/skills/mxl-decompile/scripts/mxl-decompile.ps1 +++ b/.claude/skills/mxl-decompile/scripts/mxl-decompile.ps1 @@ -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 diff --git a/.claude/skills/mxl-decompile/scripts/mxl-decompile.py b/.claude/skills/mxl-decompile/scripts/mxl-decompile.py index a7446066..4a03139f 100644 --- a/.claude/skills/mxl-decompile/scripts/mxl-decompile.py +++ b/.claude/skills/mxl-decompile/scripts/mxl-decompile.py @@ -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, diff --git a/docs/mxl-dsl-spec.md b/docs/mxl-dsl-spec.md index 9498231c..037ded82 100644 --- a/docs/mxl-dsl-spec.md +++ b/docs/mxl-dsl-spec.md @@ -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 обычно шире фактической доработки. diff --git a/tests/skills/cases/mxl-compile/snapshots/text-multilang/Template.xml b/tests/skills/cases/mxl-compile/snapshots/text-multilang/Template.xml new file mode 100644 index 00000000..cd9f3567 --- /dev/null +++ b/tests/skills/cases/mxl-compile/snapshots/text-multilang/Template.xml @@ -0,0 +1,86 @@ + + + + ru + ru + + ru + Русский + Русский + + + + 3 + + + 0 + + + 0 + + 1 + + + ru + Только по-русски + + + + + + 1 + + 1 + + + ru + Поставщик + + + en + Supplier + + + + + + 2 + + 2 + + + ru + Итого: [Сумма] + + + en + Total: [Сумма] + + + + + + + true + 1 + 1 + 1 + + Шапка + + Rows + 0 + 0 + -1 + -1 + + + + + 10 + + + 0 + Template + + \ No newline at end of file diff --git a/tests/skills/cases/mxl-compile/text-multilang.json b/tests/skills/cases/mxl-compile/text-multilang.json new file mode 100644 index 00000000..e20a815e --- /dev/null +++ b/tests/skills/cases/mxl-compile/text-multilang.json @@ -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": [ + "en", + "Supplier", + "Поставщик" + ] + } + } +}